feat(docs): Add power profiler
This commit is contained in:
parent
0df7110058
commit
4ef11ac4aa
8 changed files with 1045 additions and 0 deletions
|
@ -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",
|
||||
|
|
100
docs/src/components/custom-board-form.js
Normal file
100
docs/src/components/custom-board-form.js
Normal file
|
@ -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
docs/src/components/power-estimate.js
Normal file
266
docs/src/components/power-estimate.js
Normal file
|
@ -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;
|
||||
|
||||
// Nordic power profiler kit accuracy
|
||||
const measurementAccuracy = 0.2;
|
||||
|
||||
const batVolt = lithiumIonAverageVoltage;
|
||||
|
||||
const palette = [
|
||||
"#bbdefb",
|
||||
"#90caf9",
|
||||
"#64b5f6",
|
||||
"#42a5f5",
|
||||
"#2196f3",
|
||||
"#1e88e5",
|
||||
"#1976d2",
|
||||
];
|
||||
|
||||
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;
|
81
docs/src/css/power-estimate.css
Normal file
81
docs/src/css/power-estimate.css
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
.powerEstimate {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.powerEstimate > h3 > span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.powerEstimateBarSection {
|
||||
transition: all 0.2s ease;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.powerEstimateBarSection.rightSection {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.powerEstimateTooltipWrap {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transform: translateY(calc(-100% - 8px));
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.powerEstimateBarSection: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);
|
||||
}
|
||||
|
||||
.rightSection .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;
|
||||
}
|
||||
|
||||
.rightSection .powerEstimateTooltip:after {
|
||||
left: unset;
|
||||
right: 27px;
|
||||
margin-right: -8px;
|
||||
}
|
195
docs/src/css/power-profiler.css
Normal file
195
docs/src/css/power-profiler.css
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
.profilerSection {
|
||||
margin: 10px 0;
|
||||
padding: 10px 20px;
|
||||
background: var(--ifm-background-surface-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
|
||||
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
|
||||
}
|
||||
|
||||
.profilerInput {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.profilerInput label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profilerDisclaimer {
|
||||
padding: 20px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span[tooltip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
span[tooltip]::before {
|
||||
content: attr(tooltip);
|
||||
font-size: 13px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
width: 220px;
|
||||
border-radius: 4px;
|
||||
background: var(--ifm-background-surface-color);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
|
||||
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
|
||||
transition: opacity 0.2s ease;
|
||||
transform: translate(-50%, -100%);
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
span[tooltip]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-top: 8px solid var(--ifm-background-surface-color);
|
||||
border-right: 8px solid transparent;
|
||||
border-left: 8px solid transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease;
|
||||
transform: translateX(-50%);
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
span[tooltip]:hover::before {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
span[tooltip]:hover::after {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
input[type="checkbox"].toggleInput {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + .toggle {
|
||||
margin: 6px 2px;
|
||||
height: 20px;
|
||||
width: 48px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 20px;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input[type="checkbox"] + .toggle > .toggleThumb {
|
||||
height: 16px;
|
||||
border-radius: 20px;
|
||||
transform: translate(2px, 2px);
|
||||
width: 16px;
|
||||
background: var(--ifm-color-white);
|
||||
box-shadow: var(--ifm-global-shadow-lw);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + .toggle {
|
||||
background: var(--ifm-color-primary);
|
||||
}
|
||||
|
||||
input[type="checkbox"]:checked + .toggle > .toggleThumb {
|
||||
transform: translate(30px, 2px);
|
||||
}
|
||||
|
||||
select {
|
||||
border: solid 1px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
height: 34px;
|
||||
width: 200px;
|
||||
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
padding: 3px 5px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
select > option {
|
||||
background: var(--ifm-background-surface-color);
|
||||
}
|
||||
|
||||
.inputBox {
|
||||
border: solid 1px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.inputBox > input {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
padding: 3px 10px;
|
||||
border: none;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.inputBox > span {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-left: solid 1px rgba(0, 0, 0, 0.5);
|
||||
padding: 3px 10px;
|
||||
}
|
||||
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
.inputBox > input::-webkit-outer-spin-button,
|
||||
.inputBox > input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
.inputBox > input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.disclaimerHolder {
|
||||
position: absolute;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
padding: 20px 20px;
|
||||
background: var(--ifm-background-surface-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: rgba(0, 0, 0, 0.03) 0px 10px 20px 0px,
|
||||
rgba(0, 0, 0, 0.1) 0px 1px 4px 0px;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.disclaimer > button {
|
||||
border: none;
|
||||
background: var(--ifm-color-primary);
|
||||
color: var(--ifm-color-white);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px;
|
||||
}
|
78
docs/src/data/power.js
Normal file
78
docs/src/data/power.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* This file holds all current measurements related to ZMK features and hardware
|
||||
* All current measurements are in micro amps. Measurements were taken on a Nordic Power Profiler Kit
|
||||
* The test device to get these values was three nice!nanos (nRF52840).
|
||||
*/
|
||||
|
||||
export const zmkBase = {
|
||||
hostConnection: 23, // How much current it takes to have an idle host connection
|
||||
standalone: {
|
||||
idle: 0, // No extra idle current
|
||||
typing: 315, // Current while holding down a key. Represents polling+BLE notification power
|
||||
},
|
||||
central: {
|
||||
idle: 490, // Idle current for connection to right half
|
||||
typing: 380, // Current while holding down a key. Represents polling+BLE notification power
|
||||
},
|
||||
peripheral: {
|
||||
idle: 20, // Idle current for connection to left half
|
||||
typing: 365, // Current while holding down a key. Represents polling+BLE notification power
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* ZMK board power measurements
|
||||
*
|
||||
* Power supply can be an LDO or switching
|
||||
* Quiescent and other quiescent are measured in micro amps
|
||||
*
|
||||
* Switching efficiency represents the efficiency of converting from
|
||||
* 3.8V (average li-ion voltage) to the output voltage of the power supply
|
||||
*/
|
||||
export const zmkBoards = {
|
||||
"nice!nano": {
|
||||
name: "nice!nano",
|
||||
powerSupply: {
|
||||
type: "LDO",
|
||||
outputVoltage: 3.3,
|
||||
quiescentMicroA: 55,
|
||||
},
|
||||
otherQuiescentMicroA: 4,
|
||||
},
|
||||
"nice!60": {
|
||||
powerSupply: {
|
||||
type: "SWITCHING",
|
||||
outputVoltage: 3.3,
|
||||
efficiency: 0.95,
|
||||
quiescentMicroA: 4,
|
||||
},
|
||||
otherQuiescentMicroA: 4,
|
||||
},
|
||||
};
|
||||
|
||||
export const underglowPower = {
|
||||
firmware: 60, // ZMK power usage while underglow feature is turned on (SPIM mostly)
|
||||
ledOn: 20000, // Estimated power consumption of a WS2812B at 100% (can be anywhere from 10mA to 30mA)
|
||||
ledOff: 460, // Quiescent current of a WS2812B
|
||||
};
|
||||
|
||||
export const displayPower = {
|
||||
// Based on GoodDisplay's 1.02in epaper
|
||||
EPAPER: {
|
||||
activePercent: 0.05, // Estimated one refresh per minute taking three seconds
|
||||
active: 1500, // Power draw during refresh
|
||||
sleep: 5, // Idle power draw of an epaper
|
||||
},
|
||||
// 128x32 SSD1306
|
||||
OLED: {
|
||||
activePercent: 0.5, // Estimated sleeping half the time (based on idle)
|
||||
active: 10000, // Estimated power draw when about half the pixels are on
|
||||
sleep: 7, // Deep sleep power draw (display off)
|
||||
},
|
||||
};
|
297
docs/src/pages/power-profiler.js
Normal file
297
docs/src/pages/power-profiler.js
Normal file
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import classnames from "classnames";
|
||||
import Layout from "@theme/Layout";
|
||||
import styles from "./styles.module.css";
|
||||
import PowerEstimate from "../components/power-estimate";
|
||||
import CustomBoardForm from "../components/custom-board-form";
|
||||
import { useInput } from "../utils/hooks";
|
||||
import { zmkBoards } from "../data/power";
|
||||
import "../css/power-profiler.css";
|
||||
|
||||
const Disclaimer = `This profiler makes many assumptions about typing
|
||||
activity, battery characteristics, hardware behavior, and
|
||||
doesn't account for error of user inputs. For example battery
|
||||
mAh, which is often incorrectly advertised higher than it's actual capacity.
|
||||
While it tries to estimate power usage using real power readings of ZMK,
|
||||
every person will have different results that may be worse or even
|
||||
better than the estimation given here.`;
|
||||
|
||||
function PowerProfiler() {
|
||||
const { value: board, bind: bindBoard } = useInput("");
|
||||
const { value: split, bind: bindSplit } = useInput(false);
|
||||
const { value: batteryMilliAh, bind: bindBatteryMilliAh } = useInput(110);
|
||||
|
||||
const { value: psuType, bind: bindPsuType } = useInput("");
|
||||
const { value: outputV, bind: bindOutputV } = useInput(3.3);
|
||||
const { value: quiescentMicroA, bind: bindQuiescentMicroA } = useInput(55);
|
||||
const {
|
||||
value: otherQuiescentMicroA,
|
||||
bind: bindOtherQuiescentMicroA,
|
||||
} = useInput(0);
|
||||
const { value: efficiency, bind: bindEfficiency } = useInput(0.9);
|
||||
|
||||
const { value: bondedQty, bind: bindBondedQty } = useInput(1);
|
||||
const { value: percentAsleep, bind: bindPercentAsleep } = useInput(0.5);
|
||||
|
||||
const { value: glowEnabled, bind: bindGlowEnabled } = useInput(false);
|
||||
const { value: glowQuantity, bind: bindGlowQuantity } = useInput(10);
|
||||
const { value: glowBrightness, bind: bindGlowBrightness } = useInput(1);
|
||||
|
||||
const { value: displayEnabled, bind: bindDisplayEnabled } = useInput(false);
|
||||
const { value: displayType, bind: bindDisplayType } = useInput("");
|
||||
|
||||
const [disclaimerAcknowledged, setDisclaimerAcknowledged] = useState(
|
||||
typeof window !== "undefined"
|
||||
? localStorage.getItem("zmkPowerProfilerDisclaimer") === "true"
|
||||
: false
|
||||
);
|
||||
|
||||
const currentBoard =
|
||||
board === "custom"
|
||||
? {
|
||||
powerSupply: {
|
||||
type: psuType,
|
||||
outputVoltage: outputV,
|
||||
quiescentMicroA: quiescentMicroA,
|
||||
efficiency,
|
||||
},
|
||||
otherQuiescentMicroA: otherQuiescentMicroA,
|
||||
}
|
||||
: zmkBoards[board];
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title={`ZMK Power Profiler`}
|
||||
description="Estimate your keyboard's power usage and battery life on ZMK."
|
||||
>
|
||||
<header className={classnames("hero hero--primary", styles.heroBanner)}>
|
||||
<div className="container">
|
||||
<h1 className="hero__title">ZMK Power Profiler</h1>
|
||||
<p className="hero__subtitle">
|
||||
{"Estimate your keyboard's power usage and battery life on ZMK."}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<section className="container">
|
||||
<div className="profilerSection">
|
||||
<h3>Keyboard Specifications</h3>
|
||||
<div className="row">
|
||||
<div className="col col--4">
|
||||
<div className="profilerInput">
|
||||
<label>Board</label>
|
||||
<select {...bindBoard}>
|
||||
<option hidden value="">
|
||||
Select a board
|
||||
</option>
|
||||
{Object.keys(zmkBoards).map((b) => (
|
||||
<option key={b}>{b}</option>
|
||||
))}
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col--4">
|
||||
<div className="profilerInput">
|
||||
<label>Split Keyboard</label>
|
||||
<input
|
||||
id="split"
|
||||
checked={split}
|
||||
{...bindSplit}
|
||||
className="toggleInput"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label htmlFor="split" className="toggle">
|
||||
<div className="toggleThumb" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col--4">
|
||||
<div className="profilerInput">
|
||||
<label>Battery Size</label>
|
||||
<div className="inputBox">
|
||||
<input {...bindBatteryMilliAh} type="number" />
|
||||
<span>mAh</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{board === "custom" && (
|
||||
<CustomBoardForm
|
||||
bindPsuType={bindPsuType}
|
||||
bindOutputV={bindOutputV}
|
||||
bindEfficiency={bindEfficiency}
|
||||
bindQuiescentMicroA={bindQuiescentMicroA}
|
||||
bindOtherQuiescentMicroA={bindOtherQuiescentMicroA}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="profilerSection">
|
||||
<h3>Usage Values</h3>
|
||||
<div className="row">
|
||||
<div className="col col--4">
|
||||
<div className="profilerInput">
|
||||
<label>
|
||||
Bonded Bluetooth Profiles{" "}
|
||||
<span tooltip="The average number of host devices connected at once">
|
||||
ⓘ
|
||||
</span>
|
||||
</label>
|
||||
<input {...bindBondedQty} type="range" min="1" max="5" />
|
||||
<span>{bondedQty}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col--4">
|
||||
<div className="profilerInput">
|
||||
<label>
|
||||
Percentage Asleep{" "}
|
||||
<span tooltip="How much time the keyboard is in deep sleep (15 min. default timeout)">
|
||||
ⓘ
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
{...bindPercentAsleep}
|
||||
type="range"
|
||||
min="0"
|
||||
step=".1"
|
||||
max="1"
|
||||
/>
|
||||
<span>{Math.round(percentAsleep * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="profilerSection">
|
||||
<h3>Features</h3>
|
||||
<div className="row">
|
||||
<div className="col col--4">
|
||||
<div className="profilerInput">
|
||||
<label>RGB Underglow</label>
|
||||
<input
|
||||
checked={glowEnabled}
|
||||
id="glow"
|
||||
{...bindGlowEnabled}
|
||||
className="toggleInput"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label htmlFor="glow" className="toggle">
|
||||
<div className="toggleThumb" />
|
||||
</label>
|
||||
</div>
|
||||
{glowEnabled && (
|
||||
<>
|
||||
<div className="profilerInput">
|
||||
<label>LED Quantity</label>
|
||||
<div className="inputBox">
|
||||
<input {...bindGlowQuantity} type="number" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="profilerInput">
|
||||
<label>Brightness</label>
|
||||
<input
|
||||
{...bindGlowBrightness}
|
||||
type="range"
|
||||
min="0"
|
||||
step=".01"
|
||||
max="1"
|
||||
/>
|
||||
<span>{Math.round(glowBrightness * 100)}%</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="col col--4">
|
||||
<div className="profilerInput">
|
||||
<label>Display</label>
|
||||
<input
|
||||
checked={displayEnabled}
|
||||
id="display"
|
||||
{...bindDisplayEnabled}
|
||||
className="toggleInput"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label htmlFor="display" className="toggle">
|
||||
<div className="toggleThumb" />
|
||||
</label>
|
||||
</div>
|
||||
{displayEnabled && (
|
||||
<div className="profilerInput">
|
||||
<label>Display Type</label>
|
||||
<select {...bindDisplayType}>
|
||||
<option hidden selected>
|
||||
Select type
|
||||
</option>
|
||||
<option value="EPAPER">ePaper</option>
|
||||
<option value="OLED">OLED</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{split ? (
|
||||
<>
|
||||
<PowerEstimate
|
||||
board={currentBoard}
|
||||
splitType="central"
|
||||
batteryMilliAh={batteryMilliAh}
|
||||
usage={{ bondedQty, percentAsleep }}
|
||||
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
|
||||
display={{ displayEnabled, displayType }}
|
||||
/>
|
||||
<PowerEstimate
|
||||
board={currentBoard}
|
||||
splitType="peripheral"
|
||||
batteryMilliAh={batteryMilliAh}
|
||||
usage={{ bondedQty, percentAsleep }}
|
||||
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
|
||||
display={{ displayEnabled, displayType }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<PowerEstimate
|
||||
board={currentBoard}
|
||||
splitType="standalone"
|
||||
batteryMilliAh={batteryMilliAh}
|
||||
usage={{ bondedQty, percentAsleep }}
|
||||
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
|
||||
display={{ displayEnabled, displayType }}
|
||||
/>
|
||||
)}
|
||||
<div className="row">
|
||||
<div className="col col--8 col--offset-2 profilerDisclaimer">
|
||||
Disclaimer: {Disclaimer}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{!disclaimerAcknowledged && (
|
||||
<div className="disclaimerHolder">
|
||||
<div className="disclaimer">
|
||||
<h3>Disclaimer</h3>
|
||||
<p>{Disclaimer}</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDisclaimerAcknowledged(true);
|
||||
localStorage.setItem("zmkPowerProfilerDisclaimer", true);
|
||||
}}
|
||||
>
|
||||
I Understand
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default PowerProfiler;
|
23
docs/src/utils/hooks.js
Normal file
23
docs/src/utils/hooks.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The ZMK Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export const useInput = (initialValue) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
return {
|
||||
value,
|
||||
setValue,
|
||||
bind: {
|
||||
value,
|
||||
onChange: (event) => {
|
||||
const target = event.target;
|
||||
setValue(target.type === "checkbox" ? target.checked : target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue