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 |
@@ -3,3 +3,4 @@ venv
|
||||
node_modules
|
||||
build
|
||||
dist/
|
||||
cameras.txt
|
||||
|
||||
@@ -23,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,6 +6,14 @@ 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/)
|
||||
@@ -19,6 +27,7 @@ Connect to the device's access point (e.g., FTYC811847AGFDZ) and run `node dist/
|
||||
|
||||
## Running
|
||||
|
||||
### 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).
|
||||
@@ -27,11 +36,60 @@ 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. For debugging purposes, you can send the packets to a specific camera by setting `--discovery_ip` to its IP address.
|
||||
|
||||
Clicking on the image will take you to a page that has audio streaming. Click the button below the image to mute/unmute the audio.
|
||||
|
||||
To customize the camera names, edit the file `cameras.txt` and add the names of your cameras, one per line, in the format `CAMERA_ID=descriptive name` (you must restart the HTTP server for the changes to take effect).
|
||||
|
||||
#### Settings
|
||||
|
||||
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>`
|
||||
|
||||
```yml
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<link rel="shortcut icon" href="/favicon.ico">
|
||||
<title>${name}</title>
|
||||
</head><body>
|
||||
<h2>${name}</h2><a href="/camera/${id}"><img src="/camera/${id}"/></a><hr/>
|
||||
<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,
|
||||
@@ -49,14 +51,12 @@
|
||||
audio_context.resume();
|
||||
|
||||
const evtSource = new EventSource("/audio/${id}");
|
||||
if (${debug}) {
|
||||
evtSource.onopen = (e) => {
|
||||
console.log("evtsource open");
|
||||
}
|
||||
evtSource.onerror = (e) => {
|
||||
console.log("evtsource error", e);
|
||||
}
|
||||
}
|
||||
let endsAt = 0;
|
||||
let startAt = 0;
|
||||
evtSource.onmessage = (e) => {
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
# This is a simple name mapping for cameras, in the form
|
||||
#
|
||||
# CAMERA_ID=descriptive name
|
||||
#
|
||||
# Blank lines and lines starting with "#" are ignored.
|
||||
#
|
||||
# Any updates to this file require restarting the HTTP server.
|
||||
# This file is meant to be updated manually, it is NOT updated dynamically.
|
||||
|
||||
FTYC811847AGFDZ=Office camera
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
};
|
||||
+58
-22
@@ -1,11 +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 } from "../logger.js";
|
||||
import { buildLogger, logger } from "../logger.js";
|
||||
|
||||
const majorVersion = process.versions.node.split(".").map(Number)[0];
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.command(
|
||||
@@ -13,22 +17,38 @@ yargs(hideBin(process.argv))
|
||||
"start http server",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option("ansi", { default: false })
|
||||
.option("slow_startup", { default: false })
|
||||
.option("audio", { describe: "Also stream audio from camera", default: true })
|
||||
.option("color", { describe: "Use color in logs", default: undefined })
|
||||
.boolean(["ansi", "audio", "slow_startup", "color"])
|
||||
.option("log_level", { describe: "Set log level", default: "info" })
|
||||
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||
.option("attempt_to_fix_packet_loss", { default: false })
|
||||
.option("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;
|
||||
buildLogger(argv.log_level, argv.color);
|
||||
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(
|
||||
@@ -36,22 +56,38 @@ yargs(hideBin(process.argv))
|
||||
"configure a camera",
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option("ansi", { default: false })
|
||||
.option("slow_startup", { default: false })
|
||||
.option("color", { describe: "Use color in logs", default: undefined })
|
||||
.boolean(["ansi", "slow_startup", "color"])
|
||||
.option("log_level", { describe: "Set log level", default: "info" })
|
||||
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||
.option("attempt_to_fix_packet_loss", { default: false })
|
||||
.option("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;
|
||||
buildLogger(argv.log_level, argv.color);
|
||||
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
|
||||
};
|
||||
|
||||
+22
-8
@@ -1,8 +1,10 @@
|
||||
import EventEmitter from "node:events";
|
||||
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
|
||||
import { createSocket, RemoteInfo } from "node:dgram";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
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;
|
||||
@@ -11,11 +13,15 @@ const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) =
|
||||
if (cmd_id != Commands.PunchPkt) {
|
||||
return;
|
||||
}
|
||||
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 = (discovery_ip: string): EventEmitter => {
|
||||
export const discoverDevices = (discovery_ips: string[]): EventEmitter => {
|
||||
const sock = createSocket("udp4");
|
||||
const SEND_PORT = 32108;
|
||||
const ee = new EventEmitter();
|
||||
@@ -27,18 +33,26 @@ export const discoverDevices = (discovery_ip: string): EventEmitter => {
|
||||
|
||||
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo));
|
||||
|
||||
let timers = [];
|
||||
sock.on("listening", () => {
|
||||
let ls_buf = create_LanSearch();
|
||||
sock.setBroadcast(true);
|
||||
discovery_ips.forEach((discovery_ip) => {
|
||||
logger.info(`Searching for devices on ${discovery_ip}`);
|
||||
|
||||
let buf = create_LanSearch();
|
||||
setInterval(() => {
|
||||
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;
|
||||
};
|
||||
|
||||
+358
@@ -18,11 +18,26 @@ 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
|
||||
@@ -51,6 +66,307 @@ lut = {
|
||||
[0xf171] = "RlyHelloAck2",
|
||||
}
|
||||
|
||||
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)
|
||||
local packet_length = buffer:len()
|
||||
@@ -88,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
|
||||
@@ -137,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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
+173
-60
@@ -1,9 +1,9 @@
|
||||
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
|
||||
import { XqBytesDec } from "./func_replacements.js";
|
||||
import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
|
||||
import { Session } from "./session.js";
|
||||
import { u16_swap, u32_swap } from "./utils.js";
|
||||
import { create_P2pRdy, DevSerial, SendListWifi, SendUsrChk } from "./impl.js";
|
||||
import { logger } from "./logger.js";
|
||||
import { Session } from "./session.js";
|
||||
import { config } from "./settings.js";
|
||||
|
||||
export const notImpl = (session: Session, dv: DataView) => {
|
||||
const raw = dv.readU16();
|
||||
@@ -31,18 +31,55 @@ export const handle_P2PRdy = (session: Session, _: DataView) => {
|
||||
};
|
||||
|
||||
export const makeP2pRdy = (dev: DevSerial): DataView => {
|
||||
const len = dev.prefix.length + dev.suffix.length + 8;
|
||||
const outbuf = new DataView(new Uint8Array(0x14).buffer); // 8 = serial u64
|
||||
// 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) {
|
||||
logger.error(`Expected start_type to be 0xa11, got 0x${start_type.toString(16)}`);
|
||||
@@ -54,24 +91,28 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
||||
XqBytesDec(dv.add(20), payload_len - 4, rotate_chr);
|
||||
}
|
||||
|
||||
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 ? "" : "not "; // 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
|
||||
|
||||
logger.info(`Camera ${session.devName}: ${charging}charging, battery at ${power / 1000}V, 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(),
|
||||
@@ -90,75 +131,125 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
||||
const buf = SendListWifi(session);
|
||||
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;
|
||||
logger.debug("payload len", payload_len);
|
||||
let msg_count = (payload_len - 9) / msg_len;
|
||||
let remote_msg_count = dv.add(startat).readU32LE();
|
||||
logger.debug("should get messages:", msg_count, "in payload: ", remote_msg_count);
|
||||
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(),
|
||||
};
|
||||
logger.info(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
||||
startat += msg_len;
|
||||
logger.debug("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
|
||||
@@ -168,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();
|
||||
@@ -234,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);
|
||||
@@ -241,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();
|
||||
};
|
||||
|
||||
+42
-33
@@ -1,12 +1,12 @@
|
||||
import { RemoteInfo } from "dgram";
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import http from "node:http";
|
||||
|
||||
import { logger } from "./logger.js";
|
||||
import { opt } from "./options.js";
|
||||
import { config } from "./settings.js";
|
||||
import { discoverDevices } from "./discovery.js";
|
||||
import { DevSerial, SendDevStatus } from "./impl.js";
|
||||
import { DevSerial } from "./impl.js";
|
||||
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
|
||||
import { addExifToJpeg, createExifOrientation } from "./exif.js";
|
||||
|
||||
// @ts-expect-error TS2307
|
||||
import favicon from "./cam.ico.gz";
|
||||
@@ -18,30 +18,20 @@ const responses: Record<string, http.ServerResponse[]> = {};
|
||||
const audioResponses: Record<string, http.ServerResponse[]> = {};
|
||||
const sessions: Record<string, Session> = {};
|
||||
|
||||
// Text file containing the mapping of camera names.
|
||||
const nameFile = "cameras.txt";
|
||||
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
|
||||
const oMap = [1, 8, 3, 6];
|
||||
const oMapMirror = [2, 7, 4, 5];
|
||||
const orientations = [1, 2, 3, 4, 5, 6, 7, 8].reduce((acc, cur) => {
|
||||
return { [cur]: createExifOrientation(cur), ...acc };
|
||||
}, {});
|
||||
|
||||
// Reads the mapping of serial numbers to camera names from the text file.
|
||||
const cameraNames = Object.assign(
|
||||
{},
|
||||
...(existsSync(nameFile) ? readFileSync(nameFile, "utf8") : "")
|
||||
.toString()
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split("\n")
|
||||
.filter((l) => !l.startsWith("#"))
|
||||
.filter((l) => l.trim() != "")
|
||||
.map((l) => {
|
||||
let kv = l.split("=");
|
||||
return { [kv[0].trim()]: kv[1].trim() };
|
||||
}),
|
||||
);
|
||||
|
||||
// Returns the camera name (custom name, if it exists, otherwise its ID).
|
||||
const cameraName = (id: string): string => cameraNames[id] || id;
|
||||
const cameraName = (id: string): string => config.cameras[id].alias || id;
|
||||
|
||||
// The HTTP server.
|
||||
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
logger.info(`Mapping camera names: ${JSON.stringify(cameraNames)}`);
|
||||
export const serveHttp = (port: number) => {
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url.startsWith("/ui/")) {
|
||||
let devId = req.url.split("/")[2];
|
||||
@@ -60,7 +50,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
.toString()
|
||||
.replace(/\${id}/g, devId)
|
||||
.replace(/\${name}/g, cameraName(devId))
|
||||
.replace(/\${audio}/g, with_audio.toString());
|
||||
.replace(/\${audio}/g, config.cameras[devId].audio ? "true" : "false");
|
||||
res.end(ui);
|
||||
return;
|
||||
}
|
||||
@@ -86,11 +76,27 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
if (req.url.startsWith("/favicon.ico")) {
|
||||
res.setHeader("Content-Type", "image/x-icon");
|
||||
res.setHeader("Content-Encoding", "gzip");
|
||||
res.end(favicon);
|
||||
res.end(Buffer.from(favicon));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url.startsWith("/camera/")) {
|
||||
if (req.url.startsWith("/rotate/")) {
|
||||
let devId = req.url.split("/")[2];
|
||||
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];
|
||||
@@ -129,10 +135,9 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
}
|
||||
});
|
||||
|
||||
let devEv = discoverDevices(opts.discovery_ip);
|
||||
let devEv = discoverDevices(config.discovery_ips);
|
||||
|
||||
const startSession = (s: Session) => {
|
||||
s.send(SendDevStatus(s));
|
||||
startVideoStream(s);
|
||||
logger.info(`Camera ${s.devName} is now ready to stream`);
|
||||
};
|
||||
@@ -146,13 +151,18 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||
responses[dev.devId] = [];
|
||||
audioResponses[dev.devId] = [];
|
||||
const s = makeSession(Handlers, dev, rinfo, startSession, 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);
|
||||
@@ -163,7 +173,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
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");
|
||||
@@ -174,7 +184,6 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
||||
});
|
||||
});
|
||||
}
|
||||
sessions[dev.devId] = s;
|
||||
});
|
||||
|
||||
logger.info(`Starting HTTP server on port ${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,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 = {
|
||||
ansi: boolean;
|
||||
discovery_ip: string;
|
||||
slow_startup: boolean;
|
||||
attempt_to_fix_packet_loss: boolean;
|
||||
};
|
||||
|
||||
Generated
+15
@@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"dependencies": {
|
||||
"winston": "^3.13.0",
|
||||
"yaml": "^2.4.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -14,6 +15,9 @@
|
||||
"mocha": "^10.2.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
@@ -1711,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",
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"winston": "^3.13.0",
|
||||
"yaml": "^2.4.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
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 }) => {
|
||||
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.discovery_ip);
|
||||
let devEv = discoverDevices(config.discovery_ips);
|
||||
if (password == "") {
|
||||
throw new Error("You must set a non-zero-length password");
|
||||
}
|
||||
|
||||
const onLogin = (s: Session) => {
|
||||
configureWifi(ssid, password)(s);
|
||||
logger.info(`WiFi config for camera ${s.devName} is done`);
|
||||
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) => {
|
||||
@@ -26,13 +29,35 @@ export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; passwo
|
||||
return;
|
||||
}
|
||||
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
|
||||
const s = makeSession(Handlers, dev, rinfo, onLogin, opts);
|
||||
const s = makeSession(Handlers, dev, rinfo, onLogin, 10000);
|
||||
let configured = {};
|
||||
|
||||
s.eventEmitter.on("disconnect", () => {
|
||||
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
|
||||
|
||||
+58
-25
@@ -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, makeP2pRdy, noop, notImpl } from "./handlers.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;
|
||||
@@ -22,7 +32,7 @@ export type Session = {
|
||||
frame_is_bad: boolean;
|
||||
frame_was_fixed: boolean;
|
||||
started: boolean;
|
||||
options: opt;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
|
||||
@@ -37,10 +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()];
|
||||
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 = (
|
||||
@@ -48,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) => {
|
||||
@@ -60,22 +74,11 @@ export const makeSession = (
|
||||
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
|
||||
|
||||
sock.on("listening", () => {
|
||||
const startup = () => {
|
||||
const buf = makeP2pRdy(dev);
|
||||
session.send(buf);
|
||||
// The YsxLite sends a P2PAlive message immediately after P2PRdy
|
||||
const alive_buf = create_P2pAlive();
|
||||
session.send(alive_buf);
|
||||
session.started = true;
|
||||
};
|
||||
if (options.slow_startup) {
|
||||
setTimeout(startup, 3000);
|
||||
} else {
|
||||
startup();
|
||||
}
|
||||
});
|
||||
|
||||
const SEND_PORT = 32108;
|
||||
sock.bind();
|
||||
const sessTimer = setInterval(() => {
|
||||
const delta = Date.now() - session.lastReceivedPacket;
|
||||
@@ -84,33 +87,62 @@ export const makeSession = (
|
||||
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];
|
||||
// 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 };
|
||||
}
|
||||
logger.log("trace", `>> ${cmd}`);
|
||||
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
|
||||
sock.send(new Uint8Array(msg.buffer), ra.port, session.dst_ip);
|
||||
},
|
||||
ackDrw: (id: number) => {
|
||||
logger.debug(`Removing ${id} from pending`);
|
||||
delete unackedDrw[id];
|
||||
},
|
||||
dst_ip: ra.address,
|
||||
curImage: [],
|
||||
rcvSeqId: 0,
|
||||
frame_is_bad: false,
|
||||
frame_was_fixed: false,
|
||||
options: options,
|
||||
unackedDrw,
|
||||
close: () => {
|
||||
session.eventEmitter.emit("disconnect");
|
||||
},
|
||||
};
|
||||
|
||||
session.eventEmitter.on("disconnect", () => {
|
||||
@@ -119,6 +151,7 @@ export const makeSession = (
|
||||
session.connected = false;
|
||||
session.timers.forEach((x) => clearInterval(x));
|
||||
session.timers = [];
|
||||
sock.close();
|
||||
});
|
||||
|
||||
session.eventEmitter.on("login", () => {
|
||||
@@ -128,9 +161,9 @@ export const makeSession = (
|
||||
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);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -145,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;
|
||||
|
||||
+123
-8
@@ -1,10 +1,13 @@
|
||||
// vim: nowrap
|
||||
import "../shim.ts";
|
||||
|
||||
import assert from "assert";
|
||||
|
||||
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
||||
import { makeP2pRdy, parseDevStatusAck, parseListWifi } from "../handlers.js";
|
||||
import { hexdump } from "../hexdump.js";
|
||||
import { SendStartVideo, SendDevStatus, SendUsrChk, SendWifiDetails, parse_PunchPkt } 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", () => {
|
||||
@@ -122,11 +125,7 @@ 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";
|
||||
@@ -141,6 +140,85 @@ describe("parse packet", () => {
|
||||
|
||||
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", () => {
|
||||
it("builds a good SendUsrChk", () => {
|
||||
@@ -169,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