Skip to content

Commit 5f6aab2

Browse files
committed
Add battery cell voltage widget
1 parent 4d8fc60 commit 5f6aab2

File tree

3 files changed

+182
-2
lines changed

3 files changed

+182
-2
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ Specific widgets expect quite concrete facts as input:
331331
Uses `video.decode_and_handover_ms` fact
332332
* `GPSWidget` - displays GPS fix type (no fix / 2D fix / 3D fix etc) and GPS coordinates.
333333
Uses `mavlink.gps_raw.fix_type`, `mavlink.gps_raw.lat` and `mavlink.gps_raw.lon` facts
334+
* `{"type": "BatteryCellWidget", "template": "%.2fV", "critical_voltage": 3.5, "max_voltage": 4.2, "num_cells": "even"}` -
335+
displays the individual cell voltage; the number of cells can be provided explicitly or auto-detected
336+
from `max_voltage` and current pack voltage. Text changes color to yellow (20%) transitioning to
337+
red (0%) when voltage approaches the `critical_voltage`. Change `critical_voltage` to ~3 for LiIon
338+
and 3.5 for LiPo. This widget takes millivolts and is designed to work with `os_mon.power.voltage`,
339+
but can probably be used with Mavlink as well.
334340
* `{"type": "IconSelectorWidget", "ranges_and_icons": [{"range": [0, 10], "icon_path": "0_10.png"}, {"range": [11, 20], ...}]}` - shows
335341
different icon depending on the range where the value lands to.
336342

os_monitor_demo_osd.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@
4646
{"name": "os_mon.power.power",
4747
"convert": "x / 1000000"}
4848
]
49+
},
50+
{
51+
"type": "BatteryCellWidget",
52+
"name": "Individual battery cell voltage",
53+
"x": 350,
54+
"y": 90,
55+
"template": "(cell: %.2fV)",
56+
"__comment1": "for LiPo set critical to 3.5; for LiIon critical to ~2.8-3",
57+
"critical_voltage": 3.5,
58+
"max_voltage": 4.2,
59+
"__comment2": "num_cells can be 'auto' - to be autodetected, 'even' - autodetect even number (2/4/6/8..) or the actual number of cells eg 4 or 6 etc",
60+
"num_cells": "even",
61+
"facts": [
62+
{"name": "os_mon.power.voltage"}
63+
]
4964
}
5065
]
5166
}

src/osd.cpp

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* displays a surface that is provided via shm by external program. Right now it is used to display
1313
* MSP/Displayport OSD.
1414
*/
15+
#include <cmath>
1516
extern "C" {
1617
#include "drm.h"
1718
#include "mavlink.h"
@@ -344,6 +345,60 @@ class Fact {
344345
return type != T_UNDEF;
345346
}
346347

348+
operator bool() {
349+
switch (type) {
350+
case T_BOOL:
351+
return getBoolValue();
352+
case T_UINT:
353+
return getUintValue() != 0;
354+
case T_INT:
355+
return getIntValue() != 0;
356+
case T_DOUBLE:
357+
return getDoubleValue() != 0.0;
358+
case T_STRING:
359+
return getStrValue() != "";
360+
}
361+
}
362+
363+
operator long() {
364+
switch (type) {
365+
case T_BOOL:
366+
return getBoolValue() ? 1 : 0;
367+
case T_UINT:
368+
return (long)getUintValue();
369+
case T_INT:
370+
return getIntValue();
371+
case T_DOUBLE:
372+
return round(getDoubleValue());
373+
}
374+
}
375+
376+
operator ulong() {
377+
switch (type) {
378+
case T_BOOL:
379+
return getBoolValue() ? 1 : 0;
380+
case T_UINT:
381+
return getUintValue();
382+
case T_INT:
383+
return (ulong)getIntValue();
384+
case T_DOUBLE:
385+
return round(getDoubleValue());
386+
}
387+
}
388+
389+
operator double() {
390+
switch (type) {
391+
case T_BOOL:
392+
return getBoolValue() ? 1.0 : 0.0;
393+
case T_UINT:
394+
return getUintValue() * 1.0;
395+
case T_INT:
396+
return getIntValue() * 1.0;
397+
case T_DOUBLE:
398+
return getDoubleValue();
399+
}
400+
}
401+
347402
// TODO: try to cast instead of crash
348403
bool getBoolValue() const {
349404
assertType(T_BOOL);
@@ -1229,6 +1284,85 @@ class GPSWidget: public Widget {
12291284
}
12301285
};
12311286

1287+
/**
1288+
* Widget that shows approximate voltage of a battery cell.
1289+
* If the number of cells is 0, it estimates it from the pack voltage based on max_voltage_mv;
1290+
* If the number of cells is -1, it estimates only even cell numbers (2, 4, 6, 8, ...) - this
1291+
* fixes the situation when, eg, discharged to 20v 6s LiIon would be recognized as 5s.
1292+
*
1293+
* Widget's text is drawn in white when battery is above 20% from critical. And below 20% it
1294+
* gradually transitions from yellow through orange to red.
1295+
*/
1296+
class BatteryCellWidget: public TplTextWidget {
1297+
public:
1298+
float warn_percentage = 0.2;
1299+
1300+
BatteryCellWidget(int pos_x, int pos_y,
1301+
int critical_voltage_mv, int max_voltage_mv, int num_cells,
1302+
std::string tpl, uint num_args) :
1303+
TplTextWidget(pos_x, pos_y, tpl, num_args), critical_voltage_mv(critical_voltage_mv),
1304+
max_voltage_mv(max_voltage_mv), num_cells(num_cells) {
1305+
assert(num_args == 1);
1306+
};
1307+
1308+
virtual void setFact(uint idx, Fact fact) {
1309+
assert(idx == 0);
1310+
// replace the pack value with per-cell value
1311+
long voltage_mv = fact.getIntValue();
1312+
int cells;
1313+
if (num_cells > 0) {
1314+
cells = num_cells;
1315+
} else if (num_cells == 0) {
1316+
// estimate any number of cells
1317+
cells = (voltage_mv / max_voltage_mv) + 1;
1318+
} else {
1319+
// estimate even number of cells
1320+
cells = (voltage_mv / max_voltage_mv) + 1;
1321+
if (cells % 2 != 0) {
1322+
cells++;
1323+
}
1324+
}
1325+
long cell_voltage_mv = voltage_mv / cells;
1326+
args[0] = Fact(FactMeta("volts"), (double)cell_voltage_mv / 1000.0);
1327+
}
1328+
1329+
1330+
virtual void draw(cairo_t *cr) {
1331+
auto [x, y] = xy(cr);
1332+
const Fact& fact = args[0];
1333+
auto cell_voltage = fact.getDoubleValue();
1334+
auto cell_voltage_mv = cell_voltage * 1000;
1335+
1336+
std::unique_ptr<std::string> msg = render_tpl();
1337+
1338+
if (cell_voltage_mv <= critical_voltage_mv) {
1339+
// Draw in red
1340+
cairo_set_source_rgba(cr, 255.0, 0, 0, 1);
1341+
} else {
1342+
// Now we know voltage is above critical
1343+
float remaining_percentage =
1344+
(float)(cell_voltage_mv - critical_voltage_mv) /
1345+
(max_voltage_mv - critical_voltage_mv);
1346+
1347+
if (remaining_percentage < warn_percentage) {
1348+
// Calculate green based on remaining percentage (0--warn_percentage% range)
1349+
double green_value = 255.0 * (remaining_percentage / warn_percentage);
1350+
// Transition from yellow through orange to red
1351+
cairo_set_source_rgba(cr, 255.0, green_value, 0, 1);
1352+
} else {
1353+
// White when above 20%
1354+
cairo_set_source_rgba(cr, 255.0, 255.0, 255.0, 1);
1355+
}
1356+
}
1357+
cairo_move_to(cr, x, y);
1358+
cairo_show_text(cr, msg->c_str());
1359+
}
1360+
protected:
1361+
int critical_voltage_mv;
1362+
int max_voltage_mv;
1363+
int num_cells;
1364+
};
1365+
12321366
class DebugWidget: public Widget {
12331367
public:
12341368
DebugWidget(int pos_x, int pos_y, uint num_args) :
@@ -1570,11 +1704,36 @@ class Osd {
15701704
matchers);
15711705
} else if (type == "GPSWidget") {
15721706
addWidget(new GPSWidget(x, y, (uint)matchers.size()), matchers);
1573-
} else if(type == "PopupWidget") {
1707+
} else if (type == "BatteryCellWidget") {
1708+
int critical_mv = 3500;
1709+
int max_mv = 4200;
1710+
int num_cells = -1;
1711+
auto tpl = widget_j.at("template").template get<std::string>();
1712+
if (widget_j.contains("critical_voltage")) {
1713+
critical_mv = (int)(widget_j.at("critical_voltage").template get<float>() * 1000);
1714+
}
1715+
if (widget_j.contains("max_voltage")) {
1716+
max_mv = (int)(widget_j.at("max_voltage").template get<float>() * 1000);
1717+
}
1718+
if (widget_j.contains("num_cells")) {
1719+
std::string cells = widget_j["num_cells"];
1720+
if (cells == "auto") {
1721+
num_cells = 0;
1722+
} else if (cells == "even") {
1723+
num_cells = -1;
1724+
} else {
1725+
num_cells = widget_j["num_cells"].get<int>();
1726+
}
1727+
}
1728+
assert(critical_mv < max_mv);
1729+
addWidget(new BatteryCellWidget(x, y, critical_mv, max_mv, num_cells,
1730+
tpl, (uint)matchers.size()),
1731+
matchers);
1732+
} else if (type == "PopupWidget") {
15741733
auto timeout_ms = widget_j.at("timeout_ms").template get<uint>();
15751734
addWidget(new PopupWidget(x, y, timeout_ms, (uint)matchers.size()),
15761735
matchers);
1577-
} else if(type == "DebugWidget") {
1736+
} else if (type == "DebugWidget") {
15781737
addWidget(new DebugWidget(x, y, (uint)matchers.size()), matchers);
15791738
} else {
15801739
spdlog::warn("Widget '{}': unknown type: {}", name, type);

0 commit comments

Comments
 (0)