diff --git a/examples/node/package.json b/examples/node/package.json index 70a5753..11dfca6 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -17,6 +17,8 @@ "format": "yarn run prettier --write ./ts/*" }, "dependencies": { - "ansi-colors": "^4.1.3" + "@types/terminal-kit": "^2.5.6", + "ansi-colors": "^4.1.3", + "terminal-kit": "^3.1.1" } } diff --git a/examples/node/ts/scanner_ui.ts b/examples/node/ts/scanner_ui.ts new file mode 100644 index 0000000..0ecc5a2 --- /dev/null +++ b/examples/node/ts/scanner_ui.ts @@ -0,0 +1,270 @@ +import * as readline from "readline/promises"; +import * as fs from "fs"; +import * as timer from "timers/promises"; +import * as tui from "terminal-kit"; +import { RF24, CrcLength, DataRate } from "@rf24/rf24"; + +const io = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const CACHE_MAX = 6; + +/** + * A class to encapsulate a single progress bar for each channel. + */ +export class ProgressBar { + x: number; + y: number; + isColOdd: boolean; + label: string; + total: number; + history: Array; + sum: number; + width: number; + + constructor(x: number, y: number, label: string, isColOdd: boolean) { + this.x = x; + this.y = y; + this.label = label; + this.isColOdd = isColOdd; + this.total = 0; + this.history = []; + for (let i = 0; i < CACHE_MAX; ++i) { + this.history.push(false); + } + this.sum = 0; + this.width = Math.floor(tui.terminal.width / 6) - (label.length + 4); + } + + /** + * Update the progress bar's values. + */ + update(foundSignal: boolean) { + const oldSum = this.sum; + this.sum = 0; + this.history.shift(); + this.history.push(foundSignal); + this.history.forEach((val) => { + this.sum += Number(val); + }); + this.total += Number(foundSignal); + if (this.sum != oldSum) { + this.draw(); + } + } + + /** + * Draw the progress bar. + */ + draw() { + let filled = ""; + const filledWidth = Math.ceil(this.width * (this.sum / CACHE_MAX)); + for (let i = 0; i < filledWidth; ++i) { + filled += "="; + } + let bg = ""; + const bgWidth = this.width - filledWidth; + for (let i = 0; i < bgWidth; ++i) { + bg += "-"; + } + const total = + this.total == 0 + ? "-" + : Math.min(this.total, 0xf).toString(16).toUpperCase(); + tui.terminal.moveTo(this.x, this.y); + if (this.isColOdd) { + // draw yellow bar + tui.terminal + .yellow(`${this.label} `) + .magenta(filled) + .yellow(`${bg} ${total} `); + } else { + //draw white bar + tui.terminal + .white(`${this.label} `) + .magenta(filled) + .white(`${bg} ${total} `); + } + } +} + +const CHANNELS = 126; + +export class App { + radio: RF24; + progressBars: Array; + + constructor(dataRate: DataRate) { + // The radio's CE Pin uses a GPIO number. + // On Linux, consider the device path `/dev/gpiochip`: + // - `` is the gpio chip's identifying number. + // Using RPi4 (or earlier), this number is `0` (the default). + // Using the RPi5, this number is actually `4`. + // The radio's CE pin must connected to a pin exposed on the specified chip. + const cePin = 22; // for GPIO22 + // try detecting RPi5 first; fall back to default + const gpioChip = fs.existsSync("/dev/gpiochip4") ? 4 : 0; + + // The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). + // On Linux, consider the device path `/dev/spidev.`: + // - `` is the SPI bus number (defaults to `0`) + // - `` is the CSN pin (must be unique for each device on the same SPI bus) + const csnPin = 0; // aka CE0 for SPI bus 0 (/dev/spidev0.0) + + // create a radio object for the specified hardware config: + this.radio = new RF24(cePin, csnPin, { + devGpioChip: gpioChip, + }); + + // initialize the nRF24L01 on the spi bus + this.radio.begin(); + + // This is the worst possible configuration. + // The intention here is to pick up as much noise as possible. + this.radio.addressLength = 2; + + // For this example, we will use the worst possible addresses + const address = [ + Buffer.from([0x55, 0x55]), + Buffer.from([0xaa, 0xaa]), + Buffer.from([0xa0, 0xaa]), + Buffer.from([0x0a, 0xaa]), + Buffer.from([0xa5, 0xaa]), + Buffer.from([0x5a, 0xaa]), + ]; + for (let pipe = 0; pipe < address.length; pipe++) { + this.radio.openRxPipe(pipe, address[pipe]); + } + + this.radio.dataRate = dataRate; + // turn off auto-ack related features + this.radio.setAutoAck(false); + this.radio.dynamicPayloads = false; + this.radio.crcLength = CrcLength.Disabled; + + this.progressBars = Array(CHANNELS); + const bar_w = Math.floor(tui.terminal.width / 6); + for (let i = 0; i < 21; ++i) { + // 21 rows + for (let j = i; j < i + 21 * 6; j += 21) { + // 6 columns + const isColOdd = Math.floor(j / 21) % 2 > 0; + const label = (2400 + j).toString(); + const y = i + 4; + const x = bar_w * Math.floor(j / 21) + 1; + this.progressBars[j] = new ProgressBar(x, y, label, isColOdd); + } + } + } + + /** + * The scanner behavior. + */ + async run(duration: number, dataRate: string) { + let sweeps = 0; + let channel = 0; + tui.terminal.clear(); + this.progressBars.forEach((bar) => { + bar.draw(); + }); + tui.terminal.moveTo(1, 1, "Channels are labeled in Hz."); + tui.terminal.moveTo( + 1, + 2, + "Signal counts are clamped to a single hexadecimal digit.", + ); + + const timeout = Date.now() + (duration || 30) * 1000; + let prevSec = 0; + while (Date.now() < timeout) { + await this.scan(channel); + + channel += 1; + if (channel >= CHANNELS) { + channel = 0; + sweeps += 1; + } + const currSec = Math.floor(Date.now() / 1000); + if (currSec != prevSec) { + const remaining = (Math.floor(timeout / 1000) - currSec) + .toString() + .padStart(3); + tui.terminal.moveTo( + 1, + 3, + `Scanning for ${remaining} seconds at ${dataRate}.`, + ); + prevSec = currSec; + } + } + + tui.terminal.clear(); + let noisyChannels = 0; + const sweepsWidth = sweeps.toString().length; + for (let i = 0; i < CHANNELS; ++i) { + const total = this.progressBars[i].total; + const percentage = ((total / sweeps) * 100).toPrecision(3); + const paddedTotal = total.toString().padStart(sweepsWidth); + if (total > 0) { + console.log( + ` ${i.toString().padStart(3)}: ${paddedTotal}`, + `/ ${sweeps} (${percentage} %)`, + ); + noisyChannels += 1; + } + } + console.log( + `${noisyChannels} channels detected signals out of`, + `${sweeps} passes on the entire spectrum`, + ); + } + + /** + * scan a specified channel + */ + async scan(channel: number) { + this.radio.channel = channel; + this.radio.asRx(); + await timer.setTimeout(0.13); // needs to be at least 130 microseconds + const rpd = this.radio.rpd; + this.radio.asTx(); + const foundSignal = this.radio.available() || rpd || this.radio.rpd; + + if (foundSignal) { + this.radio.flushRx(); // discard any packets (noise) saved in RX FIFO + } + this.progressBars[channel].update(foundSignal); + } +} + +export async function main() { + console.log(module.filename); + + // Ask user for desired data rate (default to 1 Mbps) + const dRatePrompt = + "Select the desired DataRate: (defaults to 1 Mbps)\n" + + "1. 1 Mbps\n2. 2 Mbps\n3. 250 Kbps\n"; + const answer = parseInt(await io.question(dRatePrompt)) || 0; + let dataRate = DataRate.Mbps1; + let dataRateString = "1 Mbps"; + if (answer == 2) { + dataRate = DataRate.Mbps2; + dataRateString = "2 Mbps"; + } else if (answer == 3) { + dataRate = DataRate.Kbps250; + dataRateString = "250 Kbps"; + } + const app = new App(dataRate); + let duration = NaN; + while (Number.isNaN(duration)) { + duration = parseInt( + await io.question("How long (in seconds) to perform scan? "), + ); + } + app.run(duration, dataRateString); + io.close(); +} + +main(); diff --git a/examples/python/scanner_curses.py b/examples/python/scanner_curses.py index 60267d6..d7064fd 100644 --- a/examples/python/scanner_curses.py +++ b/examples/python/scanner_curses.py @@ -216,7 +216,7 @@ def main(): try: std_scr = init_curses() timer_prompt = "Scanning for {:>3} seconds at " + OFFERED_DATA_RATES[data_rate] - std_scr.addstr(0, 0, "Channels are labeled in MHz.") + std_scr.addstr(0, 0, "Channels are labeled in Hz.") std_scr.addstr(1, 0, "Signal counts are clamped to a single hexadecimal digit.") bars = init_display(std_scr) channel, val = (0, False) diff --git a/yarn.lock b/yarn.lock index ba87490..db0166f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,18 @@ # yarn lockfile v1 +"@cronvel/get-pixels@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@cronvel/get-pixels/-/get-pixels-3.4.1.tgz#6028de81152e73870ebec0c254f6b727afbd9a34" + integrity sha512-gB5C5nDIacLUdsMuW8YsM9SzK3vaFANe4J11CVXpovpy7bZUGrcJKmc6m/0gWG789pKr6XSZY2aEetjFvSRw5g== + dependencies: + jpeg-js "^0.4.4" + ndarray "^1.0.19" + ndarray-pack "^1.1.1" + node-bitmap "0.0.1" + omggif "^1.0.10" + pngjs "^6.0.0" + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" @@ -180,6 +192,11 @@ dependencies: "@types/unist" "*" +"@types/nextgen-events@*": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@types/nextgen-events/-/nextgen-events-1.1.4.tgz#120d19a9cc38208732fe0244c8d1c9f5c0a42082" + integrity sha512-YczHp+887i3MpHUOCOztk7y10SklNZ3aQlToKnu0LON0ZdFpgwq8POtnATAoFz8V1IxyR6d8pp8ZyYkUIy26Cw== + "@types/node@^22.7.5": version "22.9.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.0.tgz#b7f16e5c3384788542c72dc3d561a7ceae2c0365" @@ -187,6 +204,13 @@ dependencies: undici-types "~6.19.8" +"@types/terminal-kit@^2.5.6": + version "2.5.6" + resolved "https://registry.yarnpkg.com/@types/terminal-kit/-/terminal-kit-2.5.6.tgz#6d0c613a706c0317ec5a99863cb791d500b6be0b" + integrity sha512-S5kRC7wzduRj/Wrc8BCRPfQBSWi3bj3CCUBIkmrzBzrc0sjgxPqYZPvdDxhuBGCsOPZFJiDSrzUa9mYXVOOm4g== + dependencies: + "@types/nextgen-events" "*" + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" @@ -370,6 +394,11 @@ character-entities-legacy@^3.0.0: resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== +chroma-js@^2.4.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.6.0.tgz#578743dd359698a75067a19fa5571dec54d0b70b" + integrity sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -401,6 +430,13 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +cwise-compiler@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5" + integrity sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ== + dependencies: + uniq "^1.0.0" + debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" @@ -676,6 +712,16 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +iota-array@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/iota-array/-/iota-array-1.0.0.tgz#81ef57fe5d05814cd58c2483632a99c30a0e8087" + integrity sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA== + +is-buffer@^1.0.2: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -698,6 +744,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +jpeg-js@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" + integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg== + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -727,6 +778,11 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" +lazyness@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/lazyness/-/lazyness-1.2.0.tgz#5dc0f02c37280436b21f0e4918ce6e72a109c657" + integrity sha512-KenL6EFbwxBwRxG93t0gcUyi0Nw0Ub31FJKN1laA4UscdkL1K1AxUd0gYZdcLU3v+x+wcFi4uQKS5hL+fk500g== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -860,6 +916,37 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +ndarray-pack@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ndarray-pack/-/ndarray-pack-1.2.1.tgz#8caebeaaa24d5ecf70ff86020637977da8ee585a" + integrity sha512-51cECUJMT0rUZNQa09EoKsnFeDL4x2dHRT0VR5U2H5ZgEcm95ZDWcMA5JShroXjHOejmAD/fg8+H+OvUnVXz2g== + dependencies: + cwise-compiler "^1.1.2" + ndarray "^1.0.13" + +ndarray@^1.0.13, ndarray@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/ndarray/-/ndarray-1.0.19.tgz#6785b5f5dfa58b83e31ae5b2a058cfd1ab3f694e" + integrity sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ== + dependencies: + iota-array "^1.0.0" + is-buffer "^1.0.2" + +nextgen-events@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/nextgen-events/-/nextgen-events-1.5.3.tgz#89a657720fffbfdef646f2769ac6fcc6c4f79789" + integrity sha512-P6qw6kenNXP+J9XlKJNi/MNHUQ+Lx5K8FEcSfX7/w8KJdZan5+BB5MKzuNgL2RTjHG1Svg8SehfseVEp8zAqwA== + +node-bitmap@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/node-bitmap/-/node-bitmap-0.0.1.tgz#180eac7003e0c707618ef31368f62f84b2a69091" + integrity sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA== + +omggif@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/omggif/-/omggif-1.0.10.tgz#ddaaf90d4a42f532e9e7cb3a95ecdd47f17c7b19" + integrity sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw== + oniguruma-to-js@0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz#8d899714c21f5c7d59a3c0008ca50e848086d740" @@ -915,6 +1002,11 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pngjs@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821" + integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -972,6 +1064,18 @@ semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +seventh@^0.9.2: + version "0.9.2" + resolved "https://registry.yarnpkg.com/seventh/-/seventh-0.9.2.tgz#76057e48e2b035ae79ebc2a82fc49d2ae23beeaf" + integrity sha512-C+dnbBXIEycnrN6/CpFt/Rt8ccMzAX3wbwJU61RTfC8lYPMzSkKkAVWnUEMTZDHdvtlrTupZeCUK4G+uP4TmRQ== + dependencies: + setimmediate "^1.0.5" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1001,6 +1105,11 @@ space-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== +string-kit@^0.18.1: + version "0.18.3" + resolved "https://registry.yarnpkg.com/string-kit/-/string-kit-0.18.3.tgz#9e422134ef54d3101ddfb570025d8775b20de3bf" + integrity sha512-G8cBS7wxxHhwQrKU0Y8SjZJRtCzZ61bMmMCO1bWm6N6y2obT0koGK8uWYloMOaVPPr8zk7Ic995uEd4Jw504AQ== + stringify-entities@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" @@ -1021,6 +1130,20 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +terminal-kit@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/terminal-kit/-/terminal-kit-3.1.1.tgz#d78235c2393777a126d98c09ccd4d24d8a9d7b0e" + integrity sha512-R+R47zBQ14Ax2NZCLeuVl2GwonDwQN4iAsjQZICW8gMzaV+VIJMvL4qhUQtzDOhENADyNPQvY1Vz5G0bHHkLEA== + dependencies: + "@cronvel/get-pixels" "^3.4.1" + chroma-js "^2.4.2" + lazyness "^1.2.0" + ndarray "^1.0.19" + nextgen-events "^1.5.3" + seventh "^0.9.2" + string-kit "^0.18.1" + tree-kit "^0.8.7" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -1033,6 +1156,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tree-kit@^0.8.7: + version "0.8.7" + resolved "https://registry.yarnpkg.com/tree-kit/-/tree-kit-0.8.7.tgz#46472eee7619861ec81b9cbeabe27fa117fb98ab" + integrity sha512-BA/cp8KBvbBDkunxIuoBqzz3pYHL7J8QdzbKohK09urOpHFYqEe/xWGKkECEQG+LvfREd1GNqH3643GYFX8wSQ== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -1090,6 +1218,11 @@ undici-types@~6.19.8: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +uniq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA== + unist-util-is@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"