Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59c77729c | ||
|
|
21189a8ae9 | ||
|
|
8c71e84875 | ||
|
|
bac2b4fd4f | ||
|
|
c81d7d4182 | ||
|
|
a93564cd11 | ||
|
|
d7d3b91528 | ||
|
|
204677ceae | ||
|
|
03893f8d81 | ||
|
|
dc43a66209 | ||
|
|
6bed63b61f | ||
|
|
2a3d047f48 | ||
|
|
1edb7370cf | ||
|
|
6c7856054e | ||
|
|
f29206a1e1 | ||
|
|
449210ec23 | ||
|
|
3d003be371 | ||
|
|
7b153d6ff8 | ||
|
|
f5da7a34bd | ||
|
|
e496b33ff5 | ||
|
|
f9e2dc63dc | ||
|
|
80e50560e6 | ||
|
|
2ec4d603c6 | ||
|
|
45b8b159d1 | ||
|
|
c51e4f8a7a | ||
|
|
68fe0fb798 | ||
|
|
7ca795c917 | ||
|
|
d14e16c53d | ||
|
|
6ca18b057b | ||
|
|
1439d5f632 |
@@ -3,3 +3,4 @@ venv
|
|||||||
node_modules
|
node_modules
|
||||||
build
|
build
|
||||||
dist/
|
dist/
|
||||||
|
cameras.txt
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ node_modules:
|
|||||||
npm install
|
npm install
|
||||||
|
|
||||||
test: node_modules
|
test: node_modules
|
||||||
./node_modules/.bin/mocha --require ts-node/register tests/fn.test.js
|
npm run test
|
||||||
|
|
||||||
install-wireshark-dissector:
|
install-wireshark-dissector:
|
||||||
mkdir -p ~/.local/lib/wireshark/plugins
|
mkdir -p ~/.local/lib/wireshark/plugins
|
||||||
|
|||||||
@@ -6,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))
|
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
|
## 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/)
|
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/)
|
||||||
|
|||||||
+12
-5
@@ -1,3 +1,4 @@
|
|||||||
|
import process from "node:process";
|
||||||
import { hideBin } from "yargs/helpers";
|
import { hideBin } from "yargs/helpers";
|
||||||
import yargs from "yargs/yargs";
|
import yargs from "yargs/yargs";
|
||||||
|
|
||||||
@@ -5,7 +6,9 @@ import { serveHttp } from "../http_server.js";
|
|||||||
import { opt } from "../options.js";
|
import { opt } from "../options.js";
|
||||||
import { pair } from "../pair.js";
|
import { pair } from "../pair.js";
|
||||||
|
|
||||||
import { buildLogger } from "../logger.js";
|
import { buildLogger, logger } from "../logger.js";
|
||||||
|
|
||||||
|
const majorVersion = process.versions.node.split(".").map(Number)[0];
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.command(
|
.command(
|
||||||
@@ -14,10 +17,9 @@ yargs(hideBin(process.argv))
|
|||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("ansi", { default: false })
|
.option("ansi", { default: false })
|
||||||
.option("slow_startup", { default: false })
|
|
||||||
.option("audio", { describe: "Also stream audio from camera", default: true })
|
.option("audio", { describe: "Also stream audio from camera", default: true })
|
||||||
.option("color", { describe: "Use color in logs", default: undefined })
|
.option("color", { describe: "Use color in logs", default: undefined })
|
||||||
.boolean(["ansi", "audio", "slow_startup", "color"])
|
.boolean(["ansi", "audio", "color"])
|
||||||
.option("log_level", { describe: "Set log level", default: "info" })
|
.option("log_level", { describe: "Set log level", default: "info" })
|
||||||
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||||
.option("attempt_to_fix_packet_loss", { default: false })
|
.option("attempt_to_fix_packet_loss", { default: false })
|
||||||
@@ -28,6 +30,9 @@ yargs(hideBin(process.argv))
|
|||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as opt;
|
const opts: opt = argv as opt;
|
||||||
buildLogger(argv.log_level, argv.color);
|
buildLogger(argv.log_level, argv.color);
|
||||||
|
if (majorVersion < 16) {
|
||||||
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
|
}
|
||||||
serveHttp(opts, argv.port, argv.audio || false);
|
serveHttp(opts, argv.port, argv.audio || false);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -37,9 +42,8 @@ yargs(hideBin(process.argv))
|
|||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
.option("ansi", { default: false })
|
.option("ansi", { default: false })
|
||||||
.option("slow_startup", { default: false })
|
|
||||||
.option("color", { describe: "Use color in logs", default: undefined })
|
.option("color", { describe: "Use color in logs", default: undefined })
|
||||||
.boolean(["ansi", "slow_startup", "color"])
|
.boolean(["ansi", "color"])
|
||||||
.option("log_level", { describe: "Set log level", default: "info" })
|
.option("log_level", { describe: "Set log level", default: "info" })
|
||||||
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
|
||||||
.option("attempt_to_fix_packet_loss", { default: false })
|
.option("attempt_to_fix_packet_loss", { default: false })
|
||||||
@@ -51,6 +55,9 @@ yargs(hideBin(process.argv))
|
|||||||
(argv) => {
|
(argv) => {
|
||||||
const opts: opt = argv as unknown as opt;
|
const opts: opt = argv as unknown as opt;
|
||||||
buildLogger(argv.log_level, argv.color);
|
buildLogger(argv.log_level, argv.color);
|
||||||
|
if (majorVersion < 16) {
|
||||||
|
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
|
||||||
|
}
|
||||||
pair({ opts, ssid: argv.ssid, password: argv.password });
|
pair({ opts, ssid: argv.ssid, password: argv.password });
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,10 +46,12 @@ export const ControlCommands = {
|
|||||||
ListWifi: 0x0360, // CMD_NET_WIFI_SCAN
|
ListWifi: 0x0360, // CMD_NET_WIFI_SCAN
|
||||||
ListWifiAck: 0x0361,
|
ListWifiAck: 0x0361,
|
||||||
StartVideo: 0x1030, // CMD_PEER_LIVEVIDEO_START
|
StartVideo: 0x1030, // CMD_PEER_LIVEVIDEO_START
|
||||||
|
StartVideoAck: 0x1031,
|
||||||
StopVideo: 0x1130, // CMD_PEER_LIVEVIDEO_STOP
|
StopVideo: 0x1130, // CMD_PEER_LIVEVIDEO_STOP
|
||||||
Shutdown: 0x1010, //CMD_SYSTEM_SHUTDOWN,
|
Shutdown: 0x1010, //CMD_SYSTEM_SHUTDOWN,
|
||||||
Reboot: 0x1110, //CMD_SYSTEM_REBOOT,
|
Reboot: 0x1110, //CMD_SYSTEM_REBOOT,
|
||||||
VideoParamSet: 0x1830, // CMD_PEER_VIDEOPARAM_SET
|
VideoParamSet: 0x1830, // CMD_PEER_VIDEOPARAM_SET
|
||||||
|
VideoParamSetAck: 0x1831,
|
||||||
VideoParamGet: 0x1930, // CMD_PEER_VIDEOPARAM_GET
|
VideoParamGet: 0x1930, // CMD_PEER_VIDEOPARAM_GET
|
||||||
IRToggle: 0x0a30, // CMD_PEER_IRCUT_ONOFF
|
IRToggle: 0x0a30, // CMD_PEER_IRCUT_ONOFF
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-9
@@ -1,7 +1,8 @@
|
|||||||
import EventEmitter from "node:events";
|
|
||||||
import { create_LanSearchExt, create_LanSearch, parse_PunchPkt } from "./impl.js";
|
|
||||||
import { createSocket, RemoteInfo } from "node:dgram";
|
import { createSocket, RemoteInfo } from "node:dgram";
|
||||||
|
import EventEmitter from "node:events";
|
||||||
|
|
||||||
import { Commands } from "./datatypes.js";
|
import { Commands } from "./datatypes.js";
|
||||||
|
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => {
|
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => {
|
||||||
@@ -27,25 +28,24 @@ export const discoverDevices = (discovery_ip: string): EventEmitter => {
|
|||||||
|
|
||||||
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo));
|
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo));
|
||||||
|
|
||||||
|
let timers = [];
|
||||||
sock.on("listening", () => {
|
sock.on("listening", () => {
|
||||||
sock.setBroadcast(true);
|
sock.setBroadcast(true);
|
||||||
logger.info(`Searching for devices on ${discovery_ip}`);
|
logger.info(`Searching for devices on ${discovery_ip}`);
|
||||||
|
|
||||||
let ls_buf = create_LanSearch();
|
let ls_buf = create_LanSearch();
|
||||||
let lse_buf = create_LanSearchExt();
|
let int = setInterval(() => {
|
||||||
setInterval(() => {
|
|
||||||
logger.log("trace", `>> LanSearch`);
|
logger.log("trace", `>> LanSearch`);
|
||||||
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
logger.log("trace", `>> LanSearchExt`);
|
}, 3000);
|
||||||
sock.send(new Uint8Array(lse_buf.buffer), SEND_PORT, discovery_ip);
|
timers.push(int);
|
||||||
}, 2000);
|
|
||||||
logger.log("trace", `>> LanSearch`);
|
logger.log("trace", `>> LanSearch`);
|
||||||
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
|
||||||
logger.log("trace", `>> LanSearchExt`);
|
|
||||||
sock.send(new Uint8Array(lse_buf.buffer), SEND_PORT, discovery_ip);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
sock.bind();
|
sock.bind();
|
||||||
|
|
||||||
|
sock.on("close", () => timers.forEach((timer) => clearInterval(timer)));
|
||||||
|
ee.on("close", () => sock.close());
|
||||||
return ee;
|
return ee;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,8 +18,22 @@ ilnk_proto.fields.start = ProtoField.uint16("iLnkP2P.start", "Start", base.HE
|
|||||||
ilnk_proto.fields.cmd_dest = ProtoField.uint16("iLnkP2P.cmd_dest", "Dest", base.HEX)
|
ilnk_proto.fields.cmd_dest = ProtoField.uint16("iLnkP2P.cmd_dest", "Dest", base.HEX)
|
||||||
ilnk_proto.fields.auth_token = ProtoField.bytes("iLnkP2P.auth_token", "CMD auth token", base.DASH)
|
ilnk_proto.fields.auth_token = ProtoField.bytes("iLnkP2P.auth_token", "CMD auth token", base.DASH)
|
||||||
ilnk_proto.fields.cmd_payload = ProtoField.bytes("iLnkP2P.payload", "CMD Payload", base.DASH)
|
ilnk_proto.fields.cmd_payload = ProtoField.bytes("iLnkP2P.payload", "CMD Payload", base.DASH)
|
||||||
|
ilnk_proto.fields.warning = ProtoField.string("iLnkP2P.warning", "Warning")
|
||||||
|
--
|
||||||
-- jpeg | audio | continuation type?
|
-- jpeg | audio | continuation type?
|
||||||
ilnk_proto.fields.data_payload = ProtoField.bytes("iLnkP2P.data_payload", "Data Payload", base.DASH)
|
ilnk_proto.fields.data_payload = ProtoField.bytes("iLnkP2P.data_payload", "Data Payload", base.DASH)
|
||||||
|
ilnk_proto.fields.payload_type = ProtoField.string("iLnkP2P.payload_type", "Payload type")
|
||||||
|
ilnk_proto.fields.payload_subtype = ProtoField.string("iLnkP2P.payload_type", "Payload type")
|
||||||
|
ilnk_proto.fields.payload_len = ProtoField.uint32("iLnkP2P.payload_len", "Payload len")
|
||||||
|
ilnk_proto.fields.frame_no = ProtoField.uint32("iLnkP2P.frame_no", "Frame no")
|
||||||
|
-- audio
|
||||||
|
ilnk_proto.fields.audio_header = ProtoField.bytes("iLnkP2P.audio_header", "Audio Header")
|
||||||
|
ilnk_proto.fields.hdr_type = ProtoField.uint16("iLnkP2P.hdr_type", "Header Type")
|
||||||
|
ilnk_proto.fields.hdr_streamid = ProtoField.uint16("iLnkP2P.hdr_streamid", "Header Stream ID")
|
||||||
|
ilnk_proto.fields.hdr_frameno = ProtoField.uint32("iLnkP2P.hdr_frameno", "Header Frame")
|
||||||
|
ilnk_proto.fields.hdr_len = ProtoField.uint16("iLnkP2P.hdr_len", "Header Len")
|
||||||
|
ilnk_proto.fields.hdr_ver = ProtoField.uint16("iLnkP2P.hdr_ver", "Header version")
|
||||||
|
ilnk_proto.fields.hdr_res = ProtoField.uint16("iLnkP2P.hdr_red", "Header resolution")
|
||||||
|
|
||||||
ilnk_proto.fields.encrypted = ProtoField.bool("iLnkP2P.encrypted", "Encrypted")
|
ilnk_proto.fields.encrypted = ProtoField.bool("iLnkP2P.encrypted", "Encrypted")
|
||||||
ilnk_proto.fields.cmd_type = ProtoField.string("iLnkP2P.cmd_type", "Cmd Pkt Type")
|
ilnk_proto.fields.cmd_type = ProtoField.string("iLnkP2P.cmd_type", "Cmd Pkt Type")
|
||||||
@@ -51,6 +65,12 @@ lut = {
|
|||||||
[0xf171] = "RlyHelloAck2",
|
[0xf171] = "RlyHelloAck2",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
control_lut = {
|
||||||
|
[0x2010] = "ConnectUser",
|
||||||
|
[0x2011] = "ConnectUserAck",
|
||||||
|
[0x0811] = "ConnectUserAck",
|
||||||
|
}
|
||||||
|
|
||||||
-- Define a function to dissect the packets
|
-- Define a function to dissect the packets
|
||||||
function ilnk_proto.dissector(buffer, pinfo, tree)
|
function ilnk_proto.dissector(buffer, pinfo, tree)
|
||||||
local packet_length = buffer:len()
|
local packet_length = buffer:len()
|
||||||
@@ -88,7 +108,12 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
|
|||||||
subtree:add(ilnk_proto.fields.len, b_pkt_len)
|
subtree:add(ilnk_proto.fields.len, b_pkt_len)
|
||||||
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
|
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
|
||||||
subtree:add(ilnk_proto.fields.m_stream_id, buffer(5, 1))
|
subtree:add(ilnk_proto.fields.m_stream_id, buffer(5, 1))
|
||||||
|
if pkt_len < 12 then
|
||||||
|
subtree:add(ilnk_proto.fields.warning, "Short read"):set_generated()
|
||||||
|
return
|
||||||
|
end
|
||||||
subtree:add(ilnk_proto.fields.pkt_seq, buffer(6, 2))
|
subtree:add(ilnk_proto.fields.pkt_seq, buffer(6, 2))
|
||||||
|
pinfo.cols.info:set(buffer(6, 2):uint())
|
||||||
local b_payload_len = buffer(0xc, 2)
|
local b_payload_len = buffer(0xc, 2)
|
||||||
local payload_len = buffer(0xc, 2):le_uint()
|
local payload_len = buffer(0xc, 2):le_uint()
|
||||||
|
|
||||||
@@ -137,7 +162,39 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
|
|||||||
subtree:add(ilnk_proto.fields.cmd_payload, payload_tvb:range(0, payload_len -4))
|
subtree:add(ilnk_proto.fields.cmd_payload, payload_tvb:range(0, payload_len -4))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
local payload_type
|
||||||
|
local payload_subtype
|
||||||
local payload_tvb = ByteArray.tvb(buffer(8, packet_length-8):bytes(), "Data Payload")
|
local payload_tvb = ByteArray.tvb(buffer(8, packet_length-8):bytes(), "Data Payload")
|
||||||
|
if payload_tvb:range(0, 4):uint() == 0xffd8ffdb then
|
||||||
|
payload_subtype = "new frame"
|
||||||
|
-- start of new frame
|
||||||
|
end
|
||||||
|
if payload_tvb:range(0, 4):uint() == 0x55aa15a8 then
|
||||||
|
payload_type = "audio"
|
||||||
|
subtree:add(ilnk_proto.fields.audio_header, buffer(8, 32))
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_type, buffer(12, 1))
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_streamid, buffer(13, 1))
|
||||||
|
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_frameno, buffer(20, 4), buffer(20, 4):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_len, buffer(24, 4), buffer(24, 4):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_ver, buffer(28, 1), buffer(28, 1):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.hdr_res, buffer(29, 1), buffer(29, 1):le_uint())
|
||||||
|
|
||||||
|
subtree:add(ilnk_proto.fields.payload_len, buffer(24, 4), buffer(24, 4):le_uint())
|
||||||
|
subtree:add(ilnk_proto.fields.frame_no, buffer(20, 4), buffer(20, 4):le_uint())
|
||||||
|
if payload_tvb:range(4, 1):uint() == 0x06 then
|
||||||
|
payload_subtype = "audio data"
|
||||||
|
elseif payload_tvb:range(4, 1):uint() == 0x03 then
|
||||||
|
payload_subtype = "maybe audio metadata"
|
||||||
|
else
|
||||||
|
payload_subtype = "REALLY not sure audio data"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
payload_type = "jpeg"
|
||||||
|
payload_subtype = "jpeg continuation"
|
||||||
|
end
|
||||||
|
subtree:add(ilnk_proto.fields.payload_type, buffer(8, 4), payload_type)
|
||||||
|
subtree:add(ilnk_proto.fields.payload_subtype, buffer(12, 1), payload_subtype)
|
||||||
subtree:add(ilnk_proto.fields.data_payload, payload_tvb:range(0, packet_length-8))
|
subtree:add(ilnk_proto.fields.data_payload, payload_tvb:range(0, packet_length-8))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+135
-86
@@ -4,6 +4,7 @@ import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
|
|||||||
import { Session } from "./session.js";
|
import { Session } from "./session.js";
|
||||||
import { u16_swap, u32_swap } from "./utils.js";
|
import { u16_swap, u32_swap } from "./utils.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
import { hexdump } from "./hexdump.js";
|
||||||
|
|
||||||
export const notImpl = (session: Session, dv: DataView) => {
|
export const notImpl = (session: Session, dv: DataView) => {
|
||||||
const raw = dv.readU16();
|
const raw = dv.readU16();
|
||||||
@@ -54,111 +55,143 @@ export const createResponseForControlCommand = (session: Session, dv: DataView):
|
|||||||
XqBytesDec(dv.add(20), payload_len - 4, rotate_chr);
|
XqBytesDec(dv.add(20), payload_len - 4, rotate_chr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.ConnectUserAck) {
|
switch (cmd_id) {
|
||||||
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
|
case ControlCommands.ConnectUserAck:
|
||||||
session.ticket = [...c];
|
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
|
||||||
session.eventEmitter.emit("login");
|
session.ticket = [...c];
|
||||||
return [];
|
session.eventEmitter.emit("login");
|
||||||
}
|
return [];
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.DevStatusAck) {
|
case ControlCommands.DevStatusAck:
|
||||||
// ParseDevStatus -> offset relevant?
|
// ParseDevStatus -> offset relevant?
|
||||||
let charging = u32_swap(dv.add(0x28).readU32()) & 1 ? "" : "not "; // 0x14000101 v 0x14000100
|
let charging = u32_swap(dv.add(0x28).readU32()) & 1 ? "" : "not "; // 0x14000101 v 0x14000100
|
||||||
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts
|
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts
|
||||||
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
|
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
|
||||||
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
|
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
|
||||||
|
|
||||||
logger.info(`Camera ${session.devName}: ${charging}charging, battery at ${power / 1000}V, Wifi ${dbm} dBm`);
|
logger.info(`Camera ${session.devName}: ${charging}charging, battery at ${power / 1000}V, Wifi ${dbm} dBm`);
|
||||||
}
|
return [];
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.WifiSettingsAck) {
|
case ControlCommands.WifiSettingsAck:
|
||||||
const wifiSettings = {
|
const wifiSettings = {
|
||||||
enable: dv.add(0x14).readU32(),
|
enable: dv.add(0x14).readU32(),
|
||||||
status: dv.add(0x18).readU32(),
|
status: dv.add(0x18).readU32(),
|
||||||
mode: dv.add(0x1c).readU32LE(),
|
mode: dv.add(0x1c).readU32LE(),
|
||||||
channel: dv.add(0x20).readU32(),
|
channel: dv.add(0x20).readU32(),
|
||||||
authtype: dv.add(0x24).readU32(),
|
authtype: dv.add(0x24).readU32(),
|
||||||
dhcp: dv.add(0x28).readU32(),
|
dhcp: dv.add(0x28).readU32(),
|
||||||
ssid: dv.add(0x2c).readString(0x20),
|
ssid: dv.add(0x2c).readString(0x20),
|
||||||
psk: dv.add(0x4c).readString(0x80),
|
psk: dv.add(0x4c).readString(0x80),
|
||||||
ip: dv.add(0xcc).readString(0x10),
|
ip: dv.add(0xcc).readString(0x10),
|
||||||
mask: dv.add(0xdc).readString(0x10),
|
mask: dv.add(0xdc).readString(0x10),
|
||||||
gw: dv.add(0xec).readString(0x10),
|
gw: dv.add(0xec).readString(0x10),
|
||||||
dns1: dv.add(0xfc).readString(0x10),
|
dns1: dv.add(0xfc).readString(0x10),
|
||||||
dns2: dv.add(0x10c).readString(0x10),
|
dns2: dv.add(0x10c).readString(0x10),
|
||||||
};
|
|
||||||
const buf = SendListWifi(session);
|
|
||||||
logger.info(`Current Wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
|
||||||
return [buf];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmd_id == ControlCommands.ListWifiAck) {
|
|
||||||
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);
|
|
||||||
startat += 4;
|
|
||||||
let items = [];
|
|
||||||
for (let i = 0; i < msg_count; i++) {
|
|
||||||
const wifiListItem = {
|
|
||||||
// startat = msg_len * i + 0x14;
|
|
||||||
ssid: dv.add(startat).readString(0x40),
|
|
||||||
mac: dv.add(startat + 0x40).readByteArray(8),
|
|
||||||
security: dv.add(startat + 0x48).readU32LE(),
|
|
||||||
dbm0: dv.add(startat + 0x4c).readU32LE(),
|
|
||||||
dbm1: dv.add(startat + 0x50).readU32LE(),
|
|
||||||
mode: dv.add(startat + 0x54).readU32LE(),
|
|
||||||
channel: dv.add(startat + 0x58).readU32LE(),
|
|
||||||
};
|
};
|
||||||
logger.info(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
const buf = SendListWifi(session);
|
||||||
startat += msg_len;
|
logger.info(`Current Wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
|
||||||
logger.debug("ended at", startat);
|
return [buf];
|
||||||
items.push(wifiListItem);
|
|
||||||
}
|
case ControlCommands.ListWifiAck:
|
||||||
|
if (payload_len == 4) {
|
||||||
|
logger.debug("ListWifi returned []");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let startat = 0x10;
|
||||||
|
let msg_len = 91;
|
||||||
|
let msg_count = (payload_len - 9) / msg_len;
|
||||||
|
let remote_msg_count = dv.add(startat).readU32LE();
|
||||||
|
logger.debug(`should get messages: ${msg_count} in payload: ${remote_msg_count}`);
|
||||||
|
startat += 4;
|
||||||
|
let items = [];
|
||||||
|
for (let i = 0; i < msg_count; i++) {
|
||||||
|
const wifiListItem = {
|
||||||
|
// startat = msg_len * i + 0x14;
|
||||||
|
ssid: dv.add(startat).readString(0x40),
|
||||||
|
mac: dv.add(startat + 0x40).readByteArray(8),
|
||||||
|
security: dv.add(startat + 0x48).readU32LE(),
|
||||||
|
dbm0: dv.add(startat + 0x4c).readU32LE(),
|
||||||
|
dbm1: dv.add(startat + 0x50).readU32LE(),
|
||||||
|
mode: dv.add(startat + 0x54).readU32LE(),
|
||||||
|
channel: dv.add(startat + 0x58).readU32LE(),
|
||||||
|
};
|
||||||
|
logger.info(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
|
||||||
|
startat += msg_len;
|
||||||
|
logger.debug("ended at", startat);
|
||||||
|
items.push(wifiListItem);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
case ControlCommands.StartVideoAck:
|
||||||
|
logger.debug("Start video ack");
|
||||||
|
return [];
|
||||||
|
case ControlCommands.VideoParamSetAck:
|
||||||
|
logger.debug("Video param set ack");
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
logger.info(`Unhandled control command: 0x${cmd_id.toString(16)}`);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const deal_with_data = (session: Session, dv: DataView) => {
|
const deal_with_data = (session: Session, dv: DataView) => {
|
||||||
const pkt_len = dv.add(2).readU16();
|
const pkt_len = dv.add(2).readU16();
|
||||||
// data
|
|
||||||
const JPEG_HEADER = [0xff, 0xd8, 0xff, 0xdb];
|
// 12 equals start of header (0x8) + header length (0x4)
|
||||||
const AUDIO_HEADER = [0x55, 0xaa, 0x15, 0xa8];
|
if (pkt_len < 12) {
|
||||||
const m_hdr = dv.add(8).readByteArray(4);
|
logger.debug("Got a short Drw packet, ignoring");
|
||||||
let is_new_image = true;
|
return;
|
||||||
let audio = true;
|
|
||||||
const pkt_id = dv.add(6).readU16();
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
is_new_image = is_new_image && m_hdr.add(i).readU8() == JPEG_HEADER[i];
|
|
||||||
audio = audio && m_hdr.add(i).readU8() == AUDIO_HEADER[i];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio) {
|
const FRAME_HEADER = [0x55, 0xaa, 0x15, 0xa8];
|
||||||
// "stream_head_t->type == 0x06" per pdf
|
const m_hdr = dv.add(8).readByteArray(4);
|
||||||
if (dv.add(12).readU8() == 0x06) {
|
const pkt_id = dv.add(6).readU16();
|
||||||
|
const STREAM_TYPE_AUDIO = 0x06;
|
||||||
|
const STREAM_TYPE_JPEG = 0x03;
|
||||||
|
|
||||||
|
const startNewFrame = (buf: ArrayBuffer) => {
|
||||||
|
if (session.curImage.length > 0 && !session.frame_is_bad) {
|
||||||
|
session.eventEmitter.emit("frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.frame_was_fixed = false;
|
||||||
|
session.frame_is_bad = false;
|
||||||
|
session.curImage = [Buffer.from(buf)];
|
||||||
|
session.rcvSeqId = pkt_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_framed = m_hdr.startsWith(FRAME_HEADER);
|
||||||
|
|
||||||
|
if (is_framed) {
|
||||||
|
const stream_type = dv.add(12).readU8();
|
||||||
|
if (stream_type == STREAM_TYPE_AUDIO) {
|
||||||
const audio_len = u16_swap(dv.add(8 + 16).readU16());
|
const audio_len = u16_swap(dv.add(8 + 16).readU16());
|
||||||
// may have received the next 'data' packet id as an audio frame -- jpeg was fine
|
|
||||||
if (pkt_id == session.rcvSeqId + 1) {
|
|
||||||
session.rcvSeqId = session.rcvSeqId + 1;
|
|
||||||
}
|
|
||||||
const audio_buf = dv.add(32 + 8).readByteArray(audio_len).buffer; // 8 for pkt header, 32 for `stream_head_t`
|
const audio_buf = dv.add(32 + 8).readByteArray(audio_len).buffer; // 8 for pkt header, 32 for `stream_head_t`
|
||||||
session.eventEmitter.emit("audio", { gap: false, data: Buffer.from(audio_buf) });
|
session.eventEmitter.emit("audio", { gap: false, data: Buffer.from(audio_buf) });
|
||||||
|
} else if (stream_type == STREAM_TYPE_JPEG) {
|
||||||
|
const to_read = pkt_len - 4 - 32;
|
||||||
|
if (to_read > 0) {
|
||||||
|
// some cameras do not send the data with the frame, but rather as a followup message
|
||||||
|
// skip 8 bytes (drw header) + 32 bytes (data frame)
|
||||||
|
const data = dv.add(32 + 8).readByteArray(to_read);
|
||||||
|
startNewFrame(data.buffer);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
logger.debug(`Ignoring data frame with stream type ${stream_type} - not implemented`);
|
||||||
// not sure what these are for, there's one per frame. maybe alignment?
|
// not sure what these are for, there's one per frame. maybe alignment?
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const JPEG_HEADER = [0xff, 0xd8, 0xff, 0xdb];
|
||||||
|
// a new JPEG image may begin either
|
||||||
|
// - as a frame with stream_type == 0x03
|
||||||
|
// - as unframed data, started by JPEG_HEADER
|
||||||
|
// but for both types of cameras, unframed data which does not start with JPEG_HEADER
|
||||||
|
// are segments of the (potentially already started) JPEG image
|
||||||
const data = dv.add(8).readByteArray(pkt_len - 4);
|
const data = dv.add(8).readByteArray(pkt_len - 4);
|
||||||
if (is_new_image) {
|
// this only happens on un-framed-cameras, which start the JPEG image directly
|
||||||
if (session.curImage.length > 0 && !session.frame_is_bad) {
|
let is_new_image = m_hdr.startsWith(JPEG_HEADER);
|
||||||
session.eventEmitter.emit("frame");
|
|
||||||
}
|
|
||||||
|
|
||||||
session.frame_was_fixed = false;
|
if (is_new_image) {
|
||||||
session.frame_is_bad = false;
|
startNewFrame(data.buffer);
|
||||||
session.curImage = [Buffer.from(data.buffer)];
|
|
||||||
session.rcvSeqId = pkt_id;
|
|
||||||
} else {
|
} else {
|
||||||
if (pkt_id <= session.rcvSeqId) {
|
if (pkt_id <= session.rcvSeqId) {
|
||||||
// retransmit
|
// retransmit
|
||||||
@@ -168,7 +201,10 @@ const deal_with_data = (session: Session, dv: DataView) => {
|
|||||||
let b = Buffer.from(data.buffer);
|
let b = Buffer.from(data.buffer);
|
||||||
|
|
||||||
if (pkt_id > session.rcvSeqId + 1) {
|
if (pkt_id > session.rcvSeqId + 1) {
|
||||||
session.frame_is_bad = true;
|
if (!session.frame_is_bad) {
|
||||||
|
session.frame_is_bad = true;
|
||||||
|
logger.debug(`Dropping corrupt frame ${pkt_id}, expected ${session.rcvSeqId + 1}`);
|
||||||
|
}
|
||||||
// this should always be enabled but currently it seems to cause more visual distortion
|
// this should always be enabled but currently it seems to cause more visual distortion
|
||||||
// than just missing some frames
|
// than just missing some frames
|
||||||
if (!session.options.attempt_to_fix_packet_loss) {
|
if (!session.options.attempt_to_fix_packet_loss) {
|
||||||
@@ -234,6 +270,17 @@ const makeDrwAck = (dv: DataView): DataView => {
|
|||||||
}
|
}
|
||||||
return outbuf;
|
return outbuf;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handle_DrwAck = (session: Session, dv: DataView) => {
|
||||||
|
const packetlen = dv.add(2).readU16();
|
||||||
|
const str_type = dv.add(4).readU8();
|
||||||
|
const str_id = dv.add(5).readU8();
|
||||||
|
const ack_count = dv.add(6).readU16();
|
||||||
|
for (let i = 0; i < ack_count; i++) {
|
||||||
|
const ack_id = dv.add(8 + i * 2).readU16();
|
||||||
|
session.ackDrw(ack_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
export const handle_Drw = (session: Session, dv: DataView) => {
|
export const handle_Drw = (session: Session, dv: DataView) => {
|
||||||
const ack = makeDrwAck(dv);
|
const ack = makeDrwAck(dv);
|
||||||
session.send(ack);
|
session.send(ack);
|
||||||
@@ -241,8 +288,10 @@ export const handle_Drw = (session: Session, dv: DataView) => {
|
|||||||
const m_stream = dv.add(5).readU8(); // data = 1, control = 0
|
const m_stream = dv.add(5).readU8(); // data = 1, control = 0
|
||||||
if (m_stream == 1) {
|
if (m_stream == 1) {
|
||||||
deal_with_data(session, dv);
|
deal_with_data(session, dv);
|
||||||
} else {
|
} else if (m_stream == 0) {
|
||||||
const b = createResponseForControlCommand(session, dv);
|
const b = createResponseForControlCommand(session, dv);
|
||||||
b.forEach(session.send);
|
b.forEach(session.send);
|
||||||
|
} else {
|
||||||
|
logger.warning(`Received a Drw packet with stream tag: ${m_stream}, which is not implemented`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-3
@@ -96,7 +96,7 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
if (req.url.startsWith("/favicon.ico")) {
|
if (req.url.startsWith("/favicon.ico")) {
|
||||||
res.setHeader("Content-Type", "image/x-icon");
|
res.setHeader("Content-Type", "image/x-icon");
|
||||||
res.setHeader("Content-Encoding", "gzip");
|
res.setHeader("Content-Encoding", "gzip");
|
||||||
res.end(favicon);
|
res.end(Buffer.from(favicon));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,14 +104,14 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
|
|||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
let curPos = camSettings[devId]?.orientation || 0;
|
let curPos = camSettings[devId]?.orientation || 0;
|
||||||
let nextPos = (curPos + 1) % 4;
|
let nextPos = (curPos + 1) % 4;
|
||||||
logger.info(`Rotating ${devId} to ${nextPos}`);
|
logger.debug(`Rotating ${devId} to ${nextPos}`);
|
||||||
camSettings[devId].orientation = nextPos;
|
camSettings[devId].orientation = nextPos;
|
||||||
res.writeHead(204);
|
res.writeHead(204);
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
} else if (req.url.startsWith("/mirror/")) {
|
} else if (req.url.startsWith("/mirror/")) {
|
||||||
let devId = req.url.split("/")[2];
|
let devId = req.url.split("/")[2];
|
||||||
logger.info(`Mirroring ${devId}`);
|
logger.debug(`Mirroring ${devId}`);
|
||||||
camSettings[devId].mirror = !camSettings[devId].mirror;
|
camSettings[devId].mirror = !camSettings[devId].mirror;
|
||||||
res.writeHead(204);
|
res.writeHead(204);
|
||||||
res.end();
|
res.end();
|
||||||
|
|||||||
@@ -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,5 @@
|
|||||||
export type opt = {
|
export type opt = {
|
||||||
ansi: boolean;
|
ansi: boolean;
|
||||||
discovery_ip: string;
|
discovery_ip: string;
|
||||||
slow_startup: boolean;
|
|
||||||
attempt_to_fix_packet_loss: boolean;
|
attempt_to_fix_packet_loss: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,5 +15,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { RemoteInfo } from "dgram";
|
|||||||
|
|
||||||
import { opt } from "./options.js";
|
import { opt } from "./options.js";
|
||||||
import { discoverDevices } from "./discovery.js";
|
import { discoverDevices } from "./discovery.js";
|
||||||
import { DevSerial } from "./impl.js";
|
import { DevSerial, SendReboot, SendWifiSettings } from "./impl.js";
|
||||||
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
|
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
@@ -16,8 +16,15 @@ export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; passwo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onLogin = (s: Session) => {
|
const onLogin = (s: Session) => {
|
||||||
|
logger.info(`Configuring camera ${s.devName}`);
|
||||||
configureWifi(ssid, password)(s);
|
configureWifi(ssid, password)(s);
|
||||||
logger.info(`WiFi config for camera ${s.devName} is done`);
|
logger.info(`WiFi config for camera ${s.devName} is done`);
|
||||||
|
|
||||||
|
logger.info(`Validating WiFi settings on ${s.devName}`);
|
||||||
|
s.send(SendWifiSettings(s));
|
||||||
|
|
||||||
|
logger.info(`Asking ${s.devName} to reboot`);
|
||||||
|
s.send(SendReboot(s));
|
||||||
};
|
};
|
||||||
|
|
||||||
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
|
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
|
||||||
|
|||||||
+43
-23
@@ -2,13 +2,15 @@ import { createSocket, RemoteInfo } from "node:dgram";
|
|||||||
import EventEmitter from "node:events";
|
import EventEmitter from "node:events";
|
||||||
|
|
||||||
import { Commands, CommandsByValue } from "./datatypes.js";
|
import { Commands, CommandsByValue } from "./datatypes.js";
|
||||||
import { handle_Drw, handle_P2PAlive, handle_P2PRdy, makeP2pRdy, noop, notImpl } from "./handlers.js";
|
import { handle_Drw, handle_DrwAck, handle_P2PAlive, handle_P2PRdy, makeP2pRdy, notImpl, noop } from "./handlers.js";
|
||||||
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
|
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
|
||||||
import { opt } from "./options.js";
|
import { opt } from "./options.js";
|
||||||
import { logger } from "./logger.js";
|
import { logger } from "./logger.js";
|
||||||
|
|
||||||
export type Session = {
|
export type Session = {
|
||||||
send: (msg: DataView) => void;
|
send: (msg: DataView) => void;
|
||||||
|
ackDrw: (id: number) => void;
|
||||||
|
unackedDrw: { [id: number]: { sent_ts: number; data: DataView } };
|
||||||
outgoingCommandId: number;
|
outgoingCommandId: number;
|
||||||
ticket: number[];
|
ticket: number[];
|
||||||
eventEmitter: EventEmitter;
|
eventEmitter: EventEmitter;
|
||||||
@@ -37,10 +39,13 @@ type msgCb = (
|
|||||||
const handleIncoming: msgCb = (session, handlers, msg, rinfo) => {
|
const handleIncoming: msgCb = (session, handlers, msg, rinfo) => {
|
||||||
const ab = new Uint8Array(msg).buffer;
|
const ab = new Uint8Array(msg).buffer;
|
||||||
const dv = new DataView(ab);
|
const dv = new DataView(ab);
|
||||||
const cmd = CommandsByValue[dv.readU16()];
|
const raw = dv.readU16();
|
||||||
|
const cmd = CommandsByValue[raw];
|
||||||
logger.log("trace", `<< ${cmd}`);
|
logger.log("trace", `<< ${cmd}`);
|
||||||
handlers[cmd](session, dv, rinfo);
|
handlers[cmd](session, dv, rinfo);
|
||||||
session.lastReceivedPacket = Date.now();
|
if (raw != Commands.P2PAlive && raw != Commands.P2PAliveAck) {
|
||||||
|
session.lastReceivedPacket = Date.now();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeSession = (
|
export const makeSession = (
|
||||||
@@ -50,6 +55,7 @@ export const makeSession = (
|
|||||||
onLogin: (s: Session) => void,
|
onLogin: (s: Session) => void,
|
||||||
options: opt,
|
options: opt,
|
||||||
): Session => {
|
): Session => {
|
||||||
|
let unackedDrw = {};
|
||||||
const sock = createSocket("udp4");
|
const sock = createSocket("udp4");
|
||||||
|
|
||||||
sock.on("error", (err) => {
|
sock.on("error", (err) => {
|
||||||
@@ -60,23 +66,11 @@ export const makeSession = (
|
|||||||
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
|
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
|
||||||
|
|
||||||
sock.on("listening", () => {
|
sock.on("listening", () => {
|
||||||
const startup = () => {
|
const buf = makeP2pRdy(dev);
|
||||||
const buf = makeP2pRdy(dev);
|
session.send(buf);
|
||||||
session.send(buf);
|
session.started = true;
|
||||||
// The YsxLite sends a P2PAlive message immediately after P2PRdy
|
|
||||||
const alive_buf = create_P2pAlive();
|
|
||||||
session.send(alive_buf);
|
|
||||||
session.started = true;
|
|
||||||
};
|
|
||||||
if (options.slow_startup) {
|
|
||||||
logger.info("Delaying session startup");
|
|
||||||
setTimeout(startup, 6500);
|
|
||||||
} else {
|
|
||||||
startup();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const SEND_PORT = 32108;
|
|
||||||
sock.bind();
|
sock.bind();
|
||||||
const sessTimer = setInterval(() => {
|
const sessTimer = setInterval(() => {
|
||||||
const delta = Date.now() - session.lastReceivedPacket;
|
const delta = Date.now() - session.lastReceivedPacket;
|
||||||
@@ -85,26 +79,51 @@ export const makeSession = (
|
|||||||
let buf = create_P2pAlive();
|
let buf = create_P2pAlive();
|
||||||
session.send(buf);
|
session.send(buf);
|
||||||
}
|
}
|
||||||
if (delta > 8000) {
|
if (delta > 5000) {
|
||||||
session.eventEmitter.emit("disconnect");
|
session.eventEmitter.emit("disconnect");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 400);
|
}, 400);
|
||||||
|
|
||||||
|
const resendTimer = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, value] of Object.entries(session.unackedDrw)) {
|
||||||
|
const { sent_ts, data } = value;
|
||||||
|
if (now - sent_ts > 100) {
|
||||||
|
const pkt_id = data.add(6).readU16();
|
||||||
|
logger.debug(`Resending packet ${pkt_id} as ${session.outgoingCommandId}`);
|
||||||
|
data.add(6).writeU16(session.outgoingCommandId);
|
||||||
|
session.outgoingCommandId++;
|
||||||
|
delete session.unackedDrw[key];
|
||||||
|
session.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
const session: Session = {
|
const session: Session = {
|
||||||
outgoingCommandId: 0,
|
outgoingCommandId: 0,
|
||||||
ticket: [0, 0, 0, 0],
|
ticket: [0, 0, 0, 0],
|
||||||
lastReceivedPacket: 0,
|
lastReceivedPacket: 0,
|
||||||
eventEmitter: new EventEmitter(),
|
eventEmitter: new EventEmitter(),
|
||||||
connected: true,
|
connected: true,
|
||||||
timers: [sessTimer],
|
timers: [sessTimer, resendTimer],
|
||||||
devName: dev.devId,
|
devName: dev.devId,
|
||||||
started: false,
|
started: false,
|
||||||
send: (msg: DataView) => {
|
send: (msg: DataView) => {
|
||||||
const raw = msg.readU16();
|
const raw = msg.readU16();
|
||||||
const cmd = CommandsByValue[raw];
|
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}`);
|
logger.log("trace", `>> ${cmd}`);
|
||||||
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
|
sock.send(new Uint8Array(msg.buffer), ra.port, session.dst_ip);
|
||||||
|
},
|
||||||
|
ackDrw: (id: number) => {
|
||||||
|
logger.debug(`Removing ${id} from pending`);
|
||||||
|
delete unackedDrw[id];
|
||||||
},
|
},
|
||||||
dst_ip: ra.address,
|
dst_ip: ra.address,
|
||||||
curImage: [],
|
curImage: [],
|
||||||
@@ -112,6 +131,7 @@ export const makeSession = (
|
|||||||
frame_is_bad: false,
|
frame_is_bad: false,
|
||||||
frame_was_fixed: false,
|
frame_was_fixed: false,
|
||||||
options: options,
|
options: options,
|
||||||
|
unackedDrw,
|
||||||
};
|
};
|
||||||
|
|
||||||
session.eventEmitter.on("disconnect", () => {
|
session.eventEmitter.on("disconnect", () => {
|
||||||
@@ -146,13 +166,13 @@ export const Handlers: Record<keyof typeof Commands, PacketHandler> = {
|
|||||||
PunchPkt: notImpl,
|
PunchPkt: notImpl,
|
||||||
P2PAlive: handle_P2PAlive,
|
P2PAlive: handle_P2PAlive,
|
||||||
P2pRdy: handle_P2PRdy,
|
P2pRdy: handle_P2PRdy,
|
||||||
DrwAck: noop,
|
DrwAck: handle_DrwAck,
|
||||||
Drw: handle_Drw,
|
Drw: handle_Drw,
|
||||||
|
|
||||||
|
P2PAliveAck: noop,
|
||||||
Close: notImpl,
|
Close: notImpl,
|
||||||
LanSearchExt: notImpl,
|
LanSearchExt: notImpl,
|
||||||
LanSearch: notImpl,
|
LanSearch: notImpl,
|
||||||
P2PAliveAck: notImpl,
|
|
||||||
Hello: notImpl,
|
Hello: notImpl,
|
||||||
P2pReq: notImpl,
|
P2pReq: notImpl,
|
||||||
LstReq: notImpl,
|
LstReq: notImpl,
|
||||||
|
|||||||
@@ -59,6 +59,17 @@ DataView.prototype.writeString = function (str) {
|
|||||||
const bytes = [...str].map((_, i) => str.charCodeAt(i));
|
const bytes = [...str].map((_, i) => str.charCodeAt(i));
|
||||||
return this.writeByteArray(bytes);
|
return this.writeByteArray(bytes);
|
||||||
};
|
};
|
||||||
|
DataView.prototype.startsWith = function (arr) {
|
||||||
|
if (this.byteLength < arr.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
if (this.add(i).readU8() != arr[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface DataView {
|
interface DataView {
|
||||||
@@ -77,6 +88,7 @@ declare global {
|
|||||||
writeU32(n: number): void;
|
writeU32(n: number): void;
|
||||||
writeU64(n: bigint): void;
|
writeU64(n: bigint): void;
|
||||||
writeU8(n: number): void;
|
writeU8(n: number): void;
|
||||||
|
startsWith(arr: number[]): boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default global;
|
export default global;
|
||||||
|
|||||||
+2
-7
@@ -2,9 +2,9 @@ import "../shim.ts";
|
|||||||
|
|
||||||
import assert from "assert";
|
import assert from "assert";
|
||||||
|
|
||||||
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
|
||||||
import { hexdump } from "../hexdump.js";
|
import { hexdump } from "../hexdump.js";
|
||||||
import { SendStartVideo, SendDevStatus, SendUsrChk, SendWifiDetails, parse_PunchPkt } from "../impl.ts";
|
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
|
||||||
|
import { parse_PunchPkt, SendDevStatus, SendStartVideo, SendUsrChk, SendWifiDetails } from "../impl.ts";
|
||||||
import { placeholderTypes, sprintf } from "../utils.js";
|
import { placeholderTypes, sprintf } from "../utils.js";
|
||||||
|
|
||||||
describe("debug_tools", () => {
|
describe("debug_tools", () => {
|
||||||
@@ -122,11 +122,6 @@ describe("module", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hstrToBA = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16))).buffer;
|
const hstrToBA = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16))).buffer;
|
||||||
describe("events", () => {
|
|
||||||
it("emits login event upon logging in", () => {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("parse packet", () => {
|
describe("parse packet", () => {
|
||||||
it("parses PunchPkt", () => {
|
it("parses PunchPkt", () => {
|
||||||
const in_pkt_str = "f14100144241544400000000000262ca574f4e4a4d000000";
|
const in_pkt_str = "f14100144241544400000000000262ca574f4e4a4d000000";
|
||||||
|
|||||||
@@ -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>]
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user