Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59c77729c | ||
|
|
21189a8ae9 | ||
|
|
8c71e84875 | ||
|
|
bac2b4fd4f | ||
|
|
c81d7d4182 | ||
|
|
a93564cd11 | ||
|
|
d7d3b91528 | ||
|
|
204677ceae | ||
|
|
03893f8d81 | ||
|
|
dc43a66209 | ||
|
|
6bed63b61f | ||
|
|
2a3d047f48 | ||
|
|
1edb7370cf | ||
|
|
6c7856054e | ||
|
|
f29206a1e1 | ||
|
|
449210ec23 | ||
|
|
3d003be371 | ||
|
|
7b153d6ff8 | ||
|
|
f5da7a34bd | ||
|
|
e496b33ff5 | ||
|
|
f9e2dc63dc | ||
|
|
80e50560e6 | ||
|
|
2ec4d603c6 | ||
|
|
45b8b159d1 | ||
|
|
c51e4f8a7a | ||
|
|
68fe0fb798 | ||
|
|
7ca795c917 | ||
|
|
d14e16c53d | ||
|
|
6ca18b057b | ||
|
|
1439d5f632 | ||
|
|
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
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ venv
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
dist/
|
dist/
|
||||||
|
cameras.txt
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -25,7 +23,7 @@ node_modules:
|
|||||||
npm install
|
npm install
|
||||||
|
|
||||||
test: node_modules
|
test: node_modules
|
||||||
./node_modules/.bin/mocha --require ts-node/register tests/fn.test.js
|
npm run test
|
||||||
|
|
||||||
install-wireshark-dissector:
|
install-wireshark-dissector:
|
||||||
mkdir -p ~/.local/lib/wireshark/plugins
|
mkdir -p ~/.local/lib/wireshark/plugins
|
||||||
|
|||||||
@@ -6,25 +6,28 @@ 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))
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Multi camera support
|
||||||
|
- Audio & video streaming
|
||||||
|
- Rotation / mirroring of video streams
|
||||||
|
- Friendly names for cameras
|
||||||
|
- Ability to configure "blank" cameras with Wifi settings
|
||||||
|
|
||||||
|
## 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 +35,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 +167,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.
@@ -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
|
||||||
+25
-9
@@ -1,3 +1,4 @@
|
|||||||
|
import process from "node:process";
|
||||||
import { hideBin } from "yargs/helpers";
|
import { hideBin } from "yargs/helpers";
|
||||||
import yargs from "yargs/yargs";
|
import yargs from "yargs/yargs";
|
||||||
|
|
||||||
@@ -5,17 +6,22 @@ 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, logger } from "../logger.js";
|
||||||
|
|
||||||
|
const majorVersion = process.versions.node.split(".").map(Number)[0];
|
||||||
|
|
||||||
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("audio", { describe: "Also stream audio from camera", default: true })
|
||||||
.boolean(["debug", "ansi", "audio"])
|
.option("color", { describe: "Use color in logs", default: undefined })
|
||||||
.option("discovery_ip", { default: "192.168.1.255" })
|
.boolean(["ansi", "audio", "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 +29,10 @@ yargs(hideBin(process.argv))
|
|||||||
},
|
},
|
||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as opt;
|
const opts: opt = argv as opt;
|
||||||
|
buildLogger(argv.log_level, argv.color);
|
||||||
|
if (majorVersion < 16) {
|
||||||
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
|
}
|
||||||
serveHttp(opts, argv.port, argv.audio || false);
|
serveHttp(opts, argv.port, argv.audio || false);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -31,17 +41,23 @@ 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("color", { describe: "Use color in logs", default: undefined })
|
||||||
.option("discovery_ip", { default: "192.168.1.255" })
|
.boolean(["ansi", "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);
|
||||||
|
if (majorVersion < 16) {
|
||||||
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
|
}
|
||||||
pair({ opts, ssid: argv.ssid, password: argv.password });
|
pair({ opts, ssid: argv.ssid, password: argv.password });
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ export const ControlCommands = {
|
|||||||
ListWifi: 0x0360, // CMD_NET_WIFI_SCAN
|
ListWifi: 0x0360, // CMD_NET_WIFI_SCAN
|
||||||
ListWifiAck: 0x0361,
|
ListWifiAck: 0x0361,
|
||||||
StartVideo: 0x1030, // CMD_PEER_LIVEVIDEO_START
|
StartVideo: 0x1030, // CMD_PEER_LIVEVIDEO_START
|
||||||
|
StartVideoAck: 0x1031,
|
||||||
StopVideo: 0x1130, // CMD_PEER_LIVEVIDEO_STOP
|
StopVideo: 0x1130, // CMD_PEER_LIVEVIDEO_STOP
|
||||||
Shutdown: 0x1010, //CMD_SYSTEM_SHUTDOWN,
|
Shutdown: 0x1010, //CMD_SYSTEM_SHUTDOWN,
|
||||||
Reboot: 0x1110, //CMD_SYSTEM_REBOOT,
|
Reboot: 0x1110, //CMD_SYSTEM_REBOOT,
|
||||||
VideoParamSet: 0x1830, // CMD_PEER_VIDEOPARAM_SET
|
VideoParamSet: 0x1830, // CMD_PEER_VIDEOPARAM_SET
|
||||||
|
VideoParamSetAck: 0x1831,
|
||||||
VideoParamGet: 0x1930, // CMD_PEER_VIDEOPARAM_GET
|
VideoParamGet: 0x1930, // CMD_PEER_VIDEOPARAM_GET
|
||||||
IRToggle: 0x0a30, // CMD_PEER_IRCUT_ONOFF
|
IRToggle: 0x0a30, // CMD_PEER_IRCUT_ONOFF
|
||||||
};
|
};
|
||||||
|
|||||||
+21
-16
@@ -1,22 +1,22 @@
|
|||||||
import EventEmitter from "node:events";
|
|
||||||
import { 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 EventEmitter from "node:events";
|
||||||
|
|
||||||
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo, debug: boolean) => {
|
import { Commands } from "./datatypes.js";
|
||||||
|
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
|
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,21 +26,26 @@ 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));
|
||||||
|
|
||||||
|
let timers = [];
|
||||||
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();
|
||||||
setInterval(() => {
|
let int = 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);
|
||||||
}, 2000);
|
}, 3000);
|
||||||
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
|
timers.push(int);
|
||||||
|
logger.log("trace", `>> LanSearch`);
|
||||||
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
});
|
});
|
||||||
|
|
||||||
sock.bind();
|
sock.bind();
|
||||||
|
|
||||||
|
sock.on("close", () => timers.forEach((timer) => clearInterval(timer)));
|
||||||
|
ee.on("close", () => sock.close());
|
||||||
return ee;
|
return ee;
|
||||||
};
|
};
|
||||||
|
|||||||
+85
-7
@@ -18,13 +18,30 @@ ilnk_proto.fields.start = ProtoField.uint16("iLnkP2P.start", "Start", base.HE
|
|||||||
ilnk_proto.fields.cmd_dest = ProtoField.uint16("iLnkP2P.cmd_dest", "Dest", base.HEX)
|
ilnk_proto.fields.cmd_dest = ProtoField.uint16("iLnkP2P.cmd_dest", "Dest", base.HEX)
|
||||||
ilnk_proto.fields.auth_token = ProtoField.bytes("iLnkP2P.auth_token", "CMD auth token", base.DASH)
|
ilnk_proto.fields.auth_token = ProtoField.bytes("iLnkP2P.auth_token", "CMD auth token", base.DASH)
|
||||||
ilnk_proto.fields.cmd_payload = ProtoField.bytes("iLnkP2P.payload", "CMD Payload", base.DASH)
|
ilnk_proto.fields.cmd_payload = ProtoField.bytes("iLnkP2P.payload", "CMD Payload", base.DASH)
|
||||||
|
ilnk_proto.fields.warning = ProtoField.string("iLnkP2P.warning", "Warning")
|
||||||
|
--
|
||||||
-- jpeg | audio | continuation type?
|
-- jpeg | audio | continuation type?
|
||||||
ilnk_proto.fields.data_payload = ProtoField.bytes("iLnkP2P.data_payload", "Data Payload", base.DASH)
|
ilnk_proto.fields.data_payload = ProtoField.bytes("iLnkP2P.data_payload", "Data Payload", base.DASH)
|
||||||
|
ilnk_proto.fields.payload_type = ProtoField.string("iLnkP2P.payload_type", "Payload type")
|
||||||
|
ilnk_proto.fields.payload_subtype = ProtoField.string("iLnkP2P.payload_type", "Payload type")
|
||||||
|
ilnk_proto.fields.payload_len = ProtoField.uint32("iLnkP2P.payload_len", "Payload len")
|
||||||
|
ilnk_proto.fields.frame_no = ProtoField.uint32("iLnkP2P.frame_no", "Frame no")
|
||||||
|
-- audio
|
||||||
|
ilnk_proto.fields.audio_header = ProtoField.bytes("iLnkP2P.audio_header", "Audio Header")
|
||||||
|
ilnk_proto.fields.hdr_type = ProtoField.uint16("iLnkP2P.hdr_type", "Header Type")
|
||||||
|
ilnk_proto.fields.hdr_streamid = ProtoField.uint16("iLnkP2P.hdr_streamid", "Header Stream ID")
|
||||||
|
ilnk_proto.fields.hdr_frameno = ProtoField.uint32("iLnkP2P.hdr_frameno", "Header Frame")
|
||||||
|
ilnk_proto.fields.hdr_len = ProtoField.uint16("iLnkP2P.hdr_len", "Header Len")
|
||||||
|
ilnk_proto.fields.hdr_ver = ProtoField.uint16("iLnkP2P.hdr_ver", "Header version")
|
||||||
|
ilnk_proto.fields.hdr_res = ProtoField.uint16("iLnkP2P.hdr_red", "Header resolution")
|
||||||
|
|
||||||
ilnk_proto.fields.encrypted = ProtoField.bool("iLnkP2P.encrypted", "Encrypted")
|
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 +63,13 @@ 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)
|
|
||||||
|
control_lut = {
|
||||||
|
[0x2010] = "ConnectUser",
|
||||||
|
[0x2011] = "ConnectUserAck",
|
||||||
|
[0x0811] = "ConnectUserAck",
|
||||||
|
}
|
||||||
|
|
||||||
-- 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 +78,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))
|
||||||
@@ -78,7 +108,12 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
|
|||||||
subtree:add(ilnk_proto.fields.len, b_pkt_len)
|
subtree:add(ilnk_proto.fields.len, b_pkt_len)
|
||||||
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
|
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
|
||||||
subtree:add(ilnk_proto.fields.m_stream_id, buffer(5, 1))
|
subtree:add(ilnk_proto.fields.m_stream_id, buffer(5, 1))
|
||||||
|
if pkt_len < 12 then
|
||||||
|
subtree:add(ilnk_proto.fields.warning, "Short read"):set_generated()
|
||||||
|
return
|
||||||
|
end
|
||||||
subtree:add(ilnk_proto.fields.pkt_seq, buffer(6, 2))
|
subtree:add(ilnk_proto.fields.pkt_seq, buffer(6, 2))
|
||||||
|
pinfo.cols.info:set(buffer(6, 2):uint())
|
||||||
local b_payload_len = buffer(0xc, 2)
|
local b_payload_len = buffer(0xc, 2)
|
||||||
local payload_len = buffer(0xc, 2):le_uint()
|
local payload_len = buffer(0xc, 2):le_uint()
|
||||||
|
|
||||||
@@ -127,7 +162,39 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
|
|||||||
subtree:add(ilnk_proto.fields.cmd_payload, payload_tvb:range(0, payload_len -4))
|
subtree:add(ilnk_proto.fields.cmd_payload, payload_tvb:range(0, payload_len -4))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
local payload_type
|
||||||
|
local payload_subtype
|
||||||
local payload_tvb = ByteArray.tvb(buffer(8, packet_length-8):bytes(), "Data Payload")
|
local payload_tvb = ByteArray.tvb(buffer(8, packet_length-8):bytes(), "Data Payload")
|
||||||
|
if payload_tvb:range(0, 4):uint() == 0xffd8ffdb then
|
||||||
|
payload_subtype = "new frame"
|
||||||
|
-- start of new frame
|
||||||
|
end
|
||||||
|
if payload_tvb:range(0, 4):uint() == 0x55aa15a8 then
|
||||||
|
payload_type = "audio"
|
||||||
|
subtree:add(ilnk_proto.fields.audio_header, buffer(8, 32))
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_type, buffer(12, 1))
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_streamid, buffer(13, 1))
|
||||||
|
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_frameno, buffer(20, 4), buffer(20, 4):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_len, buffer(24, 4), buffer(24, 4):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_ver, buffer(28, 1), buffer(28, 1):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_res, buffer(29, 1), buffer(29, 1):le_uint())
|
||||||
|
|
||||||
|
subtree:add(ilnk_proto.fields.payload_len, buffer(24, 4), buffer(24, 4):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.frame_no, buffer(20, 4), buffer(20, 4):le_uint())
|
||||||
|
if payload_tvb:range(4, 1):uint() == 0x06 then
|
||||||
|
payload_subtype = "audio data"
|
||||||
|
elseif payload_tvb:range(4, 1):uint() == 0x03 then
|
||||||
|
payload_subtype = "maybe audio metadata"
|
||||||
|
else
|
||||||
|
payload_subtype = "REALLY not sure audio data"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
payload_type = "jpeg"
|
||||||
|
payload_subtype = "jpeg continuation"
|
||||||
|
end
|
||||||
|
subtree:add(ilnk_proto.fields.payload_type, buffer(8, 4), payload_type)
|
||||||
|
subtree:add(ilnk_proto.fields.payload_subtype, buffer(12, 1), payload_subtype)
|
||||||
subtree:add(ilnk_proto.fields.data_payload, payload_tvb:range(0, packet_length-8))
|
subtree:add(ilnk_proto.fields.data_payload, payload_tvb:range(0, packet_length-8))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -140,4 +207,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;
|
||||||
|
};
|
||||||
+141
-93
@@ -1,14 +1,15 @@
|
|||||||
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";
|
||||||
|
import { hexdump } from "./hexdump.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 +26,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,122 +46,152 @@ 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) {
|
switch (cmd_id) {
|
||||||
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
|
case ControlCommands.ConnectUserAck:
|
||||||
session.ticket = [...c];
|
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
|
||||||
session.eventEmitter.emit("login");
|
session.ticket = [...c];
|
||||||
return [];
|
session.eventEmitter.emit("login");
|
||||||
}
|
return [];
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.DevStatusAck) {
|
case 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`);
|
||||||
}
|
return [];
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.WifiSettingsAck) {
|
case ControlCommands.WifiSettingsAck:
|
||||||
const wifiSettings = {
|
const wifiSettings = {
|
||||||
enable: dv.add(0x14).readU32(),
|
enable: dv.add(0x14).readU32(),
|
||||||
status: dv.add(0x18).readU32(),
|
status: dv.add(0x18).readU32(),
|
||||||
mode: dv.add(0x1c).readU32LE(),
|
mode: dv.add(0x1c).readU32LE(),
|
||||||
channel: dv.add(0x20).readU32(),
|
channel: dv.add(0x20).readU32(),
|
||||||
authtype: dv.add(0x24).readU32(),
|
authtype: dv.add(0x24).readU32(),
|
||||||
dhcp: dv.add(0x28).readU32(),
|
dhcp: dv.add(0x28).readU32(),
|
||||||
ssid: dv.add(0x2c).readString(0x20),
|
ssid: dv.add(0x2c).readString(0x20),
|
||||||
psk: dv.add(0x4c).readString(0x80),
|
psk: dv.add(0x4c).readString(0x80),
|
||||||
ip: dv.add(0xcc).readString(0x10),
|
ip: dv.add(0xcc).readString(0x10),
|
||||||
mask: dv.add(0xdc).readString(0x10),
|
mask: dv.add(0xdc).readString(0x10),
|
||||||
gw: dv.add(0xec).readString(0x10),
|
gw: dv.add(0xec).readString(0x10),
|
||||||
dns1: dv.add(0xfc).readString(0x10),
|
dns1: dv.add(0xfc).readString(0x10),
|
||||||
dns2: dv.add(0x10c).readString(0x10),
|
dns2: dv.add(0x10c).readString(0x10),
|
||||||
};
|
|
||||||
const buf = SendListWifi(session);
|
|
||||||
console.log(`Current wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
|
||||||
return [buf];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.ListWifiAck) {
|
|
||||||
let startat = 0x10;
|
|
||||||
let msg_len = 91;
|
|
||||||
console.log("payload len", payload_len);
|
|
||||||
let msg_count = (payload_len - 9) / msg_len;
|
|
||||||
let remote_msg_count = dv.add(startat).readU32LE();
|
|
||||||
console.log("should get messages:", msg_count, "in payload: ", remote_msg_count);
|
|
||||||
startat += 4;
|
|
||||||
let items = [];
|
|
||||||
for (let i = 0; i < msg_count; i++) {
|
|
||||||
const wifiListItem = {
|
|
||||||
// startat = msg_len * i + 0x14;
|
|
||||||
ssid: dv.add(startat).readString(0x40),
|
|
||||||
mac: dv.add(startat + 0x40).readByteArray(8),
|
|
||||||
security: dv.add(startat + 0x48).readU32LE(),
|
|
||||||
dbm0: dv.add(startat + 0x4c).readU32LE(),
|
|
||||||
dbm1: dv.add(startat + 0x50).readU32LE(),
|
|
||||||
mode: dv.add(startat + 0x54).readU32LE(),
|
|
||||||
channel: dv.add(startat + 0x58).readU32LE(),
|
|
||||||
};
|
};
|
||||||
console.log(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
const buf = SendListWifi(session);
|
||||||
startat += msg_len;
|
logger.info(`Current Wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
||||||
console.log("ended at", startat);
|
return [buf];
|
||||||
items.push(wifiListItem);
|
|
||||||
}
|
case ControlCommands.ListWifiAck:
|
||||||
|
if (payload_len == 4) {
|
||||||
|
logger.debug("ListWifi returned []");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let startat = 0x10;
|
||||||
|
let msg_len = 91;
|
||||||
|
let msg_count = (payload_len - 9) / msg_len;
|
||||||
|
let remote_msg_count = dv.add(startat).readU32LE();
|
||||||
|
logger.debug(`should get messages: ${msg_count} in payload: ${remote_msg_count}`);
|
||||||
|
startat += 4;
|
||||||
|
let items = [];
|
||||||
|
for (let i = 0; i < msg_count; i++) {
|
||||||
|
const wifiListItem = {
|
||||||
|
// startat = msg_len * i + 0x14;
|
||||||
|
ssid: dv.add(startat).readString(0x40),
|
||||||
|
mac: dv.add(startat + 0x40).readByteArray(8),
|
||||||
|
security: dv.add(startat + 0x48).readU32LE(),
|
||||||
|
dbm0: dv.add(startat + 0x4c).readU32LE(),
|
||||||
|
dbm1: dv.add(startat + 0x50).readU32LE(),
|
||||||
|
mode: dv.add(startat + 0x54).readU32LE(),
|
||||||
|
channel: dv.add(startat + 0x58).readU32LE(),
|
||||||
|
};
|
||||||
|
logger.info(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
||||||
|
startat += msg_len;
|
||||||
|
logger.debug("ended at", startat);
|
||||||
|
items.push(wifiListItem);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
case ControlCommands.StartVideoAck:
|
||||||
|
logger.debug("Start video ack");
|
||||||
|
return [];
|
||||||
|
case ControlCommands.VideoParamSetAck:
|
||||||
|
logger.debug("Video param set ack");
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
logger.info(`Unhandled control command: 0x${cmd_id.toString(16)}`);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const deal_with_data = (session: Session, dv: DataView) => {
|
const deal_with_data = (session: Session, dv: DataView) => {
|
||||||
const pkt_len = dv.add(2).readU16();
|
const pkt_len = dv.add(2).readU16();
|
||||||
// data
|
|
||||||
const JPEG_HEADER = [0xff, 0xd8, 0xff, 0xdb];
|
// 12 equals start of header (0x8) + header length (0x4)
|
||||||
const AUDIO_HEADER = [0x55, 0xaa, 0x15, 0xa8];
|
if (pkt_len < 12) {
|
||||||
const m_hdr = dv.add(8).readByteArray(4);
|
logger.debug("Got a short Drw packet, ignoring");
|
||||||
let is_new_image = true;
|
return;
|
||||||
let audio = true;
|
|
||||||
const pkt_id = dv.add(6).readU16();
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
is_new_image = is_new_image && m_hdr.add(i).readU8() == JPEG_HEADER[i];
|
|
||||||
audio = audio && m_hdr.add(i).readU8() == AUDIO_HEADER[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio) {
|
const FRAME_HEADER = [0x55, 0xaa, 0x15, 0xa8];
|
||||||
// "stream_head_t->type == 0x06" per pdf
|
const m_hdr = dv.add(8).readByteArray(4);
|
||||||
if (dv.add(12).readU8() == 0x06) {
|
const pkt_id = dv.add(6).readU16();
|
||||||
|
const STREAM_TYPE_AUDIO = 0x06;
|
||||||
|
const STREAM_TYPE_JPEG = 0x03;
|
||||||
|
|
||||||
|
const startNewFrame = (buf: ArrayBuffer) => {
|
||||||
|
if (session.curImage.length > 0 && !session.frame_is_bad) {
|
||||||
|
session.eventEmitter.emit("frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.frame_was_fixed = false;
|
||||||
|
session.frame_is_bad = false;
|
||||||
|
session.curImage = [Buffer.from(buf)];
|
||||||
|
session.rcvSeqId = pkt_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_framed = m_hdr.startsWith(FRAME_HEADER);
|
||||||
|
|
||||||
|
if (is_framed) {
|
||||||
|
const stream_type = dv.add(12).readU8();
|
||||||
|
if (stream_type == STREAM_TYPE_AUDIO) {
|
||||||
const audio_len = u16_swap(dv.add(8 + 16).readU16());
|
const audio_len = u16_swap(dv.add(8 + 16).readU16());
|
||||||
// may have received the next 'data' packet id as an audio frame -- jpeg was fine
|
|
||||||
if (pkt_id == session.rcvSeqId + 1) {
|
|
||||||
session.rcvSeqId = session.rcvSeqId + 1;
|
|
||||||
}
|
|
||||||
const audio_buf = dv.add(32 + 8).readByteArray(audio_len).buffer; // 8 for pkt header, 32 for `stream_head_t`
|
const audio_buf = dv.add(32 + 8).readByteArray(audio_len).buffer; // 8 for pkt header, 32 for `stream_head_t`
|
||||||
session.eventEmitter.emit("audio", { gap: false, data: Buffer.from(audio_buf) });
|
session.eventEmitter.emit("audio", { gap: false, data: Buffer.from(audio_buf) });
|
||||||
|
} else if (stream_type == STREAM_TYPE_JPEG) {
|
||||||
|
const to_read = pkt_len - 4 - 32;
|
||||||
|
if (to_read > 0) {
|
||||||
|
// some cameras do not send the data with the frame, but rather as a followup message
|
||||||
|
// skip 8 bytes (drw header) + 32 bytes (data frame)
|
||||||
|
const data = dv.add(32 + 8).readByteArray(to_read);
|
||||||
|
startNewFrame(data.buffer);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
logger.debug(`Ignoring data frame with stream type ${stream_type} - not implemented`);
|
||||||
// not sure what these are for, there's one per frame. maybe alignment?
|
// not sure what these are for, there's one per frame. maybe alignment?
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const JPEG_HEADER = [0xff, 0xd8, 0xff, 0xdb];
|
||||||
|
// a new JPEG image may begin either
|
||||||
|
// - as a frame with stream_type == 0x03
|
||||||
|
// - as unframed data, started by JPEG_HEADER
|
||||||
|
// but for both types of cameras, unframed data which does not start with JPEG_HEADER
|
||||||
|
// are segments of the (potentially already started) JPEG image
|
||||||
const data = dv.add(8).readByteArray(pkt_len - 4);
|
const data = dv.add(8).readByteArray(pkt_len - 4);
|
||||||
if (is_new_image) {
|
// this only happens on un-framed-cameras, which start the JPEG image directly
|
||||||
if (session.curImage.length > 0 && !session.frame_is_bad) {
|
let is_new_image = m_hdr.startsWith(JPEG_HEADER);
|
||||||
session.eventEmitter.emit("frame");
|
|
||||||
}
|
|
||||||
|
|
||||||
session.frame_was_fixed = false;
|
if (is_new_image) {
|
||||||
session.frame_is_bad = false;
|
startNewFrame(data.buffer);
|
||||||
session.curImage = [Buffer.from(data.buffer)];
|
|
||||||
session.rcvSeqId = pkt_id;
|
|
||||||
} else {
|
} else {
|
||||||
if (pkt_id <= session.rcvSeqId) {
|
if (pkt_id <= session.rcvSeqId) {
|
||||||
// retransmit
|
// retransmit
|
||||||
@@ -169,7 +201,10 @@ const deal_with_data = (session: Session, dv: DataView) => {
|
|||||||
let b = Buffer.from(data.buffer);
|
let b = Buffer.from(data.buffer);
|
||||||
|
|
||||||
if (pkt_id > session.rcvSeqId + 1) {
|
if (pkt_id > session.rcvSeqId + 1) {
|
||||||
session.frame_is_bad = true;
|
if (!session.frame_is_bad) {
|
||||||
|
session.frame_is_bad = true;
|
||||||
|
logger.debug(`Dropping corrupt frame ${pkt_id}, expected ${session.rcvSeqId + 1}`);
|
||||||
|
}
|
||||||
// this should always be enabled but currently it seems to cause more visual distortion
|
// this should always be enabled but currently it seems to cause more visual distortion
|
||||||
// than just missing some frames
|
// than just missing some frames
|
||||||
if (!session.options.attempt_to_fix_packet_loss) {
|
if (!session.options.attempt_to_fix_packet_loss) {
|
||||||
@@ -235,6 +270,17 @@ const makeDrwAck = (dv: DataView): DataView => {
|
|||||||
}
|
}
|
||||||
return outbuf;
|
return outbuf;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handle_DrwAck = (session: Session, dv: DataView) => {
|
||||||
|
const packetlen = dv.add(2).readU16();
|
||||||
|
const str_type = dv.add(4).readU8();
|
||||||
|
const str_id = dv.add(5).readU8();
|
||||||
|
const ack_count = dv.add(6).readU16();
|
||||||
|
for (let i = 0; i < ack_count; i++) {
|
||||||
|
const ack_id = dv.add(8 + i * 2).readU16();
|
||||||
|
session.ackDrw(ack_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
export const handle_Drw = (session: Session, dv: DataView) => {
|
export const handle_Drw = (session: Session, dv: DataView) => {
|
||||||
const ack = makeDrwAck(dv);
|
const ack = makeDrwAck(dv);
|
||||||
session.send(ack);
|
session.send(ack);
|
||||||
@@ -242,8 +288,10 @@ export const handle_Drw = (session: Session, dv: DataView) => {
|
|||||||
const m_stream = dv.add(5).readU8(); // data = 1, control = 0
|
const m_stream = dv.add(5).readU8(); // data = 1, control = 0
|
||||||
if (m_stream == 1) {
|
if (m_stream == 1) {
|
||||||
deal_with_data(session, dv);
|
deal_with_data(session, dv);
|
||||||
} else {
|
} else if (m_stream == 0) {
|
||||||
const b = createResponseForControlCommand(session, dv);
|
const b = createResponseForControlCommand(session, dv);
|
||||||
b.forEach(session.send);
|
b.forEach(session.send);
|
||||||
|
} else {
|
||||||
|
logger.warning(`Received a Drw packet with stream tag: ${m_stream}, which is not implemented`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+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(Buffer.from(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.debug(`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.debug(`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" });
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { createSocket, RemoteInfo } from "node:dgram";
|
||||||
|
export const mockServer = (onMessage: (msg: DataView) => Uint8Array[]) => {
|
||||||
|
const sock = createSocket("udp4");
|
||||||
|
const SEND_PORT = 32108;
|
||||||
|
sock.bind(SEND_PORT);
|
||||||
|
sock.on("message", (msg, rinfo: RemoteInfo) => {
|
||||||
|
const dv = new DataView(new Uint8Array(msg).buffer);
|
||||||
|
onMessage(dv).forEach((out) => {
|
||||||
|
sock.send(out, rinfo.port, rinfo.address);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return sock;
|
||||||
|
};
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
export type opt = {
|
export type opt = {
|
||||||
debug: boolean;
|
|
||||||
ansi: boolean;
|
ansi: boolean;
|
||||||
discovery_ip: string;
|
discovery_ip: string;
|
||||||
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",
|
||||||
|
|||||||
+6
-2
@@ -3,16 +3,20 @@
|
|||||||
"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"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,30 +2,42 @@ import { RemoteInfo } from "dgram";
|
|||||||
|
|
||||||
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, SendReboot, SendWifiSettings } 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) => {
|
||||||
|
logger.info(`Configuring camera ${s.devName}`);
|
||||||
|
configureWifi(ssid, password)(s);
|
||||||
|
logger.info(`WiFi config for camera ${s.devName} is done`);
|
||||||
|
|
||||||
|
logger.info(`Validating WiFi settings on ${s.devName}`);
|
||||||
|
s.send(SendWifiSettings(s));
|
||||||
|
|
||||||
|
logger.info(`Asking ${s.devName} to reboot`);
|
||||||
|
s.send(SendReboot(s));
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
+55
-27
@@ -2,13 +2,15 @@ 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_DrwAck, handle_P2PAlive, handle_P2PRdy, makeP2pRdy, notImpl, noop } 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;
|
||||||
|
ackDrw: (id: number) => void;
|
||||||
|
unackedDrw: { [id: number]: { sent_ts: number; data: DataView } };
|
||||||
outgoingCommandId: number;
|
outgoingCommandId: number;
|
||||||
ticket: number[];
|
ticket: number[];
|
||||||
eventEmitter: EventEmitter;
|
eventEmitter: EventEmitter;
|
||||||
@@ -21,6 +23,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,13 +39,13 @@ type msgCb = (
|
|||||||
const handleIncoming: msgCb = (session, handlers, msg, rinfo) => {
|
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 raw = dv.readU16();
|
||||||
if (session.options.debug) {
|
const cmd = CommandsByValue[raw];
|
||||||
console.log(`<< ${cmd}`);
|
logger.log("trace", `<< ${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();
|
if (raw != Commands.P2PAlive && raw != Commands.P2PAliveAck) {
|
||||||
|
session.lastReceivedPacket = Date.now();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeSession = (
|
export const makeSession = (
|
||||||
@@ -52,6 +55,7 @@ export const makeSession = (
|
|||||||
onLogin: (s: Session) => void,
|
onLogin: (s: Session) => void,
|
||||||
options: opt,
|
options: opt,
|
||||||
): Session => {
|
): Session => {
|
||||||
|
let unackedDrw = {};
|
||||||
const sock = createSocket("udp4");
|
const sock = createSocket("udp4");
|
||||||
|
|
||||||
sock.on("error", (err) => {
|
sock.on("error", (err) => {
|
||||||
@@ -62,41 +66,64 @@ 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 buf = makeP2pRdy(dev);
|
||||||
session.send(buf);
|
session.send(buf);
|
||||||
|
session.started = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
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 > 5000) {
|
||||||
|
session.eventEmitter.emit("disconnect");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 400);
|
}, 400);
|
||||||
|
|
||||||
|
const resendTimer = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, value] of Object.entries(session.unackedDrw)) {
|
||||||
|
const { sent_ts, data } = value;
|
||||||
|
if (now - sent_ts > 100) {
|
||||||
|
const pkt_id = data.add(6).readU16();
|
||||||
|
logger.debug(`Resending packet ${pkt_id} as ${session.outgoingCommandId}`);
|
||||||
|
data.add(6).writeU16(session.outgoingCommandId);
|
||||||
|
session.outgoingCommandId++;
|
||||||
|
delete session.unackedDrw[key];
|
||||||
|
session.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
outgoingCommandId: 0,
|
outgoingCommandId: 0,
|
||||||
ticket: [0, 0, 0, 0],
|
ticket: [0, 0, 0, 0],
|
||||||
lastReceivedPacket: 0,
|
lastReceivedPacket: 0,
|
||||||
eventEmitter: new EventEmitter(),
|
eventEmitter: new EventEmitter(),
|
||||||
connected: true,
|
connected: true,
|
||||||
timers: [sessTimer],
|
timers: [sessTimer, resendTimer],
|
||||||
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) {
|
// send command
|
||||||
console.log(`>> ${cmd}`);
|
if (raw == 0xf1d0 && msg.add(4).readU8() == 0xd1) {
|
||||||
if (raw != Commands.P2PAlive) {
|
const packet_id = msg.add(6).readU16();
|
||||||
console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 0 }));
|
logger.debug(`Sending Drw Packet with id ${packet_id}`);
|
||||||
}
|
unackedDrw[packet_id] = { sent_ts: Date.now(), data: msg };
|
||||||
}
|
}
|
||||||
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
|
logger.log("trace", `>> ${cmd}`);
|
||||||
|
sock.send(new Uint8Array(msg.buffer), ra.port, session.dst_ip);
|
||||||
|
},
|
||||||
|
ackDrw: (id: number) => {
|
||||||
|
logger.debug(`Removing ${id} from pending`);
|
||||||
|
delete unackedDrw[id];
|
||||||
},
|
},
|
||||||
dst_ip: ra.address,
|
dst_ip: ra.address,
|
||||||
curImage: [],
|
curImage: [],
|
||||||
@@ -104,10 +131,11 @@ export const makeSession = (
|
|||||||
frame_is_bad: false,
|
frame_is_bad: false,
|
||||||
frame_was_fixed: false,
|
frame_was_fixed: false,
|
||||||
options: options,
|
options: options,
|
||||||
|
unackedDrw,
|
||||||
};
|
};
|
||||||
|
|
||||||
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 +143,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;
|
||||||
@@ -138,13 +166,13 @@ export const Handlers: Record<keyof typeof Commands, PacketHandler> = {
|
|||||||
PunchPkt: notImpl,
|
PunchPkt: notImpl,
|
||||||
P2PAlive: handle_P2PAlive,
|
P2PAlive: handle_P2PAlive,
|
||||||
P2pRdy: handle_P2PRdy,
|
P2pRdy: handle_P2PRdy,
|
||||||
DrwAck: noop,
|
DrwAck: handle_DrwAck,
|
||||||
Drw: handle_Drw,
|
Drw: handle_Drw,
|
||||||
|
|
||||||
|
P2PAliveAck: noop,
|
||||||
Close: notImpl,
|
Close: notImpl,
|
||||||
LanSearchExt: notImpl,
|
LanSearchExt: notImpl,
|
||||||
LanSearch: notImpl,
|
LanSearch: notImpl,
|
||||||
P2PAliveAck: notImpl,
|
|
||||||
Hello: notImpl,
|
Hello: notImpl,
|
||||||
P2pReq: notImpl,
|
P2pReq: notImpl,
|
||||||
LstReq: notImpl,
|
LstReq: notImpl,
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ DataView.prototype.writeString = function (str) {
|
|||||||
const bytes = [...str].map((_, i) => str.charCodeAt(i));
|
const bytes = [...str].map((_, i) => str.charCodeAt(i));
|
||||||
return this.writeByteArray(bytes);
|
return this.writeByteArray(bytes);
|
||||||
};
|
};
|
||||||
|
DataView.prototype.startsWith = function (arr) {
|
||||||
|
if (this.byteLength < arr.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
if (this.add(i).readU8() != arr[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface DataView {
|
interface DataView {
|
||||||
@@ -77,6 +88,7 @@ declare global {
|
|||||||
writeU32(n: number): void;
|
writeU32(n: number): void;
|
||||||
writeU64(n: bigint): void;
|
writeU64(n: bigint): void;
|
||||||
writeU8(n: number): void;
|
writeU8(n: number): void;
|
||||||
|
startsWith(arr: number[]): boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default global;
|
export default global;
|
||||||
|
|||||||
+15
-6
@@ -2,10 +2,9 @@ import "../shim.ts";
|
|||||||
|
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
|
||||||
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 { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
||||||
|
import { parse_PunchPkt, SendDevStatus, SendStartVideo, SendUsrChk, SendWifiDetails } from "../impl.ts";
|
||||||
import { placeholderTypes, sprintf } from "../utils.js";
|
import { placeholderTypes, sprintf } from "../utils.js";
|
||||||
|
|
||||||
describe("debug_tools", () => {
|
describe("debug_tools", () => {
|
||||||
@@ -123,9 +122,19 @@ describe("module", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hstrToBA = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16))).buffer;
|
const hstrToBA = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16))).buffer;
|
||||||
describe("events", () => {
|
describe("parse packet", () => {
|
||||||
it("emits login event upon logging in", () => {
|
it("parses PunchPkt", () => {
|
||||||
// TODO
|
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", () => {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { mockServer } from "../mock_server.js";
|
||||||
|
import { discoverDevices } from "../discovery.js";
|
||||||
|
import { buildLogger } from "../logger.js";
|
||||||
|
import { Commands } from "../datatypes.js";
|
||||||
|
|
||||||
|
import assert from "assert";
|
||||||
|
|
||||||
|
const hstrToU8 = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16)));
|
||||||
|
describe("integration", () => {
|
||||||
|
it("discovers a device", () => {
|
||||||
|
// LanSearch (server) -> PunchPkt (camera) -> discovered serial
|
||||||
|
buildLogger("trace");
|
||||||
|
const EXPECTED_SERIAL = "BATD156362WONJM";
|
||||||
|
const punchPkt = "f14100144241544400000000000262ca574f4e4a4d000000";
|
||||||
|
const mockSock = mockServer((msg) => {
|
||||||
|
const cmd = msg.readU16();
|
||||||
|
if (cmd == Commands.LanSearch) {
|
||||||
|
const buf = hstrToU8(punchPkt);
|
||||||
|
return [buf];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
const ev = discoverDevices("127.0.0.1");
|
||||||
|
ev.on("discover", (rinfo, dev) => {
|
||||||
|
assert.deepEqual(dev.devId, EXPECTED_SERIAL);
|
||||||
|
ev.emit("close");
|
||||||
|
mockSock.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// TODO
|
||||||
|
it("emits login event upon logging in", () => {
|
||||||
|
// LanSearch (server) -> PunchPkt (camera)
|
||||||
|
// vvv need to call makeSession vvv
|
||||||
|
// P2pRdy (server) -> P2pRdy (camera)
|
||||||
|
// Drw<Login> (server) -> [DrwAck, Drw<LoginAck>]
|
||||||
|
});
|
||||||
|
});
|
||||||
+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