Browse Source

feat(docs): Add power profiler

xmkb
Nick Winans 4 years ago committed by Pete Johanson
parent
commit
4ef11ac4aa
  1. 5
      docs/docusaurus.config.js
  2. 100
      docs/src/components/custom-board-form.js
  3. 266
      docs/src/components/power-estimate.js
  4. 81
      docs/src/css/power-estimate.css
  5. 195
      docs/src/css/power-profiler.css
  6. 78
      docs/src/data/power.js
  7. 297
      docs/src/pages/power-profiler.js
  8. 23
      docs/src/utils/hooks.js

5
docs/docusaurus.config.js

@ -29,6 +29,11 @@ module.exports = { @@ -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

@ -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;

266
docs/src/components/power-estimate.js

@ -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;

81
docs/src/css/power-estimate.css

@ -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;
}

195
docs/src/css/power-profiler.css

@ -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;
}

78
docs/src/data/power.js

@ -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)
},
};

297
docs/src/pages/power-profiler.js

@ -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;

23
docs/src/utils/hooks.js

@ -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…
Cancel
Save