Nick Winans
4 years ago
committed by
Pete Johanson
8 changed files with 1045 additions and 0 deletions
@ -0,0 +1,100 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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