Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80b6b0a67 | ||
|
|
9f6f0ca8fa | ||
|
|
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 }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run tsc
|
||||
- run: npm run build
|
||||
- run: npm test
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -3,3 +3,4 @@ venv
|
||||
node_modules
|
||||
build
|
||||
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
|
||||
~/node_modules/.bin/frida-compile -o $@ frida-hooks.js
|
||||
@@ -8,15 +8,13 @@ venv: requirements.txt
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
touch venv
|
||||
|
||||
build: node_modules
|
||||
typecheck: node_modules
|
||||
npm run tsc
|
||||
build: node_modules
|
||||
npm run build
|
||||
|
||||
run: node_modules
|
||||
./node_modules/.bin/ts-node --esm cmd/bin http_server --port=1234
|
||||
|
||||
pair: node_modules
|
||||
./node_modules/.bin/ts-node --esm cmd/bin pair
|
||||
run: build
|
||||
node dist/bin.cjs http_server --port=1234
|
||||
|
||||
hook: bundle.js venv
|
||||
./venv/bin/python3 -u loader3.py
|
||||
@@ -25,7 +23,7 @@ node_modules:
|
||||
npm install
|
||||
|
||||
test: node_modules
|
||||
./node_modules/.bin/mocha --require ts-node/register tests/fn.test.js
|
||||
npm run test
|
||||
|
||||
install-wireshark-dissector:
|
||||
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))
|
||||
|
||||
## 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
|
||||
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
|
||||
$ node dist/bin.cjs http_server --help
|
||||
|
||||
start http server
|
||||
|
||||
Options:
|
||||
--help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
--debug [boolean] [default: false]
|
||||
--ansi [boolean] [default: false]
|
||||
--audio [boolean] [default: false]
|
||||
--discovery_ip [default: "192.168.1.255"]
|
||||
--attempt_to_fix_packet_loss [default: false]
|
||||
--port HTTP Port to listen on [number] [default: 5000]
|
||||
```
|
||||
### HTTP Server
|
||||
To execute the HTTP server, run `node dist/bin.cjs http_server`; you can access the JPEG stream at http://localhost:5000/.
|
||||
|
||||
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>`.
|
||||
```bash
|
||||
$ node dist/bin.cjs pair --help
|
||||
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>`
|
||||
|
||||
configure a camera
|
||||
```yml
|
||||
http_server:
|
||||
port: 5000
|
||||
|
||||
Options:
|
||||
--help Show help [boolean]
|
||||
--version Show version number [boolean]
|
||||
--debug [boolean] [default: false]
|
||||
--ansi [boolean] [default: false]
|
||||
--discovery_ip [default: "192.168.1.255"]
|
||||
--attempt_to_fix_packet_loss [default: false]
|
||||
--ssid [string] [required]
|
||||
--password [string] [required]
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<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>
|
||||
const alaw_to_s16_table = [
|
||||
-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];
|
||||
};
|
||||
|
||||
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 channels =1;
|
||||
const sample_rate = 8000;
|
||||
const audioBuffer = audio_context.createBuffer(channels, 960, sample_rate); // 960??
|
||||
//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
|
||||
audio_context.resume();
|
||||
|
||||
const evtSource = new EventSource('/audio/${id}');
|
||||
const evtSource = new EventSource("/audio/${id}");
|
||||
evtSource.onopen = (e) => {
|
||||
console.log("evtsource open");
|
||||
}
|
||||
evtSource.onerror = (e) => {
|
||||
console.log("evtsource error", e);
|
||||
}
|
||||
let endsAt = 0;
|
||||
let startAt = 0;
|
||||
evtSource.onmessage = (e) => {
|
||||
@@ -55,8 +76,28 @@
|
||||
source_node.start(startAt);
|
||||
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>
|
||||
</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 yargs from "yargs/yargs";
|
||||
|
||||
import { captureSingle } from "../capture_single.js";
|
||||
import { serveHttp } from "../http_server.js";
|
||||
import { opt } from "../options.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))
|
||||
.command(
|
||||
@@ -11,19 +17,38 @@ yargs(hideBin(process.argv))
|
||||
"start http server",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option("debug", { default: false })
|
||||
.option("ansi", { default: false })
|
||||
.option("audio", { default: false })
|
||||
.boolean(["debug", "ansi", "audio"])
|
||||
.option("discovery_ip", { default: "192.168.1.255" })
|
||||
.option("attempt_to_fix_packet_loss", { default: false })
|
||||
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
|
||||
.option("color", { describe: "Use color in logs" })
|
||||
.boolean(["audio", "color"])
|
||||
.option("config_file", { describe: "Specify config file" })
|
||||
.option("log_level", { describe: "Set log level" })
|
||||
.option("discovery_ip", { describe: "Camera discovery IP address" })
|
||||
.option("port", { describe: "HTTP Port to listen on" })
|
||||
.string(["log_level", "discovery_ip", "config_file"])
|
||||
.number(["port"])
|
||||
.strict();
|
||||
},
|
||||
(argv) => {
|
||||
const opts: opt = argv as opt;
|
||||
serveHttp(opts, argv.port, argv.audio || false);
|
||||
if (argv.config_file !== undefined) {
|
||||
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(
|
||||
@@ -31,18 +56,38 @@ yargs(hideBin(process.argv))
|
||||
"configure a camera",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option("debug", { default: false })
|
||||
.option("ansi", { default: false })
|
||||
.boolean(["debug", "ansi"])
|
||||
.option("discovery_ip", { default: "192.168.1.255" })
|
||||
.option("attempt_to_fix_packet_loss", { default: false })
|
||||
.demandOption("ssid")
|
||||
.demandOption("password")
|
||||
.option("log_level", { describe: "Set log level", default: "info" })
|
||||
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||
.option("ssid", { describe: "Wifi network for the camera to connect to" })
|
||||
.option("password", { describe: "Wifi network password" })
|
||||
.demandOption(["ssid", "password"])
|
||||
.string(["ssid", "password"]);
|
||||
},
|
||||
(argv) => {
|
||||
const opts: opt = argv as unknown as opt;
|
||||
pair({ opts, ssid: argv.ssid, password: argv.password });
|
||||
buildLogger(argv.log_level, undefined);
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
ListWifiAck: 0x0361,
|
||||
StartVideo: 0x1030, // CMD_PEER_LIVEVIDEO_START
|
||||
StartVideoAck: 0x1031,
|
||||
StopVideo: 0x1130, // CMD_PEER_LIVEVIDEO_STOP
|
||||
Shutdown: 0x1010, //CMD_SYSTEM_SHUTDOWN,
|
||||
Reboot: 0x1110, //CMD_SYSTEM_REBOOT,
|
||||
VideoParamSet: 0x1830, // CMD_PEER_VIDEOPARAM_SET
|
||||
VideoParamSetAck: 0x1831,
|
||||
VideoParamGet: 0x1930, // CMD_PEER_VIDEOPARAM_GET
|
||||
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 { 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 dv = new DataView(ab);
|
||||
const cmd_id = dv.readU16();
|
||||
if (cmd_id != Commands.PunchPkt) {
|
||||
return;
|
||||
}
|
||||
if (debug) {
|
||||
console.log("Discovery got a PunchPkt");
|
||||
if (config.blacklisted_ips.indexOf(rinfo.address) !== -1) {
|
||||
logger.debug(`Dropping packet of blacklisted IP: ${rinfo.address}`);
|
||||
return;
|
||||
}
|
||||
logger.debug("Received a PunchPkt message");
|
||||
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 SEND_PORT = 32108;
|
||||
const ee = new EventEmitter();
|
||||
@@ -26,21 +31,28 @@ export const discoverDevices = (debug: boolean, discovery_ip: string): EventEmit
|
||||
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", () => {
|
||||
let ls_buf = create_LanSearch();
|
||||
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();
|
||||
setInterval(() => {
|
||||
console.log(".");
|
||||
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
|
||||
}, 2000);
|
||||
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
|
||||
let int = setInterval(() => {
|
||||
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
|
||||
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||
}, 3000);
|
||||
timers.push(int);
|
||||
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
|
||||
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||
});
|
||||
});
|
||||
|
||||
sock.bind();
|
||||
|
||||
sock.on("close", () => timers.forEach((timer) => clearInterval(timer)));
|
||||
ee.on("close", () => sock.close());
|
||||
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.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.warning = ProtoField.string("iLnkP2P.warning", "Warning")
|
||||
--
|
||||
-- jpeg | audio | continuation type?
|
||||
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.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")
|
||||
|
||||
-- PunchPkt
|
||||
ilnk_proto.fields.serial = ProtoField.string("iLnkP2P.serial", "Serial")
|
||||
|
||||
lut = {
|
||||
[0xf1f0] = "Close",
|
||||
[0xf132] = "LanSearchExt",
|
||||
@@ -46,11 +64,308 @@ lut = {
|
||||
[0xf169] = "ListenReqAck",
|
||||
[0xf170] = "RlyHelloAck",
|
||||
[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
|
||||
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")
|
||||
|
||||
-- Add the entire packet as a field
|
||||
local packetname = lut[buffer(0, 2):uint()]
|
||||
subtree:add(ilnk_proto.fields.type, packetname)
|
||||
local packettype = buffer(0, 2):uint()
|
||||
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
|
||||
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
|
||||
subtree:add(ilnk_proto.fields.len, buffer(2, 2))
|
||||
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.m_type, buffer(4, 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))
|
||||
local b_payload_len = buffer(0xc, 2)
|
||||
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
|
||||
subtree:add_le(ilnk_proto.fields.start, buffer(8, 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_dest, buffer(0xe, 2))
|
||||
-- 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))
|
||||
end
|
||||
else
|
||||
local payload_type
|
||||
local payload_subtype
|
||||
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))
|
||||
end
|
||||
end
|
||||
@@ -140,4 +508,15 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
|
||||
-- subtree:add(ilnk_proto.fields.payload, buffer(2, packet_length-2))
|
||||
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;
|
||||
};
|
||||
+179
-67
@@ -1,14 +1,14 @@
|
||||
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
|
||||
import { XqBytesDec } from "./func_replacements.js";
|
||||
import { hexdump } from "./hexdump.js";
|
||||
import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
|
||||
import { create_P2pRdy, DevSerial, SendListWifi, SendUsrChk } from "./impl.js";
|
||||
import { logger } from "./logger.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 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) => {};
|
||||
@@ -25,54 +25,94 @@ export const handle_P2PAlive = (session: Session, _: DataView) => {
|
||||
};
|
||||
|
||||
export const handle_P2PRdy = (session: Session, _: DataView) => {
|
||||
// TODO - config
|
||||
const b = SendUsrChk(session, "admin", "admin");
|
||||
session.send(b);
|
||||
};
|
||||
|
||||
export const makePunchPkt = (dev: DevSerial): DataView => {
|
||||
const len = dev.prefix.length + dev.suffix.length + 8;
|
||||
export const makeP2pRdy = (dev: DevSerial): DataView => {
|
||||
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(4).writeU64(dev.serialU64);
|
||||
outbuf.add(8 + dev.prefix.length).writeString(dev.suffix);
|
||||
outbuf.add(8 + devPrefixLength).writeString(dev.suffix);
|
||||
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[] => {
|
||||
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 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) {
|
||||
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 [];
|
||||
}
|
||||
const rotate_chr = 4;
|
||||
if (payload_len > rotate_chr) {
|
||||
// 20 = 16 (header) + 4 (??)
|
||||
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);
|
||||
session.ticket = [...c];
|
||||
session.eventEmitter.emit("login");
|
||||
return [];
|
||||
}
|
||||
|
||||
if (cmd_id == ControlCommands.DevStatusAck) {
|
||||
case ControlCommands.DevStatusAck:
|
||||
// ParseDevStatus -> offset relevant?
|
||||
let charging = u32_swap(dv.add(0x28).readU32()) & 1; // 0x14000101 v 0x14000100
|
||||
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts?
|
||||
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
|
||||
const status = parseDevStatusAck(dv);
|
||||
// > -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 = {
|
||||
enable: dv.add(0x14).readU32(),
|
||||
status: dv.add(0x18).readU32(),
|
||||
@@ -89,77 +129,127 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
||||
dns2: dv.add(0x10c).readString(0x10),
|
||||
};
|
||||
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];
|
||||
}
|
||||
|
||||
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 msg_len = 91;
|
||||
console.log("payload len", payload_len);
|
||||
let msg_count = (payload_len - 9) / msg_len;
|
||||
let remote_msg_count = dv.add(startat).readU32LE();
|
||||
console.log("should get messages:", msg_count, "in payload: ", remote_msg_count);
|
||||
const msg_len = 0x5c; // 0x58 + 0x4 of the last u32
|
||||
const msg_count = dv.add(startat).readU32LE();
|
||||
startat += 4;
|
||||
let items = [];
|
||||
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 = {
|
||||
// startat = msg_len * i + 0x14;
|
||||
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(),
|
||||
dbm0: dv.add(startat + 0x4c).readU32LE(),
|
||||
dbm1: dv.add(startat + 0x50).readU32LE(),
|
||||
mode: dv.add(startat + 0x54).readU32LE(),
|
||||
channel: dv.add(startat + 0x58).readU32LE(),
|
||||
};
|
||||
console.log(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
||||
startat += msg_len;
|
||||
console.log("ended at", startat);
|
||||
items.push(wifiListItem);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
return items;
|
||||
};
|
||||
|
||||
const deal_with_data = (session: Session, dv: DataView) => {
|
||||
const pkt_len = dv.add(2).readU16();
|
||||
// data
|
||||
const JPEG_HEADER = [0xff, 0xd8, 0xff, 0xdb];
|
||||
const AUDIO_HEADER = [0x55, 0xaa, 0x15, 0xa8];
|
||||
const m_hdr = dv.add(8).readByteArray(4);
|
||||
let is_new_image = true;
|
||||
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];
|
||||
|
||||
// 12 equals start of header (0x8) + header length (0x4)
|
||||
if (pkt_len < 12) {
|
||||
logger.log("trace", "Got a short Drw packet, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
// "stream_head_t->type == 0x06" per pdf
|
||||
if (dv.add(12).readU8() == 0x06) {
|
||||
const audio_len = u16_swap(dv.add(8 + 16).readU16());
|
||||
// may have received the next 'data' packet id as an audio frame -- jpeg was fine
|
||||
if (pkt_id == session.rcvSeqId + 1) {
|
||||
session.rcvSeqId = session.rcvSeqId + 1;
|
||||
}
|
||||
const audio_buf = dv.add(32 + 8).readByteArray(audio_len).buffer; // 8 for pkt header, 32 for `stream_head_t`
|
||||
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) {
|
||||
const FRAME_HEADER = [0x55, 0xaa, 0x15, 0xa8];
|
||||
const m_hdr = dv.add(8).readByteArray(4);
|
||||
const pkt_id = dv.add(6).readU16();
|
||||
const STREAM_TYPE_AUDIO = 0x06;
|
||||
const STREAM_TYPE_JPEG = 0x03;
|
||||
|
||||
const startNewFrame = (buf: ArrayBuffer) => {
|
||||
if (session.curImage.length > 0 && !session.frame_is_bad) {
|
||||
session.eventEmitter.emit("frame");
|
||||
}
|
||||
|
||||
session.frame_was_fixed = false;
|
||||
session.frame_is_bad = false;
|
||||
session.curImage = [Buffer.from(data.buffer)];
|
||||
session.curImage = [Buffer.from(buf)];
|
||||
session.rcvSeqId = pkt_id;
|
||||
};
|
||||
|
||||
let is_framed = m_hdr.startsWith(FRAME_HEADER);
|
||||
|
||||
if (is_framed) {
|
||||
const stream_type = dv.add(12).readU8();
|
||||
if (stream_type == STREAM_TYPE_AUDIO) {
|
||||
const audio_len = 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 {
|
||||
if (pkt_id <= session.rcvSeqId) {
|
||||
// retransmit
|
||||
@@ -169,14 +259,17 @@ const deal_with_data = (session: Session, dv: DataView) => {
|
||||
let b = Buffer.from(data.buffer);
|
||||
|
||||
if (pkt_id > session.rcvSeqId + 1) {
|
||||
if (!session.frame_is_bad) {
|
||||
session.frame_is_bad = true;
|
||||
// this should always be enabled but currently it seems to cause more visual distortion
|
||||
// than just missing some frames
|
||||
if (!session.options.attempt_to_fix_packet_loss) {
|
||||
logger.debug(`Dropping corrupt frame ${pkt_id}, expected ${session.rcvSeqId + 1}`);
|
||||
}
|
||||
// this should always be enabled but currently it seems to cause more
|
||||
// visual distortion than just missing some frames
|
||||
if (!config.cameras[session.devName]?.fix_packet_loss) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.curImage.length == 1) return; // header does not have markers
|
||||
if (session.curImage.length <= 1) return; // header does not have markers
|
||||
|
||||
let lastFrameSlice = session.curImage[session.curImage.length - 1];
|
||||
const lastResetMarker = findAllResetMarkers(lastFrameSlice).pop();
|
||||
@@ -235,6 +328,17 @@ const makeDrwAck = (dv: DataView): DataView => {
|
||||
}
|
||||
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) => {
|
||||
const ack = makeDrwAck(dv);
|
||||
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
|
||||
if (m_stream == 1) {
|
||||
deal_with_data(session, dv);
|
||||
} else {
|
||||
} else if (m_stream == 0) {
|
||||
const b = createResponseForControlCommand(session, dv);
|
||||
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();
|
||||
};
|
||||
|
||||
+93
-29
@@ -1,18 +1,37 @@
|
||||
import { RemoteInfo } from "dgram";
|
||||
import { readFileSync } from "node:fs";
|
||||
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 { DevSerial } from "./impl.js";
|
||||
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
||||
import { addExifToJpeg, createExifOrientation } from "./exif.js";
|
||||
|
||||
let BOUNDARY = "a very good boundary line";
|
||||
let responses: Record<string, http.ServerResponse[]> = {};
|
||||
let audioResponses: Record<string, http.ServerResponse[]> = {};
|
||||
let sessions: Record<string, Session> = {};
|
||||
// @ts-expect-error TS2307
|
||||
import favicon from "./cam.ico.gz";
|
||||
// @ts-expect-error TS2307
|
||||
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) => {
|
||||
if (req.url.startsWith("/ui/")) {
|
||||
let devId = req.url.split("/")[2];
|
||||
@@ -27,8 +46,12 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
res.end("Nothing online");
|
||||
return;
|
||||
}
|
||||
const ui = readFileSync("asd.html").toString();
|
||||
res.end(ui.replace(/\${id}/g, devId));
|
||||
const ui = html_template
|
||||
.toString()
|
||||
.replace(/\${id}/g, devId)
|
||||
.replace(/\${name}/g, cameraName(devId))
|
||||
.replace(/\${audio}/g, config.cameras[devId].audio ? "true" : "false");
|
||||
res.end(ui);
|
||||
return;
|
||||
}
|
||||
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`);
|
||||
audioResponses[devId].push(res);
|
||||
logger.info(`Audio stream requested for camera ${devId}`);
|
||||
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];
|
||||
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];
|
||||
|
||||
if (s === undefined) {
|
||||
res.writeHead(400);
|
||||
res.end("invalid ID");
|
||||
res.end(`Camera ${devId} not discovered`);
|
||||
return;
|
||||
}
|
||||
if (!s.connected) {
|
||||
res.writeHead(400);
|
||||
res.end("Nothing online");
|
||||
res.end(`Camera ${devId} offline`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -69,35 +116,53 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
responses[devId].push(res);
|
||||
res.on("close", () => {
|
||||
responses[devId] = responses[devId].filter((r) => r !== res);
|
||||
console.log("Conn closed, kicked");
|
||||
logger.info(`Video stream closed for camera ${devId}`);
|
||||
});
|
||||
} 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) =>
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
if (dev.devId in sessions) {
|
||||
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
|
||||
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
|
||||
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||
responses[dev.devId] = [];
|
||||
audioResponses[dev.devId] = [];
|
||||
const s = makeSession(Handlers, dev, rinfo, startVideoStream, opts);
|
||||
|
||||
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
||||
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] || {}) };
|
||||
|
||||
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)]);
|
||||
const header = Buffer.from(`\r\n--${BOUNDARY}\r\nContent-Length: ${assembled.length}\r\nContent-Type: image/jpeg\r\n\r\n`);
|
||||
responses[dev.devId].forEach((res) => {
|
||||
res.write(header);
|
||||
res.write(assembled);
|
||||
@@ -105,10 +170,10 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
});
|
||||
|
||||
s.eventEmitter.on("disconnect", () => {
|
||||
console.log("deleting from sessions");
|
||||
logger.info(`Camera ${dev.devId} disconnected`);
|
||||
delete sessions[dev.devId];
|
||||
});
|
||||
if (with_audio) {
|
||||
if (config.cameras[dev.devId].audio) {
|
||||
s.eventEmitter.on("audio", ({ gap, data }) => {
|
||||
// ew, maybe WS?
|
||||
var b64encoded = Buffer.from(data).toString("base64");
|
||||
@@ -119,9 +184,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);
|
||||
};
|
||||
|
||||
@@ -119,7 +119,13 @@ export const SendReboot = (session: Session): DataView => {
|
||||
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) {
|
||||
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_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(0x38).writeByteArray(str2byte(password));
|
||||
cmd_payload.add(0xb8).writeByteArray(str2byte(mask_reversed));
|
||||
cmd_payload.add(0xc8).writeByteArray(str2byte(m_ip));
|
||||
cmd_payload.add(0xb8).writeByteArray(str2byte(m_ip));
|
||||
cmd_payload.add(0xc8).writeByteArray(str2byte(mask_reversed));
|
||||
cmd_payload.add(0xd8).writeByteArray(str2byte(m_gw));
|
||||
cmd_payload.add(0xe8).writeByteArray(str2byte(m_dns1));
|
||||
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);
|
||||
};
|
||||
|
||||
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 => {
|
||||
const outbuf = new DataView(new Uint8Array(4).buffer);
|
||||
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 = {
|
||||
debug: boolean;
|
||||
ansi: boolean;
|
||||
discovery_ip: string;
|
||||
attempt_to_fix_packet_loss: boolean;
|
||||
};
|
||||
|
||||
Generated
+268
-6
@@ -5,14 +5,27 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.2",
|
||||
"winston": "^3.13.0",
|
||||
"yaml": "^2.4.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/yargs": "^17.0.32",
|
||||
"esbuild": "^0.20.2",
|
||||
"mocha": "^10.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"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": {
|
||||
@@ -27,6 +40,16 @@
|
||||
"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": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
|
||||
@@ -34,6 +57,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
@@ -49,6 +73,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -64,6 +89,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -79,6 +105,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
@@ -94,6 +121,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -109,6 +137,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
@@ -124,6 +153,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -139,6 +169,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
@@ -154,6 +185,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -169,6 +201,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -184,6 +217,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -199,6 +233,7 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -214,6 +249,7 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -229,6 +265,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -244,6 +281,7 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -259,6 +297,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -274,6 +313,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
@@ -289,6 +329,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
@@ -304,6 +345,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
@@ -319,6 +361,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
@@ -334,6 +377,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -349,6 +393,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -364,6 +409,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -431,6 +477,11 @@
|
||||
"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": {
|
||||
"version": "17.0.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
|
||||
@@ -523,6 +574,11 @@
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -645,6 +701,15 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
|
||||
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
@@ -779,6 +881,11 @@
|
||||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -816,6 +923,11 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
@@ -929,8 +1041,12 @@
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
@@ -991,6 +1107,17 @@
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -1046,6 +1178,22 @@
|
||||
"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": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
@@ -1136,8 +1284,7 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.3",
|
||||
@@ -1169,6 +1316,14 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -1238,6 +1393,19 @@
|
||||
"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": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
@@ -1262,7 +1430,6 @@
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
|
||||
@@ -1287,6 +1462,30 @@
|
||||
"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": {
|
||||
"version": "4.2.3",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -1350,6 +1554,14 @@
|
||||
"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": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
@@ -1422,12 +1634,51 @@
|
||||
"dev": 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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"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": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
|
||||
@@ -1464,6 +1715,17 @@
|
||||
"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": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
|
||||
+7
-2
@@ -3,16 +3,21 @@
|
||||
"scripts": {
|
||||
"test": "mocha tests",
|
||||
"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": {
|
||||
"@types/yargs": "^17.0.32",
|
||||
"esbuild": "^0.20.2",
|
||||
"mocha": "^10.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"esbuild": "^0.20.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"winston": "^3.13.0",
|
||||
"yaml": "^2.4.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,63 @@
|
||||
import { RemoteInfo } from "dgram";
|
||||
|
||||
import { opt } from "./options.js";
|
||||
import { config } from "./settings.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 { logger } from "./logger.js";
|
||||
|
||||
export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; password: string }) => {
|
||||
console.log(`Will configure any devices found to join ${ssid}`);
|
||||
export const pair = ({ ssid, password }: { ssid: string; password: string }) => {
|
||||
logger.info(`Will configure any devices found to join ${ssid}`);
|
||||
let sessions: Record<string, Session> = {};
|
||||
|
||||
let devEv = discoverDevices(opts.debug, opts.discovery_ip);
|
||||
let devEv = discoverDevices(config.discovery_ips);
|
||||
if (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) => {
|
||||
if (dev.devId in sessions) {
|
||||
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
|
||||
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
|
||||
return;
|
||||
}
|
||||
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
|
||||
const s = makeSession(Handlers, dev, rinfo, onLogin, opts);
|
||||
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||
const s = makeSession(Handlers, dev, rinfo, onLogin, 10000);
|
||||
let configured = {};
|
||||
|
||||
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 configured[dev.devId];
|
||||
});
|
||||
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 ........
|
||||
|
||||
-- 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 { Commands, CommandsByValue } from "./datatypes.js";
|
||||
import { handle_Drw, handle_P2PAlive, handle_P2PRdy, makePunchPkt, noop, notImpl } from "./handlers.js";
|
||||
import { hexdump } from "./hexdump.js";
|
||||
import {
|
||||
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 { opt } from "./options.js";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
export type Session = {
|
||||
send: (msg: DataView) => void;
|
||||
ackDrw: (id: number) => void;
|
||||
unackedDrw: { [id: number]: { sent_ts: number; data: DataView } };
|
||||
outgoingCommandId: number;
|
||||
ticket: number[];
|
||||
eventEmitter: EventEmitter;
|
||||
@@ -21,7 +31,8 @@ export type Session = {
|
||||
rcvSeqId: number;
|
||||
frame_is_bad: boolean;
|
||||
frame_was_fixed: boolean;
|
||||
options: opt;
|
||||
started: boolean;
|
||||
close: () => 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 ab = new Uint8Array(msg).buffer;
|
||||
const dv = new DataView(ab);
|
||||
const cmd = CommandsByValue[dv.readU16()];
|
||||
if (session.options.debug) {
|
||||
console.log(`<< ${cmd}`);
|
||||
console.log(hexdump(msg.buffer, { ansi: session.options.ansi, ansiColor: 1 }));
|
||||
}
|
||||
const raw = dv.readU16();
|
||||
const cmd = CommandsByValue[raw];
|
||||
logger.log("trace", `<< ${cmd}`);
|
||||
handlers[cmd](session, dv, rinfo);
|
||||
if (raw != Commands.P2PAlive && raw != Commands.P2PAliveAck) {
|
||||
session.lastReceivedPacket = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
export const makeSession = (
|
||||
@@ -50,8 +61,9 @@ export const makeSession = (
|
||||
dev: DevSerial,
|
||||
ra: RemoteInfo,
|
||||
onLogin: (s: Session) => void,
|
||||
options: opt,
|
||||
timeoutMs: number,
|
||||
): Session => {
|
||||
let unackedDrw = {};
|
||||
const sock = createSocket("udp4");
|
||||
|
||||
sock.on("error", (err) => {
|
||||
@@ -62,68 +74,96 @@ export const makeSession = (
|
||||
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
|
||||
|
||||
sock.on("listening", () => {
|
||||
const buf = makePunchPkt(dev);
|
||||
const buf = makeP2pRdy(dev);
|
||||
session.send(buf);
|
||||
session.started = true;
|
||||
});
|
||||
|
||||
const SEND_PORT = 32108;
|
||||
sock.bind();
|
||||
const sessTimer = setInterval(() => {
|
||||
const delta = Date.now() - session.lastReceivedPacket;
|
||||
if (session.started) {
|
||||
if (delta > 600) {
|
||||
let buf = create_P2pAlive();
|
||||
session.send(buf);
|
||||
}
|
||||
if (delta > 8000) {
|
||||
if (delta > timeoutMs) {
|
||||
logger.warning(`Camera ${session.devName} timed out`);
|
||||
session.eventEmitter.emit("disconnect");
|
||||
}
|
||||
}
|
||||
}, 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 = {
|
||||
outgoingCommandId: 0,
|
||||
ticket: [0, 0, 0, 0],
|
||||
lastReceivedPacket: 0,
|
||||
eventEmitter: new EventEmitter(),
|
||||
connected: true,
|
||||
timers: [sessTimer],
|
||||
timers: [sessTimer, resendTimer],
|
||||
devName: dev.devId,
|
||||
started: false,
|
||||
send: (msg: DataView) => {
|
||||
const raw = msg.readU16();
|
||||
const cmd = CommandsByValue[raw];
|
||||
if (options.debug) {
|
||||
console.log(`>> ${cmd}`);
|
||||
if (raw != Commands.P2PAlive) {
|
||||
console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 0 }));
|
||||
// send command
|
||||
if (raw == 0xf1d0 && msg.add(4).readU8() == 0xd1) {
|
||||
const packet_id = msg.add(6).readU16();
|
||||
logger.debug(`Sending Drw Packet with id ${packet_id}`);
|
||||
unackedDrw[packet_id] = { sent_ts: Date.now(), data: msg };
|
||||
}
|
||||
}
|
||||
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
|
||||
logger.log("trace", `>> ${cmd}`);
|
||||
sock.send(new Uint8Array(msg.buffer), ra.port, session.dst_ip);
|
||||
},
|
||||
ackDrw: (id: number) => {
|
||||
logger.debug(`Removing ${id} from pending`);
|
||||
delete unackedDrw[id];
|
||||
},
|
||||
dst_ip: ra.address,
|
||||
curImage: [],
|
||||
rcvSeqId: 0,
|
||||
frame_is_bad: false,
|
||||
frame_was_fixed: false,
|
||||
options: options,
|
||||
unackedDrw,
|
||||
close: () => {
|
||||
session.eventEmitter.emit("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.connected = false;
|
||||
session.timers.forEach((x) => clearInterval(x));
|
||||
session.timers = [];
|
||||
sock.close();
|
||||
});
|
||||
|
||||
session.eventEmitter.on("login", () => {
|
||||
console.log(`Logged in - ${session.devName}`);
|
||||
logger.info(`Logging in to camera ${session.devName}`);
|
||||
onLogin(session);
|
||||
});
|
||||
return session;
|
||||
};
|
||||
|
||||
export const configureWifi = (ssid: string, password: string) => {
|
||||
export const configureWifi = (ssid: string, password: string, channel: number) => {
|
||||
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,
|
||||
P2PAlive: handle_P2PAlive,
|
||||
P2pRdy: handle_P2PRdy,
|
||||
DrwAck: noop,
|
||||
DrwAck: handle_DrwAck,
|
||||
Drw: handle_Drw,
|
||||
Close: handle_Close,
|
||||
|
||||
Close: notImpl,
|
||||
P2PAliveAck: noop,
|
||||
LanSearchExt: notImpl,
|
||||
LanSearch: notImpl,
|
||||
P2PAliveAck: notImpl,
|
||||
Hello: notImpl,
|
||||
P2pReq: 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));
|
||||
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 {
|
||||
interface DataView {
|
||||
@@ -77,6 +88,7 @@ declare global {
|
||||
writeU32(n: number): void;
|
||||
writeU64(n: bigint): void;
|
||||
writeU8(n: number): void;
|
||||
startsWith(arr: number[]): boolean;
|
||||
}
|
||||
}
|
||||
export default global;
|
||||
|
||||
+136
-7
@@ -1,11 +1,13 @@
|
||||
// vim: nowrap
|
||||
import "../shim.ts";
|
||||
|
||||
import assert from "assert";
|
||||
|
||||
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 { 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";
|
||||
|
||||
describe("debug_tools", () => {
|
||||
@@ -123,9 +125,99 @@ describe("module", () => {
|
||||
});
|
||||
|
||||
const hstrToBA = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16))).buffer;
|
||||
describe("events", () => {
|
||||
it("emits login event upon logging in", () => {
|
||||
// TODO
|
||||
const BATohstr = (ba) => [...new Uint8Array(ba.buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
describe("parse packet", () => {
|
||||
it("parses PunchPkt", () => {
|
||||
const in_pkt_str = "f14100144241544400000000000262ca574f4e4a4d000000";
|
||||
const pkt = new DataView(hstrToBA(in_pkt_str));
|
||||
const expected = {
|
||||
prefix: "BATD",
|
||||
serial: "156362",
|
||||
suffix: "WONJM",
|
||||
serialU64: BigInt(156362),
|
||||
devId: "BATD156362WONJM",
|
||||
};
|
||||
|
||||
assert.deepEqual(parse_PunchPkt(pkt), expected);
|
||||
});
|
||||
{
|
||||
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", () => {
|
||||
@@ -155,10 +247,47 @@ describe("make packet", () => {
|
||||
it("builds a good WifiSettingsSet", () => {
|
||||
const sess = { outgoingCommandId: 2, ticket: [1, 2, 3, 4] };
|
||||
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 got = SendWifiDetails(sess, "skynet", "supercrap", true);
|
||||
const got = SendWifiDetails(sess, "skynet", "supercrap", 0, true);
|
||||
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",
|
||||
"esModuleInterop": true,
|
||||
"preserveConstEnums": true,
|
||||
"outDir": "./build/",
|
||||
"noEmit": true,
|
||||
"rootDir": "./",
|
||||
"strict": false,
|
||||
"sourceMap": false
|
||||
|
||||
+19
@@ -2,6 +2,7 @@ struct avparamset_t {
|
||||
uint32_t paramType;
|
||||
uint32_t paramValue;
|
||||
};
|
||||
|
||||
struct datetime_t {
|
||||
uint32_t now; // the code to _write_ these uses long ))
|
||||
uint32_t tz;
|
||||
@@ -99,3 +100,21 @@ struct stream_head_t {
|
||||
short sample;
|
||||
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