diff options
author | Nick Winans <nick@winans.codes> | 2021-02-16 14:34:09 -0600 |
---|---|---|
committer | Pete Johanson <peter@peterjohanson.com> | 2021-03-11 16:31:34 -0500 |
commit | 4ef11ac4aa5185994db19ef3f69a8c54c70fb06c (patch) | |
tree | 4053be2ed00fa01f03868ac455d07dc4e1ec1496 /docs/src | |
parent | 0df71100581d040178bd0fe8ec0382d84dc59a40 (diff) |
feat(docs): Add power profiler
Diffstat (limited to 'docs/src')
-rw-r--r-- | docs/src/components/custom-board-form.js | 100 | ||||
-rw-r--r-- | docs/src/components/power-estimate.js | 266 | ||||
-rw-r--r-- | docs/src/css/power-estimate.css | 81 | ||||
-rw-r--r-- | docs/src/css/power-profiler.css | 195 | ||||
-rw-r--r-- | docs/src/data/power.js | 78 | ||||
-rw-r--r-- | docs/src/pages/power-profiler.js | 297 | ||||
-rw-r--r-- | docs/src/utils/hooks.js | 23 |
7 files changed, 1040 insertions, 0 deletions
diff --git a/docs/src/components/custom-board-form.js b/docs/src/components/custom-board-form.js new file mode 100644 index 0000000..0279f6b --- /dev/null +++ b/docs/src/components/custom-board-form.js @@ -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; diff --git a/docs/src/components/power-estimate.js b/docs/src/components/power-estimate.js new file mode 100644 index 0000000..2619862 --- /dev/null +++ b/docs/src/components/power-estimate.js @@ -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; diff --git a/docs/src/css/power-estimate.css b/docs/src/css/power-estimate.css new file mode 100644 index 0000000..e876ec2 --- /dev/null +++ b/docs/src/css/power-estimate.css @@ -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; +} diff --git a/docs/src/css/power-profiler.css b/docs/src/css/power-profiler.css new file mode 100644 index 0000000..94c4a5d --- /dev/null +++ b/docs/src/css/power-profiler.css @@ -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; +} diff --git a/docs/src/data/power.js b/docs/src/data/power.js new file mode 100644 index 0000000..bf34f17 --- /dev/null +++ b/docs/src/data/power.js @@ -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) + }, +}; diff --git a/docs/src/pages/power-profiler.js b/docs/src/pages/power-profiler.js new file mode 100644 index 0000000..ca46f5c --- /dev/null +++ b/docs/src/pages/power-profiler.js @@ -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; diff --git a/docs/src/utils/hooks.js b/docs/src/utils/hooks.js new file mode 100644 index 0000000..b8fb27b --- /dev/null +++ b/docs/src/utils/hooks.js @@ -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); + }, + }, + }; +}; |