-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
1,045 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/* | ||
* Copyright (c) 2021 The ZMK Contributors | ||
* | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
import React from "react"; | ||
import PropTypes from "prop-types"; | ||
|
||
function CustomBoardForm({ | ||
bindPsuType, | ||
bindOutputV, | ||
bindEfficiency, | ||
bindQuiescentMicroA, | ||
bindOtherQuiescentMicroA, | ||
}) { | ||
return ( | ||
<div className="profilerSection"> | ||
<h3>Custom Board</h3> | ||
<div className="row"> | ||
<div className="col col--4"> | ||
<div className="profilerInput"> | ||
<label>Power Supply Type</label> | ||
<select {...bindPsuType}> | ||
<option hidden value=""> | ||
Select a PSU type | ||
</option> | ||
<option value="LDO">LDO</option> | ||
<option value="SWITCHING">Switching</option> | ||
</select> | ||
</div> | ||
</div> | ||
<div className="col col--4"> | ||
<div className="profilerInput"> | ||
<label> | ||
Output Voltage{" "} | ||
<span tooltip="Output Voltage of the PSU used by the system"> | ||
ⓘ | ||
</span> | ||
</label> | ||
<input {...bindOutputV} type="range" min="1.8" step=".1" max="5" /> | ||
<span>{parseFloat(bindOutputV.value).toFixed(1)}V</span> | ||
</div> | ||
{bindPsuType.value === "SWITCHING" && ( | ||
<div className="profilerInput"> | ||
<label> | ||
PSU Efficiency{" "} | ||
<span tooltip="The estimated efficiency with a VIN of 3.8 and the output voltage entered above"> | ||
ⓘ | ||
</span> | ||
</label> | ||
<input | ||
{...bindEfficiency} | ||
type="range" | ||
min=".50" | ||
step=".01" | ||
max="1" | ||
/> | ||
<span>{Math.round(bindEfficiency.value * 100)}%</span> | ||
</div> | ||
)} | ||
</div> | ||
<div className="col col--4"> | ||
<div className="profilerInput"> | ||
<label> | ||
PSU Quiescent{" "} | ||
<span tooltip="The standby usage of the PSU">ⓘ</span> | ||
</label> | ||
<div className="inputBox"> | ||
<input {...bindQuiescentMicroA} type="number" /> | ||
<span>µA</span> | ||
</div> | ||
</div> | ||
<div className="profilerInput"> | ||
<label> | ||
Other Quiescent{" "} | ||
<span tooltip="Any other standby usage of the board (voltage dividers, extra ICs, etc)"> | ||
ⓘ | ||
</span> | ||
</label> | ||
<div className="inputBox"> | ||
<input {...bindOtherQuiescentMicroA} type="number" /> | ||
<span>µA</span> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
CustomBoardForm.propTypes = { | ||
bindPsuType: PropTypes.Object, | ||
bindOutputV: PropTypes.Object, | ||
bindEfficiency: PropTypes.Object, | ||
bindQuiescentMicroA: PropTypes.Object, | ||
bindOtherQuiescentMicroA: PropTypes.Object, | ||
}; | ||
|
||
export default CustomBoardForm; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
/* | ||
* Copyright (c) 2021 The ZMK Contributors | ||
* | ||
* SPDX-License-Identifier: MIT | ||
*/ | ||
|
||
import React from "react"; | ||
import PropTypes from "prop-types"; | ||
import { displayPower, underglowPower, zmkBase } from "../data/power"; | ||
import "../css/power-estimate.css"; | ||
|
||
// Average monthly discharge percent | ||
const lithiumIonMonthlyDischargePercent = 5; | ||
// Average voltage of a lithium ion battery based of discharge graphs | ||
const lithiumIonAverageVoltage = 3.8; | ||
// Average discharge efficiency of li-ion https://en.wikipedia.org/wiki/Lithium-ion_battery | ||
const lithiumIonDischargeEfficiency = 0.85; | ||
// Range of the discharge efficiency | ||
const lithiumIonDischargeEfficiencyRange = 0.05; | ||
|
||
// Proportion of time spent typing (keys being pressed down and scanning). Estimated to 2%. | ||
const timeSpentTyping = 0.02; | ||
|
||
// 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; |
Oops, something went wrong.