summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Winans <nick@winans.codes>2021-02-16 14:34:09 -0600
committerPete Johanson <peter@peterjohanson.com>2021-03-11 16:31:34 -0500
commit4ef11ac4aa5185994db19ef3f69a8c54c70fb06c (patch)
tree4053be2ed00fa01f03868ac455d07dc4e1ec1496
parent0df71100581d040178bd0fe8ec0382d84dc59a40 (diff)
feat(docs): Add power profiler
-rw-r--r--docs/docusaurus.config.js5
-rw-r--r--docs/src/components/custom-board-form.js100
-rw-r--r--docs/src/components/power-estimate.js266
-rw-r--r--docs/src/css/power-estimate.css81
-rw-r--r--docs/src/css/power-profiler.css195
-rw-r--r--docs/src/data/power.js78
-rw-r--r--docs/src/pages/power-profiler.js297
-rw-r--r--docs/src/utils/hooks.js23
8 files changed, 1045 insertions, 0 deletions
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index f22610a..ab7ec12 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -30,6 +30,11 @@ module.exports = {
},
{ to: "blog", label: "Blog", position: "left" },
{
+ to: "power-profiler",
+ label: "Power Profiler",
+ position: "left",
+ },
+ {
href: "https://github.com/zmkfirmware/zmk",
label: "GitHub",
position: "right",
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);
+ },
+ },
+ };
+};