394 lines
15 KiB
JavaScript
394 lines
15 KiB
JavaScript
/*
|
|
* 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, backlightLEDs } 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: backlightEnabled, bind: bindBacklightEnabled } =
|
|
useInput(false);
|
|
const { value: backlightQuantity, bind: bindBacklightQuantity } =
|
|
useInput(60);
|
|
const { value: backlightColor, bind: bindBacklightColor } = useInput("");
|
|
const { value: backlightVoltage, bind: bindBacklightVoltage } = useInput(2.2);
|
|
const { value: backlightResistance, bind: bindBacklightResistance } =
|
|
useInput(100);
|
|
const { value: backlightBrightness, bind: bindBacklightBrightness } =
|
|
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];
|
|
|
|
const currentBacklightVoltage =
|
|
backlightColor === "custom"
|
|
? backlightVoltage
|
|
: backlightLEDs[backlightColor || "White"];
|
|
|
|
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>Backlight</label>
|
|
<input
|
|
checked={backlightEnabled}
|
|
id="backlight"
|
|
{...bindBacklightEnabled}
|
|
className="toggleInput"
|
|
type="checkbox"
|
|
/>
|
|
<label htmlFor="backlight" className="toggle">
|
|
<div className="toggleThumb" />
|
|
</label>
|
|
</div>
|
|
{backlightEnabled && (
|
|
<>
|
|
<div className="profilerInput">
|
|
<label>LED Quantity</label>
|
|
<div className="inputBox">
|
|
<input {...bindBacklightQuantity} type="number" />
|
|
</div>
|
|
</div>
|
|
<div className="profilerInput">
|
|
<label>LED Forward Voltage</label>
|
|
<select {...bindBacklightColor}>
|
|
{Object.entries(backlightLEDs).map(([c, v]) => (
|
|
<option key={c} value={c}>
|
|
{c} ({v.toFixed(1)} V)
|
|
</option>
|
|
))}
|
|
<option value="custom">Custom</option>
|
|
</select>
|
|
{backlightColor === "custom" && (
|
|
<div className="profilerInput">
|
|
<div className="inputBox">
|
|
<input {...bindBacklightVoltage} type="number" />
|
|
<span>V</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="profilerInput">
|
|
<label>LED Resistance</label>
|
|
<div className="inputBox">
|
|
<input {...bindBacklightResistance} type="number" />
|
|
<span>ohm</span>
|
|
</div>
|
|
</div>
|
|
<div className="profilerInput">
|
|
<label>Brightness</label>
|
|
<input
|
|
{...bindBacklightBrightness}
|
|
type="range"
|
|
min="0"
|
|
step=".01"
|
|
max="1"
|
|
/>
|
|
<span>{Math.round(backlightBrightness * 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 }}
|
|
backlight={{
|
|
backlightEnabled,
|
|
backlightVoltage: currentBacklightVoltage,
|
|
backlightResistance,
|
|
backlightBrightness,
|
|
backlightQuantity,
|
|
}}
|
|
display={{ displayEnabled, displayType }}
|
|
/>
|
|
<PowerEstimate
|
|
board={currentBoard}
|
|
splitType="peripheral"
|
|
batteryMilliAh={batteryMilliAh}
|
|
usage={{ bondedQty, percentAsleep }}
|
|
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
|
|
backlight={{
|
|
backlightEnabled,
|
|
backlightVoltage: currentBacklightVoltage,
|
|
backlightResistance,
|
|
backlightBrightness,
|
|
backlightQuantity,
|
|
}}
|
|
display={{ displayEnabled, displayType }}
|
|
/>
|
|
</>
|
|
) : (
|
|
<PowerEstimate
|
|
board={currentBoard}
|
|
splitType="standalone"
|
|
batteryMilliAh={batteryMilliAh}
|
|
usage={{ bondedQty, percentAsleep }}
|
|
underglow={{ glowEnabled, glowBrightness, glowQuantity }}
|
|
backlight={{
|
|
backlightEnabled,
|
|
backlightVoltage: currentBacklightVoltage,
|
|
backlightResistance,
|
|
backlightBrightness,
|
|
backlightQuantity,
|
|
}}
|
|
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;
|