Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59a2c1ad0 | ||
|
|
58c58e38ac | ||
|
|
b5d4ab6024 | ||
|
|
2ce568c019 | ||
|
|
5a0b36f42b | ||
|
|
74863f675e | ||
|
|
9654dc2841 | ||
|
|
9c19c05274 | ||
|
|
a547546055 | ||
|
|
c4007961c7 | ||
|
|
15f70b55af | ||
|
|
8f4c5f09a4 | ||
|
|
06cafff105 | ||
|
|
e2a4352659 | ||
|
|
5bb0038d9b | ||
|
|
b653368d89 | ||
|
|
690c1b4146 | ||
|
|
66796267f2 | ||
|
|
11ab1597f6 | ||
|
|
3edccdc4ab | ||
|
|
c308307fea | ||
|
|
ca6928aa7c | ||
|
|
632308a50e | ||
|
|
40bb81f22e | ||
|
|
e9487f9603 | ||
|
|
82d9115ae6 | ||
|
|
31baea2415 | ||
|
|
da867078b1 | ||
|
|
dc75519f3d | ||
|
|
8246ea52ef | ||
|
|
72de55b26f | ||
|
|
6b5026dcf1 | ||
|
|
a3a5a9404c |
@@ -28,6 +28,7 @@ jobs:
|
|||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
- run: npm run tsc
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: run hook install-wireshark-dissector test build
|
.PHONY: run hook install-wireshark-dissector test build typecheck
|
||||||
|
|
||||||
bundle.js: frida-hooks.js func_replacements.js
|
bundle.js: frida-hooks.js func_replacements.js
|
||||||
~/node_modules/.bin/frida-compile -o $@ frida-hooks.js
|
~/node_modules/.bin/frida-compile -o $@ frida-hooks.js
|
||||||
@@ -8,15 +8,13 @@ venv: requirements.txt
|
|||||||
./venv/bin/pip install -r requirements.txt
|
./venv/bin/pip install -r requirements.txt
|
||||||
touch venv
|
touch venv
|
||||||
|
|
||||||
build: node_modules
|
typecheck: node_modules
|
||||||
npm run tsc
|
npm run tsc
|
||||||
|
build: node_modules
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
run: node_modules
|
run: build
|
||||||
./node_modules/.bin/ts-node --esm cmd/bin http_server --port=1234
|
node dist/bin.cjs http_server --port=1234
|
||||||
|
|
||||||
pair: node_modules
|
|
||||||
./node_modules/.bin/ts-node --esm cmd/bin pair
|
|
||||||
|
|
||||||
hook: bundle.js venv
|
hook: bundle.js venv
|
||||||
./venv/bin/python3 -u loader3.py
|
./venv/bin/python3 -u loader3.py
|
||||||
|
|||||||
@@ -6,25 +6,20 @@ Re-implementation of the "iLnk"/"iLnkP2P"/"PPPP" protocol used on some cheap (\<
|
|||||||
|
|
||||||
Per pictures of the [X5](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb.jpg?raw=true), [A9](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb_a9.jpg?raw=true) the main chip is TXW817 ([chinese](https://www.taixin-semi.com/Product/ProductDetail?productId=306), [eng, google translate](https://www-taixin--semi-com.translate.goog/Product/ProductDetail?productId=306&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp))
|
Per pictures of the [X5](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb.jpg?raw=true), [A9](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb_a9.jpg?raw=true) the main chip is TXW817 ([chinese](https://www.taixin-semi.com/Product/ProductDetail?productId=306), [eng, google translate](https://www-taixin--semi-com.translate.goog/Product/ProductDetail?productId=306&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp))
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Run `make build` or `npm run build` to build the server artifact. You can also find some pre-built files [in the CI results](https://github.com/DavidVentura/cam-reverse/actions) or [in the releases](https://github.com/DavidVentura/cam-reverse/releases/)
|
||||||
|
|
||||||
|
## Pairing a new camera
|
||||||
|
|
||||||
|
Ensure your device in access point mode (the blue LED blinks slowly to indicate that); optionally, press the MODE button for 5s to switch to access point mode.
|
||||||
|
|
||||||
|
Connect to the device's access point (e.g., FTYC811847AGFDZ) and run `node dist/bin.cjs pair --ssid <SSID> --password <PASSWORD>`.
|
||||||
|
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
To execute the HTTP server, run `node bin.cjs`; you can access the JPEG stream at http://localhost:5000/ and (optionally) a file `audio.pcm` will be created.
|
|
||||||
|
|
||||||
```bash
|
To execute the HTTP server, run `node dist/bin.cjs http_server`; you can access the JPEG stream at http://localhost:5000/.
|
||||||
$ node dist/bin.cjs http_server --help
|
|
||||||
|
|
||||||
start http server
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help Show help [boolean]
|
|
||||||
--version Show version number [boolean]
|
|
||||||
--debug [boolean] [default: false]
|
|
||||||
--ansi [boolean] [default: false]
|
|
||||||
--audio [boolean] [default: false]
|
|
||||||
--discovery_ip [default: "192.168.1.255"]
|
|
||||||
--attempt_to_fix_packet_loss [default: false]
|
|
||||||
--port HTTP Port to listen on [number] [default: 5000]
|
|
||||||
```
|
|
||||||
|
|
||||||
The roundtrip delay when using MJPEG is [~350ms](pics/delay.jpg?raw=true).
|
The roundtrip delay when using MJPEG is [~350ms](pics/delay.jpg?raw=true).
|
||||||
|
|
||||||
@@ -32,28 +27,11 @@ There's a basic UI which can display multiple cameras:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
The server will send a broadcast packet every few seconds to discover all the cameras available; this means that it *must* run in the same broadcast domain (VLAN) as your cameras.
|
The server will send a broadcast packet every few seconds to discover all the cameras available; this means that it *must* run in the same broadcast domain (VLAN) as your cameras. For debugging purposes, you can send the packets to a specific camera by setting `--discovery_ip` to its IP address.
|
||||||
|
|
||||||
Clicking on the image will take you to a page that has audio streaming enabled.
|
Clicking on the image will take you to a page that has audio streaming. Click the button below the image to mute/unmute the audio.
|
||||||
|
|
||||||
## Pairing a new camera
|
To customize the camera names, edit the file `cameras.txt` and add the names of your cameras, one per line, in the format `CAMERA_ID=descriptive name` (you must restart the HTTP server for the changes to take effect).
|
||||||
|
|
||||||
Connect to the device's access point and run `node bin.cjs pair --ssid <SSID> --password <PASSWORD>`.
|
|
||||||
```bash
|
|
||||||
$ node dist/bin.cjs pair --help
|
|
||||||
|
|
||||||
configure a camera
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help Show help [boolean]
|
|
||||||
--version Show version number [boolean]
|
|
||||||
--debug [boolean] [default: false]
|
|
||||||
--ansi [boolean] [default: false]
|
|
||||||
--discovery_ip [default: "192.168.1.255"]
|
|
||||||
--attempt_to_fix_packet_loss [default: false]
|
|
||||||
--ssid [string] [required]
|
|
||||||
--password [string] [required]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
@@ -181,6 +159,8 @@ Every 8-10s
|
|||||||
|
|
||||||
There are some other strings in the APK ending in `-$$` which decode to other ips/hostnames.
|
There are some other strings in the APK ending in `-$$` which decode to other ips/hostnames.
|
||||||
|
|
||||||
|
Most of the IPs point to AWS compute instances, and this connection is probably used to see live streams over the Internet using the app. It's fine (and recommended!) to block outgoing traffic from the cameras, as it won't affect the HTTP server.
|
||||||
|
|
||||||
|
|
||||||
## Other stuff
|
## Other stuff
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,62 @@
|
|||||||
<html>
|
<html>
|
||||||
<h2>${id}</h2><a href="/camera/${id}"><img src="/camera/${id}"/></a><hr/>
|
<head>
|
||||||
|
<link rel="shortcut icon" href="/favicon.ico">
|
||||||
|
<title>${name}</title>
|
||||||
|
</head><body>
|
||||||
|
<h2>${name}</h2><a href="/camera/${id}"><img style="height: 640px" src="/camera/${id}"/></a><hr/>
|
||||||
|
<button onclick="toggle_audio()" id=audio disabled=true>Audio: disabled</button>
|
||||||
|
<button onclick="fetch('/rotate/${id}')">Rotate</button>
|
||||||
|
<button onclick="fetch('/mirror/${id}')">Mirror</button>
|
||||||
<script>
|
<script>
|
||||||
const alaw_to_s16_table = [
|
const alaw_to_s16_table = [
|
||||||
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752,
|
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752,
|
||||||
-2624, -3008, -2880, -2240, -2112, -2496, -2368, -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, -22016,
|
-2624, -3008, -2880, -2240, -2112, -2496, -2368, -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, -22016,
|
||||||
-20992, -24064, -23040, -17920, -16896, -19968, -18944, -30208, -29184, -32256, -31232, -26112, -25088, -28160,
|
-20992, -24064, -23040, -17920, -16896, -19968, -18944, -30208, -29184, -32256, -31232, -26112, -25088, -28160,
|
||||||
-27136, -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, -15104, -14592, -16128, -15616, -13056, -12544,
|
-27136, -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, -15104, -14592, -16128, -15616, -13056, -12544,
|
||||||
-14080, -13568, -344, -328, -376, -360, -280, -264, -312, -296, -472, -456, -504, -488, -408, -392, -440, -424, -88,
|
-14080, -13568, -344, -328, -376, -360, -280, -264, -312, -296, -472, -456, -504, -488, -408, -392, -440, -424, -88,
|
||||||
-72, -120, -104, -24, -8, -56, -40, -216, -200, -248, -232, -152, -136, -184, -168, -1376, -1312, -1504, -1440, -1120,
|
-72, -120, -104, -24, -8, -56, -40, -216, -200, -248, -232, -152, -136, -184, -168, -1376, -1312, -1504, -1440, -1120,
|
||||||
-1056, -1248, -1184, -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, -688, -656, -752, -720, -560, -528, -624,
|
-1056, -1248, -1184, -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, -688, -656, -752, -720, -560, -528, -624,
|
||||||
-592, -944, -912, -1008, -976, -816, -784, -880, -848, 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, 7552, 7296,
|
-592, -944, -912, -1008, -976, -816, -784, -880, -848, 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, 7552, 7296,
|
||||||
8064, 7808, 6528, 6272, 7040, 6784, 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, 3776, 3648, 4032, 3904, 3264,
|
8064, 7808, 6528, 6272, 7040, 6784, 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, 3776, 3648, 4032, 3904, 3264,
|
||||||
3136, 3520, 3392, 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, 30208, 29184, 32256, 31232, 26112, 25088,
|
3136, 3520, 3392, 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, 30208, 29184, 32256, 31232, 26112, 25088,
|
||||||
28160, 27136, 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, 15104, 14592, 16128, 15616, 13056, 12544, 14080,
|
28160, 27136, 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, 15104, 14592, 16128, 15616, 13056, 12544, 14080,
|
||||||
13568, 344, 328, 376, 360, 280, 264, 312, 296, 472, 456, 504, 488, 408, 392, 440, 424, 88, 72, 120, 104, 24, 8, 56,
|
13568, 344, 328, 376, 360, 280, 264, 312, 296, 472, 456, 504, 488, 408, 392, 440, 424, 88, 72, 120, 104, 24, 8, 56,
|
||||||
40, 216, 200, 248, 232, 152, 136, 184, 168, 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, 1888, 1824, 2016, 1952,
|
40, 216, 200, 248, 232, 152, 136, 184, 168, 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, 1888, 1824, 2016, 1952,
|
||||||
1632, 1568, 1760, 1696, 688, 656, 752, 720, 560, 528, 624, 592, 944, 912, 1008, 976, 816, 784, 880, 848,
|
1632, 1568, 1760, 1696, 688, 656, 752, 720, 560, 528, 624, 592, 944, 912, 1008, 976, 816, 784, 880, 848,
|
||||||
];
|
];
|
||||||
|
|
||||||
const alaw_to_s16 = (a_val) => {
|
const alaw_to_s16 = (a_val) => {
|
||||||
return alaw_to_s16_table[a_val];
|
return alaw_to_s16_table[a_val];
|
||||||
};
|
};
|
||||||
|
|
||||||
const audio_context = new AudioContext();
|
var audio_context;
|
||||||
|
const audio_button = document.getElementById('audio');
|
||||||
|
audio_button.disabled = !${audio};
|
||||||
|
update_audio_button();
|
||||||
|
|
||||||
|
function setup_audio() {
|
||||||
|
audio_context = new AudioContext();
|
||||||
const gain_node = audio_context.createGain(); // Declare gain node
|
const gain_node = audio_context.createGain(); // Declare gain node
|
||||||
const channels =1;
|
const channels =1;
|
||||||
const sample_rate = 8000;
|
const sample_rate = 8000;
|
||||||
const audioBuffer = audio_context.createBuffer(channels, 960, sample_rate); // 960??
|
const audioBuffer = audio_context.createBuffer(channels, 960, sample_rate); // 960??
|
||||||
//const audioBuffer = audio_context.createBuffer(channels, decoded.length, sample_rate);
|
//const audioBuffer = audio_context.createBuffer(channels, decoded.length, sample_rate);
|
||||||
|
|
||||||
gain_node.connect(audio_context.destination); // Connect gain node to speakers
|
audio_context.onstatechange = () => {
|
||||||
|
console.log("Audio state is now ", audio_context.state);
|
||||||
|
update_audio_button(audio_context.state == "running");
|
||||||
|
};
|
||||||
|
|
||||||
|
gain_node.connect(audio_context.destination); // Connect gain node to speakers
|
||||||
audio_context.resume();
|
audio_context.resume();
|
||||||
|
|
||||||
const evtSource = new EventSource('/audio/${id}');
|
const evtSource = new EventSource("/audio/${id}");
|
||||||
evtSource.onopen = (e) => {
|
evtSource.onopen = (e) => {
|
||||||
console.log("evtsource open");
|
console.log("evtsource open");
|
||||||
}
|
}
|
||||||
|
evtSource.onerror = (e) => {
|
||||||
|
console.log("evtsource error", e);
|
||||||
|
}
|
||||||
let endsAt = 0;
|
let endsAt = 0;
|
||||||
let startAt = 0;
|
let startAt = 0;
|
||||||
evtSource.onmessage = (e) => {
|
evtSource.onmessage = (e) => {
|
||||||
@@ -55,8 +76,28 @@
|
|||||||
source_node.start(startAt);
|
source_node.start(startAt);
|
||||||
endsAt = now + audioBuffer.duration * 1000;
|
endsAt = now + audioBuffer.duration * 1000;
|
||||||
};
|
};
|
||||||
evtSource.onerror = (e) => {
|
}
|
||||||
console.log("evtsource error", e);
|
|
||||||
|
function update_audio_button(on) {
|
||||||
|
if (${audio}) {
|
||||||
|
audio_button.innerText = "Audio: " + (on ? "on \u{1F508}" : "off \u{1F507}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle_audio() {
|
||||||
|
if (audio_context == undefined) {
|
||||||
|
setup_audio();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (audio_context.state == "running") {
|
||||||
|
audio_context.suspend();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (audio_context.state == "suspended") {
|
||||||
|
audio_context.resume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Unknown audio stream status");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
BIN
Binary file not shown.
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# This is a simple name mapping for cameras, in the form
|
||||||
|
#
|
||||||
|
# CAMERA_ID=descriptive name
|
||||||
|
#
|
||||||
|
# Blank lines and lines starting with "#" are ignored.
|
||||||
|
#
|
||||||
|
# Any updates to this file require restarting the HTTP server.
|
||||||
|
# This file is meant to be updated manually, it is NOT updated dynamically.
|
||||||
|
|
||||||
|
FTYC811847AGFDZ=Office camera
|
||||||
+18
-9
@@ -5,17 +5,21 @@ import { serveHttp } from "../http_server.js";
|
|||||||
import { opt } from "../options.js";
|
import { opt } from "../options.js";
|
||||||
import { pair } from "../pair.js";
|
import { pair } from "../pair.js";
|
||||||
|
|
||||||
|
import { buildLogger } from "../logger.js";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.command(
|
.command(
|
||||||
"http_server",
|
"http_server",
|
||||||
"start http server",
|
"start http server",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("debug", { default: false })
|
|
||||||
.option("ansi", { default: false })
|
.option("ansi", { default: false })
|
||||||
.option("audio", { default: false })
|
.option("slow_startup", { default: false })
|
||||||
.boolean(["debug", "ansi", "audio"])
|
.option("audio", { describe: "Also stream audio from camera", default: true })
|
||||||
.option("discovery_ip", { default: "192.168.1.255" })
|
.option("color", { describe: "Use color in logs", default: undefined })
|
||||||
|
.boolean(["ansi", "audio", "slow_startup", "color"])
|
||||||
|
.option("log_level", { describe: "Set log level", default: "info" })
|
||||||
|
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||||
.option("attempt_to_fix_packet_loss", { default: false })
|
.option("attempt_to_fix_packet_loss", { default: false })
|
||||||
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
|
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
|
||||||
.number(["port"])
|
.number(["port"])
|
||||||
@@ -23,6 +27,7 @@ yargs(hideBin(process.argv))
|
|||||||
},
|
},
|
||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as opt;
|
const opts: opt = argv as opt;
|
||||||
|
buildLogger(argv.log_level, argv.color);
|
||||||
serveHttp(opts, argv.port, argv.audio || false);
|
serveHttp(opts, argv.port, argv.audio || false);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -31,17 +36,21 @@ yargs(hideBin(process.argv))
|
|||||||
"configure a camera",
|
"configure a camera",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("debug", { default: false })
|
|
||||||
.option("ansi", { default: false })
|
.option("ansi", { default: false })
|
||||||
.boolean(["debug", "ansi"])
|
.option("slow_startup", { default: false })
|
||||||
.option("discovery_ip", { default: "192.168.1.255" })
|
.option("color", { describe: "Use color in logs", default: undefined })
|
||||||
|
.boolean(["ansi", "slow_startup", "color"])
|
||||||
|
.option("log_level", { describe: "Set log level", default: "info" })
|
||||||
|
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||||
.option("attempt_to_fix_packet_loss", { default: false })
|
.option("attempt_to_fix_packet_loss", { default: false })
|
||||||
.demandOption("ssid")
|
.option("ssid", { describe: "Wifi network for the camera to connect to" })
|
||||||
.demandOption("password")
|
.option("password", { describe: "Wifi network password" })
|
||||||
|
.demandOption(["ssid", "password"])
|
||||||
.string(["ssid", "password"]);
|
.string(["ssid", "password"]);
|
||||||
},
|
},
|
||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as unknown as opt;
|
const opts: opt = argv as unknown as opt;
|
||||||
|
buildLogger(argv.log_level, argv.color);
|
||||||
pair({ opts, ssid: argv.ssid, password: argv.password });
|
pair({ opts, ssid: argv.ssid, password: argv.password });
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+17
-12
@@ -1,22 +1,21 @@
|
|||||||
import EventEmitter from "node:events";
|
import EventEmitter from "node:events";
|
||||||
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
|
import { create_LanSearchExt, create_LanSearch, parse_PunchPkt } from "./impl.js";
|
||||||
import { createSocket, RemoteInfo } from "node:dgram";
|
import { createSocket, RemoteInfo } from "node:dgram";
|
||||||
import { Commands } from "./datatypes.js";
|
import { Commands } from "./datatypes.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo, debug: boolean) => {
|
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => {
|
||||||
const ab = new Uint8Array(msg).buffer;
|
const ab = new Uint8Array(msg).buffer;
|
||||||
const dv = new DataView(ab);
|
const dv = new DataView(ab);
|
||||||
const cmd_id = dv.readU16();
|
const cmd_id = dv.readU16();
|
||||||
if (cmd_id != Commands.PunchPkt) {
|
if (cmd_id != Commands.PunchPkt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (debug) {
|
logger.debug("Received a PunchPkt message");
|
||||||
console.log("Discovery got a PunchPkt");
|
|
||||||
}
|
|
||||||
ee.emit("discover", rinfo, parse_PunchPkt(dv));
|
ee.emit("discover", rinfo, parse_PunchPkt(dv));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const discoverDevices = (debug: boolean, discovery_ip: string): EventEmitter => {
|
export const discoverDevices = (discovery_ip: string): EventEmitter => {
|
||||||
const sock = createSocket("udp4");
|
const sock = createSocket("udp4");
|
||||||
const SEND_PORT = 32108;
|
const SEND_PORT = 32108;
|
||||||
const ee = new EventEmitter();
|
const ee = new EventEmitter();
|
||||||
@@ -26,18 +25,24 @@ export const discoverDevices = (debug: boolean, discovery_ip: string): EventEmit
|
|||||||
sock.close();
|
sock.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo, debug));
|
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo));
|
||||||
|
|
||||||
sock.on("listening", () => {
|
sock.on("listening", () => {
|
||||||
sock.setBroadcast(true);
|
sock.setBroadcast(true);
|
||||||
console.log(`Searching for devices on ${discovery_ip}`);
|
logger.info(`Searching for devices on ${discovery_ip}`);
|
||||||
|
|
||||||
let buf = create_LanSearch();
|
let ls_buf = create_LanSearch();
|
||||||
|
let lse_buf = create_LanSearchExt();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
console.log(".");
|
logger.log("trace", `>> LanSearch`);
|
||||||
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
|
logger.log("trace", `>> LanSearchExt`);
|
||||||
|
sock.send(new Uint8Array(lse_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
|
logger.log("trace", `>> LanSearch`);
|
||||||
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
|
logger.log("trace", `>> LanSearchExt`);
|
||||||
|
sock.send(new Uint8Array(lse_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
});
|
});
|
||||||
|
|
||||||
sock.bind();
|
sock.bind();
|
||||||
|
|||||||
+28
-7
@@ -25,6 +25,9 @@ ilnk_proto.fields.encrypted = ProtoField.bool("iLnkP2P.encrypted", "Encrypted"
|
|||||||
ilnk_proto.fields.cmd_type = ProtoField.string("iLnkP2P.cmd_type", "Cmd Pkt Type")
|
ilnk_proto.fields.cmd_type = ProtoField.string("iLnkP2P.cmd_type", "Cmd Pkt Type")
|
||||||
ilnk_proto.fields.decrypted_data = ProtoField.bytes("iLnkP2P.decrypted_data", "Decrypted data")
|
ilnk_proto.fields.decrypted_data = ProtoField.bytes("iLnkP2P.decrypted_data", "Decrypted data")
|
||||||
|
|
||||||
|
-- PunchPkt
|
||||||
|
ilnk_proto.fields.serial = ProtoField.string("iLnkP2P.serial", "Serial")
|
||||||
|
|
||||||
lut = {
|
lut = {
|
||||||
[0xf1f0] = "Close",
|
[0xf1f0] = "Close",
|
||||||
[0xf132] = "LanSearchExt",
|
[0xf132] = "LanSearchExt",
|
||||||
@@ -46,11 +49,7 @@ lut = {
|
|||||||
[0xf169] = "ListenReqAck",
|
[0xf169] = "ListenReqAck",
|
||||||
[0xf170] = "RlyHelloAck",
|
[0xf170] = "RlyHelloAck",
|
||||||
[0xf171] = "RlyHelloAck2",
|
[0xf171] = "RlyHelloAck2",
|
||||||
__index = function(tbl, key)
|
|
||||||
return "UNK " .. string.format("0x%X", key)
|
|
||||||
end
|
|
||||||
}
|
}
|
||||||
setmetatable(lut, lut)
|
|
||||||
|
|
||||||
-- Define a function to dissect the packets
|
-- Define a function to dissect the packets
|
||||||
function ilnk_proto.dissector(buffer, pinfo, tree)
|
function ilnk_proto.dissector(buffer, pinfo, tree)
|
||||||
@@ -59,11 +58,22 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
|
|||||||
local subtree = tree:add(ilnk_proto, buffer(), "iLnkP2P")
|
local subtree = tree:add(ilnk_proto, buffer(), "iLnkP2P")
|
||||||
|
|
||||||
-- Add the entire packet as a field
|
-- Add the entire packet as a field
|
||||||
local packetname = lut[buffer(0, 2):uint()]
|
local packettype = buffer(0, 2):uint()
|
||||||
subtree:add(ilnk_proto.fields.type, packetname)
|
local packetname = lut[packettype]
|
||||||
|
packetname = packetname or "UNK " .. string.format("0x%X", packettype)
|
||||||
|
subtree:add(ilnk_proto.fields.type, buffer(0, 2), packetname)
|
||||||
|
|
||||||
-- Set the protocol description in the packet list
|
-- Set the protocol description in the packet list
|
||||||
pinfo.cols.protocol:set("iLnkP2P")
|
pinfo.cols.protocol:set("iLnkP2P")
|
||||||
|
pinfo.cols.info:set(packetname)
|
||||||
|
if packetname == "PunchPkt" or packetname == "P2pRdy" then
|
||||||
|
local len = buffer(2, 2)
|
||||||
|
subtree:add(ilnk_proto.fields.len, len)
|
||||||
|
local serial_prefix = buffer(4, 4):string()
|
||||||
|
local serial_no = UInt64(buffer(12, 4):uint(), buffer(8, 4):uint())
|
||||||
|
local serial_suffix = buffer(16, 5):string()
|
||||||
|
subtree:add(ilnk_proto.fields.serial, buffer(4, len:uint()-3), serial_prefix..serial_no..serial_suffix)
|
||||||
|
end
|
||||||
if packetname == "DrwAck" then
|
if packetname == "DrwAck" then
|
||||||
subtree:add(ilnk_proto.fields.len, buffer(2, 2))
|
subtree:add(ilnk_proto.fields.len, buffer(2, 2))
|
||||||
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
|
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
|
||||||
@@ -140,4 +150,15 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
|
|||||||
-- subtree:add(ilnk_proto.fields.payload, buffer(2, packet_length-2))
|
-- subtree:add(ilnk_proto.fields.payload, buffer(2, packet_length-2))
|
||||||
end
|
end
|
||||||
|
|
||||||
udp_table = DissectorTable.get("udp.port"):add(49512, ilnk_proto)
|
local function heuristic_checker(buffer, pinfo, tree)
|
||||||
|
length = buffer:len()
|
||||||
|
if length < 2 then return false end
|
||||||
|
local packetname = lut[buffer(0, 2):uint()]
|
||||||
|
if packetname ~= nil then
|
||||||
|
ilnk_proto.dissector(buffer, pinfo, tree)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
udp_table2 = DissectorTable.get("udp.port"):add(32108, ilnk_proto)
|
||||||
|
h = ilnk_proto:register_heuristic("udp", heuristic_checker)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Create a minimal EXIF segment with orientation
|
||||||
|
export const createExifOrientation = (orientation: number) => {
|
||||||
|
const tiffHeader = Buffer.from("49492A0008000000", "hex");
|
||||||
|
const ifdEntry = Buffer.concat([
|
||||||
|
Buffer.from("0100", "hex"), // Number of IFD entries
|
||||||
|
Buffer.from("1201030001000000", "hex"), // Tag, Type, Count
|
||||||
|
Buffer.from(orientation.toString(16).padStart(2, "0"), "hex"), // Orientation value
|
||||||
|
Buffer.from("0000", "hex"), // No more IFDs
|
||||||
|
Buffer.from("0000000000", "hex"), // padding??
|
||||||
|
]);
|
||||||
|
|
||||||
|
const exifData = Buffer.concat([Buffer.from("457869660000", "hex"), tiffHeader, ifdEntry]);
|
||||||
|
const segmentLength = Buffer.from([(exifData.length + 2) >> 8, (exifData.length + 2) & 0xff]);
|
||||||
|
const exifHeader = Buffer.concat([Buffer.from("FFE1", "hex"), segmentLength]);
|
||||||
|
|
||||||
|
return Buffer.concat([exifHeader, exifData]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addExifToJpeg = (jpegData: Buffer, exifSegment: Buffer) => {
|
||||||
|
// Check for existing EXIF (simplified check)
|
||||||
|
if (jpegData.includes(Buffer.from("FFE1", "hex"))) {
|
||||||
|
throw new Error("JPEG already contains EXIF segment");
|
||||||
|
}
|
||||||
|
|
||||||
|
const soiEnd = 2; // After FFD8
|
||||||
|
const modifiedJpeg = Buffer.concat([jpegData.subarray(0, soiEnd), exifSegment, jpegData.subarray(soiEnd)]);
|
||||||
|
return modifiedJpeg;
|
||||||
|
};
|
||||||
+14
-15
@@ -1,14 +1,14 @@
|
|||||||
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
|
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
|
||||||
import { XqBytesDec } from "./func_replacements.js";
|
import { XqBytesDec } from "./func_replacements.js";
|
||||||
import { hexdump } from "./hexdump.js";
|
|
||||||
import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
|
import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
|
||||||
import { Session } from "./session.js";
|
import { Session } from "./session.js";
|
||||||
import { u16_swap, u32_swap } from "./utils.js";
|
import { u16_swap, u32_swap } from "./utils.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export const notImpl = (_: Session, dv: DataView) => {
|
export const notImpl = (session: Session, dv: DataView) => {
|
||||||
const raw = dv.readU16();
|
const raw = dv.readU16();
|
||||||
const cmd = CommandsByValue[raw];
|
const cmd = CommandsByValue[raw];
|
||||||
console.log(`^^ ${cmd} (${raw.toString(16)}) and it's not implemented yet`);
|
logger.debug(`^^ ${cmd} (${raw.toString(16)}) and it's not implemented yet`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const noop = (_: Session, __: DataView) => {};
|
export const noop = (_: Session, __: DataView) => {};
|
||||||
@@ -25,11 +25,12 @@ export const handle_P2PAlive = (session: Session, _: DataView) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const handle_P2PRdy = (session: Session, _: DataView) => {
|
export const handle_P2PRdy = (session: Session, _: DataView) => {
|
||||||
|
// TODO - config
|
||||||
const b = SendUsrChk(session, "admin", "admin");
|
const b = SendUsrChk(session, "admin", "admin");
|
||||||
session.send(b);
|
session.send(b);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makePunchPkt = (dev: DevSerial): DataView => {
|
export const makeP2pRdy = (dev: DevSerial): DataView => {
|
||||||
const len = dev.prefix.length + dev.suffix.length + 8;
|
const len = dev.prefix.length + dev.suffix.length + 8;
|
||||||
const outbuf = new DataView(new Uint8Array(0x14).buffer); // 8 = serial u64
|
const outbuf = new DataView(new Uint8Array(0x14).buffer); // 8 = serial u64
|
||||||
outbuf.add(0).writeString(dev.prefix);
|
outbuf.add(0).writeString(dev.prefix);
|
||||||
@@ -44,15 +45,13 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
|||||||
const payload_len = u16_swap(dv.add(0xc).readU16());
|
const payload_len = u16_swap(dv.add(0xc).readU16());
|
||||||
|
|
||||||
if (start_type != 0x110a) {
|
if (start_type != 0x110a) {
|
||||||
console.error(`Expected start_type to be 0xa11, got 0x${start_type.toString(16)}`);
|
logger.error(`Expected start_type to be 0xa11, got 0x${start_type.toString(16)}`);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const rotate_chr = 4;
|
const rotate_chr = 4;
|
||||||
if (payload_len > rotate_chr) {
|
if (payload_len > rotate_chr) {
|
||||||
// 20 = 16 (header) + 4 (??)
|
// 20 = 16 (header) + 4 (??)
|
||||||
XqBytesDec(dv.add(20), payload_len - 4, rotate_chr);
|
XqBytesDec(dv.add(20), payload_len - 4, rotate_chr);
|
||||||
console.log("Decrypted");
|
|
||||||
console.log(hexdump(dv));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.ConnectUserAck) {
|
if (cmd_id == ControlCommands.ConnectUserAck) {
|
||||||
@@ -64,12 +63,12 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
|||||||
|
|
||||||
if (cmd_id == ControlCommands.DevStatusAck) {
|
if (cmd_id == ControlCommands.DevStatusAck) {
|
||||||
// ParseDevStatus -> offset relevant?
|
// ParseDevStatus -> offset relevant?
|
||||||
let charging = u32_swap(dv.add(0x28).readU32()) & 1; // 0x14000101 v 0x14000100
|
let charging = u32_swap(dv.add(0x28).readU32()) & 1 ? "" : "not "; // 0x14000101 v 0x14000100
|
||||||
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts?
|
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts
|
||||||
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
|
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
|
||||||
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
|
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
|
||||||
|
|
||||||
console.log(`charging? ${charging}, batlevel? ${power}, wifi dbm: ${dbm}`);
|
logger.info(`Camera ${session.devName}: ${charging}charging, battery at ${power / 1000}V, Wifi ${dbm} dBm`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.WifiSettingsAck) {
|
if (cmd_id == ControlCommands.WifiSettingsAck) {
|
||||||
@@ -89,17 +88,17 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
|||||||
dns2: dv.add(0x10c).readString(0x10),
|
dns2: dv.add(0x10c).readString(0x10),
|
||||||
};
|
};
|
||||||
const buf = SendListWifi(session);
|
const buf = SendListWifi(session);
|
||||||
console.log(`Current wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
logger.info(`Current Wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
||||||
return [buf];
|
return [buf];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.ListWifiAck) {
|
if (cmd_id == ControlCommands.ListWifiAck) {
|
||||||
let startat = 0x10;
|
let startat = 0x10;
|
||||||
let msg_len = 91;
|
let msg_len = 91;
|
||||||
console.log("payload len", payload_len);
|
logger.debug("payload len", payload_len);
|
||||||
let msg_count = (payload_len - 9) / msg_len;
|
let msg_count = (payload_len - 9) / msg_len;
|
||||||
let remote_msg_count = dv.add(startat).readU32LE();
|
let remote_msg_count = dv.add(startat).readU32LE();
|
||||||
console.log("should get messages:", msg_count, "in payload: ", remote_msg_count);
|
logger.debug("should get messages:", msg_count, "in payload: ", remote_msg_count);
|
||||||
startat += 4;
|
startat += 4;
|
||||||
let items = [];
|
let items = [];
|
||||||
for (let i = 0; i < msg_count; i++) {
|
for (let i = 0; i < msg_count; i++) {
|
||||||
@@ -113,9 +112,9 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
|||||||
mode: dv.add(startat + 0x54).readU32LE(),
|
mode: dv.add(startat + 0x54).readU32LE(),
|
||||||
channel: dv.add(startat + 0x58).readU32LE(),
|
channel: dv.add(startat + 0x58).readU32LE(),
|
||||||
};
|
};
|
||||||
console.log(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
logger.info(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
||||||
startat += msg_len;
|
startat += msg_len;
|
||||||
console.log("ended at", startat);
|
logger.debug("ended at", startat);
|
||||||
items.push(wifiListItem);
|
items.push(wifiListItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-24
@@ -1,18 +1,57 @@
|
|||||||
import { RemoteInfo } from "dgram";
|
import { RemoteInfo } from "dgram";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync, existsSync } from "node:fs";
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
|
|
||||||
|
import { logger } from "./logger.js";
|
||||||
import { opt } from "./options.js";
|
import { opt } from "./options.js";
|
||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial } from "./impl.js";
|
import { DevSerial, SendDevStatus } from "./impl.js";
|
||||||
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
||||||
|
import { addExifToJpeg, createExifOrientation } from "./exif.js";
|
||||||
|
|
||||||
let BOUNDARY = "a very good boundary line";
|
// @ts-expect-error TS2307
|
||||||
let responses: Record<string, http.ServerResponse[]> = {};
|
import favicon from "./cam.ico.gz";
|
||||||
let audioResponses: Record<string, http.ServerResponse[]> = {};
|
// @ts-expect-error TS2307
|
||||||
let sessions: Record<string, Session> = {};
|
import html_template from "./asd.html";
|
||||||
|
|
||||||
|
const BOUNDARY = "a very good boundary line";
|
||||||
|
const responses: Record<string, http.ServerResponse[]> = {};
|
||||||
|
const audioResponses: Record<string, http.ServerResponse[]> = {};
|
||||||
|
const sessions: Record<string, Session> = {};
|
||||||
|
|
||||||
|
// Text file containing the mapping of camera names.
|
||||||
|
const nameFile = "cameras.txt";
|
||||||
|
|
||||||
|
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
|
||||||
|
const oMap = [1, 8, 3, 6];
|
||||||
|
const oMapMirror = [2, 7, 4, 5];
|
||||||
|
const orientations = [1, 2, 3, 4, 5, 6, 7, 8].reduce((acc, cur) => {
|
||||||
|
return { [cur]: createExifOrientation(cur), ...acc };
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
let camSettings: { [key: string]: { orientation: number; mirror: boolean } } = {};
|
||||||
|
|
||||||
|
// Reads the mapping of serial numbers to camera names from the text file.
|
||||||
|
const cameraNames = Object.assign(
|
||||||
|
{},
|
||||||
|
...(existsSync(nameFile) ? readFileSync(nameFile, "utf8") : "")
|
||||||
|
.toString()
|
||||||
|
.replace(/\r\n/g, "\n")
|
||||||
|
.split("\n")
|
||||||
|
.filter((l) => !l.startsWith("#"))
|
||||||
|
.filter((l) => l.trim() != "")
|
||||||
|
.map((l) => {
|
||||||
|
let kv = l.split("=");
|
||||||
|
return { [kv[0].trim()]: kv[1].trim() };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Returns the camera name (custom name, if it exists, otherwise its ID).
|
||||||
|
const cameraName = (id: string): string => cameraNames[id] || id;
|
||||||
|
|
||||||
|
// The HTTP server.
|
||||||
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||||
|
logger.info(`Mapping camera names: ${JSON.stringify(cameraNames)}`);
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.url.startsWith("/ui/")) {
|
if (req.url.startsWith("/ui/")) {
|
||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
@@ -27,8 +66,12 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
res.end("Nothing online");
|
res.end("Nothing online");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ui = readFileSync("asd.html").toString();
|
const ui = html_template
|
||||||
res.end(ui.replace(/\${id}/g, devId));
|
.toString()
|
||||||
|
.replace(/\${id}/g, devId)
|
||||||
|
.replace(/\${name}/g, cameraName(devId))
|
||||||
|
.replace(/\${audio}/g, with_audio.toString());
|
||||||
|
res.end(ui);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (req.url.startsWith("/audio/")) {
|
if (req.url.startsWith("/audio/")) {
|
||||||
@@ -46,22 +89,46 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
}
|
}
|
||||||
res.setHeader("Content-Type", `text/event-stream`);
|
res.setHeader("Content-Type", `text/event-stream`);
|
||||||
audioResponses[devId].push(res);
|
audioResponses[devId].push(res);
|
||||||
|
logger.info(`Audio stream requested for camera ${devId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.url.startsWith("/camera/")) {
|
if (req.url.startsWith("/favicon.ico")) {
|
||||||
|
res.setHeader("Content-Type", "image/x-icon");
|
||||||
|
res.setHeader("Content-Encoding", "gzip");
|
||||||
|
res.end(favicon);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url.startsWith("/rotate/")) {
|
||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
console.log("requested for", devId);
|
let curPos = camSettings[devId]?.orientation || 0;
|
||||||
|
let nextPos = (curPos + 1) % 4;
|
||||||
|
logger.info(`Rotating ${devId} to ${nextPos}`);
|
||||||
|
camSettings[devId].orientation = nextPos;
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
} else if (req.url.startsWith("/mirror/")) {
|
||||||
|
let devId = req.url.split("/")[2];
|
||||||
|
logger.info(`Mirroring ${devId}`);
|
||||||
|
camSettings[devId].mirror = !camSettings[devId].mirror;
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
} else if (req.url.startsWith("/camera/")) {
|
||||||
|
let devId = req.url.split("/")[2];
|
||||||
|
logger.info(`Video stream requested for camera ${devId}`);
|
||||||
let s = sessions[devId];
|
let s = sessions[devId];
|
||||||
|
|
||||||
if (s === undefined) {
|
if (s === undefined) {
|
||||||
res.writeHead(400);
|
res.writeHead(400);
|
||||||
res.end("invalid ID");
|
res.end(`Camera ${devId} not discovered`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!s.connected) {
|
if (!s.connected) {
|
||||||
res.writeHead(400);
|
res.writeHead(400);
|
||||||
res.end("Nothing online");
|
res.end(`Camera ${devId} offline`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,35 +136,53 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
responses[devId].push(res);
|
responses[devId].push(res);
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
responses[devId] = responses[devId].filter((r) => r !== res);
|
responses[devId] = responses[devId].filter((r) => r !== res);
|
||||||
console.log("Conn closed, kicked");
|
logger.info(`Video stream closed for camera ${devId}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.write(`<html>`);
|
res.write("<html>");
|
||||||
|
res.write("<head>");
|
||||||
|
res.write(`<link rel="shortcut icon" href="/favicon.ico">`);
|
||||||
|
res.write("<title>All cameras</title>");
|
||||||
|
res.write("</head>");
|
||||||
|
res.write("<body>");
|
||||||
|
res.write("<h1>All cameras</h1><hr/>");
|
||||||
Object.keys(sessions).forEach((id) =>
|
Object.keys(sessions).forEach((id) =>
|
||||||
res.write(`<h2>${id}</h2><a href="/ui/${id}"><img src="/camera/${id}"/></a><hr/>`),
|
res.write(`<h2>${cameraName(id)}</h2><a href="/ui/${id}"><img src="/camera/${id}"/></a><hr/>`),
|
||||||
);
|
);
|
||||||
res.write(`</html>`);
|
res.write("</body>");
|
||||||
|
res.write("</html>");
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let devEv = discoverDevices(opts.debug, opts.discovery_ip);
|
let devEv = discoverDevices(opts.discovery_ip);
|
||||||
|
|
||||||
|
const startSession = (s: Session) => {
|
||||||
|
s.send(SendDevStatus(s));
|
||||||
|
startVideoStream(s);
|
||||||
|
logger.info(`Camera ${s.devName} is now ready to stream`);
|
||||||
|
};
|
||||||
|
|
||||||
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
|
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
|
||||||
if (dev.devId in sessions) {
|
if (dev.devId in sessions) {
|
||||||
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
|
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
|
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||||
responses[dev.devId] = [];
|
responses[dev.devId] = [];
|
||||||
audioResponses[dev.devId] = [];
|
audioResponses[dev.devId] = [];
|
||||||
const s = makeSession(Handlers, dev, rinfo, startVideoStream, opts);
|
const s = makeSession(Handlers, dev, rinfo, startSession, opts);
|
||||||
|
|
||||||
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
||||||
|
|
||||||
s.eventEmitter.on("frame", () => {
|
s.eventEmitter.on("frame", () => {
|
||||||
const assembled = Buffer.concat(s.curImage);
|
// Add an EXIF header to indicate if the image should be rotated or mirrored
|
||||||
|
let orientation = camSettings[dev.devId].orientation;
|
||||||
|
orientation = camSettings[dev.devId].mirror ? oMapMirror[orientation] : oMap[orientation];
|
||||||
|
const exifSegment = orientations[orientation];
|
||||||
|
const jpegHeader = addExifToJpeg(s.curImage[0], exifSegment);
|
||||||
|
const assembled = Buffer.concat([jpegHeader, ...s.curImage.slice(1)]);
|
||||||
responses[dev.devId].forEach((res) => {
|
responses[dev.devId].forEach((res) => {
|
||||||
res.write(header);
|
res.write(header);
|
||||||
res.write(assembled);
|
res.write(assembled);
|
||||||
@@ -105,7 +190,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
s.eventEmitter.on("disconnect", () => {
|
s.eventEmitter.on("disconnect", () => {
|
||||||
console.log("deleting from sessions");
|
logger.info(`Camera ${dev.devId} disconnected`);
|
||||||
delete sessions[dev.devId];
|
delete sessions[dev.devId];
|
||||||
});
|
});
|
||||||
if (with_audio) {
|
if (with_audio) {
|
||||||
@@ -120,8 +205,9 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
sessions[dev.devId] = s;
|
sessions[dev.devId] = s;
|
||||||
|
camSettings[dev.devId] = { orientation: 0, mirror: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Starting HTTP server on port ${port}`);
|
logger.info(`Starting HTTP server on port ${port}`);
|
||||||
server.listen(port);
|
server.listen(port);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -155,6 +155,13 @@ export const SendUsrChk = (session: Session, username: string, password: string)
|
|||||||
return makeDataReadWrite(session, ControlCommands.ConnectUser, cmd_payload);
|
return makeDataReadWrite(session, ControlCommands.ConnectUser, cmd_payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const create_LanSearchExt = (): DataView => {
|
||||||
|
const outbuf = new DataView(new Uint8Array(4).buffer);
|
||||||
|
outbuf.writeU16(Commands.LanSearchExt);
|
||||||
|
outbuf.add(2).writeU16(0x0);
|
||||||
|
return outbuf;
|
||||||
|
};
|
||||||
|
|
||||||
export const create_LanSearch = (): DataView => {
|
export const create_LanSearch = (): DataView => {
|
||||||
const outbuf = new DataView(new Uint8Array(4).buffer);
|
const outbuf = new DataView(new Uint8Array(4).buffer);
|
||||||
outbuf.writeU16(Commands.LanSearch);
|
outbuf.writeU16(Commands.LanSearch);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { isatty } from "node:tty";
|
||||||
|
import { addColors, config, createLogger, format, transports as wtransports } from "winston";
|
||||||
|
|
||||||
|
const myFormat = format.printf(({ level, message, timestamp }) => {
|
||||||
|
return `${timestamp} [${level}] ${message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const transports = {
|
||||||
|
console: new wtransports.Console(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export let logger = undefined;
|
||||||
|
|
||||||
|
export const buildLogger = (level: string, colorize: boolean | undefined) => {
|
||||||
|
let use_color = colorize === undefined ? isatty(1) : colorize;
|
||||||
|
const fmt = use_color
|
||||||
|
? format.combine(format.colorize(), format.timestamp(), myFormat)
|
||||||
|
: format.combine(format.timestamp(), myFormat);
|
||||||
|
logger = createLogger({
|
||||||
|
levels: { ...config.syslog.levels, trace: 10 },
|
||||||
|
level,
|
||||||
|
format: fmt,
|
||||||
|
transports: [transports.console],
|
||||||
|
});
|
||||||
|
addColors({ trace: "white" });
|
||||||
|
};
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
export type opt = {
|
export type opt = {
|
||||||
debug: boolean;
|
|
||||||
ansi: boolean;
|
ansi: boolean;
|
||||||
discovery_ip: string;
|
discovery_ip: string;
|
||||||
|
slow_startup: boolean;
|
||||||
attempt_to_fix_packet_loss: boolean;
|
attempt_to_fix_packet_loss: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+253
-6
@@ -5,16 +5,25 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.20.2",
|
"winston": "^3.13.0",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/yargs": "^17.0.32",
|
"@types/yargs": "^17.0.32",
|
||||||
|
"esbuild": "^0.20.2",
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^10.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@colors/colors": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.90"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@cspotcode/source-map-support": {
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
@@ -27,6 +36,16 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dabh/diagnostics": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
|
||||||
|
"dependencies": {
|
||||||
|
"colorspace": "1.1.x",
|
||||||
|
"enabled": "2.0.x",
|
||||||
|
"kuler": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||||
@@ -34,6 +53,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"aix"
|
"aix"
|
||||||
@@ -49,6 +69,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -64,6 +85,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -79,6 +101,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -94,6 +117,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -109,6 +133,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -124,6 +149,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -139,6 +165,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -154,6 +181,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -169,6 +197,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -184,6 +213,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -199,6 +229,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -214,6 +245,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -229,6 +261,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -244,6 +277,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -259,6 +293,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -274,6 +309,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -289,6 +325,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
@@ -304,6 +341,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
@@ -319,6 +357,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"sunos"
|
"sunos"
|
||||||
@@ -334,6 +373,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -349,6 +389,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -364,6 +405,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -431,6 +473,11 @@
|
|||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/triple-beam": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.32",
|
"version": "17.0.32",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
||||||
@@ -523,6 +570,11 @@
|
|||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/async": {
|
||||||
|
"version": "3.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
|
||||||
|
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -645,6 +697,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^1.9.3",
|
||||||
|
"color-string": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -661,6 +722,37 @@
|
|||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/color-string": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.0.0",
|
||||||
|
"simple-swizzle": "^0.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color/node_modules/color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color/node_modules/color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
|
||||||
|
},
|
||||||
|
"node_modules/colorspace": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
|
||||||
|
"dependencies": {
|
||||||
|
"color": "^3.1.3",
|
||||||
|
"text-hex": "1.0.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -722,10 +814,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/enabled": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.20.2",
|
"version": "0.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||||
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
@@ -779,6 +877,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fecha": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@@ -816,6 +919,11 @@
|
|||||||
"flat": "cli.js"
|
"flat": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fn.name": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@@ -929,8 +1037,12 @@
|
|||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
"dev": true
|
},
|
||||||
|
"node_modules/is-arrayish": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
|
||||||
},
|
},
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@@ -991,6 +1103,17 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-stream": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-unicode-supported": {
|
"node_modules/is-unicode-supported": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||||
@@ -1015,6 +1138,11 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kuler": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
@@ -1046,6 +1174,22 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/logform": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@colors/colors": "1.6.0",
|
||||||
|
"@types/triple-beam": "^1.3.2",
|
||||||
|
"fecha": "^4.2.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"triple-beam": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/make-error": {
|
"node_modules/make-error": {
|
||||||
"version": "1.3.6",
|
"version": "1.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
@@ -1136,8 +1280,7 @@
|
|||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
@@ -1169,6 +1312,14 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/one-time": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
|
||||||
|
"dependencies": {
|
||||||
|
"fn.name": "1.x.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/p-limit": {
|
"node_modules/p-limit": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
@@ -1238,6 +1389,19 @@
|
|||||||
"safe-buffer": "^5.1.0"
|
"safe-buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -1262,7 +1426,6 @@
|
|||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1278,6 +1441,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-stable-stringify": {
|
||||||
|
"version": "2.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
|
||||||
|
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/serialize-javascript": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||||
@@ -1287,6 +1458,30 @@
|
|||||||
"randombytes": "^2.1.0"
|
"randombytes": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-swizzle": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arrayish": "^0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stack-trace": {
|
||||||
|
"version": "0.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||||
|
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
@@ -1338,6 +1533,11 @@
|
|||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-hex": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -1350,6 +1550,14 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/triple-beam": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-node": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
@@ -1422,12 +1630,51 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/winston": {
|
||||||
|
"version": "3.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz",
|
||||||
|
"integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@colors/colors": "^1.6.0",
|
||||||
|
"@dabh/diagnostics": "^2.0.2",
|
||||||
|
"async": "^3.2.3",
|
||||||
|
"is-stream": "^2.0.0",
|
||||||
|
"logform": "^2.4.0",
|
||||||
|
"one-time": "^1.0.0",
|
||||||
|
"readable-stream": "^3.4.0",
|
||||||
|
"safe-stable-stringify": "^2.3.1",
|
||||||
|
"stack-trace": "0.0.x",
|
||||||
|
"triple-beam": "^1.3.0",
|
||||||
|
"winston-transport": "^4.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/winston-transport": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==",
|
||||||
|
"dependencies": {
|
||||||
|
"logform": "^2.3.2",
|
||||||
|
"readable-stream": "^3.6.0",
|
||||||
|
"triple-beam": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/workerpool": {
|
"node_modules/workerpool": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
|
||||||
|
|||||||
+3
-2
@@ -3,16 +3,17 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha tests",
|
"test": "mocha tests",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"build": "esbuild cmd/bin.ts --bundle --platform=node --outfile=dist/bin.cjs --target=node12"
|
"build": "esbuild cmd/bin.ts --bundle --platform=node --outfile=dist/bin.cjs --target=node12 --loader:.gz=binary --loader:.html=text"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/yargs": "^17.0.32",
|
"@types/yargs": "^17.0.32",
|
||||||
|
"esbuild": "^0.20.2",
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^10.2.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"esbuild": "^0.20.2",
|
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"winston": "^3.13.0",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,28 +4,33 @@ import { opt } from "./options.js";
|
|||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial } from "./impl.js";
|
import { DevSerial } from "./impl.js";
|
||||||
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
|
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; password: string }) => {
|
export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; password: string }) => {
|
||||||
console.log(`Will configure any devices found to join ${ssid}`);
|
logger.info(`Will configure any devices found to join ${ssid}`);
|
||||||
let sessions: Record<string, Session> = {};
|
let sessions: Record<string, Session> = {};
|
||||||
|
|
||||||
let devEv = discoverDevices(opts.debug, opts.discovery_ip);
|
let devEv = discoverDevices(opts.discovery_ip);
|
||||||
if (password == "") {
|
if (password == "") {
|
||||||
throw new Error("You must set a non-zero-length password");
|
throw new Error("You must set a non-zero-length password");
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLogin = configureWifi(ssid, password);
|
const onLogin = (s: Session) => {
|
||||||
|
configureWifi(ssid, password)(s);
|
||||||
|
logger.info(`WiFi config for camera ${s.devName} is done`);
|
||||||
|
};
|
||||||
|
|
||||||
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
|
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
|
||||||
if (dev.devId in sessions) {
|
if (dev.devId in sessions) {
|
||||||
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
|
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
|
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||||
const s = makeSession(Handlers, dev, rinfo, onLogin, opts);
|
const s = makeSession(Handlers, dev, rinfo, onLogin, opts);
|
||||||
|
|
||||||
s.eventEmitter.on("disconnect", () => {
|
s.eventEmitter.on("disconnect", () => {
|
||||||
console.log("deleting from sessions");
|
logger.info(`Camera ${dev.devId} disconnected`);
|
||||||
|
logger.info("Press CONTROL+C if you're done setting up your cameras");
|
||||||
delete sessions[dev.devId];
|
delete sessions[dev.devId];
|
||||||
});
|
});
|
||||||
sessions[dev.devId] = s;
|
sessions[dev.devId] = s;
|
||||||
|
|||||||
+30
-22
@@ -2,10 +2,10 @@ import { createSocket, RemoteInfo } from "node:dgram";
|
|||||||
import EventEmitter from "node:events";
|
import EventEmitter from "node:events";
|
||||||
|
|
||||||
import { Commands, CommandsByValue } from "./datatypes.js";
|
import { Commands, CommandsByValue } from "./datatypes.js";
|
||||||
import { handle_Drw, handle_P2PAlive, handle_P2PRdy, makePunchPkt, noop, notImpl } from "./handlers.js";
|
import { handle_Drw, handle_P2PAlive, handle_P2PRdy, makeP2pRdy, noop, notImpl } from "./handlers.js";
|
||||||
import { hexdump } from "./hexdump.js";
|
|
||||||
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
|
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
|
||||||
import { opt } from "./options.js";
|
import { opt } from "./options.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export type Session = {
|
export type Session = {
|
||||||
send: (msg: DataView) => void;
|
send: (msg: DataView) => void;
|
||||||
@@ -21,6 +21,7 @@ export type Session = {
|
|||||||
rcvSeqId: number;
|
rcvSeqId: number;
|
||||||
frame_is_bad: boolean;
|
frame_is_bad: boolean;
|
||||||
frame_was_fixed: boolean;
|
frame_was_fixed: boolean;
|
||||||
|
started: boolean;
|
||||||
options: opt;
|
options: opt;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,10 +38,7 @@ const handleIncoming: msgCb = (session, handlers, msg, rinfo) => {
|
|||||||
const ab = new Uint8Array(msg).buffer;
|
const ab = new Uint8Array(msg).buffer;
|
||||||
const dv = new DataView(ab);
|
const dv = new DataView(ab);
|
||||||
const cmd = CommandsByValue[dv.readU16()];
|
const cmd = CommandsByValue[dv.readU16()];
|
||||||
if (session.options.debug) {
|
logger.log("trace", `<< ${cmd}`);
|
||||||
console.log(`<< ${cmd}`);
|
|
||||||
console.log(hexdump(msg.buffer, { ansi: session.options.ansi, ansiColor: 1 }));
|
|
||||||
}
|
|
||||||
handlers[cmd](session, dv, rinfo);
|
handlers[cmd](session, dv, rinfo);
|
||||||
session.lastReceivedPacket = Date.now();
|
session.lastReceivedPacket = Date.now();
|
||||||
};
|
};
|
||||||
@@ -62,20 +60,34 @@ export const makeSession = (
|
|||||||
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
|
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
|
||||||
|
|
||||||
sock.on("listening", () => {
|
sock.on("listening", () => {
|
||||||
const buf = makePunchPkt(dev);
|
const startup = () => {
|
||||||
session.send(buf);
|
const buf = makeP2pRdy(dev);
|
||||||
|
session.send(buf);
|
||||||
|
// The YsxLite sends a P2PAlive message immediately after P2PRdy
|
||||||
|
const alive_buf = create_P2pAlive();
|
||||||
|
session.send(alive_buf);
|
||||||
|
session.started = true;
|
||||||
|
};
|
||||||
|
if (options.slow_startup) {
|
||||||
|
logger.info("Delaying session startup");
|
||||||
|
setTimeout(startup, 6500);
|
||||||
|
} else {
|
||||||
|
startup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const SEND_PORT = 32108;
|
const SEND_PORT = 32108;
|
||||||
sock.bind();
|
sock.bind();
|
||||||
const sessTimer = setInterval(() => {
|
const sessTimer = setInterval(() => {
|
||||||
const delta = Date.now() - session.lastReceivedPacket;
|
const delta = Date.now() - session.lastReceivedPacket;
|
||||||
if (delta > 600) {
|
if (session.started) {
|
||||||
let buf = create_P2pAlive();
|
if (delta > 600) {
|
||||||
session.send(buf);
|
let buf = create_P2pAlive();
|
||||||
}
|
session.send(buf);
|
||||||
if (delta > 8000) {
|
}
|
||||||
session.eventEmitter.emit("disconnect");
|
if (delta > 8000) {
|
||||||
|
session.eventEmitter.emit("disconnect");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 400);
|
}, 400);
|
||||||
|
|
||||||
@@ -87,15 +99,11 @@ export const makeSession = (
|
|||||||
connected: true,
|
connected: true,
|
||||||
timers: [sessTimer],
|
timers: [sessTimer],
|
||||||
devName: dev.devId,
|
devName: dev.devId,
|
||||||
|
started: false,
|
||||||
send: (msg: DataView) => {
|
send: (msg: DataView) => {
|
||||||
const raw = msg.readU16();
|
const raw = msg.readU16();
|
||||||
const cmd = CommandsByValue[raw];
|
const cmd = CommandsByValue[raw];
|
||||||
if (options.debug) {
|
logger.log("trace", `>> ${cmd}`);
|
||||||
console.log(`>> ${cmd}`);
|
|
||||||
if (raw != Commands.P2PAlive) {
|
|
||||||
console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 0 }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
|
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
|
||||||
},
|
},
|
||||||
dst_ip: ra.address,
|
dst_ip: ra.address,
|
||||||
@@ -107,7 +115,7 @@ export const makeSession = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
session.eventEmitter.on("disconnect", () => {
|
session.eventEmitter.on("disconnect", () => {
|
||||||
console.log(`Disconnected from ${session.devName} - ${session.dst_ip}`);
|
logger.info(`Disconnected from camera ${session.devName} at ${session.dst_ip}`);
|
||||||
session.dst_ip = "0.0.0.0";
|
session.dst_ip = "0.0.0.0";
|
||||||
session.connected = false;
|
session.connected = false;
|
||||||
session.timers.forEach((x) => clearInterval(x));
|
session.timers.forEach((x) => clearInterval(x));
|
||||||
@@ -115,7 +123,7 @@ export const makeSession = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
session.eventEmitter.on("login", () => {
|
session.eventEmitter.on("login", () => {
|
||||||
console.log(`Logged in - ${session.devName}`);
|
logger.info(`Logging in to camera ${session.devName}`);
|
||||||
onLogin(session);
|
onLogin(session);
|
||||||
});
|
});
|
||||||
return session;
|
return session;
|
||||||
|
|||||||
+16
-2
@@ -3,9 +3,8 @@ import "../shim.ts";
|
|||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
|
||||||
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
||||||
import { createResponseForControlCommand } from "../handlers.js";
|
|
||||||
import { hexdump } from "../hexdump.js";
|
import { hexdump } from "../hexdump.js";
|
||||||
import { SendStartVideo, SendDevStatus, SendUsrChk, SendWifiDetails } from "../impl.ts";
|
import { SendStartVideo, SendDevStatus, SendUsrChk, SendWifiDetails, parse_PunchPkt } from "../impl.ts";
|
||||||
import { placeholderTypes, sprintf } from "../utils.js";
|
import { placeholderTypes, sprintf } from "../utils.js";
|
||||||
|
|
||||||
describe("debug_tools", () => {
|
describe("debug_tools", () => {
|
||||||
@@ -128,6 +127,21 @@ describe("events", () => {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe("parse packet", () => {
|
||||||
|
it("parses PunchPkt", () => {
|
||||||
|
const in_pkt_str = "f14100144241544400000000000262ca574f4e4a4d000000";
|
||||||
|
const pkt = new DataView(hstrToBA(in_pkt_str));
|
||||||
|
const expected = {
|
||||||
|
prefix: "BATD",
|
||||||
|
serial: "156362",
|
||||||
|
suffix: "WONJM",
|
||||||
|
serialU64: BigInt(156362),
|
||||||
|
devId: "BATD156362WONJM",
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(parse_PunchPkt(pkt), expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
describe("make packet", () => {
|
describe("make packet", () => {
|
||||||
it("builds a good SendUsrChk", () => {
|
it("builds a good SendUsrChk", () => {
|
||||||
const expected_str =
|
const expected_str =
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"preserveConstEnums": true,
|
"preserveConstEnums": true,
|
||||||
"outDir": "./build/",
|
"noEmit": true,
|
||||||
"rootDir": "./",
|
"rootDir": "./",
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"sourceMap": false
|
"sourceMap": false
|
||||||
|
|||||||
Reference in New Issue
Block a user