Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f19d215ba | ||
|
|
f85248bc6a | ||
|
|
80b1e4643b | ||
|
|
2e68a64e92 | ||
|
|
04fb1a8e4c | ||
|
|
2facd91fb5 | ||
|
|
75401621cf | ||
|
|
17d00eaba9 | ||
|
|
db98e6685c | ||
|
|
40dab0c017 | ||
|
|
6b28e91b9a | ||
|
|
cdc5be9948 | ||
|
|
4a6a58bd7b | ||
|
|
0db9cfd198 | ||
|
|
7b9d8fb5b7 | ||
|
|
0ef5581b3b | ||
|
|
5060a4d044 | ||
|
|
27ab7d9af2 | ||
|
|
ea533d786d | ||
|
|
e2751c65bc | ||
|
|
ceec51b22d | ||
|
|
a45b706ad0 | ||
|
|
f065fbca9a | ||
|
|
760dbc5820 | ||
|
|
d5d4161dd8 | ||
|
|
f4b7b1a463 | ||
|
|
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,29 @@ 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
|
### HTTP Server
|
||||||
$ node dist/bin.cjs http_server --help
|
To execute the HTTP server, run `node dist/bin.cjs http_server`; you can access the JPEG stream at http://localhost:5000/.
|
||||||
|
|
||||||
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,29 +36,61 @@ 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.
|
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.
|
||||||
|
|
||||||
Clicking on the image will take you to a page that has audio streaming enabled.
|
|
||||||
|
|
||||||
## Pairing a new camera
|
#### Settings
|
||||||
|
|
||||||
Connect to the device's access point and run `node bin.cjs pair --ssid <SSID> --password <PASSWORD>`.
|
You can provide a config file in `yml` format, then pass it as an argument: `node bin.cjs http_server --config_file <your_config.yml>`
|
||||||
```bash
|
|
||||||
$ node dist/bin.cjs pair --help
|
|
||||||
|
|
||||||
configure a camera
|
```yml
|
||||||
|
http_server:
|
||||||
|
port: 5000
|
||||||
|
|
||||||
Options:
|
logging:
|
||||||
--help Show help [boolean]
|
level: debug
|
||||||
--version Show version number [boolean]
|
use_color: true
|
||||||
--debug [boolean] [default: false]
|
|
||||||
--ansi [boolean] [default: false]
|
cameras:
|
||||||
--discovery_ip [default: "192.168.1.255"]
|
FTYC477360FAWUK:
|
||||||
--attempt_to_fix_packet_loss [default: false]
|
alias: "A9"
|
||||||
--ssid [string] [required]
|
rotate: 1
|
||||||
--password [string] [required]
|
mirror: false
|
||||||
|
fix_packet_loss: yes
|
||||||
|
audio: true
|
||||||
|
BATC609531EXLVS:
|
||||||
|
alias: "X5"
|
||||||
|
|
||||||
|
# If you are crossing broadcast domains (VLANs) then
|
||||||
|
# you need to specify all IPs as unicast targets
|
||||||
|
discovery_ips:
|
||||||
|
- 192.168.40.101
|
||||||
|
- 192.168.40.102
|
||||||
|
- 192.168.40.103
|
||||||
|
- 192.168.40.104
|
||||||
|
- 192.168.40.105
|
||||||
|
|
||||||
|
# If you are in the same broadcast domain, then
|
||||||
|
# it's easier to just use the broadcast address of your network
|
||||||
|
# discovery_ips:
|
||||||
|
# - 192.168.1.255
|
||||||
|
|
||||||
|
blacklisted_ips:
|
||||||
|
- 192.168.40.102
|
||||||
```
|
```
|
||||||
|
|
||||||
|
All keys are optional
|
||||||
|
|
||||||
|
You must restart the HTTP server for changes to the settings file to take effect.
|
||||||
|
|
||||||
|
### Single capture mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node bin.cjs frame --discovery_ip 192.168.40.104 --out out.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
The protocol is weirdly complex, though very little communication is necessary to use the device
|
The protocol is weirdly complex, though very little communication is necessary to use the device
|
||||||
@@ -181,6 +217,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,5 +1,12 @@
|
|||||||
<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,
|
||||||
@@ -22,20 +29,34 @@
|
|||||||
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);
|
||||||
|
|
||||||
|
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
|
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,39 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { DevSerial } from "./impl.js";
|
||||||
|
import { RemoteInfo } from "dgram";
|
||||||
|
import { logger } from "./logger.js";
|
||||||
|
import { startVideoStream } from "./session.js";
|
||||||
|
import { discoverDevices } from "./discovery.js";
|
||||||
|
import { Session } from "./session.js";
|
||||||
|
import { Handlers, makeSession } from "./session.js";
|
||||||
|
import { config } from "./settings.js";
|
||||||
|
|
||||||
|
const sessions: Record<string, Session> = {};
|
||||||
|
export const captureSingle = ({ discovery_ip, out_file }: { discovery_ip: string; out_file: string }) => {
|
||||||
|
let devEv = discoverDevices([discovery_ip]);
|
||||||
|
|
||||||
|
const startSession = (s: Session) => {
|
||||||
|
startVideoStream(s);
|
||||||
|
logger.info(`Camera ${s.devName} is now ready to stream`);
|
||||||
|
};
|
||||||
|
|
||||||
|
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
|
||||||
|
if (dev.devId in sessions) {
|
||||||
|
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||||
|
const s = makeSession(Handlers, dev, rinfo, startSession, 5000);
|
||||||
|
sessions[dev.devId] = s;
|
||||||
|
config.cameras[dev.devId] = { fix_packet_loss: false };
|
||||||
|
|
||||||
|
s.eventEmitter.on("frame", () => {
|
||||||
|
const assembled = Buffer.concat(s.curImage);
|
||||||
|
fs.writeFileSync(out_file, assembled);
|
||||||
|
logger.info(`Got frame. Exiting`);
|
||||||
|
devEv.emit("close");
|
||||||
|
s.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
+64
-19
@@ -1,9 +1,15 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
import { captureSingle } from "../capture_single.js";
|
||||||
import { serveHttp } from "../http_server.js";
|
import { serveHttp } from "../http_server.js";
|
||||||
import { opt } from "../options.js";
|
|
||||||
import { pair } from "../pair.js";
|
import { pair } from "../pair.js";
|
||||||
|
import { loadConfig, config } from "../settings.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(
|
||||||
@@ -11,19 +17,38 @@ yargs(hideBin(process.argv))
|
|||||||
"start http server",
|
"start http server",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("debug", { default: false })
|
.option("color", { describe: "Use color in logs" })
|
||||||
.option("ansi", { default: false })
|
.boolean(["audio", "color"])
|
||||||
.option("audio", { default: false })
|
.option("config_file", { describe: "Specify config file" })
|
||||||
.boolean(["debug", "ansi", "audio"])
|
.option("log_level", { describe: "Set log level" })
|
||||||
.option("discovery_ip", { default: "192.168.1.255" })
|
.option("discovery_ip", { describe: "Camera discovery IP address" })
|
||||||
.option("attempt_to_fix_packet_loss", { default: false })
|
.option("port", { describe: "HTTP Port to listen on" })
|
||||||
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
|
.string(["log_level", "discovery_ip", "config_file"])
|
||||||
.number(["port"])
|
.number(["port"])
|
||||||
.strict();
|
.strict();
|
||||||
},
|
},
|
||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as opt;
|
if (argv.config_file !== undefined) {
|
||||||
serveHttp(opts, argv.port, argv.audio || false);
|
loadConfig(argv.config_file);
|
||||||
|
}
|
||||||
|
if (argv.port) {
|
||||||
|
config.http_server.port = argv.port;
|
||||||
|
}
|
||||||
|
if (argv.color !== undefined) {
|
||||||
|
config.logging.use_color = argv.color;
|
||||||
|
}
|
||||||
|
if (argv.log_level !== undefined) {
|
||||||
|
config.logging.level = argv.log_level;
|
||||||
|
}
|
||||||
|
if (argv.discovery_ip !== undefined) {
|
||||||
|
config.discovery_ips = [argv.discovery_ip];
|
||||||
|
}
|
||||||
|
|
||||||
|
buildLogger(config.logging.level, config.logging.use_color);
|
||||||
|
if (majorVersion < 16) {
|
||||||
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
|
}
|
||||||
|
serveHttp(config.http_server.port);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.command(
|
.command(
|
||||||
@@ -31,18 +56,38 @@ yargs(hideBin(process.argv))
|
|||||||
"configure a camera",
|
"configure a camera",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("debug", { default: false })
|
.option("log_level", { describe: "Set log level", default: "info" })
|
||||||
.option("ansi", { default: false })
|
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||||
.boolean(["debug", "ansi"])
|
.option("ssid", { describe: "Wifi network for the camera to connect to" })
|
||||||
.option("discovery_ip", { default: "192.168.1.255" })
|
.option("password", { describe: "Wifi network password" })
|
||||||
.option("attempt_to_fix_packet_loss", { default: false })
|
.demandOption(["ssid", "password"])
|
||||||
.demandOption("ssid")
|
|
||||||
.demandOption("password")
|
|
||||||
.string(["ssid", "password"]);
|
.string(["ssid", "password"]);
|
||||||
},
|
},
|
||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as unknown as opt;
|
buildLogger(argv.log_level, undefined);
|
||||||
pair({ opts, ssid: argv.ssid, password: argv.password });
|
if (majorVersion < 16) {
|
||||||
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
|
}
|
||||||
|
pair({ ssid: argv.ssid, password: argv.password });
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.command(
|
||||||
|
"frame",
|
||||||
|
"capture a single frame from the first discovered camera",
|
||||||
|
(yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("log_level", { describe: "Set log level", default: "info" })
|
||||||
|
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||||
|
.option("out", { describe: "Path for output file" })
|
||||||
|
.demandOption(["out"])
|
||||||
|
.string(["out", "discovery_ip"]);
|
||||||
|
},
|
||||||
|
(argv) => {
|
||||||
|
buildLogger(argv.log_level, undefined);
|
||||||
|
if (majorVersion < 16) {
|
||||||
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
|
}
|
||||||
|
captureSingle({ discovery_ip: argv.discovery_ip, out_file: argv.out });
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
http_server:
|
||||||
|
port: 5000
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level: debug
|
||||||
|
use_color: true
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
FTYC477360FAWUK:
|
||||||
|
alias: "A9"
|
||||||
|
rotate: 1
|
||||||
|
mirror: false
|
||||||
|
fix_packet_loss: yes
|
||||||
|
audio: true
|
||||||
|
BATC609531EXLVS:
|
||||||
|
alias: "X5"
|
||||||
|
|
||||||
|
# If you are crossing broadcast domains (VLANs) then
|
||||||
|
# you need to specify all IPs as unicast targets
|
||||||
|
discovery_ips:
|
||||||
|
- 192.168.40.101
|
||||||
|
- 192.168.40.102
|
||||||
|
- 192.168.40.103
|
||||||
|
- 192.168.40.104
|
||||||
|
- 192.168.40.105
|
||||||
|
- 192.168.1.1
|
||||||
|
|
||||||
|
# If you are in the same broadcast domain, then
|
||||||
|
# it's easier to just use the broadcast address of your network
|
||||||
|
# discovery_ips:
|
||||||
|
# - 192.168.1.255
|
||||||
|
|
||||||
|
blacklisted_ips:
|
||||||
|
- 192.168.40.102
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-15
@@ -1,22 +1,27 @@
|
|||||||
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";
|
||||||
|
import { config } from "./settings.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) {
|
if (config.blacklisted_ips.indexOf(rinfo.address) !== -1) {
|
||||||
console.log("Discovery got a PunchPkt");
|
logger.debug(`Dropping packet of blacklisted IP: ${rinfo.address}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
logger.debug("Received a PunchPkt message");
|
||||||
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_ips: 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 +31,28 @@ 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", () => {
|
||||||
|
let ls_buf = create_LanSearch();
|
||||||
sock.setBroadcast(true);
|
sock.setBroadcast(true);
|
||||||
console.log(`Searching for devices on ${discovery_ip}`);
|
discovery_ips.forEach((discovery_ip) => {
|
||||||
|
logger.info(`Searching for devices on ${discovery_ip}`);
|
||||||
|
|
||||||
let buf = create_LanSearch();
|
let int = setInterval(() => {
|
||||||
setInterval(() => {
|
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
|
||||||
console.log(".");
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
|
}, 3000);
|
||||||
}, 2000);
|
timers.push(int);
|
||||||
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
|
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
+386
-7
@@ -18,13 +18,31 @@ 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.cmd_name = ProtoField.string("iLnkP2P.cmd_name", "Cmd Pkt")
|
||||||
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 +64,308 @@ 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 = {
|
||||||
|
[0x0000] = "CFGID_VERSION",
|
||||||
|
[0x0000] = "CMD_ACK_OK",
|
||||||
|
[0x0001] = "CFGID_LANGUAGE",
|
||||||
|
[0x0001] = "CMD_ACK_UNAUTH",
|
||||||
|
[0x0002] = "CFGID_PRODUCTE",
|
||||||
|
[0x0002] = "CMD_ACK_NO_PRIVILEGE",
|
||||||
|
[0x0003] = "CFGID_UPGRADE",
|
||||||
|
[0x0003] = "CMD_ACK_INVALID_PARAM",
|
||||||
|
[0x0004] = "CFGID_P2P",
|
||||||
|
[0x0004] = "CMD_ACK_CMDEXCUTE_FAILED",
|
||||||
|
[0x0005] = "CFGID_TZ",
|
||||||
|
[0x0005] = "CMD_ACK_NONE_RESULT",
|
||||||
|
[0x0006] = "CFGID_USER",
|
||||||
|
[0x0006] = "CMD_ACK_UNKNOWN",
|
||||||
|
[0x0007] = "CFGID_OPR",
|
||||||
|
[0x0008] = "CFGID_SERIAL",
|
||||||
|
[0x0009] = "CFGID_WIRED",
|
||||||
|
[0x000a] = "CFGID_WLAN",
|
||||||
|
[0x000b] = "CFGID_OSD",
|
||||||
|
[0x000c] = "CFGID_IMG",
|
||||||
|
[0x000d] = "CFGID_CMOS",
|
||||||
|
[0x000e] = "CFGID_PTZ",
|
||||||
|
[0x000f] = "CFGID_AUDIO",
|
||||||
|
[0x0010] = "CFGID_VIDEO",
|
||||||
|
[0x0011] = "CFGID_RECPOLICY",
|
||||||
|
[0x0012] = "CFGID_RESCH",
|
||||||
|
[0x0013] = "CFGID_MDALARM",
|
||||||
|
[0x0014] = "CFGID_ADCALARM",
|
||||||
|
[0x0015] = "CFGID_INPUTALARM",
|
||||||
|
[0x0016] = "CFGID_SMTP",
|
||||||
|
[0x0017] = "CFGID_FTP",
|
||||||
|
[0x0018] = "CFGID_PUSH",
|
||||||
|
[0x0019] = "CFGID_WLANPMK",
|
||||||
|
[0x00fe] = "CGICMD",
|
||||||
|
[0x00ff] = "BINCMD",
|
||||||
|
[0x03e8] = "CMD_ACK_ILLIGAL",
|
||||||
|
[0x0eff] = "CMD_DEV_BROADCAST",
|
||||||
|
[0x1000] = "CMD_SYSTEM_DFTCFG_IMPORT",
|
||||||
|
[0x1001] = "CMD_SYSTEM_DFTCFG_EXPORT",
|
||||||
|
[0x1002] = "CMD_SYSTEM_DFTCFG_RECOVERY",
|
||||||
|
[0x1003] = "CMD_SYSTEM_ITEMDFTCFG_RECOVERY",
|
||||||
|
[0x1004] = "CMD_SYSTEM_CRNCFG_EXPORT",
|
||||||
|
[0x1005] = "CMD_SYSTEM_CRNCFG_IMPORT",
|
||||||
|
[0x1006] = "CMD_SYSTEM_DFTCFG_CREATE",
|
||||||
|
[0x1007] = "CMD_SYSTEM_UPGRAD_SET",
|
||||||
|
[0x1008] = "CMD_SYSTEM_STATUS_GET",
|
||||||
|
[0x1009] = "CMD_SYSTEM_UPGRAD_GET",
|
||||||
|
[0x1010] = "CMD_SYSTEM_SHUTDOWN",
|
||||||
|
[0x1011] = "CMD_SYSTEM_REBOOT",
|
||||||
|
[0x1012] = "CMD_SYSTEM_INF_GET",
|
||||||
|
[0x1013] = "CMD_SYSTEM_ALIAS_SET",
|
||||||
|
[0x1020] = "CMD_SYSTEM_USER_CHK",
|
||||||
|
[0x1021] = "CMD_SYSTEM_USER_SET",
|
||||||
|
[0x1022] = "CMD_SYSTEM_USER_GET",
|
||||||
|
[0x1023] = "CMD_SYSTEM_USER_CHG",
|
||||||
|
[0x1030] = "CMD_SYSTEM_P2PPARAM_SET",
|
||||||
|
[0x1031] = "CMD_SYSTEM_OPRPOLICY_SET",
|
||||||
|
[0x1032] = "CMD_SYSTEM_OPRPOLICY_GET",
|
||||||
|
[0x1033] = "CMD_SYSTEM_P2PPARAM_GET",
|
||||||
|
[0x1040] = "CMD_SYSTEM_DATETIME_SET",
|
||||||
|
[0x1041] = "CMD_SYSTEM_DATETIME_GET",
|
||||||
|
[0x1051] = "CMD_NOTIFICATION",
|
||||||
|
[0x1100] = "ACK_SYSTEM_DFTCFG_IMPORT",
|
||||||
|
[0x1101] = "ACK_SYSTEM_DFTCFG_EXPORT",
|
||||||
|
[0x1102] = "ACK_SYSTEM_DFTCFG_RECOVERY",
|
||||||
|
[0x1103] = "ACK_SYSTEM_ITEMDFTCFG_RECOVERY",
|
||||||
|
[0x1104] = "ACK_SYSTEM_CRNCFG_EXPORT",
|
||||||
|
[0x1105] = "ACK_SYSTEM_CRNCFG_IMPORT",
|
||||||
|
[0x1106] = "ACK_SYSTEM_DFTCFG_CREATE",
|
||||||
|
[0x1107] = "ACK_SYSTEM_UPGRAD_SET",
|
||||||
|
[0x1108] = "ACK_SYSTEM_STATUS_GET",
|
||||||
|
[0x1109] = "ACK_SYSTEM_UPGRAD_GET",
|
||||||
|
[0x1110] = "ACK_SYSTEM_SHUTDOWN",
|
||||||
|
[0x1111] = "ACK_SYSTEM_REBOOT",
|
||||||
|
[0x1112] = "ACK_SYSTEM_INF_GET",
|
||||||
|
[0x1113] = "ACK_SYSTEM_ALIAS_SET",
|
||||||
|
[0x1120] = "ACK_SYSTEM_USER_CHK",
|
||||||
|
[0x1121] = "ACK_SYSTEM_USER_SET",
|
||||||
|
[0x1122] = "ACK_SYSTEM_USER_GET",
|
||||||
|
[0x1123] = "ACK_SYSTEM_USER_CHG",
|
||||||
|
[0x1130] = "ACK_SYSTEM_P2PPARAM_SET",
|
||||||
|
[0x1131] = "ACK_SYSTEM_OPRPOLICY_SET",
|
||||||
|
[0x1132] = "ACK_SYSTEM_OPRPOLICY_GET",
|
||||||
|
[0x1133] = "ACK_SYSTEM_P2PPARAM_GET",
|
||||||
|
[0x1140] = "ACK_SYSTEM_DATETIME_SET",
|
||||||
|
[0x1141] = "ACK_SYSTEM_DATETIME_GET",
|
||||||
|
[0x1151] = "ACK_NOTIFICATION",
|
||||||
|
[0x2000] = "CMD_SD_FORMAT",
|
||||||
|
[0x2001] = "CMD_SD_RECPOLICY_SET",
|
||||||
|
[0x2002] = "CMD_SD_RECPOLICY_GET",
|
||||||
|
[0x2003] = "CMD_SD_RECORDING_NOW",
|
||||||
|
[0x2004] = "CMD_SD_INFO_GET",
|
||||||
|
[0x2005] = "CMD_SD_RECORDFILE_GET",
|
||||||
|
[0x2006] = "CMD_SD_RECORDSCH_GET",
|
||||||
|
[0x2007] = "CMD_SD_RECORDSCH_SET",
|
||||||
|
[0x2008] = "CMD_SD_RETRIVEL",
|
||||||
|
[0x2009] = "CMD_SD_PICFILE_GET",
|
||||||
|
[0x200a] = "CMD_SD_PIC_CAPTURE",
|
||||||
|
[0x200b] = "CMD_SD_REC_DEL",
|
||||||
|
[0x200c] = "CMD_SD_PIC_DEL",
|
||||||
|
[0x200d] = "CMD_SD_SPL_DEL",
|
||||||
|
[0x2100] = "ACK_SD_FORMAT",
|
||||||
|
[0x2101] = "ACK_SD_RECPOLICY_SET",
|
||||||
|
[0x2102] = "ACK_SD_RECPOLICY_GET",
|
||||||
|
[0x2103] = "ACK_SD_RECORDING_NOW",
|
||||||
|
[0x2104] = "ACK_SD_INFO_GET",
|
||||||
|
[0x2105] = "ACK_SD_RECORDFILE_GET",
|
||||||
|
[0x2106] = "ACK_SD_RECORDSCH_GET",
|
||||||
|
[0x2107] = "ACK_SD_RECORDSCH_SET",
|
||||||
|
[0x2108] = "ACK_SD_RETRIVEL",
|
||||||
|
[0x2109] = "ACK_SD_PICFILE_GET",
|
||||||
|
[0x210a] = "ACK_SD_PIC_CAPTURE",
|
||||||
|
[0x210b] = "ACK_SD_REC_DEL",
|
||||||
|
[0x210c] = "ACK_SD_PIC_DEL",
|
||||||
|
[0x210d] = "ACK_SD_SPL_DEL",
|
||||||
|
[0x3000] = "CMD_PEER_LIVEAUDIO_START",
|
||||||
|
[0x3001] = "CMD_PEER_LIVEAUDIO_STOP",
|
||||||
|
[0x3002] = "CMD_LOCAL_LIVEAUDIO_START",
|
||||||
|
[0x3003] = "CMD_LOCAL_LIVEAUDIO_STOP",
|
||||||
|
[0x3004] = "CMD_PEER_AUDIOPARAM_SET",
|
||||||
|
[0x3005] = "CMD_PEER_AUDIOPARAM_GET",
|
||||||
|
[0x3006] = "CMD_PEER_AUDIOFILE_STARTPLAY",
|
||||||
|
[0x3007] = "CMD_PEER_AUDIOFILE_STOPPLAY",
|
||||||
|
[0x3008] = "CMD_PEER_AUDIOFILELIST_GET",
|
||||||
|
[0x300a] = "CMD_PEER_IRCUT_ONOFF",
|
||||||
|
[0x300b] = "CMD_PEER_LIGHTFILL_ONOFF",
|
||||||
|
[0x3010] = "CMD_PEER_LIVEVIDEO_START",
|
||||||
|
[0x3011] = "CMD_PEER_LIVEVIDEO_STOP",
|
||||||
|
[0x3012] = "CMD_PEER_PLAYBACK_START",
|
||||||
|
[0x3013] = "CMD_PEER_PLAYBACK_STOP",
|
||||||
|
[0x3014] = "CMD_PEER_PLAYBACK_SEEK",
|
||||||
|
[0x3015] = "CMD_PEER_PLAYBACK_SPEED",
|
||||||
|
[0x3016] = "CMD_PEER_PLAYBACK_PAUSE",
|
||||||
|
[0x3017] = "CMD_PEER_PLAYBACK_RESUME",
|
||||||
|
[0x3018] = "CMD_PEER_VIDEOPARAM_SET",
|
||||||
|
[0x3019] = "CMD_PEER_VIDEOPARAM_GET",
|
||||||
|
[0x301a] = "CMD_SNAPSHOT_GET",
|
||||||
|
[0x301b] = "CMD_PEER_PLAYBACK_END",
|
||||||
|
[0x301c] = "CMD_PEER_PLAYBACK_STEP",
|
||||||
|
[0x3020] = "CMD_DOORBELL_CALL_OPEN",
|
||||||
|
[0x3021] = "CMD_DOORBELL_CALL_CLOSE",
|
||||||
|
[0x3022] = "CMD_DOORBELL_CALL_ACCEPT",
|
||||||
|
[0x3023] = "CMD_DOORBELL_CALL_REJECT",
|
||||||
|
[0x3024] = "CMD_LOCAL_LIVEVIDIO_SEND_ON",
|
||||||
|
[0x3025] = "CMD_LOCAL_LIVEVIDIO_SEND_OFF",
|
||||||
|
[0x3026] = "CMD_LOCAL_AUDIO_STATUS_SET",
|
||||||
|
[0x3027] = "CMD_LOCAL_AUDIO_STATUS_GET",
|
||||||
|
[0x3028] = "CMD_LOCAL_AVREC_START",
|
||||||
|
[0x3029] = "CMD_LOCAL_AVREC_STOP",
|
||||||
|
[0x3030] = "CMD_LOCAL_PLAYBACK_START",
|
||||||
|
[0x3031] = "CMD_LOCAL_PLAYBACK_STOP",
|
||||||
|
[0x3032] = "CMD_LOCAL_PLAYBACK_SEEK",
|
||||||
|
[0x3033] = "CMD_LOCAL_PLAYBACK_PAUSE",
|
||||||
|
[0x3034] = "CMD_LOCAL_PLAYBACK_RESUME",
|
||||||
|
[0x3035] = "CMD_LOCAL_PLAYBACK_START1",
|
||||||
|
[0x3036] = "CMD_LOCAL_PLAYBACK_STEP",
|
||||||
|
[0x3037] = "CMD_LOCAL_PLAYBACK_START2",
|
||||||
|
[0x3040] = "CMD_LOCAL_MJREC_START",
|
||||||
|
[0x3041] = "CMD_LOCAL_MJREC_STOP",
|
||||||
|
[0x3100] = "ACK_PEER_LIVEAUDIO_START",
|
||||||
|
[0x3101] = "ACK_PEER_LIVEAUDIO_STOP",
|
||||||
|
[0x3102] = "ACK_LOCAL_LIVEAUDIO_START",
|
||||||
|
[0x3103] = "ACK_LOCAL_LIVEAUDIO_STOP",
|
||||||
|
[0x3104] = "ACK_PEER_AUDIOPARAM_SET",
|
||||||
|
[0x3105] = "ACK_PEER_AUDIOPARAM_GET",
|
||||||
|
[0x3106] = "ACK_PEER_AUDIOFILE_STARTPLAY",
|
||||||
|
[0x3107] = "ACK_PEER_AUDIOFILE_STOPPLAY",
|
||||||
|
[0x3108] = "ACK_PEER_AUDIOFILELIST_GET",
|
||||||
|
[0x310a] = "ACK_PEER_IRCUT_ONOFF",
|
||||||
|
[0x310b] = "ACK_PEER_LIGHTFILL_ONOFF",
|
||||||
|
[0x3110] = "ACK_PEER_LIVEVIDEO_START",
|
||||||
|
[0x3111] = "ACK_PEER_LIVEVIDEO_STOP",
|
||||||
|
[0x3112] = "ACK_PEER_PLAYBACK_START",
|
||||||
|
[0x3113] = "ACK_PEER_PLAYBACK_STOP",
|
||||||
|
[0x3114] = "ACK_PEER_PLAYBACK_SEEK",
|
||||||
|
[0x3115] = "ACK_PEER_PLAYBACK_SPEED",
|
||||||
|
[0x3116] = "ACK_PEER_PLAYBACK_PAUSE",
|
||||||
|
[0x3117] = "ACK_PEER_PLAYBACK_RESUME",
|
||||||
|
[0x3118] = "ACK_PEER_VIDEOPARAM_SET",
|
||||||
|
[0x3119] = "ACK_PEER_VIDEOPARAM_GET",
|
||||||
|
[0x311a] = "ACK_SNAPSHOT_GET",
|
||||||
|
[0x311b] = "ACK_PEER_PLAYBACK_END",
|
||||||
|
[0x311c] = "ACK_PEER_PLAYBACK_STEP",
|
||||||
|
[0x4000] = "CMD_FILE_CTRL",
|
||||||
|
[0x4005] = "CMD_FILETRANSFER_FILELIST_GET",
|
||||||
|
[0x4010] = "CMD_LOCALPATH",
|
||||||
|
[0x4101] = "ACK_FILE_CREATE",
|
||||||
|
[0x4102] = "ACK_FILE_RENAME",
|
||||||
|
[0x4103] = "ACK_FILE_DELETE",
|
||||||
|
[0x4104] = "ACK_FILE_MOVE",
|
||||||
|
[0x4105] = "ACK_FILE_LIST",
|
||||||
|
[0x4110] = "ACK_FILE_DOWNLOAD",
|
||||||
|
[0x4111] = "ACK_FILE_DOWNLOAD_PAUSE",
|
||||||
|
[0x4112] = "ACK_FILE_DOWNLOAD_RESUME",
|
||||||
|
[0x4113] = "ACK_FILE_DOWNLOAD_CANCEL",
|
||||||
|
[0x4120] = "ACK_FILE_UPLOAD",
|
||||||
|
[0x4121] = "ACK_FILE_UPLOAD_PAUSE",
|
||||||
|
[0x4122] = "ACK_FILE_UPLOAD_RESUME",
|
||||||
|
[0x4123] = "ACK_FILE_UPLOAD_CANCEL",
|
||||||
|
[0x50ff] = "CMD_PASSTHROUGH_STRING_PUT",
|
||||||
|
[0x51ff] = "ACK_PASSTHROUGH_STRING_PUT",
|
||||||
|
[0x55fe] = "CMD_SESSION_CHECK",
|
||||||
|
[0x6001] = "CB_IEGET_STATUS",
|
||||||
|
[0x6001] = "CMD_NET_WIFISETTING_SET",
|
||||||
|
[0x6002] = "CB_IEGET_PARAM",
|
||||||
|
[0x6002] = "CMD_NET_WIFISETTING_GET",
|
||||||
|
[0x6003] = "CB_IEGET_CAM_PARAMS",
|
||||||
|
[0x6003] = "CMD_NET_WIFI_SCAN",
|
||||||
|
[0x6004] = "CB_IEGET_LOG",
|
||||||
|
[0x6004] = "CMD_NET_WIREDSETTING_SET",
|
||||||
|
[0x6005] = "CB_IEGET_MISC",
|
||||||
|
[0x6005] = "CMD_NET_WIREDSETTING_GET",
|
||||||
|
[0x6006] = "CB_IEGET_RECORD",
|
||||||
|
[0x6007] = "CB_IEGET_RECORD_FILE",
|
||||||
|
[0x6008] = "CB_IEGET_WIFI_SCAN",
|
||||||
|
[0x6009] = "CB_IEGET_FACTORY",
|
||||||
|
[0x600a] = "CB_IESET_IR",
|
||||||
|
[0x600b] = "CB_IESET_UPNP",
|
||||||
|
[0x600c] = "CB_IESET_ALARM",
|
||||||
|
[0x600d] = "CB_IESET_LOG",
|
||||||
|
[0x600e] = "CB_IESET_USER",
|
||||||
|
[0x600f] = "CB_IESET_ALIAS",
|
||||||
|
[0x6010] = "CB_IESET_MAIL",
|
||||||
|
[0x6011] = "CB_IESET_WIFI",
|
||||||
|
[0x6012] = "CB_CAM_CONTROL",
|
||||||
|
[0x6013] = "CB_IESET_DATE",
|
||||||
|
[0x6014] = "CB_IESET_MEDIA",
|
||||||
|
[0x6015] = "CB_IESET_SNAPSHOT",
|
||||||
|
[0x6016] = "CB_IESET_DDNS",
|
||||||
|
[0x6017] = "CB_IESET_MISC",
|
||||||
|
[0x6018] = "CB_IEGET_FTPTEST",
|
||||||
|
[0x6019] = "CB_DECODER_CONTROL",
|
||||||
|
[0x601a] = "CB_IESET_DEFAULT",
|
||||||
|
[0x601b] = "CB_IESET_MOTO",
|
||||||
|
[0x601c] = "CB_IEGET_MAILTEST",
|
||||||
|
[0x601d] = "CB_IESET_MAILTEST",
|
||||||
|
[0x601e] = "CB_IEDEL_FILE",
|
||||||
|
[0x601f] = "CB_IELOGIN",
|
||||||
|
[0x6020] = "CB_IESET_DEVICE",
|
||||||
|
[0x6021] = "CB_IESET_NETWORK",
|
||||||
|
[0x6022] = "CB_IESET_FTPTEST",
|
||||||
|
[0x6023] = "CB_IESET_DNS",
|
||||||
|
[0x6024] = "CB_IESET_OSD",
|
||||||
|
[0x6025] = "CB_IESET_FACTORY",
|
||||||
|
[0x6026] = "CB_IESET_PPPOE",
|
||||||
|
[0x6027] = "CB_IEREBOOT",
|
||||||
|
[0x6028] = "CB_IEFORMATSD",
|
||||||
|
[0x6029] = "CB_IESET_RECORDSCH",
|
||||||
|
[0x602a] = "CB_IESET_WIFISCAN",
|
||||||
|
[0x602b] = "CB_IERESTORE",
|
||||||
|
[0x602c] = "CB_IESET_FTP",
|
||||||
|
[0x602d] = "CB_IESET_RTSP",
|
||||||
|
[0x602e] = "CB_IEGET_VIDEOSTREAM",
|
||||||
|
[0x602f] = "CB_UPGRADE_APP",
|
||||||
|
[0x6030] = "CB_UPGRADE_SYS",
|
||||||
|
[0x6031] = "CB_SET_IIC",
|
||||||
|
[0x6032] = "CB_GET_IIC",
|
||||||
|
[0x6033] = "CB_IEGET_ALARMLOG",
|
||||||
|
[0x6034] = "CB_IESET_ALARMLOGCLR",
|
||||||
|
[0x6035] = "CB_IEGET_SYSWIFI",
|
||||||
|
[0x6036] = "CB_IESET_SYSWIFI",
|
||||||
|
[0x6037] = "CB_IEGET_LIVESTREAM",
|
||||||
|
[0x6040] = "CB_NOTIFICATION",
|
||||||
|
[0x6053] = "CB_IEGET_BILL",
|
||||||
|
[0x6054] = "CB_APP_VERSION",
|
||||||
|
[0x60a0] = "CB_CHECK_USER",
|
||||||
|
[0x60a1] = "CB_IESET_BILL",
|
||||||
|
[0x6101] = "ACK_NET_WIFISETTING_SET",
|
||||||
|
[0x6102] = "ACK_NET_WIFISETTING_GET",
|
||||||
|
[0x6103] = "ACK_NET_WIFI_SCAN",
|
||||||
|
[0x6104] = "ACK_NET_WIREDSETTING_SET",
|
||||||
|
[0x6105] = "ACK_NET_WIREDSETTING_GET",
|
||||||
|
[0x7000] = "CMD_FRIEND_MSG",
|
||||||
|
[0x99f0] = "CB_SET_P2PPARAM",
|
||||||
|
[0x99fe] = "CB_GET_SYSOPR",
|
||||||
|
[0x99ff] = "CB_SET_SYSOPR",
|
||||||
|
[0xf000] = "CMD_LOCAL_SESSION_INF",
|
||||||
|
[0xf001] = "CMD_LOCAL_SESSION_CHECK",
|
||||||
|
[0xf002] = "CMD_LOCAL_SESSION_GET",
|
||||||
|
[0xf003] = "CMD_LOCAL_SESSION_CTRL",
|
||||||
|
[0xf004] = "CMD_LOCAL_REC_START",
|
||||||
|
[0xf005] = "CMD_LOCAL_REC_STOP",
|
||||||
|
[0xf006] = "CMD_LOCAL_REC_MERGECTRL",
|
||||||
|
[0xf007] = "CMD_LOCAL_P2P_START",
|
||||||
|
[0xf008] = "CMD_LOCAL_P2P_STOP",
|
||||||
|
[0xf00f] = "CMD_SESSION_CLOSE",
|
||||||
|
[0xf010] = "CMD_LOCAL_PUSH_STRING",
|
||||||
|
[0xf011] = "CMD_LOCAL_PUSH_CFG",
|
||||||
|
[0xf012] = "CMD_LOCAL_RCVVID_DEC",
|
||||||
|
[0xf021] = "CMD_LOCAL_LAPSED",
|
||||||
|
[0xff01] = "CB_SET_SINGLE_SETTING_DEFAULT",
|
||||||
|
[0xff10] = "CB_GET_FILE",
|
||||||
|
[0xff11] = "CB_PUT_FILE",
|
||||||
|
[0xff12] = "CB_SET_FILE",
|
||||||
|
[0xff13] = "CB_GET_FILELIST",
|
||||||
|
[0xff14] = "CB_SET_GPIO",
|
||||||
|
[0xff15] = "CB_GET_GPIO",
|
||||||
|
[0xff16] = "CB_GET_ADC",
|
||||||
|
}
|
||||||
|
|
||||||
-- 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 +374,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,13 +404,23 @@ 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))
|
||||||
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()
|
||||||
|
if payload_len > pkt_len then
|
||||||
|
payload_len = pkt_len - 12
|
||||||
|
end
|
||||||
|
|
||||||
if not is_data_packet then
|
if not is_data_packet then
|
||||||
subtree:add_le(ilnk_proto.fields.start, buffer(8, 2))
|
subtree:add_le(ilnk_proto.fields.start, buffer(8, 2))
|
||||||
subtree:add_le(ilnk_proto.fields.cmd, buffer(0xa, 2))
|
subtree:add_le(ilnk_proto.fields.cmd, buffer(0xa, 2))
|
||||||
|
local cmdname = control_lut[buffer(0xa, 2):le_uint()] or "UNK"
|
||||||
|
pinfo.cols.info:set(cmdname)
|
||||||
|
subtree:add(ilnk_proto.fields.cmd_name, cmdname):set_generated()
|
||||||
subtree:add_le(ilnk_proto.fields.cmd_payload_len, b_payload_len)
|
subtree:add_le(ilnk_proto.fields.cmd_payload_len, b_payload_len)
|
||||||
subtree:add_le(ilnk_proto.fields.cmd_dest, buffer(0xe, 2))
|
subtree:add_le(ilnk_proto.fields.cmd_dest, buffer(0xe, 2))
|
||||||
-- inline value for short-payload bytes
|
-- inline value for short-payload bytes
|
||||||
@@ -127,7 +463,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 +508,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;
|
||||||
|
};
|
||||||
+178
-66
@@ -1,14 +1,14 @@
|
|||||||
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
|
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
|
||||||
import { XqBytesDec } from "./func_replacements.js";
|
import { XqBytesDec } from "./func_replacements.js";
|
||||||
import { hexdump } from "./hexdump.js";
|
import { create_P2pRdy, DevSerial, SendListWifi, SendUsrChk } from "./impl.js";
|
||||||
import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
|
import { logger } from "./logger.js";
|
||||||
import { Session } from "./session.js";
|
import { Session } from "./session.js";
|
||||||
import { u16_swap, u32_swap } from "./utils.js";
|
import { config } from "./settings.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,54 +25,94 @@ 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 outbuf = new DataView(new Uint8Array(0x14).buffer); // 8 = serial u64
|
const outbuf = new DataView(new Uint8Array(0x14).buffer); // 8 = serial u64
|
||||||
|
// The protocol seems to expect 4 bytes -- check the regression test
|
||||||
|
// `replies properly to PunchPkt with 3-letters-long prefix` for a case with a
|
||||||
|
// real device
|
||||||
|
const devPrefixLength = 4;
|
||||||
outbuf.add(0).writeString(dev.prefix);
|
outbuf.add(0).writeString(dev.prefix);
|
||||||
outbuf.add(4).writeU64(dev.serialU64);
|
outbuf.add(4).writeU64(dev.serialU64);
|
||||||
outbuf.add(8 + dev.prefix.length).writeString(dev.suffix);
|
outbuf.add(8 + devPrefixLength).writeString(dev.suffix);
|
||||||
return create_P2pRdy(outbuf);
|
return create_P2pRdy(outbuf);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const swVerToString = (swver: number): string => {
|
||||||
|
return (
|
||||||
|
((swver >> 24) & 255).toString() +
|
||||||
|
"." +
|
||||||
|
((swver >> 16) & 255).toString() +
|
||||||
|
"." +
|
||||||
|
((swver >> 8) & 255).toString() +
|
||||||
|
"." +
|
||||||
|
(swver & 255).toString()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export type DevStatus = {
|
||||||
|
charging: boolean;
|
||||||
|
battery_mV: number;
|
||||||
|
dbm: number;
|
||||||
|
swver: string;
|
||||||
|
};
|
||||||
|
export const parseDevStatusAck = (dv: DataView): DevStatus => {
|
||||||
|
const charging = dv.add(0x28).readU32LE() & 1; // 0x14000101 v 0x14000100
|
||||||
|
const power = dv.add(0x18).readU16LE(); // '3730' or '3765', milliVolts
|
||||||
|
const dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
|
||||||
|
const n_swver = dv.add(0x14).readU32LE();
|
||||||
|
const swver = swVerToString(n_swver);
|
||||||
|
return {
|
||||||
|
charging: charging > 0,
|
||||||
|
battery_mV: power,
|
||||||
|
dbm,
|
||||||
|
swver,
|
||||||
|
};
|
||||||
|
};
|
||||||
export const createResponseForControlCommand = (session: Session, dv: DataView): DataView[] => {
|
export const createResponseForControlCommand = (session: Session, dv: DataView): DataView[] => {
|
||||||
const start_type = dv.add(8).readU16(); // 0xa11 on control; data starts here on DATA pkt
|
const start_type = dv.add(8).readU16(); // 0xa11 on control; data starts here on DATA pkt
|
||||||
const cmd_id = dv.add(10).readU16(); // 0x1120
|
const cmd_id = dv.add(10).readU16(); // 0x1120
|
||||||
const payload_len = u16_swap(dv.add(0xc).readU16());
|
let payload_len = dv.add(0xc).readU16LE();
|
||||||
|
if (dv.byteLength > 20 && payload_len > dv.byteLength) {
|
||||||
|
logger.warning(`Received a cropped payload: ${payload_len} when packet is ${dv.byteLength}`);
|
||||||
|
payload_len = dv.byteLength - 20;
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
// the first 20 bytes are header
|
||||||
|
|
||||||
|
switch (cmd_id) {
|
||||||
|
case ControlCommands.ConnectUserAck:
|
||||||
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
|
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
|
||||||
session.ticket = [...c];
|
session.ticket = [...c];
|
||||||
session.eventEmitter.emit("login");
|
session.eventEmitter.emit("login");
|
||||||
return [];
|
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
|
const status = parseDevStatusAck(dv);
|
||||||
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts?
|
|
||||||
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}: sw: ${status.swver}, ${status.charging ? "" : "not "}charging, battery at ${
|
||||||
|
status.battery_mV
|
||||||
|
}mV, Wifi ${status.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(),
|
||||||
@@ -89,77 +129,127 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
|||||||
dns2: dv.add(0x10c).readString(0x10),
|
dns2: dv.add(0x10c).readString(0x10),
|
||||||
};
|
};
|
||||||
const buf = SendListWifi(session);
|
const buf = SendListWifi(session);
|
||||||
console.log(`Current wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
logger.info(`Current Wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
||||||
return [buf];
|
return [buf];
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.ListWifiAck) {
|
case ControlCommands.ListWifiAck:
|
||||||
|
if (payload_len == 4) {
|
||||||
|
logger.debug("ListWifi returned []");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const items = parseListWifi(dv);
|
||||||
|
session.eventEmitter.emit("ListWifi", items);
|
||||||
|
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 [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WifiListItem = {
|
||||||
|
ssid: string;
|
||||||
|
mac: string;
|
||||||
|
security: number;
|
||||||
|
dbm0: number;
|
||||||
|
dbm1: number;
|
||||||
|
mode: number;
|
||||||
|
channel: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseListWifi = (dv: DataView): WifiListItem[] => {
|
||||||
let startat = 0x10;
|
let startat = 0x10;
|
||||||
let msg_len = 91;
|
const msg_len = 0x5c; // 0x58 + 0x4 of the last u32
|
||||||
console.log("payload len", payload_len);
|
const msg_count = dv.add(startat).readU32LE();
|
||||||
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;
|
startat += 4;
|
||||||
let items = [];
|
let items = [];
|
||||||
for (let i = 0; i < msg_count; i++) {
|
for (let i = 0; i < msg_count; i++) {
|
||||||
|
if (startat + msg_len > dv.byteLength) {
|
||||||
|
logger.warning("Wifi listing got cropped");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const macBytes = dv.add(startat + 0x40).readByteArray(6).buffer;
|
||||||
|
const mb = new Uint8Array(macBytes);
|
||||||
const wifiListItem = {
|
const wifiListItem = {
|
||||||
// startat = msg_len * i + 0x14;
|
|
||||||
ssid: dv.add(startat).readString(0x40),
|
ssid: dv.add(startat).readString(0x40),
|
||||||
mac: dv.add(startat + 0x40).readByteArray(8),
|
mac: [...mb].map((b) => b.toString(16).padStart(2, "0")).join(":"),
|
||||||
security: dv.add(startat + 0x48).readU32LE(),
|
security: dv.add(startat + 0x48).readU32LE(),
|
||||||
dbm0: dv.add(startat + 0x4c).readU32LE(),
|
dbm0: dv.add(startat + 0x4c).readU32LE(),
|
||||||
dbm1: dv.add(startat + 0x50).readU32LE(),
|
dbm1: dv.add(startat + 0x50).readU32LE(),
|
||||||
mode: dv.add(startat + 0x54).readU32LE(),
|
mode: dv.add(startat + 0x54).readU32LE(),
|
||||||
channel: dv.add(startat + 0x58).readU32LE(),
|
channel: dv.add(startat + 0x58).readU32LE(),
|
||||||
};
|
};
|
||||||
console.log(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
|
||||||
startat += msg_len;
|
startat += msg_len;
|
||||||
console.log("ended at", startat);
|
|
||||||
items.push(wifiListItem);
|
items.push(wifiListItem);
|
||||||
}
|
}
|
||||||
}
|
return items;
|
||||||
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.log("trace", "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 audio_len = u16_swap(dv.add(8 + 16).readU16());
|
const STREAM_TYPE_AUDIO = 0x06;
|
||||||
// may have received the next 'data' packet id as an audio frame -- jpeg was fine
|
const STREAM_TYPE_JPEG = 0x03;
|
||||||
if (pkt_id == session.rcvSeqId + 1) {
|
|
||||||
session.rcvSeqId = session.rcvSeqId + 1;
|
const startNewFrame = (buf: ArrayBuffer) => {
|
||||||
}
|
|
||||||
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) });
|
|
||||||
} else {
|
|
||||||
// not sure what these are for, there's one per frame. maybe alignment?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const data = dv.add(8).readByteArray(pkt_len - 4);
|
|
||||||
if (is_new_image) {
|
|
||||||
if (session.curImage.length > 0 && !session.frame_is_bad) {
|
if (session.curImage.length > 0 && !session.frame_is_bad) {
|
||||||
session.eventEmitter.emit("frame");
|
session.eventEmitter.emit("frame");
|
||||||
}
|
}
|
||||||
|
|
||||||
session.frame_was_fixed = false;
|
session.frame_was_fixed = false;
|
||||||
session.frame_is_bad = false;
|
session.frame_is_bad = false;
|
||||||
session.curImage = [Buffer.from(data.buffer)];
|
session.curImage = [Buffer.from(buf)];
|
||||||
session.rcvSeqId = pkt_id;
|
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 = dv.add(8 + 16).readU16LE();
|
||||||
|
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) });
|
||||||
|
} 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 {
|
||||||
|
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?
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
// this only happens on un-framed-cameras, which start the JPEG image
|
||||||
|
// directly
|
||||||
|
let is_new_image = m_hdr.startsWith(JPEG_HEADER);
|
||||||
|
|
||||||
|
if (is_new_image) {
|
||||||
|
startNewFrame(data.buffer);
|
||||||
} else {
|
} else {
|
||||||
if (pkt_id <= session.rcvSeqId) {
|
if (pkt_id <= session.rcvSeqId) {
|
||||||
// retransmit
|
// retransmit
|
||||||
@@ -169,10 +259,13 @@ 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) {
|
||||||
|
if (!session.frame_is_bad) {
|
||||||
session.frame_is_bad = true;
|
session.frame_is_bad = true;
|
||||||
// this should always be enabled but currently it seems to cause more visual distortion
|
logger.debug(`Dropping corrupt frame ${pkt_id}, expected ${session.rcvSeqId + 1}`);
|
||||||
// than just missing some frames
|
}
|
||||||
if (!session.options.attempt_to_fix_packet_loss) {
|
// this should always be enabled but currently it seems to cause more
|
||||||
|
// visual distortion than just missing some frames
|
||||||
|
if (!config.cameras[session.devName]?.fix_packet_loss) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +328,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 +346,16 @@ 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`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
export const handle_Close = (session: Session, dv: DataView) => {
|
||||||
|
const ack = makeDrwAck(dv);
|
||||||
|
session.send(ack);
|
||||||
|
logger.info("Requested to close connection");
|
||||||
|
session.close();
|
||||||
|
};
|
||||||
|
|||||||
+92
-27
@@ -1,18 +1,37 @@
|
|||||||
import { RemoteInfo } from "dgram";
|
import { RemoteInfo } from "dgram";
|
||||||
import { readFileSync } from "node:fs";
|
|
||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
|
|
||||||
import { opt } from "./options.js";
|
import { logger } from "./logger.js";
|
||||||
|
import { config } from "./settings.js";
|
||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial } from "./impl.js";
|
import { DevSerial } from "./impl.js";
|
||||||
import { Handlers, makeSession, Session, 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";
|
||||||
|
|
||||||
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
const BOUNDARY = "a very good boundary line";
|
||||||
|
const responses: Record<string, http.ServerResponse[]> = {};
|
||||||
|
const audioResponses: Record<string, http.ServerResponse[]> = {};
|
||||||
|
const sessions: Record<string, Session> = {};
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Reads the mapping of serial numbers to camera names from the text file.
|
||||||
|
|
||||||
|
// Returns the camera name (custom name, if it exists, otherwise its ID).
|
||||||
|
const cameraName = (id: string): string => config.cameras[id].alias || id;
|
||||||
|
|
||||||
|
// The HTTP server.
|
||||||
|
export const serveHttp = (port: number) => {
|
||||||
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 +46,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, config.cameras[devId].audio ? "true" : "false");
|
||||||
|
res.end(ui);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (req.url.startsWith("/audio/")) {
|
if (req.url.startsWith("/audio/")) {
|
||||||
@@ -46,22 +69,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 = config.cameras[devId]?.rotate || 0;
|
||||||
|
let nextPos = (curPos + 1) % 4;
|
||||||
|
logger.debug(`Rotating ${devId} to ${nextPos}`);
|
||||||
|
config.cameras[devId].rotate = nextPos;
|
||||||
|
res.writeHead(204);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
} else if (req.url.startsWith("/mirror/")) {
|
||||||
|
let devId = req.url.split("/")[2];
|
||||||
|
logger.debug(`Mirroring ${devId}`);
|
||||||
|
config.cameras[devId].mirror = !config.cameras[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 +116,54 @@ 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(config.discovery_ips);
|
||||||
|
|
||||||
|
const startSession = (s: Session) => {
|
||||||
|
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, 5000);
|
||||||
|
sessions[dev.devId] = s;
|
||||||
|
config.cameras[dev.devId] = { rotate: 0, mirror: false, audio: true, ...(config.cameras[dev.devId] || {}) };
|
||||||
|
|
||||||
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 = config.cameras[dev.devId].rotate;
|
||||||
|
orientation = config.cameras[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,10 +171,10 @@ 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 (config.cameras[dev.devId].audio) {
|
||||||
s.eventEmitter.on("audio", ({ gap, data }) => {
|
s.eventEmitter.on("audio", ({ gap, data }) => {
|
||||||
// ew, maybe WS?
|
// ew, maybe WS?
|
||||||
var b64encoded = Buffer.from(data).toString("base64");
|
var b64encoded = Buffer.from(data).toString("base64");
|
||||||
@@ -119,9 +185,8 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
sessions[dev.devId] = s;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Starting HTTP server on port ${port}`);
|
logger.info(`Starting HTTP server on port ${port}`);
|
||||||
server.listen(port);
|
server.listen(port);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -119,7 +119,13 @@ export const SendReboot = (session: Session): DataView => {
|
|||||||
return makeDataReadWrite(session, ControlCommands.Reboot, dv);
|
return makeDataReadWrite(session, ControlCommands.Reboot, dv);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SendWifiDetails = (session: Session, ssid: string, password: string, dhcp: boolean): DataView => {
|
export const SendWifiDetails = (
|
||||||
|
session: Session,
|
||||||
|
ssid: string,
|
||||||
|
password: string,
|
||||||
|
channel: number,
|
||||||
|
dhcp: boolean,
|
||||||
|
): DataView => {
|
||||||
if (!dhcp) {
|
if (!dhcp) {
|
||||||
throw new Error("only DHCP is supported");
|
throw new Error("only DHCP is supported");
|
||||||
}
|
}
|
||||||
@@ -132,11 +138,14 @@ export const SendWifiDetails = (session: Session, ssid: string, password: string
|
|||||||
let m_dns1 = "0.0.0.0";
|
let m_dns1 = "0.0.0.0";
|
||||||
let m_dns2 = "0.0.0.0";
|
let m_dns2 = "0.0.0.0";
|
||||||
|
|
||||||
cmd_payload.add(0x14).writeU8(1); // DHCP ?
|
// tag_wifiParams in types/all.h
|
||||||
|
cmd_payload.add(0x0c).writeU8(channel);
|
||||||
|
cmd_payload.add(0x10).writeU8(0); // TODO: AUTH
|
||||||
|
cmd_payload.add(0x14).writeU8(1); // DHCP
|
||||||
cmd_payload.add(0x18).writeByteArray(str2byte(ssid));
|
cmd_payload.add(0x18).writeByteArray(str2byte(ssid));
|
||||||
cmd_payload.add(0x38).writeByteArray(str2byte(password));
|
cmd_payload.add(0x38).writeByteArray(str2byte(password));
|
||||||
cmd_payload.add(0xb8).writeByteArray(str2byte(mask_reversed));
|
cmd_payload.add(0xb8).writeByteArray(str2byte(m_ip));
|
||||||
cmd_payload.add(0xc8).writeByteArray(str2byte(m_ip));
|
cmd_payload.add(0xc8).writeByteArray(str2byte(mask_reversed));
|
||||||
cmd_payload.add(0xd8).writeByteArray(str2byte(m_gw));
|
cmd_payload.add(0xd8).writeByteArray(str2byte(m_gw));
|
||||||
cmd_payload.add(0xe8).writeByteArray(str2byte(m_dns1));
|
cmd_payload.add(0xe8).writeByteArray(str2byte(m_dns1));
|
||||||
cmd_payload.add(0xf8).writeByteArray(str2byte(m_dns2));
|
cmd_payload.add(0xf8).writeByteArray(str2byte(m_dns2));
|
||||||
@@ -155,6 +164,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,6 +1,4 @@
|
|||||||
export type opt = {
|
export type opt = {
|
||||||
debug: boolean;
|
|
||||||
ansi: boolean;
|
|
||||||
discovery_ip: string;
|
discovery_ip: string;
|
||||||
attempt_to_fix_packet_loss: boolean;
|
attempt_to_fix_packet_loss: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+268
-6
@@ -5,14 +5,27 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.20.2",
|
"winston": "^3.13.0",
|
||||||
|
"yaml": "^2.4.2",
|
||||||
"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"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
@@ -27,6 +40,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 +57,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"aix"
|
"aix"
|
||||||
@@ -49,6 +73,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -64,6 +89,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -79,6 +105,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
@@ -94,6 +121,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -109,6 +137,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
@@ -124,6 +153,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -139,6 +169,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
@@ -154,6 +185,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -169,6 +201,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -184,6 +217,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -199,6 +233,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -214,6 +249,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -229,6 +265,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -244,6 +281,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -259,6 +297,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -274,6 +313,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
@@ -289,6 +329,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"netbsd"
|
"netbsd"
|
||||||
@@ -304,6 +345,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"openbsd"
|
"openbsd"
|
||||||
@@ -319,6 +361,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"sunos"
|
"sunos"
|
||||||
@@ -334,6 +377,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -349,6 +393,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -364,6 +409,7 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"dev": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
@@ -431,6 +477,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 +574,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 +701,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 +726,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 +818,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 +881,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 +923,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 +1041,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 +1107,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 +1142,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 +1178,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 +1284,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 +1316,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 +1393,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 +1430,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 +1445,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 +1462,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 +1537,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 +1554,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 +1634,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",
|
||||||
@@ -1464,6 +1715,17 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.7.2",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
|||||||
+7
-2
@@ -3,16 +3,21 @@
|
|||||||
"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",
|
||||||
|
"yaml": "^2.4.2",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,63 @@
|
|||||||
import { RemoteInfo } from "dgram";
|
import { RemoteInfo } from "dgram";
|
||||||
|
|
||||||
import { opt } from "./options.js";
|
import { config } from "./settings.js";
|
||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial } from "./impl.js";
|
import { WifiListItem } from "./handlers.js";
|
||||||
|
import { DevSerial, SendListWifi } 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 = ({ ssid, password }: { 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(config.discovery_ips);
|
||||||
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(`Scanning for Wifi networks on ${s.devName} -- this may time out`);
|
||||||
|
|
||||||
|
// configureWifi(ssid, password, 0)(s);
|
||||||
|
s.send(SendListWifi(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, 10000);
|
||||||
|
let configured = {};
|
||||||
|
|
||||||
s.eventEmitter.on("disconnect", () => {
|
s.eventEmitter.on("disconnect", () => {
|
||||||
console.log("deleting from sessions");
|
logger.info(`Camera ${dev.devId} disconnected`);
|
||||||
|
if (configured[dev.devId]) {
|
||||||
|
logger.info("Press CONTROL+C if you're done setting up your cameras");
|
||||||
|
}
|
||||||
delete sessions[dev.devId];
|
delete sessions[dev.devId];
|
||||||
|
delete configured[dev.devId];
|
||||||
});
|
});
|
||||||
sessions[dev.devId] = s;
|
sessions[dev.devId] = s;
|
||||||
|
|
||||||
|
s.eventEmitter.on("ListWifi", (items: WifiListItem[]) => {
|
||||||
|
const matches = items.filter((i) => i.ssid == ssid);
|
||||||
|
if (matches.length == 0) {
|
||||||
|
logger.error(`Camera could not find SSID '${ssid}'`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (configured[dev.devId]) {
|
||||||
|
logger.info(`Got two answers from camera, ignoring second`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const match = matches[0];
|
||||||
|
logger.info(`Configuring camera ${s.devName} on ${JSON.stringify(match)}`);
|
||||||
|
configureWifi(ssid, password, match.channel)(s);
|
||||||
|
configured[dev.devId] = true;
|
||||||
|
logger.info(`WiFi config for camera ${s.devName} is done`);
|
||||||
|
logger.info(`Camera should reboot now`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -336,3 +336,6 @@ what is this??
|
|||||||
0220 00 00 00 00 00 00 00 00 ........
|
0220 00 00 00 00 00 00 00 00 ........
|
||||||
|
|
||||||
-- this looks like google keys: AIzaSyB
|
-- this looks like google keys: AIzaSyB
|
||||||
|
|
||||||
|
|
||||||
|
most useful parsing at IpcByte2ObjectParser and IlnkUtils
|
||||||
|
|||||||
+68
-28
@@ -2,13 +2,23 @@ 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 {
|
||||||
import { hexdump } from "./hexdump.js";
|
handle_Close,
|
||||||
|
handle_Drw,
|
||||||
|
handle_DrwAck,
|
||||||
|
handle_P2PAlive,
|
||||||
|
handle_P2PRdy,
|
||||||
|
makeP2pRdy,
|
||||||
|
notImpl,
|
||||||
|
noop,
|
||||||
|
} from "./handlers.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 { 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,7 +31,8 @@ export type Session = {
|
|||||||
rcvSeqId: number;
|
rcvSeqId: number;
|
||||||
frame_is_bad: boolean;
|
frame_is_bad: boolean;
|
||||||
frame_was_fixed: boolean;
|
frame_was_fixed: boolean;
|
||||||
options: opt;
|
started: boolean;
|
||||||
|
close: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
|
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
|
||||||
@@ -36,13 +47,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);
|
||||||
|
if (raw != Commands.P2PAlive && raw != Commands.P2PAliveAck) {
|
||||||
session.lastReceivedPacket = Date.now();
|
session.lastReceivedPacket = Date.now();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeSession = (
|
export const makeSession = (
|
||||||
@@ -50,8 +61,9 @@ export const makeSession = (
|
|||||||
dev: DevSerial,
|
dev: DevSerial,
|
||||||
ra: RemoteInfo,
|
ra: RemoteInfo,
|
||||||
onLogin: (s: Session) => void,
|
onLogin: (s: Session) => void,
|
||||||
options: opt,
|
timeoutMs: number,
|
||||||
): Session => {
|
): Session => {
|
||||||
|
let unackedDrw = {};
|
||||||
const sock = createSocket("udp4");
|
const sock = createSocket("udp4");
|
||||||
|
|
||||||
sock.on("error", (err) => {
|
sock.on("error", (err) => {
|
||||||
@@ -62,68 +74,96 @@ 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 (session.started) {
|
||||||
if (delta > 600) {
|
if (delta > 600) {
|
||||||
let buf = create_P2pAlive();
|
let buf = create_P2pAlive();
|
||||||
session.send(buf);
|
session.send(buf);
|
||||||
}
|
}
|
||||||
if (delta > 8000) {
|
if (delta > timeoutMs) {
|
||||||
|
logger.warning(`Camera ${session.devName} timed out`);
|
||||||
session.eventEmitter.emit("disconnect");
|
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 };
|
||||||
}
|
}
|
||||||
}
|
logger.log("trace", `>> ${cmd}`);
|
||||||
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
|
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: [],
|
||||||
rcvSeqId: 0,
|
rcvSeqId: 0,
|
||||||
frame_is_bad: false,
|
frame_is_bad: false,
|
||||||
frame_was_fixed: false,
|
frame_was_fixed: false,
|
||||||
options: options,
|
unackedDrw,
|
||||||
|
close: () => {
|
||||||
|
session.eventEmitter.emit("disconnect");
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
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));
|
||||||
session.timers = [];
|
session.timers = [];
|
||||||
|
sock.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const configureWifi = (ssid: string, password: string) => {
|
export const configureWifi = (ssid: string, password: string, channel: number) => {
|
||||||
return (s: Session) => {
|
return (s: Session) => {
|
||||||
[SendWifiDetails(s, ssid, password, true)].forEach(s.send);
|
[SendWifiDetails(s, ssid, password, channel, true)].forEach(s.send);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,13 +178,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,
|
||||||
|
Close: handle_Close,
|
||||||
|
|
||||||
Close: notImpl,
|
P2PAliveAck: noop,
|
||||||
LanSearchExt: notImpl,
|
LanSearchExt: notImpl,
|
||||||
LanSearch: notImpl,
|
LanSearch: notImpl,
|
||||||
P2PAliveAck: notImpl,
|
|
||||||
Hello: notImpl,
|
Hello: notImpl,
|
||||||
P2pReq: notImpl,
|
P2pReq: notImpl,
|
||||||
LstReq: notImpl,
|
LstReq: notImpl,
|
||||||
|
|||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
|
interface HttpServerConfig {
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoggingConfig {
|
||||||
|
level: string;
|
||||||
|
use_color?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CameraConfig {
|
||||||
|
alias?: string;
|
||||||
|
rotate?: number;
|
||||||
|
mirror?: boolean;
|
||||||
|
audio?: boolean;
|
||||||
|
fix_packet_loss?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppConfig {
|
||||||
|
http_server: HttpServerConfig;
|
||||||
|
logging: LoggingConfig;
|
||||||
|
cameras: Record<string, CameraConfig>;
|
||||||
|
discovery_ips: string[];
|
||||||
|
blacklisted_ips: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultConfig: AppConfig = {
|
||||||
|
http_server: { port: 5000 },
|
||||||
|
logging: { level: "info" },
|
||||||
|
cameras: {},
|
||||||
|
discovery_ips: ["192.168.1.255"],
|
||||||
|
blacklisted_ips: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = DefaultConfig;
|
||||||
|
|
||||||
|
export const loadConfig = (path: string) => {
|
||||||
|
const data = fs.readFileSync(path, { encoding: "utf-8" });
|
||||||
|
config = parse(data) as AppConfig;
|
||||||
|
config = { ...DefaultConfig, ...config };
|
||||||
|
};
|
||||||
|
|
||||||
|
export { config };
|
||||||
@@ -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;
|
||||||
|
|||||||
+136
-7
@@ -1,11 +1,13 @@
|
|||||||
|
// vim: nowrap
|
||||||
import "../shim.ts";
|
import "../shim.ts";
|
||||||
|
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
|
||||||
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
||||||
import { createResponseForControlCommand } from "../handlers.js";
|
import { makeP2pRdy, parseDevStatusAck, parseListWifi } from "../handlers.js";
|
||||||
import { hexdump } from "../hexdump.js";
|
import { hexdump } from "../hexdump.js";
|
||||||
import { SendStartVideo, SendDevStatus, SendUsrChk, SendWifiDetails } from "../impl.ts";
|
import { parse_PunchPkt, SendDevStatus, SendStartVideo, SendUsrChk, SendWifiDetails } from "../impl.ts";
|
||||||
|
import { buildLogger } from "../logger.js";
|
||||||
import { placeholderTypes, sprintf } from "../utils.js";
|
import { placeholderTypes, sprintf } from "../utils.js";
|
||||||
|
|
||||||
describe("debug_tools", () => {
|
describe("debug_tools", () => {
|
||||||
@@ -123,9 +125,99 @@ 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", () => {
|
const BATohstr = (ba) => [...new Uint8Array(ba.buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||||
it("emits login event upon logging in", () => {
|
describe("parse packet", () => {
|
||||||
// TODO
|
it("parses PunchPkt", () => {
|
||||||
|
const in_pkt_str = "f14100144241544400000000000262ca574f4e4a4d000000";
|
||||||
|
const pkt = new DataView(hstrToBA(in_pkt_str));
|
||||||
|
const expected = {
|
||||||
|
prefix: "BATD",
|
||||||
|
serial: "156362",
|
||||||
|
suffix: "WONJM",
|
||||||
|
serialU64: BigInt(156362),
|
||||||
|
devId: "BATD156362WONJM",
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(parse_PunchPkt(pkt), expected);
|
||||||
|
});
|
||||||
|
{
|
||||||
|
const in_pkt_str = "f14100145848410000000000000003e24b4d4d4542000000";
|
||||||
|
const pkt = new DataView(hstrToBA(in_pkt_str));
|
||||||
|
it("parses PunchPkt when prefix is 3 letters long", () => {
|
||||||
|
const expected = {
|
||||||
|
prefix: "XHA",
|
||||||
|
serial: "994",
|
||||||
|
suffix: "KMMEB",
|
||||||
|
serialU64: BigInt(994),
|
||||||
|
devId: "XHA994KMMEB",
|
||||||
|
};
|
||||||
|
assert.deepEqual(parse_PunchPkt(pkt), expected);
|
||||||
|
});
|
||||||
|
// https://github.com/DavidVentura/cam-reverse/issues/17#issuecomment-2094819873
|
||||||
|
it("replies properly to PunchPkt with 3-letters-long prefix", () => {
|
||||||
|
const dev = parse_PunchPkt(pkt);
|
||||||
|
const p2prdy = makeP2pRdy(dev);
|
||||||
|
let p2pstr = BATohstr(p2prdy);
|
||||||
|
|
||||||
|
assert.deepEqual(in_pkt_str.slice(8), p2pstr.slice(8));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
it("parses wifiscan chan0", () => {
|
||||||
|
buildLogger("warning");
|
||||||
|
const in_pkt_str =
|
||||||
|
"f1d00238d1000009110a03612c020100060000002f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101a741a15822a1010101010101b2fefefe650101010101010101010101" +
|
||||||
|
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101ab41a158605c010101010101b6fefefe650101010101010101010101" +
|
||||||
|
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101a741a158605c010101010101c3fefefe650101010101010101010101" +
|
||||||
|
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101af41a15822a1010101010101c8fefefe650101010101010101010101" +
|
||||||
|
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101933aac0b5330010101010101b4fefefe650101010101010101010101" +
|
||||||
|
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101973aac0b5330010101010101b4fefefe650101010101010101010101" +
|
||||||
|
"40425342"; // incomplete entry
|
||||||
|
let pkt = new DataView(hstrToBA(in_pkt_str));
|
||||||
|
let payload_len = pkt.add(0xc).readU16LE();
|
||||||
|
XqBytesDec(pkt.add(20), payload_len - 4, 4); // this mutates pkt
|
||||||
|
const expected = [
|
||||||
|
{ channel: 0, dbm0: 4294967219, dbm1: 100, mac: "a6:40:a0:59:23:a0", mode: 0, security: 0, ssid: "ACRC.NET" },
|
||||||
|
{ channel: 0, dbm0: 4294967223, dbm1: 100, mac: "aa:40:a0:59:61:5d", mode: 0, security: 0, ssid: "ACRC.Guest" },
|
||||||
|
{ channel: 0, dbm0: 4294967234, dbm1: 100, mac: "a6:40:a0:59:61:5d", mode: 0, security: 0, ssid: "ACRC.NET" },
|
||||||
|
{ channel: 0, dbm0: 4294967241, dbm1: 100, mac: "ae:40:a0:59:23:a0", mode: 0, security: 0, ssid: "ACRC.Guest" },
|
||||||
|
{ channel: 0, dbm0: 4294967221, dbm1: 100, mac: "92:3b:ad:0a:52:31", mode: 0, security: 0, ssid: "ACRC.NET" },
|
||||||
|
{ channel: 0, dbm0: 4294967221, dbm1: 100, mac: "96:3b:ad:0a:52:31", mode: 0, security: 0, ssid: "ACRC.Guest" },
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(parseListWifi(pkt), expected);
|
||||||
|
});
|
||||||
|
it("parses wifiscan chan2", () => {
|
||||||
|
buildLogger("warning");
|
||||||
|
const in_pkt_str =
|
||||||
|
"f1d00404d100000d110a0361f80300000b0000002f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101603735316031340101010101c701010165010101010101010301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101606035316031340101010101c7010101650101010101010103010101" + // frame
|
||||||
|
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101603735316031340101010101c2010101650101010101010103010101" +
|
||||||
|
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101676460373736320101010101b1010101650101010101010103010101" +
|
||||||
|
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101383332636065310101010101af010101650101010101010103010101" +
|
||||||
|
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101393764606465350101010101af010101650101010101010103010101" +
|
||||||
|
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101326460603964670101010101ac010101650101010101010103010101" +
|
||||||
|
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101606435316031340101010101c7010101650101010101010103010101" +
|
||||||
|
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101606035316031340101010101c2010101650101010101010103010101" +
|
||||||
|
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101643562323360620101010101b3010101650101010101010103010101" +
|
||||||
|
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101383732636065310101010101af010101650101010101010103010101" +
|
||||||
|
"40425342"; // incomplete
|
||||||
|
let pkt = new DataView(hstrToBA(in_pkt_str));
|
||||||
|
let payload_len = pkt.add(0xc).readU16LE();
|
||||||
|
XqBytesDec(pkt.add(20), payload_len - 4, 4); // this mutates pkt
|
||||||
|
const expected = [
|
||||||
|
{ ssid: "ACRC.NET", mac: "61:36:34:30:61:30", security: 0, dbm0: 198, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "", mac: "61:61:34:30:61:30", security: 0, dbm0: 198, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "ACRC.NET", mac: "61:36:34:30:61:30", security: 0, dbm0: 195, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "", mac: "66:65:61:36:36:37", security: 0, dbm0: 176, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "ACRC.NET", mac: "39:32:33:62:61:64", security: 0, dbm0: 174, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "", mac: "38:36:65:61:65:64", security: 0, dbm0: 174, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "", mac: "33:65:61:61:38:65", security: 0, dbm0: 173, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "ACRC.Guest", mac: "61:65:34:30:61:30", security: 0, dbm0: 198, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "ACRC.Guest", mac: "61:61:34:30:61:30", security: 0, dbm0: 195, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "", mac: "65:34:63:33:32:61", security: 0, dbm0: 178, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
{ ssid: "ACRC.Guest", mac: "39:36:33:62:61:64", security: 0, dbm0: 174, dbm1: 100, mode: 0, channel: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(parseListWifi(pkt), expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("make packet", () => {
|
describe("make packet", () => {
|
||||||
@@ -155,10 +247,47 @@ describe("make packet", () => {
|
|||||||
it("builds a good WifiSettingsSet", () => {
|
it("builds a good WifiSettingsSet", () => {
|
||||||
const sess = { outgoingCommandId: 2, ticket: [1, 2, 3, 4] };
|
const sess = { outgoingCommandId: 2, ticket: [1, 2, 3, 4] };
|
||||||
const _expected_str =
|
const _expected_str =
|
||||||
"f1d00118d1000002110a01600c010000010203040101010101010101010101010101010100010101726a786f647501010101010101010101010101010101010101010101010101017274716473627360710101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101312f3334342f3334342f333434010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f3101010101010101010101010101";
|
"f1d00118d1000002110a01600c01000001020304" + // drw header + ticket
|
||||||
|
"0101010101010101010101010101010100010101" + // all zeroes, but DHCP u32
|
||||||
|
// set to 1
|
||||||
|
"726a786f64750101010101010101010101010101010101010101010101010101" + // ssid
|
||||||
|
"7274716473627360710101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" + // pass
|
||||||
|
"312f312f312f31010101010101010101" + // 0.0.0.0
|
||||||
|
"312f3334342f3334342f333434010101" + // 0.255.255.255
|
||||||
|
"312f312f312f31010101010101010101" + // 0.0.0.0
|
||||||
|
"312f312f312f31010101010101010101" + // 0.0.0.0
|
||||||
|
"312f312f312f31010101010101010101" + // 0.0.0.0
|
||||||
|
"01010101"; // 0000
|
||||||
const expected = hstrToBA(_expected_str);
|
const expected = hstrToBA(_expected_str);
|
||||||
|
|
||||||
const got = SendWifiDetails(sess, "skynet", "supercrap", true);
|
const got = SendWifiDetails(sess, "skynet", "supercrap", 0, true);
|
||||||
assert.deepEqual(got.buffer, expected);
|
assert.deepEqual(got.buffer, expected);
|
||||||
});
|
});
|
||||||
|
it("builds a good WifiSettingsSet - with channel", () => {
|
||||||
|
const sess = { outgoingCommandId: 0xc, ticket: [0x30, 0x35, 0x74, 0x72] };
|
||||||
|
const _expected_str =
|
||||||
|
"f1d00118d100000c110a01600c010000303574720101010101010101030101010101010100010101404253422f4f445501010101010101010101010101010101010101010101010167606a6471607272010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101312f312f312f31010101010101010101312f3334342f3334342f333434010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f3101010101010101010101010101";
|
||||||
|
const expected = hstrToBA(_expected_str);
|
||||||
|
|
||||||
|
const got = SendWifiDetails(sess, "ACRC.NET", "fakepass", 2, true);
|
||||||
|
assert.deepEqual(got.buffer, expected);
|
||||||
|
});
|
||||||
|
it("parses devstatusack", () => {
|
||||||
|
const pkt = new DataView(
|
||||||
|
hstrToBA(
|
||||||
|
"f1d0008cd1000009110a08118000000000000000190e010101010101fefefefebefefefe01000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100010101010001030101010101010101010101010101fefefefefe010101010101010134030300",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let payload_len = pkt.add(0xc).readU16LE();
|
||||||
|
XqBytesDec(pkt.add(20), payload_len - 4, 0); // this mutates pkt
|
||||||
|
const got = parseDevStatusAck(pkt);
|
||||||
|
const expected = {
|
||||||
|
battery_mV: 0,
|
||||||
|
charging: false,
|
||||||
|
dbm: -256,
|
||||||
|
swver: "0.0.15.24",
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.deepEqual(expected, got);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+19
@@ -2,6 +2,7 @@ struct avparamset_t {
|
|||||||
uint32_t paramType;
|
uint32_t paramType;
|
||||||
uint32_t paramValue;
|
uint32_t paramValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct datetime_t {
|
struct datetime_t {
|
||||||
uint32_t now; // the code to _write_ these uses long ))
|
uint32_t now; // the code to _write_ these uses long ))
|
||||||
uint32_t tz;
|
uint32_t tz;
|
||||||
@@ -99,3 +100,21 @@ struct stream_head_t {
|
|||||||
short sample;
|
short sample;
|
||||||
short index;
|
short index;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct tag_wifiParams { /* PlaceHolder Structure */
|
||||||
|
int enable;
|
||||||
|
int noidea; // wifi status?
|
||||||
|
int mode;
|
||||||
|
int chan;
|
||||||
|
int auth;
|
||||||
|
int dhcp;
|
||||||
|
char ssid[32];
|
||||||
|
char psk[128];
|
||||||
|
char ip[16];
|
||||||
|
char mask[16];
|
||||||
|
char gw[16];
|
||||||
|
char dns1[16];
|
||||||
|
char dns2[16];
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user