Newer
Older
powermon_manager_sw / gui / gui_device.cpp
@Razvan Turiac Razvan Turiac on 8 Jul 34 KB Initial import
/* 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_device.h>
#include <gui_calibrate_dialog.h>
#include <gui_string_dialog.h>
#include <gui_timers_dialog.h>
#include <gui_device_config.h>
#include <gui_wifi_setup.h>
#include <gui_stats.h>
#include <gui_fg_stats.h>

#include <gui_about.h>

#include <wx/mstream.h>
#include <wx/bitmap.h>
#include <wx/dcbuffer.h>
#include <wx/graphics.h>

#include <string.h>

#include <model.h>


#define MONITOR_FONT wxFont(18, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL)
#define MONITOR_FONT_BOLD wxFont(18, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD)


GuiDevice::GuiDevice(wxWindow* parent, const Powermon::DeviceIdentifier &id, PowermonScanner* scanner): wxFrame(parent, wxID_ANY, wxT(""), wxDefaultPosition, wxDefaultSize, wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxCLIP_CHILDREN), 
																				mPowermon(*Powermon::createInstance()), mStatsDialog(nullptr), mFgStatsDialog(nullptr)
{
	mChart = nullptr;

	wxString title;
	title.Printf(wxT("PowerMon Manager: %s"), id.name.c_str());
	SetTitle(title);

	mMonitorDataValid = false;
	mSyncInProgress = false;

	mMonitorBackgroundColor = wxColour(0xB3, 0xDF, 0xA6);

	SetDoubleBuffered(true);

	mMenuBar = new wxMenuBar;

	mDeviceMenu = new wxMenu;

	mDeviceMenu->Append(ID_MENU_VIEW_STATS, wxT("View Statistics"));
	mDeviceMenu->Enable(ID_MENU_VIEW_STATS, false);
	mDeviceMenu->SetHelpString(ID_MENU_VIEW_STATS, wxT("View the device statistics"));

	mDeviceMenu->Append(ID_MENU_VIEW_FG_STATS, wxT("View FuelGauge Statistics"));
	mDeviceMenu->Enable(ID_MENU_VIEW_FG_STATS, false);
	mDeviceMenu->SetHelpString(ID_MENU_VIEW_FG_STATS, wxT("View the FuelGauge statistics"));

	mDeviceMenu->AppendSeparator();

	mDeviceMenu->Append(ID_MENU_DATA_LOG, wxT("Data Log"));
	mDeviceMenu->Enable(ID_MENU_DATA_LOG, false);
	mDeviceMenu->SetHelpString(ID_MENU_DATA_LOG, wxT("Access the data logging feature"));

	mDeviceMenu->Append(ID_MENU_SYNC_LOG, wxT("Sync Log Now"));
	mDeviceMenu->Enable(ID_MENU_SYNC_LOG, false);
	mDeviceMenu->SetHelpString(ID_MENU_SYNC_LOG, wxT("Synchronize the data log now"));

	mDeviceMenu->Append(ID_MENU_CLEAR_LOCAL_LOG, wxT("Clear Local Data Log"));
	mDeviceMenu->Enable(ID_MENU_CLEAR_LOCAL_LOG, false);
	mDeviceMenu->SetHelpString(ID_MENU_CLEAR_LOCAL_LOG, wxT("Clear the local copy of the data log feature"));

	mDeviceMenu->AppendSeparator();

	mDeviceMenu->Append(ID_MENU_TIMERS, wxT("Timers"));
	mDeviceMenu->Enable(ID_MENU_TIMERS, false);
	mDeviceMenu->SetHelpString(ID_MENU_TIMERS, wxT("Add, edit or delete timers"));	

	mDeviceMenu->AppendSeparator();

	mDeviceMenu->Append(ID_MENU_POWER_CONTROL, wxT("Power Control"));
	mDeviceMenu->Enable(ID_MENU_POWER_CONTROL, false);
	mDeviceMenu->SetHelpString(ID_MENU_POWER_CONTROL, wxT("Control the device power relay output"));

	mDeviceMenu->AppendSeparator();

	mDeviceMenu->Append(ID_MENU_RESET_EM, wxT("Reset Energy Meter"));
	mDeviceMenu->Enable(ID_MENU_RESET_EM, false);
	mDeviceMenu->SetHelpString(ID_MENU_RESET_EM, wxT("Reset the device energy meter"));

	mDeviceMenu->Append(ID_MENU_RESET_CM, wxT("Reset Coulomb Meter"));
	mDeviceMenu->Enable(ID_MENU_RESET_CM, false);
	mDeviceMenu->SetHelpString(ID_MENU_RESET_CM, wxT("Reset the device coulomb meter"));

	mDeviceMenu->AppendSeparator();

	mDeviceMenu->Append(ID_MENU_UNLOCK, wxT("Unlock Device"));
	mDeviceMenu->Enable(ID_MENU_UNLOCK, false);
	mDeviceMenu->SetHelpString(ID_MENU_UNLOCK, wxT("Unlock a password protected device"));

	mDeviceMenu->Append(ID_MENU_SET_USER_PASSWORD, wxT("Set User Password"));
	mDeviceMenu->Enable(ID_MENU_SET_USER_PASSWORD, false);
	mDeviceMenu->SetHelpString(ID_MENU_SET_USER_PASSWORD, wxT("Set or remove the user password protection"));

	mDeviceMenu->Append(ID_MENU_SET_MASTER_PASSWORD, wxT("Set Master Password"));
	mDeviceMenu->Enable(ID_MENU_SET_MASTER_PASSWORD, false);
	mDeviceMenu->SetHelpString(ID_MENU_SET_MASTER_PASSWORD, wxT("Set or remove the master password protection"));

	mDeviceMenu->AppendSeparator();

	mDeviceMenu->Append(ID_MENU_PAIR, wxT("Pair Device"));
	mDeviceMenu->SetHelpString(ID_MENU_PAIR, wxT("Pair the device"));

	mDeviceMenu->AppendSeparator();

	mDeviceMenu->Append(wxID_EXIT, wxT("Disconnect"));
	mDeviceMenu->SetHelpString(wxID_EXIT, wxT("Disconnect from the device and close this window"));


	mSettingsMenu = new wxMenu;

	mSettingsMenu->Append(ID_MENU_RENAME, wxT("Rename Device"));
	mSettingsMenu->Enable(ID_MENU_RENAME, false);
	mSettingsMenu->SetHelpString(ID_MENU_RENAME, wxT("Change the BLE name of a device"));


	mSettingsMenu->Append(ID_MENU_CONFIGURATION, wxT("Device Configuration"));
	mSettingsMenu->Enable(ID_MENU_CONFIGURATION, false);
	mSettingsMenu->SetHelpString(ID_MENU_CONFIGURATION, wxT("Access the device configuration"));

	mSettingsMenu->Append(ID_MENU_WIFI_SETUP, wxT("WiFi Setup"));
	mSettingsMenu->Enable(ID_MENU_WIFI_SETUP, false);
	mSettingsMenu->SetHelpString(ID_MENU_WIFI_SETUP, wxT("Configure the WiFi network PowerMon connects to"));

	mSettingsMenu->Append(ID_MENU_RESET_FACTORY, wxT("Factory Reset"));
	mSettingsMenu->Enable(ID_MENU_RESET_FACTORY, false);
	mSettingsMenu->SetHelpString(ID_MENU_RESET_FACTORY, wxT("Access the device configuration"));


	mSettingsMenu->Append(ID_MENU_SET_TIME, wxT("Set Device Time"));
	mSettingsMenu->Enable(ID_MENU_SET_TIME, false);
	mSettingsMenu->SetHelpString(ID_MENU_SET_TIME, wxT("Set the BLE device time/date"));

	mSettingsMenu->Append(ID_MENU_ZERO_OFFSET, wxT("Zero Current Offset"));
	mSettingsMenu->Enable(ID_MENU_ZERO_OFFSET, false);
	mSettingsMenu->SetHelpString(ID_MENU_ZERO_OFFSET, wxT("Zero the current offset"));

	mSettingsMenu->Append(ID_MENU_CALIBRATE_CURRENT, wxT("Calibrate Current"));
	mSettingsMenu->Enable(ID_MENU_CALIBRATE_CURRENT, false);
	mSettingsMenu->SetHelpString(ID_MENU_CALIBRATE_CURRENT, wxT("Calibrate the current reading"));

	mSettingsMenu->Append(ID_MENU_FORCE_FG_SYNC, wxT("Force SOC Sync"));
	mSettingsMenu->Enable(ID_MENU_FORCE_FG_SYNC, false);
	mSettingsMenu->SetHelpString(ID_MENU_FORCE_FG_SYNC, wxT("Force the SOC synchronization"));


	mMenuBar->Append(mDeviceMenu, wxT("&Device"));
	mMenuBar->Append(mSettingsMenu, wxT("&Settings"));
	

	wxMenu* menu = new wxMenu;
	menu->Append(wxID_ABOUT, wxT("&About"));
	mMenuBar->Append(menu, wxT("&Help"));

	SetMenuBar(mMenuBar);

	CreateStatusBar(1);
	SetStatusText(wxT("Disconnected"));


	//main window sizer & panel
	wxPanel *panel = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
	wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
	panel->SetSizer(main_sizer);


	mMonitorPanel = new wxPanel(panel, wxID_ANY, wxDefaultPosition, FromDIP(wxSize(400, 460)));
	mMonitorPanel->SetBackgroundStyle(wxBG_STYLE_PAINT);
	mMonitorPanel->SetDoubleBuffered(true);
	mMonitorPanel->SetBackgroundColour(mMonitorBackgroundColor);
	mMonitorPanel->Bind(wxEVT_PAINT, &GuiDevice::OnMonitorPaint, this);

	main_sizer->Add(mMonitorPanel, 1, wxALL | wxEXPAND, FromDIP(10));
	
	main_sizer->SetSizeHints(this);
	
	
	mPowermon.setOnConnectCallback([this, scanner]()
	{
		if (scanner)
			scanner->startBleScan();

		mPowermon.requestGetInfo([this](uint16_t status, const Powermon::DeviceInfo &info)
		{
			wxQueueEvent(this, new GuiEvent([this, status, info](const GuiEvent &event)
			{
				if (checkDeviceError(this, status))
				{
					SetStatusText(wxT("Connected"));
					updateTitle();
					
					if (info.isUserLocked())
						deviceUnlock(true);
					else
						startDevice();
				}
			}));
		});
	});


	mPowermon.setOnDisconnectCallback([this, scanner](Powermon::DisconnectReason reason)
	{
		if (scanner)
			scanner->startBleScan();

		wxQueueEvent(this, new GuiEvent([this, reason](const GuiEvent &event)
		{
			mSyncLogTimer->Stop();
			mMonitorDataTimer->Stop();
			Model::getInstance().clearDevice();

			enableMenus(false);

			if (reason == Powermon::DisconnectReason::NO_ROUTE)
			{
				wxMessageDialog msg(this, wxT("Device failed to connect"), wxT("Info"), wxOK | wxSTAY_ON_TOP | wxCENTRE);
				msg.ShowModal();
			}
			else if (reason != Powermon::DisconnectReason::CLOSED)
			{
				wxMessageDialog msg(this, wxT("Device disconnected"), wxT("Info"), wxOK | wxSTAY_ON_TOP | wxCENTRE);
				msg.ShowModal();
			}

			Close(true);
		}));
	});

	mPowermon.setOnMonitorDataCallback([this](const Powermon::MonitorData &data)
	{
		GuiEvent* event = new GuiEvent([this, data](GuiEvent &event)
		{
			mMonitorData = data;
			mMonitorDataValid = true;
			mMonitorPanel->Refresh();
		});
			
		wxQueueEvent(this, event);
	});

	
	mMonitorDataTimer = new wxTimer(this, ID_MONITOR_DATA_TIMER);
	mSyncLogTimer = new wxTimer(this, ID_SYNC_LOG_TIMER);

	//start the thread and connect
	SetStatusText(wxT("Connecting ..."));
	
	if (Powermon::hasWifi(id.hardware_revision_bcd))
	{
		if (id.address)
			mPowermon.connectWifi(id.address);
		else
			mPowermon.connectWifi(id.access_key);
	}
	else
	{
		mPowermon.connectBle(id.address);
	}
}


GuiDevice::~GuiDevice()
{
	delete mMonitorDataTimer;
	delete mSyncLogTimer;
}



void GuiDevice::enableMenus(bool state)
{
	mDeviceMenu->Enable(ID_MENU_PAIR, state);
	mDeviceMenu->Enable(ID_MENU_TIMERS, state);

	mDeviceMenu->Enable(ID_MENU_RESET_EM, state);
	mDeviceMenu->Enable(ID_MENU_RESET_CM, state);
	mDeviceMenu->Enable(ID_MENU_VIEW_STATS, state);
	mDeviceMenu->Enable(ID_MENU_VIEW_FG_STATS, state);

	mDeviceMenu->Enable(ID_MENU_POWER_CONTROL, state);
	mDeviceMenu->Enable(ID_MENU_DATA_LOG, state);
	mDeviceMenu->Enable(ID_MENU_SYNC_LOG, state);
	mDeviceMenu->Enable(ID_MENU_CLEAR_LOCAL_LOG, state);

	mDeviceMenu->Enable(ID_MENU_UNLOCK, state);
	mDeviceMenu->Enable(ID_MENU_SET_USER_PASSWORD, state);
	mDeviceMenu->Enable(ID_MENU_SET_MASTER_PASSWORD, state);
	
	mSettingsMenu->Enable(ID_MENU_RENAME, state);
	
	mSettingsMenu->Enable(ID_MENU_WIFI_SETUP, state);
	mSettingsMenu->Enable(ID_MENU_RESET_FACTORY, state);
	mSettingsMenu->Enable(ID_MENU_CONFIGURATION, state);
	mSettingsMenu->Enable(ID_MENU_SET_TIME, state);

	mSettingsMenu->Enable(ID_MENU_ZERO_OFFSET, state);
	mSettingsMenu->Enable(ID_MENU_CALIBRATE_CURRENT, state);

	mSettingsMenu->Enable(ID_MENU_FORCE_FG_SYNC, state);
}


void GuiDevice::OnSyncLogTimerEvent(wxTimerEvent &event)
{
	startSync();
}


void GuiDevice::startSync(void)
{
	mSyncEnabled = true;
	mSyncInProgress = false;
	mSyncTotalSize = 0;
	mSyncCompletedSize = 0;

	mLogFiles.clear();
	
	mPowermon.requestGetFileList([this](uint16_t status, const std::vector<Powermon::LogFileDescriptor> &list)
	{
		if (status == Powermon::RSP_SUCCESS)
		{
			mSyncCompletedSize = 0;
			mSyncTotalSize = 0;
			mSyncInProgress = true;
			mLogFiles = list;

			for(const auto &log: mLogFiles)
			{
				mSyncTotalSize += log.size;

				const wxFileName fname = Model::getInstance().getLogFileName(log.id);

				wxULongLong fsize = fname.GetSize();
				if (fsize != wxInvalidSize)
					mSyncTotalSize -= fsize.GetValue();
			}

			if (mSyncTotalSize > 0)
				readFile();
			else
				mSyncInProgress = false;
		}
		else
		{
			debug_printf("\r\nFile list error");
		}
	});
}

void GuiDevice::stopSync(void)
{
	mSyncEnabled = false;
	while(mSyncInProgress);
}


void GuiDevice::readFile(void)
{
	if (mLogFiles.size() > 0)
	{
		const wxFileName fname = Model::getInstance().getLogFileName(mLogFiles.front().id);
		if (mLogFile.Open(fname.GetFullPath(), wxFile::write_append))
		{
			mFileSize = mLogFile.Length();

			if (mFileSize < mLogFiles.front().size)
			{
				readFileBlock();
			}
			else
			{
				mLogFile.Close();

				if (mLogFiles.size())
					mLogFiles.erase(mLogFiles.begin());

				if (mSyncEnabled)
					readFile();
				else
					mSyncInProgress = false;
			}
		}
	}
	else
	{
		mSyncInProgress = false;

		if (mSyncEnabled)
		{
			wxQueueEvent(this, new GuiEvent([this](const GuiEvent &event)
			{
				mSyncLogTimer->Start(1000 * 60 * 10, wxTIMER_ONE_SHOT);
				updateStatusBar();
			}));
		}
	}
}


void GuiDevice::readFileBlock(void)
{
	const uint32_t block_size = Powermon::hasWifi(mPowermon.getLastDeviceInfo().hardware_revision_bcd) ? 128 * 1024 : 4 * 1024;

	mPowermon.requestReadFile(mLogFiles.front().id, mFileSize, block_size, 
	[this](uint16_t status, const uint8_t* data, size_t size)
	{
		if ((status == Powermon::RSP_SUCCESS) && size)
		{
			mLogFile.Write((const char*)data, size);
			mFileSize += size;
			
			mSyncCompletedSize += size;
			debug_printf("\r\nProgress: %u / %u", mSyncCompletedSize, mSyncTotalSize);

			wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
			{
				updateStatusBar();
			}));

			readFileBlock();
		}
		else
		{
			mLogFile.Close();

			if (mLogFiles.size())
				mLogFiles.erase(mLogFiles.begin());

			if (mSyncEnabled)
				readFile();
			else
				mSyncInProgress = false;
		}
	});
}


void GuiDevice::startDevice(void)
{
	if (Powermon::hasWifi(mPowermon.getLastDeviceInfo().hardware_revision_bcd))
		mMonitorDataTimer->Start(mPowermon.isLocalConnection() ? 1000 : 2000);

	Model::getInstance().setDevice(mPowermon.getLastDeviceInfo().serial);
	mSyncLogTimer->Start(1000 * 2, wxTIMER_ONE_SHOT);

	enableMenus(true);
}


void GuiDevice::OnMenuSettingsRename(wxCommandEvent &event)
{
	GuiEnterString dialog(this, wxID_ANY, wxT("Enter New Name"), false, false);

	dialog.setStringLabel(wxT("Enter name"));
	dialog.setMaxStringLength(Powermon::hasWifi(mPowermon.getLastDeviceInfo().hardware_revision_bcd) ? MAX_WIFI_NAME_LENGTH : MAX_BLE_NAME_LENGTH);		
	
	dialog.SetFocus();
	if (dialog.ShowModal() == wxID_OK)
	{
		const wxString name = dialog.getString();

		mPowermon.requestRename(name.mb_str(),
		[this, name](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status, name](const GuiEvent &event)
			{
				if (checkDeviceError(this, status))
				{
					updateTitle();
				}
			}));
		});
	}
}


void GuiDevice::OnMenuDeviceResetEM(wxCommandEvent &event)
{
	wxMessageDialog msg(this, wxT("Reset the energy meter ?"), wxT("Confirmation"), wxOK | wxCANCEL | wxICON_QUESTION | wxSTAY_ON_TOP | wxCENTRE);
	if (msg.ShowModal() == wxID_OK)
	{
		mPowermon.requestResetEnergyMeter([this](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent& event)
				{
					checkDeviceError(this, status);
				}));
		});
	}
}


void GuiDevice::OnMenuDeviceResetCM(wxCommandEvent &event)
{
	wxMessageDialog msg(this, wxT("Reset the coulomb meter ?"), wxT("Confirmation"), wxOK | wxCANCEL | wxICON_QUESTION | wxSTAY_ON_TOP | wxCENTRE);
	if (msg.ShowModal() == wxID_OK)
	{
		mPowermon.requestResetCoulombMeter([this](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent& event)
				{
					checkDeviceError(this, status);
				}));
		});
	}
}


void GuiDevice::OnMenuSettingsForceFgSync(wxCommandEvent &event)
{
	wxMessageDialog msg(this, wxT("Force the fuel gauge SoC synchronization ?"), wxT("Confirmation"), wxOK | wxCANCEL | wxICON_QUESTION | wxSTAY_ON_TOP | wxCENTRE);
	if (msg.ShowModal() == wxID_OK)
	{
		mPowermon.requestFgSynchronize([this](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
			{
				checkDeviceError(this, status);
			}));
		});
	}
}


void GuiDevice::OnMenuDeviceViewStats(wxCommandEvent &event)
{
	if (mStatsDialog == nullptr)
		mStatsDialog = new GuiStats(this, wxT("Statistics"), mPowermon);
	
	mStatsDialog->Show();
}


void GuiDevice::OnMenuDeviceViewFgStats(wxCommandEvent &event)
{
	mPowermon.requestGetFgStatistics([this](uint16_t status, const Powermon::FuelgaugeStatistics &stats)
	{
		wxQueueEvent(this, new GuiEvent([this, status, stats](const GuiEvent &event)
		{
			if (checkDeviceError(this, status))
			{
				if (mFgStatsDialog == nullptr)
					mFgStatsDialog = new GuiFgStats(this, wxT("Fuel Gauge Statistics"), stats);
	
				mFgStatsDialog->Show();
			}
		}));
	});
}


void GuiDevice::OnMenuSettingsZeroCurrentOffset(wxCommandEvent &event)
{
	wxMessageDialog msg(this, wxT("Calibrate the current offset ?"), wxT("Confirmation"), wxOK | wxCANCEL | wxICON_QUESTION | wxSTAY_ON_TOP | wxCENTRE);
	if (msg.ShowModal() == wxID_OK)
	{
		mPowermon.requestZeroCurrentOffset([this](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
			{
				checkDeviceError(this, status);
			}));
		});
	}
}


void GuiDevice::OnMenuSettingsCalibrateCurrent(wxCommandEvent &event)
{
	GuiCalibrateDialog dialog(this, wxID_ANY, wxT("Current calibration"));
		
	if (dialog.ShowModal() == wxID_OK)
	{
		mPowermon.requestCalibrateCurrent(dialog.getValue(), [this](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
			{
				checkDeviceError(this, status);
			}));
		});
	}
}


void GuiDevice::OnMenuDevicePowerControl(wxCommandEvent &event)
{
	wxMessageDialog msg(this, wxT("Turn the device power ..."), wxT("Device Power"), wxOK | wxCANCEL | wxICON_QUESTION | wxSTAY_ON_TOP | wxCENTRE);
	msg.SetOKCancelLabels(wxT("ON"), wxT("OFF"));
	const bool power_state = (msg.ShowModal() == wxID_OK);
	
	mPowermon.requestSetPowerState(power_state, [this](uint16_t status)
	{
		wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
		{
			checkDeviceError(this, status);
		}));
	});
}


void GuiDevice::OnMenuSettingsResetFactory(wxCommandEvent &event)
{
	wxMessageDialog msg(this, 
		wxT("You are about to reset your device to factory settings.\n"
			"The WiFi configuration, timers, and calibration will also be reset.\n"
			"The name will be reset to 'PowerMon-W'. All data logs will be removed!\n"
			"\n\n\nThis cannot be undone!\n\n"),
	
		wxT("Confirmation"), wxOK | wxCANCEL | wxICON_QUESTION | wxSTAY_ON_TOP | wxCENTRE);
	
	if (msg.ShowModal() != wxID_OK)
		return;

	mPowermon.requestResetConfig([this](uint16_t status)
	{
		wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
		{
			if (checkDeviceError(this, status))
			{
				wxMessageDialog msg(this, wxT("The device has been reset to factory defaults."), wxT("Info"), wxOK | wxSTAY_ON_TOP | wxCENTRE);
				msg.ShowModal();
			}
		}));
	});
}


void GuiDevice::OnMenuSettingsConfiguration(wxCommandEvent &event)
{
	mPowermon.requestGetConfig([this](uint16_t status, const PowermonConfig &config)
	{
		wxQueueEvent(this, new GuiEvent([this, status, config](const GuiEvent &event)
		{
			if (checkDeviceError(this, status))
			{
				GuiDeviceConfig dialog(this, wxID_ANY, config, mPowermon);
				
				if (dialog.ShowModal() == wxID_OK)
				{
					mPowermon.requestSetConfig(dialog.getConfiguration(), [this](uint16_t status)
					{
						wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
						{
							checkDeviceError(this, status);
						}));

					});
				}
			}
		}));
	});
}



void GuiDevice::OnMenuSettingsSetTime(wxCommandEvent &event)
{
	wxDateTime dt = wxDateTime::Now();

	wxString message_str;
	message_str.Printf(wxT("The device time will be set to:\n%s - %s"), 
							dt.Format(wxT("%I:%M %p")).mb_str(),
							dt.FormatDate().mb_str());

	wxMessageDialog msg(this, message_str, wxT("Confirmation"), wxOK | wxCANCEL | wxICON_QUESTION | wxSTAY_ON_TOP | wxCENTRE);
	if (msg.ShowModal() == wxID_OK)
	{
		mPowermon.requestSetTime(wxGetLocalTime() + (dt.IsDST() ? 3600 : 0),
		[this](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
			{
				checkDeviceError(this, status);
			}));
		});
	}
}


void GuiDevice::OnMenuDeviceTimers(wxCommandEvent &event)
{
	mPowermon.requestGetSchedules([this](uint16_t status, const std::vector<PowermonSchedule> &schedules)
	{
		wxQueueEvent(this, new GuiEvent([this, status, schedules](const GuiEvent &event)
		{
			if (checkDeviceError(this, status))
			{
				GuiTimersDialog dialog(this, schedules);

				if (dialog.ShowModal() == wxID_OK)
				{
					const std::vector<PowermonSchedule> new_schedules = dialog.getSchedules();

					mPowermon.requestClearSchedules([this, new_schedules](uint16_t status)
					{
						wxQueueEvent(this, new GuiEvent([this, status, new_schedules](const GuiEvent &event)
						{
							if (checkDeviceError(this, status))
							{
								if (new_schedules.size())
								{
									mPowermon.requestAddSchedules(new_schedules, [this](uint16_t status)
									{
										wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
										{
											checkDeviceError(this, status);
										}));
									});
								}

								mPowermon.requestCommitSchedules([this](uint16_t status)
								{
									wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
									{
										checkDeviceError(this, status);
									}));
								});
							}
						}));
					});
				}
			}
		}));
	});
}


void GuiDevice::OnMenuDeviceClearLocalLog(wxCommandEvent &event)
{
	stopSync();
	Model::getInstance().deleteLogData();
}


void GuiDevice::OnMenuDeviceSyncLog(wxCommandEvent &event)
{
	if (mSyncInProgress == false)
		startSync();
}


void GuiDevice::OnMenuDeviceDataLog(wxCommandEvent &event)
{
	if (mChart == nullptr)
		mChart = new GuiChart(this, wxT("PowerMon Data Log"));

    Model::getInstance().loadLogData(Model::getInstance().logData);

	mChart->update();
	mChart->SetFocus();
	mChart->Show();
}


void GuiDevice::OnMenuSettingsUnlock(wxCommandEvent &event)
{
	deviceUnlock(false);
}


void GuiDevice::OnMenuSettingsSetUserPassword(wxCommandEvent &event)
{
	GuiEnterString dialog(this, wxID_ANY, wxT("Enter new user password"), true, true);
	dialog.setStringLabel(wxT("Enter user password"));
	dialog.setConfirmLabel(wxT("Confirm user password"));
	dialog.setMaxStringLength(256);

	dialog.SetFocus();
	if (dialog.ShowModal() == wxID_OK)
	{
		if (dialog.getString().size())
		{
			const Powermon::AuthKey key = Powermon::getAuthKeyFromPassword(dialog.getString().mb_str());

			mPowermon.requestSetUserPasswordLock(key, [this](uint16_t status)
			{
				wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
				{
					checkDeviceError(this, status);
				}));
			});
		}
		else
		{
			mPowermon.requestClearUserPasswordLock([this](uint16_t status)
			{
				wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
				{
					if (checkDeviceError(this, status))
					{
						wxMessageDialog msg(this, 
											wxT("Device user password reset successfully.\n"),
											wxT("Confirmation"), wxOK | wxSTAY_ON_TOP | wxCENTRE);
						msg.ShowModal();
					}
				}));
			});
		}
	}
}


void GuiDevice::OnMenuSettingsSetMasterPassword(wxCommandEvent &event)
{
	GuiEnterString dialog(this, wxID_ANY, wxT("Enter new master password"), true, true);
	dialog.setStringLabel(wxT("Enter master password"));
	dialog.setConfirmLabel(wxT("Confirm master password"));
	dialog.setMaxStringLength(256);

	dialog.SetFocus();
	if (dialog.ShowModal() == wxID_OK)
	{
		if (dialog.getString().size())
		{
			const Powermon::AuthKey key = Powermon::getAuthKeyFromPassword(dialog.getString().mb_str());

			mPowermon.requestSetMasterPasswordLock(key, [this](uint16_t status)
			{
				wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
				{
					checkDeviceError(this, status);
				}));
			});
		}
		else
		{
			mPowermon.requestClearMasterPasswordLock([this](uint16_t status)
			{
				wxQueueEvent(this, new GuiEvent([this, status](const GuiEvent &event)
				{
					if (checkDeviceError(this, status))
					{
						wxMessageDialog msg(this, 
											wxT("Device master password reset successfully.\n"),
											wxT("Confirmation"), wxOK | wxSTAY_ON_TOP | wxCENTRE);
						msg.ShowModal();
					}
				}));
			});
		}
	}
}


void GuiDevice::OnMenuSettingsWifiSetup(wxCommandEvent &event)
{
	GuiWifiSetup setup(this, mPowermon);
	setup.ShowModal();
}


void GuiDevice::OnMenuDevicePair(wxCommandEvent &event)
{
	mPowermon.requestGetAccessKeys([this](uint16_t status, const Powermon::WifiAccessKey &key)
	{
		wxQueueEvent(this, new GuiEvent([this, status, key](const GuiEvent &event)
		{
			if (checkDeviceError(this, status))
			{
				Powermon::DeviceIdentifier id;

				id.serial = mPowermon.getLastDeviceInfo().serial;
				id.name = mPowermon.getLastDeviceInfo().name;
				id.hardware_revision_bcd = mPowermon.getLastDeviceInfo().hardware_revision_bcd;
				id.access_key = key;
				
				Model::getInstance().addCloudDevice(id);

				wxMessageDialog msg(this, 
					wxT("Device paired successfully.\n"),
					wxT("Instructions"), wxOK | wxSTAY_ON_TOP | wxCENTRE);
				msg.ShowModal();
			}
		}));
	});
}


void GuiDevice::OnMenuDeviceDisconnect(wxCommandEvent &event)
{
	mPowermon.setOnDisconnectCallback([](Powermon::DisconnectReason){});
	mPowermon.disconnect();
	Close(true);
}



void GuiDevice::OnMenuHelpAbout(wxCommandEvent &event)
{
	GuiAbout about(this, wxT("About"), mPowermon);
	if (about.ShowModal() == wxID_OK)
	{
		wxMessageDialog msg(this, wxT("Your PowerMon just received brand new firmware.\n"
								"It will now disconnect and update itself.\n\n"
								"DO NOT DISCONNECT THE POWER TO THE DEVICE UNTIL IT COMES BACK ON !"),
								wxT("Info"), wxOK | wxSTAY_ON_TOP | wxCENTRE);
		msg.ShowModal();

		mPowermon.disconnect();
	}
}


void GuiDevice::OnClose(wxCloseEvent &event)
{
	mPowermon.setOnDisconnectCallback([](Powermon::DisconnectReason){});
	mPowermon.disconnect();
	Destroy();
}


void GuiDevice::OnGuiEvent(GuiEvent &event)
{
	event.process();
}


void GuiDevice::updateTitle(void)
{
	wxString title;
	title.Printf(wxT("PowerMon Manager: %s"), mPowermon.getLastDeviceInfo().name.c_str());
	SetTitle(title);
}


void GuiDevice::updateStatusBar(void)
{
	if (mSyncInProgress)
	{
		const uint8_t progress = (100 * mSyncCompletedSize) / mSyncTotalSize;

		wxString str;
		str.Printf(wxT("Connected - Downloading data log ... %u%%"), progress);

		SetStatusText(str);
	}
	else
	{
		SetStatusText(wxT("Connected"));
	}
}


void GuiDevice::OnMonitorDataTimerEvent(wxTimerEvent &event)
{
	mPowermon.requestGetMonitorData([this](uint16_t status, const Powermon::MonitorData &data)
	{
		if (status == Powermon::RSP_SUCCESS)
		{
			GuiEvent* event = new GuiEvent([this, data](GuiEvent &event)
			{
				mMonitorData = data;
				mMonitorDataValid = true;
				mMonitorPanel->Refresh();
			});
			
			wxQueueEvent(this, event);
		}
	});
}


void GuiDevice::deviceUnlock(bool disconnect_on_failure)
{
	GuiEnterString dialog(this, wxID_ANY, wxT("Enter password"), true, false);
	dialog.setStringLabel(wxT("Enter password"));
	dialog.setMaxStringLength(256);

	dialog.SetFocus();
	if (dialog.ShowModal() == wxID_OK)
	{
		const Powermon::AuthKey key = Powermon::getAuthKeyFromPassword(dialog.getString().mb_str());

		mPowermon.requestUnlock(key, [this, disconnect_on_failure](uint16_t status)
		{
			wxQueueEvent(this, new GuiEvent([this, status, disconnect_on_failure](const GuiEvent &event)
			{
				if (status == Powermon::RSP_SUCCESS)
				{
					if (disconnect_on_failure)
						startDevice();
				}
				else if (status == Powermon::RSP_CANNOT_UNLOCK)
				{
					wxMessageDialog msg(this, wxT("Invalid password."), wxT("Error"), wxOK | wxSTAY_ON_TOP | wxCENTRE | wxICON_ERROR);
					msg.ShowModal();

					deviceUnlock(disconnect_on_failure);
				}
				else
				{
					checkDeviceError(this, status);
				}
			}));
		});
	}
	else
	{
		if (disconnect_on_failure)
			mPowermon.disconnect();
	}
}


void GuiDevice::OnMonitorPaint(wxPaintEvent &event)
{
	wxBufferedPaintDC dc(mMonitorPanel);

	const int32_t LEFT_BORDER = 2;
	const int32_t RIGHT_BORDER = 10;

	const wxSize size = mMonitorPanel->GetSize();

	//clean background
	dc.SetPen(wxPen(mMonitorBackgroundColor, 1));
	dc.SetBrush(wxBrush(mMonitorBackgroundColor));
	dc.DrawRectangle(0, 0, size.x, size.y);

	//set text color
	dc.SetTextBackground(mMonitorBackgroundColor);
	dc.SetTextForeground(*wxBLACK);
	
	dc.SetFont(MONITOR_FONT_BOLD);
	const int32_t row_height = dc.GetCharHeight();
	

	//draw static text
	dc.SetFont(MONITOR_FONT);

	int32_t y = 0;
	dc.DrawText(wxT("Voltage 1"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Voltage 2"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Current"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Power"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Energy Meter"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Coulomb Meter"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Battery SoC"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Battery Runtime"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Temperature"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Power Status"), LEFT_BORDER, y); y += row_height;
	dc.DrawText(wxT("Device Clock"), LEFT_BORDER, y); y += 2 * row_height;
	dc.DrawText(wxT("Device RSSI"), LEFT_BORDER, y); y += row_height;

	//draw values
	dc.SetFont(MONITOR_FONT_BOLD);
	y = 0;

	const wxString str_na = wxT("n/a");

	if (mMonitorDataValid)
	{
		wxString str;

		str.Printf(wxT("%.3f V"), mMonitorData.voltage1);
		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;

		if (!isnan(mMonitorData.voltage2))
			str.Printf(wxT("%.3f V"), mMonitorData.voltage2);
		else
			str = wxT("n/a");
		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;

		str.Printf(wxT("%.3f A"), mMonitorData.current);
		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;


		if (fabs(mMonitorData.power) >= 1000)
			str.Printf(wxT("%.3f kW"), mMonitorData.power / 1000.0);
		else
			str.Printf(wxT("%.2f W"), mMonitorData.power);

		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;


		if (fabs(mMonitorData.energy_meter) >= 1000000)
			str.Printf(wxT("%.3f kWh"), mMonitorData.energy_meter / 1000000.0);
		else
			str.Printf(wxT("%.2f Wh"), mMonitorData.energy_meter / 1000.0);

		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;


		if (fabs(mMonitorData.coulomb_meter) >= 1000000)
			str.Printf(wxT("%.3f Ah"), mMonitorData.coulomb_meter / 1000000.0);
		else
			str.Printf(wxT("%.3f Ah"), mMonitorData.coulomb_meter / 1000.0);

		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;


		if (mMonitorData.fg_soc > 100)
			str.Printf(wxT("---"));
		else
			str.Printf(wxT("%u %%"), mMonitorData.fg_soc);

		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;


		if (mMonitorData.fg_runtime > FG_RUNTIME_MAX)
			str.Printf(wxT("---"));
		else if (mMonitorData.fg_runtime == 0)
			str.Printf(wxT("under 1m"));
		else if (mMonitorData.fg_runtime > 60 * 24 * 45)
			str.Printf(wxT("over 45d"));
		else
		{
			uint32_t minutes = mMonitorData.fg_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);
		}
		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;

		str.Printf(wxT("%.0f \u00B0C"), mMonitorData.temperature);
		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;

		str = wxString(Powermon::getPowerStatusString(mMonitorData.power_status).c_str(), wxConvUTF8);
		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;


		if (mMonitorData.time)
		{
			wxDateTime dt;
			dt.Set((time_t)mMonitorData.time);
			dt.MakeUTC();

			str = dt.Format(wxT("%I:%M %p")).mb_str();
			dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;
			str = dt.Format(wxT("%b %d, %Y"));
			dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;
		}
		else
		{
			str.Printf(wxT("not set"));
			dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += 2 * row_height;
		}

		if (mMonitorData.rssi != INT16_MIN)
			str.Printf(wxT("%d dBm"), mMonitorData.rssi);
		else
			str.Printf(wxT("n/a"));
		dc.DrawText(str, size.x - dc.GetTextExtent(str).x - RIGHT_BORDER, y); y += row_height;
	}
	else
	{
		const int32_t x = size.x - dc.GetTextExtent(str_na).x - RIGHT_BORDER;

		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += row_height;
		dc.DrawText(str_na, x, y); y += 2 * row_height;
		dc.DrawText(str_na, x, y); y += row_height;
	}
}


BEGIN_EVENT_TABLE(GuiDevice, wxFrame)
EVT_CLOSE(GuiDevice::OnClose)
GUI_EVT(GuiDevice::OnGuiEvent)

EVT_ERASE_BACKGROUND(GuiDevice::OnEraseBackGround)

EVT_MENU(ID_MENU_VIEW_STATS, GuiDevice::OnMenuDeviceViewStats)
EVT_MENU(ID_MENU_VIEW_FG_STATS, GuiDevice::OnMenuDeviceViewFgStats)
EVT_MENU(ID_MENU_DATA_LOG, GuiDevice::OnMenuDeviceDataLog)
EVT_MENU(ID_MENU_SYNC_LOG, GuiDevice::OnMenuDeviceSyncLog)
EVT_MENU(ID_MENU_CLEAR_LOCAL_LOG, GuiDevice::OnMenuDeviceClearLocalLog)
EVT_MENU(ID_MENU_RESET_EM, GuiDevice::OnMenuDeviceResetEM)
EVT_MENU(ID_MENU_RESET_CM, GuiDevice::OnMenuDeviceResetCM)
EVT_MENU(ID_MENU_POWER_CONTROL, GuiDevice::OnMenuDevicePowerControl)
EVT_MENU(ID_MENU_PAIR, GuiDevice::OnMenuDevicePair)
EVT_MENU(wxID_EXIT, GuiDevice::OnMenuDeviceDisconnect)
EVT_MENU(ID_MENU_WIFI_SETUP, GuiDevice::OnMenuSettingsWifiSetup)
EVT_MENU(ID_MENU_TIMERS, GuiDevice::OnMenuDeviceTimers)
EVT_MENU(ID_MENU_RENAME, GuiDevice::OnMenuSettingsRename)
EVT_MENU(ID_MENU_CONFIGURATION, GuiDevice::OnMenuSettingsConfiguration)
EVT_MENU(ID_MENU_UNLOCK, GuiDevice::OnMenuSettingsUnlock)
EVT_MENU(ID_MENU_SET_USER_PASSWORD, GuiDevice::OnMenuSettingsSetUserPassword)
EVT_MENU(ID_MENU_SET_MASTER_PASSWORD, GuiDevice::OnMenuSettingsSetMasterPassword)


EVT_MENU(ID_MENU_RESET_FACTORY, GuiDevice::OnMenuSettingsResetFactory)
EVT_MENU(ID_MENU_SET_TIME, GuiDevice::OnMenuSettingsSetTime)
EVT_MENU(ID_MENU_ZERO_OFFSET, GuiDevice::OnMenuSettingsZeroCurrentOffset)
EVT_MENU(ID_MENU_CALIBRATE_CURRENT, GuiDevice::OnMenuSettingsCalibrateCurrent)
EVT_MENU(ID_MENU_FORCE_FG_SYNC, GuiDevice::OnMenuSettingsForceFgSync)
EVT_MENU(wxID_ABOUT, GuiDevice::OnMenuHelpAbout)

EVT_TIMER(ID_MONITOR_DATA_TIMER, GuiDevice::OnMonitorDataTimerEvent)
EVT_TIMER(ID_SYNC_LOG_TIMER, GuiDevice::OnSyncLogTimerEvent)

END_EVENT_TABLE()