forked from kofal.net/zmk
feat(docs): Add power profiler
This commit is contained in:
committed by
Pete Johanson
parent
0df7110058
commit
4ef11ac4aa
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;
|
||||
Reference in New Issue
Block a user