Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(docs): Add power profiler #312

Merged
merged 1 commit into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ module.exports = {
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
100 changes: 100 additions & 0 deletions docs/src/components/custom-board-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

import React from "react";
import PropTypes from "prop-types";

function CustomBoardForm({
bindPsuType,
bindOutputV,
bindEfficiency,
bindQuiescentMicroA,
bindOtherQuiescentMicroA,
}) {
return (
<div className="profilerSection">
<h3>Custom Board</h3>
<div className="row">
<div className="col col--4">
<div className="profilerInput">
<label>Power Supply Type</label>
<select {...bindPsuType}>
<option hidden value="">
Select a PSU type
</option>
<option value="LDO">LDO</option>
<option value="SWITCHING">Switching</option>
</select>
</div>
</div>
<div className="col col--4">
<div className="profilerInput">
<label>
Output Voltage{" "}
<span tooltip="Output Voltage of the PSU used by the system">
</span>
</label>
<input {...bindOutputV} type="range" min="1.8" step=".1" max="5" />
<span>{parseFloat(bindOutputV.value).toFixed(1)}V</span>
</div>
{bindPsuType.value === "SWITCHING" && (
<div className="profilerInput">
<label>
PSU Efficiency{" "}
<span tooltip="The estimated efficiency with a VIN of 3.8 and the output voltage entered above">
</span>
</label>
<input
{...bindEfficiency}
type="range"
min=".50"
step=".01"
max="1"
/>
<span>{Math.round(bindEfficiency.value * 100)}%</span>
</div>
)}
</div>
<div className="col col--4">
<div className="profilerInput">
<label>
PSU Quiescent{" "}
<span tooltip="The standby usage of the PSU">ⓘ</span>
</label>
<div className="inputBox">
<input {...bindQuiescentMicroA} type="number" />
<span>µA</span>
</div>
</div>
<div className="profilerInput">
<label>
Other Quiescent{" "}
<span tooltip="Any other standby usage of the board (voltage dividers, extra ICs, etc)">
</span>
</label>
<div className="inputBox">
<input {...bindOtherQuiescentMicroA} type="number" />
<span>µA</span>
</div>
</div>
</div>
</div>
</div>
);
}

CustomBoardForm.propTypes = {
bindPsuType: PropTypes.Object,
bindOutputV: PropTypes.Object,
bindEfficiency: PropTypes.Object,
bindQuiescentMicroA: PropTypes.Object,
bindOtherQuiescentMicroA: PropTypes.Object,
};

export default CustomBoardForm;
266 changes: 266 additions & 0 deletions docs/src/components/power-estimate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

import React from "react";
import PropTypes from "prop-types";
import { displayPower, underglowPower, zmkBase } from "../data/power";
import "../css/power-estimate.css";

// Average monthly discharge percent
const lithiumIonMonthlyDischargePercent = 5;
// Average voltage of a lithium ion battery based of discharge graphs
const lithiumIonAverageVoltage = 3.8;
// Average discharge efficiency of li-ion https://en.wikipedia.org/wiki/Lithium-ion_battery
const lithiumIonDischargeEfficiency = 0.85;
// Range of the discharge efficiency
const lithiumIonDischargeEfficiencyRange = 0.05;

// Proportion of time spent typing (keys being pressed down and scanning). Estimated to 2%.
const timeSpentTyping = 0.02;
Nicell marked this conversation as resolved.
Show resolved Hide resolved

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

const batVolt = lithiumIonAverageVoltage;

const palette = [
"#bbdefb",
"#90caf9",
"#64b5f6",
"#42a5f5",
"#2196f3",
"#1e88e5",
"#1976d2",
];
Comment on lines +29 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be in the styling as classes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to how they're used, I feel they're better this way. Using them as classes would be pretty cumbersome


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

return Math.round(microWatts) + "µ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", "hour", "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,
batteryMilliAh,
usage,
underglow,
display,
}) {
if (!board || !board.powerSupply.type || !batteryMilliAh) {
return (
<div className="powerEstimate">
<h3>
<span>{splitType !== "standalone" ? splitType + ": " : " "}...</span>
</h3>
<div className="powerEstimateBar">
<div
className="powerEstimateBarSection"
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 lithiumMonthlyDischargemAh =
parseInt(batteryMilliAh) * (lithiumIonMonthlyDischargePercent / 100);
const lithiumDischargeMicroA = (lithiumMonthlyDischargemAh * 1000) / 30 / 24;
const lithiumDischargeMicroW = lithiumDischargeMicroA * batVolt;

totalUsage += lithiumDischargeMicroW;
powerUsage.push({
title: "Battery Self Discharge",
usage: lithiumDischargeMicroW,
});

// Quiescent current
const quiescentMicroATotal =
parseInt(board.powerSupply.quiescentMicroA) +
parseInt(board.otherQuiescentMicroA);
const quiescentMicroW = quiescentMicroATotal * voltageEquivalent;

totalUsage += quiescentMicroW;
powerUsage.push({
title: "Board Quiescent Usage",
usage: quiescentMicroW,
});

// ZMK overall usage
const zmkMicroA =
zmkBase[splitType].idle +
(splitType !== "peripheral" ? zmkBase.hostConnection * usage.bondedQty : 0);

const zmkMicroW = zmkMicroA * voltageEquivalent;
const zmkUsage = zmkMicroW * (1 - usage.percentAsleep);

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

// ZMK typing usage
const zmkTypingMicroA = zmkBase[splitType].typing * timeSpentTyping;

const zmkTypingMicroW = zmkTypingMicroA * voltageEquivalent;
const zmkTypingUsage = zmkTypingMicroW * (1 - usage.percentAsleep);

totalUsage += zmkTypingUsage;
powerUsage.push({
title: "ZMK Typing Usage",
usage: zmkTypingUsage,
});

if (underglow.glowEnabled) {
const underglowAverageLedMicroA =
underglow.glowBrightness *
(underglowPower.ledOn - underglowPower.ledOff) +
underglowPower.ledOff;

const underglowMicroA =
underglowPower.firmware +
underglow.glowQuantity * underglowAverageLedMicroA;

const underglowMicroW = underglowMicroA * voltageEquivalent;

const underglowUsage = underglowMicroW * (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 displayMicroA = active * activePercent + sleep * (1 - activePercent);
const displayMicroW = displayMicroA * voltageEquivalent;
const displayUsage = displayMicroW * (1 - usage.percentAsleep);

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

// Calculate the average minutes of use
const estimatedAvgEffectiveMicroWH =
batteryMilliAh * batVolt * lithiumIonDischargeEfficiency * 1000;

const estimatedAvgMinutes = Math.round(
(estimatedAvgEffectiveMicroWH / totalUsage) * 60
);

// Calculate worst case for battery life
const worstLithiumIonDischargeEfficiency =
lithiumIonDischargeEfficiency - lithiumIonDischargeEfficiencyRange;

const estimatedWorstEffectiveMicroWH =
batteryMilliAh * batVolt * worstLithiumIonDischargeEfficiency * 1000;

const highestTotalUsage = totalUsage * (1 + measurementAccuracy);

const estimatedWorstMinutes = Math.round(
(estimatedWorstEffectiveMicroWH / highestTotalUsage) * 60
);

// Calculate range (+-) of minutes using average - worst
const estimatedRange = estimatedAvgMinutes - estimatedWorstMinutes;

return (
<div className="powerEstimate">
<h3>
<span>{splitType !== "standalone" ? splitType + ": " : " "}</span>
{formatMinutes(estimatedAvgMinutes, 2, true)} (±
{formatMinutes(estimatedRange, 1, false).trim()})
</h3>
<div className="powerEstimateBar">
{powerUsage.map((p, i) => (
<div
key={p.title}
className={
"powerEstimateBarSection" + (i > 1 ? " rightSection" : "")
}
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: ".875rem" }}>
~{formatUsage(p.usage)} estimated avg. consumption
</div>
</div>
</div>
</div>
))}
</div>
</div>
);
}

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

export default PowerEstimate;
Loading