Nick Winans
4 years ago
committed by
Pete Johanson
8 changed files with 1045 additions and 0 deletions
@ -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; |
@ -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; |
@ -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; |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -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)
|
||||||
|
}, |
||||||
|
}; |
@ -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; |
@ -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 new issue