Skip to content

Commit

Permalink
feat(docs): Add power profiler
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicell committed Jan 11, 2021
1 parent 0c6686f commit df28e38
Show file tree
Hide file tree
Showing 8 changed files with 968 additions and 2 deletions.
7 changes: 6 additions & 1 deletion docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ module.exports = {
},
items: [
{
to: "docs/",
to: "docs",
activeBasePath: "docs",
label: "Docs",
position: "left",
},
{ to: "blog", label: "Blog", position: "left" },
{
to: "power-profiler",
label: "Power Profiler",
position: "left",
},
{
href: "https://github.com/zmkfirmware/zmk",
label: "GitHub",
Expand Down
242 changes: 242 additions & 0 deletions docs/src/components/power-estimate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import React from "react";
import PropTypes from "prop-types";
import { displayPower, underglowPower, zmkBase } from "../data/power";
import "../css/power-estimate.css";

const lithiumIonMonthlyDischargePercent = 5;
const lithiumIonAverageVoltage = 3.8;
const lithiumIonDischargeEfficiency = 0.85;
const lithiumIonDischargeEffiecincyRange = 0.05;

const timeSpentTyping = 0.02;

const measurementAccuracy = 0.2; // Nordic power profiler kit accuracy

const batVolt = lithiumIonAverageVoltage;

const palette = [
"#bbdefb",
"#90caf9",
"#64b5f6",
"#42a5f5",
"#2196f3",
"#1e88e5",
"#1976d2",
];

function formatUsage(usage) {
if (usage > 1000) {
return (usage / 1000).toFixed(1) + "mW";
}

return Math.round(usage) + "µW";
}

function voltageEquivalentCalc(powerSupply) {
if (powerSupply.type === "LDO") {
return batVolt;
} else if (powerSupply.type === "SWITCHING") {
return powerSupply.outputVoltage / powerSupply.efficiency;
}
}

function formatMinutes(minutes, precision, floor) {
let message = "";
let count = 0;

let units = ["year", "month", "week", "day", "minute"];
let multiples = [60 * 24 * 365, 60 * 24 * 30, 60 * 24 * 7, 60 * 24, 60, 1];

for (let i = 0; i < units.length; i++) {
if (minutes >= multiples[i]) {
const timeCount = floor
? Math.floor(minutes / multiples[i])
: Math.ceil(minutes / multiples[i]);
minutes -= timeCount * multiples[i];
count++;
message +=
timeCount + (timeCount > 1 ? ` ${units[i]}s ` : ` ${units[i]} `);
}

if (count == precision) return message;
}

return message || "0 minutes";
}

function PowerEstimate({
board,
splitType,
batterymAh,
usage,
underglow,
display,
}) {
if (!board || !board.powerSupply.type || !batterymAh) {
return (
<div className="powerEstimate">
<h3>... {splitType !== "standalone" ? "on " + splitType : ""}</h3>
<div className="powerEstimateBar">
<div
className="powerEstimateBarEl"
style={{
width: "100%",
background: "#e0e0e0",
mixBlendMode: "overlay",
}}
></div>
</div>
</div>
);
}

const powerUsage = [];
let totalUsage = 0;

const voltageEquivalent = voltageEquivalentCalc(board.powerSupply);

// Lithium ion self discharge
const lithiumDischarge =
((parseInt(batterymAh) * 1000 * lithiumIonMonthlyDischargePercent) /
100 /
30 /
24) *
batVolt;
totalUsage += lithiumDischarge;

powerUsage.push({
title: "Battery Self Discharge",
usage: lithiumDischarge,
});

// Quiescent current
const quiescentTotal =
(parseInt(board.powerSupply.quiescent) + parseInt(board.otherQuiescent)) *
voltageEquivalent;
totalUsage += quiescentTotal;

powerUsage.push({
title: "Board Quiescent Usage",
usage: quiescentTotal,
});

// ZMK overall usage
const zmkUsage =
(zmkBase[splitType].idle +
(splitType !== "peripheral"
? zmkBase.hostConnection * usage.bondedQty
: 0)) *
voltageEquivalent *
(1 - usage.percentAsleep);
totalUsage += zmkUsage;

powerUsage.push({
title: "ZMK Base Usage",
usage: zmkUsage,
});

// ZMK typing usage
const zmkTyping =
zmkBase[splitType].typing *
timeSpentTyping *
voltageEquivalent *
(1 - usage.percentAsleep);
totalUsage += zmkTyping;

powerUsage.push({
title: "ZMK Typing Usage",
usage: zmkTyping,
});

if (underglow.glowEnabled) {
const underglowUsage =
(underglowPower.firmware +
underglow.glowQuantity *
(underglow.glowBrightness *
(underglowPower.ledOn - underglowPower.ledOff) +
underglowPower.ledOff)) *
voltageEquivalent *
(1 - usage.percentAsleep);
totalUsage += underglowUsage;

powerUsage.push({
title: "RGB Underglow",
usage: underglowUsage,
});
}

if (display.displayEnabled && display.displayType) {
const { activePercent, active, sleep } = displayPower[display.displayType];
const displayUsage =
(active * activePercent + sleep * (1 - activePercent)) *
voltageEquivalent *
(1 - usage.percentAsleep);
totalUsage += displayUsage;

powerUsage.push({
title: "Display",
usage: displayUsage,
});
}

const estimatedMinutes = Math.round(
((batterymAh * batVolt * lithiumIonDischargeEfficiency * 1000) /
totalUsage) *
60
);

const estimatedRange =
estimatedMinutes -
Math.round(
((batterymAh *
batVolt *
(lithiumIonDischargeEfficiency - lithiumIonDischargeEffiecincyRange) *
1000) /
(totalUsage * (1 + measurementAccuracy))) *
60
);

return (
<div className="powerEstimate">
<h3>
{formatMinutes(estimatedMinutes, 2, true)}{" "}
{splitType !== "standalone" ? "on " + splitType : ""} ±
{formatMinutes(estimatedRange, 1, false)}
</h3>
<div className="powerEstimateBar">
{powerUsage.map((p, i) => (
<div
key={p.title}
className={"powerEstimateBarEl" + (i > 1 ? " rightEl" : "")}
style={{
width: (p.usage / totalUsage) * 100 + "%",
background: palette[i],
}}
>
<div className="powerEstimateTooltipWrap">
<div className="powerEstimateTooltip">
<div>
{p.title} - {Math.round((p.usage / totalUsage) * 100) + "%"}
</div>
<div style={{ fontSize: "14px" }}>
~{formatUsage(p.usage)} estimated avg. consumption
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}

PowerEstimate.propTypes = {
board: PropTypes.Object,
splitType: PropTypes.string,
batterymAh: PropTypes.number,
usage: PropTypes.Object,
underglow: PropTypes.Object,
display: PropTypes.Object,
};

export default PowerEstimate;
71 changes: 71 additions & 0 deletions docs/src/css/power-estimate.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
.powerEstimate {
margin: 20px 0;
}

.powerEstimateBar {
height: 64px;
width: 100%;
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
border-radius: 64px;
display: flex;
justify-content: flex-start;
overflow: hidden;
}

.powerEstimateBarEl {
transition: all 0.2s ease;
flex-grow: 1;
}

.powerEstimateBarEl.rightEl {
display: flex;
justify-content: flex-end;
}

.powerEstimateTooltipWrap {
position: absolute;
visibility: hidden;
opacity: 0;
transform: translateY(calc(-100% - 8px));
transition: opacity 0.2s ease;
}

.powerEstimateBarEl:hover .powerEstimateTooltipWrap {
visibility: visible;
opacity: 1;
}

.powerEstimateTooltip {
display: block;
position: relative;
box-shadow: var(--ifm-global-shadow-tl);
width: 260px;
padding: 10px;
border-radius: 4px;
background: var(--ifm-background-surface-color);
transform: translateX(-15px);
}

.rightEl .powerEstimateTooltip {
transform: translateX(15px);
}

.powerEstimateTooltip:after {
content: "";
position: absolute;
top: 100%;
left: 27px;
margin-left: -8px;
width: 0;
height: 0;
border-top: 8px solid var(--ifm-background-surface-color);
border-right: 8px solid transparent;
border-left: 8px solid transparent;
}

.rightEl .powerEstimateTooltip:after {
left: unset;
right: 27px;
margin-right: -8px;
}
Loading

0 comments on commit df28e38

Please sign in to comment.