OpenSolo/artoo/src/ui_bottombox.cpp
Matt 958f6a0f2b UI: Updated text strings on display
- Changed "3DR Solo App" to more generic "Mobile App"
- More useful waring text strings
- CH7 high & low notification strings
2017-12-17 15:04:14 +10:00

565 lines
16 KiB
C++

#include "ui_bottombox.h"
#include "ui.h"
#include "resources-gen.h"
#include "ui_alert.h"
#include "inputs.h"
#include "sologimbal.h"
#include <stdio.h>
const UiBottomBox::Msg UiBottomBox::msgs[] = {
/// None
{ Event::None, NoUserInput, UiAlertManager::NO_TIMEOUT, "Invalid message", NULL }, // Invalid message used in case string isn't found
/// Pre-flight - PreArm
/// Pre-flight - Arm
/// Pre-flight - Artoo
{ Event::SoloAppConnected, NoUserInput, 5000, "Mobile app connected", NULL },
{ Event::SoloAppDisconnected, NoUserInput, 5000, "Mobile app disconnected", NULL },
// Persistent alerts
{ Event::ControllerValueOutOfRange, NoUserInput, 5000, "Control stick error", "Contact 3DR Support"},
{ Event::CamControlValueOutOfRange, NoUserInput, 5000, "Manual camera controls error", "Contact 3DR Support"},
// Unused
{ Event::RecordRequiresApp, NoUserInput, 5000, "Mobile app required for this action", NULL },
{ Event::UnknownBattery, NoUserInput, 5000, "Unknown controller battery", "Displayed level may not be accurate" },
/// Pre-flight - Gimbal
{ Event::GimbalConnected, NoUserInput, 5000, "Solo Gimbal connected", NULL },
{ Event::GimbalNotConnected, NoUserInput, 5000, "Solo Gimbal not found", NULL },
/// Pre-flight - GoPro
/// In-flight - RC
/// In-flight - Flight battery
{ Event::FlightBatteryLow, NoUserInput, 5000, "Flight battery at 25%", NULL },
// XXX banner not shown
{ Event::FlightBatteryCritical, NoUserInput, 5000, "Return home soon", "Flight battery low" },
{ Event::FlightBatteryFailsafe, NoUserInput, UiAlertManager::NO_TIMEOUT, "Flight battery critically low", NULL },
/// In-flight - Sensors
/// In-flight - Flight modes
{ Event::ShotInfoUpdated, NoUserInput, 5000, "Switching to ", NULL },
{ Event::ShotInfoUpdateFail, NoUserInput, 5000, "Flight mode not available", NULL },
{ Event::RTLWithoutGPS, NoUserInput, 5000, "Cannot return to home", "without GPS" },
/// In-flight - Artoo
{ Event::SoloConnectionPoor, NoUserInput, 5000, "Poor connection to Solo", NULL },
{ Event::ControllerBatteryCritical, NoUserInput, UiAlertManager::NO_TIMEOUT, "Controller battery low", "Please land soon" },
// Unused
//{ Event::ControllerBatteryFailsafe, NoUserInput, UiAlertManager::NO_TIMEOUT, "Controller battery at 0%", "Auto-landing engaged" },
/// Misc
{ Event::LandingComplete, NoUserInput, 5000, "Landing complete", NULL },
// RC Channels
{ Event::CH7low, NoUserInput, 5000, "CH-7 Off/Low", NULL },
{ Event::CH7high, NoUserInput, 5000, "CH-7 On/High", NULL },
{ Event::CH8low, NoUserInput, 5000, "CH-8 Off/Low", NULL },
{ Event::CH8high, NoUserInput, 5000, "CH-8 On/High", NULL },
/// Testing
{ Event::TestAlert, NoUserInput, 5000, "RTL not available without GPS", "(Test test)" },
//{ Event::WaitingForGps, NoUserInput, 5000, "Waiting for GPS", "Solo requires clear view of the sky" },
};
UiBottomBox::UiBottomBox() :
alertFunctionsDirty(true),
buttonFunctionsDirty(true),
gimbalFunctionsDirty(true),
persistFunctionsDirty(true),
event(Event::None),
alertStateTransition(0),
alertWaitDurationMillis(0),
alertNotify(false),
displayState(DisplayNone),
defaultDrawFunction(DrawNothing),
activeDrawFunction(DrawNothing)
{
}
void UiBottomBox::init(DisplayState state)
{
displayState = state;
setDefaultDisplay();
dirtyDisplay();
}
void UiBottomBox::update()
{
/*
* Either show the current alert in the bottom box,
* or draw the button functions.
*
* Drawing alerts is higher priority.
*/
// don't start timer until alert is first shown
if (alertNotify) {
dirtyDisplay();
updateAlertParams();
alertNotify = false;
}
if (activeDrawFunction != DrawNothing) {
drawFunction(activeDrawFunction);
}
// check state again in case active drawing function timed out
if (activeDrawFunction == DrawNothing) {
drawFunction(defaultDrawFunction);
}
}
void UiBottomBox::onEvent(Event::ID id) {
// already showing this alert? nothing to do
if (event == id) {
return;
}
event = id;
alertNotify = true;
}
void UiBottomBox::updateAlertParams()
{
if (event != Event::None) {
activeDrawFunction = DrawAlert;
alertFunctionsDirty = true;
alertStateTransition = SysTime::now();
const Msg &msg = findMsg(event);
ASSERT (isValid(msg));
alertWaitDurationMillis = msg.durationMillis;
} else {
activeDrawFunction = DrawNothing;
}
}
void UiBottomBox::clear(uint16_t color)
{
Gfx::fillRect(Gfx::Rect(0, Ui::HintBoxY, Gfx::WIDTH, Gfx::HEIGHT - Ui::HintBoxY), color);
}
const UiBottomBox::Msg & UiBottomBox::findMsg(Event::ID id)
{
for (unsigned i = 0; i < arraysize(msgs); ++i) {
if (msgs[i].event == id)
return msgs[i];
}
return msgs[MSG_INVALID];
}
void UiBottomBox::drawFunction(DrawFunction fn)
{
switch (fn) { // TODO: this could potentially replace the idle() state
case DrawABButtons:
if (buttonFunctionsDirty) {
drawButtons();
buttonFunctionsDirty = false;
}
break;
case DrawGimbalPresets:
if (gimbalFunctionsDirty) {
drawHintBoxAndCamera();
gimbalFunctionsDirty = false;
}
break;
case DrawGimbalNotConnected:
if (gimbalFunctionsDirty) {
drawGimbalNotConnected();
gimbalFunctionsDirty = false;
}
break;
case DrawAlert:
// check to see if our alert finished
if (alertComplete()) {
//event = Event::None; // TODO: might be redundant
Ui::instance.alertManager.dismiss();
setDefaultDisplay();
switchToDefaultDisplay();
return;
}
if (alertFunctionsDirty) {
drawBottomBox();
alertFunctionsDirty = false;
}
break;
case DrawPersistentAlert:
// TODO: still need to implement
break;
case DrawInvalidFlightControls:
if (persistFunctionsDirty) {
drawPersistentAlert(Event::ControllerValueOutOfRange);
persistFunctionsDirty = false;
}
break;
case DrawInvalidCameraControls:
if (persistFunctionsDirty) {
drawPersistentAlert(Event::CamControlValueOutOfRange);
persistFunctionsDirty = false;
}
break;
default:
// do nothing
break;
}
}
void UiBottomBox::setDefaultDisplay()
{
switch (displayState) {
case DisplayArming:
case DisplayTelem:
if (Inputs::isFlightControlValid()) {
defaultDrawFunction = DrawABButtons;
} else {
defaultDrawFunction = DrawInvalidFlightControls;
}
break;
case DisplayGimbal:
if (Inputs::isCameraControlValid()) {
if (SoloGimbal::instance.isConnected()) {
defaultDrawFunction = DrawGimbalPresets;
} else {
defaultDrawFunction = DrawGimbalNotConnected;
}
} else {
defaultDrawFunction = DrawInvalidCameraControls;
}
break;
default:
defaultDrawFunction = DrawNothing;
break;
}
}
void UiBottomBox::dirtyDisplay()
{
if (activeDrawFunction != DrawNothing) {
alertFunctionsDirty = true;
return;
}
switch (displayState) {
case DisplayArming:
case DisplayTelem:
buttonFunctionsDirty = true;
persistFunctionsDirty = true;
break;
case DisplayGimbal:
gimbalFunctionsDirty = true;
persistFunctionsDirty = true;
break;
default:
break;
}
}
void UiBottomBox::switchToDefaultDisplay()
{
activeDrawFunction = DrawNothing;
dirtyDisplay();
}
void UiBottomBox::defaultDraw()
{
switch (defaultDrawFunction) {
case DrawABButtons:
drawButtons();
break;
case DrawGimbalPresets:
drawHintBoxAndCamera();
break;
case DrawGimbalNotConnected:
drawGimbalNotConnected();
break;
case DrawPersistentAlert:
// TODO: still need to implement
break;
default:
break;
}
}
void UiBottomBox::drawBottomBox()
{
if (!Ui::instance.alertManager.hasAlert()) {
return;
}
const UiAlertManager::Alert & alert = Ui::instance.alertManager.currentAlert();
const Msg & msg = findMsg(event);
drawAlertMsg(alert,msg);
}
void UiBottomBox::drawPersistentAlert(Event::ID id)
{
const UiAlertManager::Alert & alert = Ui::instance.alertManager.getAlert(id);
const Msg & msg = findMsg(id);
drawAlertMsg(alert,msg);
}
void UiBottomBox::drawGimbalNotConnected()
{
const Event::ID id = Event::GimbalNotConnected;
const UiAlertManager::Alert & alert = Ui::instance.alertManager.getAlert(id);
const Msg & msg = findMsg(id);
drawAlertMsg(alert,msg);
}
void UiBottomBox::drawAlertMsg(const UiAlertManager::Alert & alert, const Msg & msg)
{
ASSERT(isValid(msg));
drawBannerMsg(msg.content_line1, msg.content_line2, alert.severity);
// Check if custom message, if message is defined here then a custom behaviour will be performed, otherwise a default behaviour will be followed
#if 0 // No custom messages at this time
switch (msg.event) {
// Alert no longer used
case Event::ShotInfoUpdated:{
char strbuf[64];
sprintf(strbuf, "Switching to %s", Ui::instance.telem.currentShotName());
drawBannerMsg(strbuf, msg.content_line2, alert.severity);
}break;
// Alert no longer used
case Event::ShotInfoUpdateFail:{
char strbuf[64];
sprintf(strbuf, "%s not available", Ui::instance.telem.failShotName());
drawBannerMsg(strbuf, msg.content_line2, alert.severity);
}break;
// Display in a standard message format otherwise
default:
drawBannerMsg(msg.content_line1, msg.content_line2, alert.severity);
break;
}
#endif
}
void UiBottomBox::drawBannerMsg(const char* content_line1, const char* content_line2, const UiAlertManager::Severity severity)
{
switch (severity) {
// Display standard banner notification
case UiAlertManager::White:
drawBannerMsg(content_line1, content_line2, UiColor::Black, HelveticaNeueLTProRoman);
break;
// Display standard banner confirmation
case UiAlertManager::Green:
drawBannerMsg(content_line1, content_line2, UiColor::Green, HelveticaNeueLTProLightWhiteOnBlack);
break;
// Display standard banner warning
case UiAlertManager::Orange:
drawBannerMsg(content_line1, content_line2, UiColor::Orange, HelveticaNeueLTProLightWhiteOnBlack);
break;
// Display standard banner alert
case UiAlertManager::Red:
drawBannerMsg(content_line1, content_line2, UiColor::Red, HelveticaNeueLTProLightWhiteOnBlack);
break;
default:
drawBannerMsg(content_line1, content_line2, UiColor::Black, HelveticaNeueLTProRoman);
break;
}
}
void UiBottomBox::drawBannerMsg(const char* content_line1, const char* content_line2, const uint16_t color, const Gfx::FontAsset &font)
{
clear(color); // draw background with appropriate background color
if (color == UiColor::Black){
// draw border
Gfx::fillRect(Gfx::Rect(20, Ui::HintBoxY, Gfx::WIDTH - 40, Ui::HintBoxBorderWeight), UiColor::Gray);
}
uint16_t color_fg;
uint16_t color_bg;
// determine foreground and background colors
if (color == UiColor::White) {
color_fg = UiColor::White;
color_bg = UiColor::Black;
} else if (color == UiColor::Green) {
color_fg = UiColor::Black;
color_bg = UiColor::Green;
} else if (color == UiColor::Orange) {
color_fg = UiColor::Black;
color_bg = UiColor::Orange;
} else if (color == UiColor::Red) {
color_fg = UiColor::Black;
color_bg = UiColor::Red;
} else {
color_fg = UiColor::White;
color_bg = UiColor::Black;
}
if (color == UiColor::White) {
// don't use writeCanvasCenterJustified multi-line variant to preserve custom spacing
if (!content_line2) {
Gfx::writeCanvasCenterJustified(Ui::HintBoxYLine1of1, content_line1, font, NULL, NULL);
} else {
Gfx::writeCanvasCenterJustified(Ui::HintBoxYLine1of2, content_line1, font, NULL, NULL);
Gfx::writeCanvasCenterJustified(Ui::HintBoxYLine2of2, content_line2, font, NULL, NULL);
}
} else {
if (!content_line2) {
Gfx::writeCanvasCenterJustified(Ui::HintBoxYLine1of1, content_line1, font, &color_fg, &color_bg);
} else {
Gfx::writeCanvasCenterJustified(Ui::HintBoxYLine1of2, content_line1, font, &color_fg, &color_bg);
Gfx::writeCanvasCenterJustified(Ui::HintBoxYLine2of2, content_line2, font, &color_fg, &color_bg);
}
}
}
void UiBottomBox::drawButtons()
{
/*
* Draw the lower region of this view.
*
* Normally shows what the mapping of the A/B buttons is,
* but can also display notifications.
*/
clear();
// draw border
Gfx::fillRect(Gfx::Rect(20, Ui::HintBoxY, Gfx::WIDTH - 40, Ui::HintBoxBorderWeight), UiColor::Gray);
static const unsigned buttonY = 208;
drawButtonFunction(27, buttonY-5, 57, buttonY, Io::ButtonA);
drawButtonFunction(168, buttonY-5, 198, buttonY, Io::ButtonB);
}
void UiBottomBox::drawHintBoxAndCamera()
{
const Gfx::FontAsset & f = HelveticaNeueLTProRoman;
unsigned y = Ui::HintBoxY;
Gfx::fillRect(Gfx::Rect(0, y, Gfx::WIDTH, Gfx::HEIGHT - y), 0x0);
Gfx::fillRect(Gfx::Rect(20, y, Gfx::WIDTH - 40, 1), UiColor::Gray);
Gfx::write(20, 202, "Hold", f);
Gfx::drawImage(61, 200, Icon_1_Btn_OnBlack);
Gfx::write(102, 202, "or", f);
Gfx::drawImage(125, 200, Icon_2_Btn_OnBlack);
Gfx::write(166, 202, "to save new preset", f);
}
const Gfx::ImageAsset & UiBottomBox::btnImgForConfig(Io::ButtonID id, const ButtonFunction::Config & cfg)
{
/*
* return the appropriate button icon for the given button,
* given its state in cfg.
*/
struct ButtonFuncImages {
const Gfx::ImageAsset & highlighted;
const Gfx::ImageAsset & enabled;
const Gfx::ImageAsset & disabled;
} static const btnImgs[2] = {
{
Icon_New_A_Btn_On,
Icon_New_A_Btn,
Icon_New_A_Btn_Dis,
},
{
Icon_New_B_Btn_On,
Icon_New_B_Btn,
Icon_New_B_Btn_Dis,
},
};
ASSERT(id == Io::ButtonA || id == Io::ButtonB);
const ButtonFuncImages & bfi = btnImgs[id - Io::ButtonA];
if (cfg.hilighted()) {
return bfi.highlighted;
}
if (cfg.enabled()) {
return bfi.enabled;
} else {
return bfi.disabled;
}
}
void UiBottomBox::drawButtonFunction(unsigned imgX, unsigned imgY,
unsigned strX, unsigned strY,
Io::ButtonID id)
{
const ButtonFunction::Config & cfg = ButtonFunction::get(id);
Gfx::drawImage(imgX, imgY, btnImgForConfig(id, cfg));
if (cfg.enabled()) {
Gfx::write(strX, strY, cfg.descriptor, HelveticaNeueLTProRoman);
} else {
Gfx::write(strX, strY, cfg.descriptor, HelveticaNeueLTProRoman16DarkGrey);
}
}
bool UiBottomBox::alertComplete() const
{
/*
* Is this alert done?
* We may have been dismissed, or may have timed out.
*/
if (alertStateTransition == 0 || event == Event::None) {
return true;
}
if (alertWaitDurationMillis == UiAlertManager::NO_TIMEOUT) {
return false;
}
return SysTime::now() - alertStateTransition > SysTime::msTicks(alertWaitDurationMillis);
}