90 Commits
Author SHA1 Message Date
David Ventura c80b6b0a67 Add content-length & newline before boundary 2024-10-02 22:43:53 +02:00
Tin LaiandTin Lai 9f6f0ca8fa fix crashes when pkt_id 1 is dropped (#33)
Signed-off-by: Tin Lai <tin@tinyiu.com>
Co-authored-by: Tin Lai <tin@tinyiu.com>
2024-07-04 11:27:06 +02:00
David Ventura 8f19d215ba split out parseDevStatusAck & test it 2024-07-01 16:03:53 +02:00
David Ventura f85248bc6a support single-frame capture 2024-07-01 16:03:21 +02:00
David Ventura 80b1e4643b add test wifidetails + fix implementation offset 2024-05-07 13:35:22 +02:00
David Ventura 2e68a64e92 update example 2024-05-07 12:06:52 +02:00
David Ventura 04fb1a8e4c configure timeout 2024-05-07 12:06:46 +02:00
David Ventura 2facd91fb5 call configureWifi after scanning to include the channel 2024-05-07 12:06:35 +02:00
David Ventura 75401621cf write channel byte 2024-05-07 12:05:57 +02:00
David Ventura 17d00eaba9 emit ListWifi on scan 2024-05-07 12:05:12 +02:00
David Ventura db98e6685c notes 2024-05-07 11:30:04 +02:00
David Ventura 40dab0c017 bump timeout for wifiscanning - should only be in pair mode.. 2024-05-07 11:30:04 +02:00
David Ventura 6b28e91b9a parse swver & remove uN_swap 2024-05-07 11:30:04 +02:00
David Ventura cdc5be9948 add wifi parsing test 2024-05-07 11:30:04 +02:00
Adriano Cunha 4a6a58bd7b Fix crash in http_server (#25)
Set camera config before referencing it. Also set audio default to true.
2024-05-06 22:09:03 +02:00
David Ventura 0db9cfd198 tolerate cut off packets & fix wifilist parsing 2024-05-06 16:52:46 +02:00
David Ventura 7b9d8fb5b7 tolerate cutoff packets 2024-05-06 16:52:30 +02:00
David Ventura 0ef5581b3b add missing channel as well 2024-05-06 15:36:08 +02:00
David Ventura 5060a4d044 change loglevel 2024-05-06 15:23:59 +02:00
David Ventura 27ab7d9af2 split long string 2024-05-06 11:53:34 +02:00
David Ventura ea533d786d add warning on timeout 2024-05-06 11:48:23 +02:00
David Ventura e2751c65bc flip ip & mask 2024-05-06 11:48:23 +02:00
David Ventura ceec51b22d add command dump 2024-05-06 11:35:50 +02:00
David Ventura a45b706ad0 add wifiparams type 2024-05-06 11:35:33 +02:00
David Ventura f065fbca9a handle close message 2024-05-06 09:25:58 +02:00
David Ventura 760dbc5820 Fix #17. Support cameras with short prefixes 2024-05-05 18:07:34 +02:00
David Ventura d5d4161dd8 implement settings support 2024-05-05 17:39:52 +02:00
David Ventura f4b7b1a463 remove ansi from options 2024-05-05 16:43:31 +02:00
David Ventura b59c77729c add notes 2024-05-03 23:07:51 +02:00
David Ventura 21189a8ae9 call npm tests from make 2024-05-03 23:02:39 +02:00
David Ventura 8c71e84875 add integration test with mock server 2024-05-03 23:02:25 +02:00
David Ventura bac2b4fd4f allow the socket to be closed from the EE 2024-05-03 23:02:04 +02:00
David Ventura c81d7d4182 ignore Drw data packets that are too short to be valid 2024-05-03 18:10:42 +02:00
David Ventura a93564cd11 do not consider p2palive messages for camera liveness 2024-05-03 15:57:55 +02:00
David Ventura d7d3b91528 add debug message on corrupt frames 2024-05-03 15:57:23 +02:00
David Ventura 204677ceae implement simple retransmission scheme 2024-05-03 12:35:29 +02:00
David Ventura 03893f8d81 remove LanSearchExt, increase search interval 2024-05-03 12:35:07 +02:00
David Ventura dc43a66209 ask for wifi settings after pairing 2024-05-03 11:46:14 +02:00
David Ventura 6bed63b61f tolerate empty answers from listwifi 2024-05-03 11:45:57 +02:00
David Ventura 2a3d047f48 add logging on unhandled command acks 2024-05-03 11:38:55 +02:00
David Ventura 1edb7370cf add 2 ack types 2024-05-03 11:38:30 +02:00
David Ventura 6c7856054e remove port form log 2024-05-03 11:19:09 +02:00
David Ventura f29206a1e1 fix comments 2024-05-03 11:18:31 +02:00
David Ventura 449210ec23 send reboot command after pairing 2024-05-03 00:15:14 +02:00
David Ventura 3d003be371 update logic to consider the JPEG type stream, still consider all untagged data as JPEG 2024-05-02 22:38:01 +02:00
David Ventura 7b153d6ff8 impl startswith 2024-05-02 22:29:02 +02:00
David Ventura f5da7a34bd reduce loglevel 2024-05-02 22:28:55 +02:00
David Ventura e496b33ff5 add hdr type 2024-05-02 22:28:44 +02:00
David Ventura f9e2dc63dc add framed headers 2024-05-02 20:53:33 +02:00
David Ventura 80e50560e6 add some more fields 2024-05-02 14:53:07 +02:00
David Ventura 2ec4d603c6 remove incorrect workarounds for undetected cameras 2024-05-02 13:23:52 +02:00
David Ventura 45b8b159d1 add features 2024-05-02 13:20:51 +02:00
David Ventura c51e4f8a7a implement rotation via injecting exif headers (#19) 2024-05-02 13:18:28 +02:00
David Ventura 68fe0fb798 warn on unhandled stream packets 2024-05-02 13:18:15 +02:00
David Ventura 7ca795c917 move example file 2024-05-02 10:02:44 +02:00
David Ventura d14e16c53d warn if node version <16 2024-05-02 10:01:11 +02:00
Adriano Cunha 6ca18b057b Fix sending favicon in http_server (#21)
`favicon` is now an array, and `end()` expects a Buffer or String.
2024-05-02 09:50:08 +02:00
David Ventura 1439d5f632 reply on the port we should 2024-05-01 23:54:33 +02:00
David Ventura 58c58e38ac increase delay & send LanSearchExt 2024-05-01 16:57:43 +02:00
David Ventura b5d4ab6024 set noEmit flag on tsc 2024-05-01 13:26:56 +02:00
David Ventura 2ce568c019 trim serial/name mapping; add a log message 2024-05-01 13:17:37 +02:00
David Ventura 5a0b36f42b add links to artifacts 2024-05-01 13:17:15 +02:00
David Ventura 74863f675e remove example output from readme 2024-05-01 13:07:11 +02:00
David Ventura 9654dc2841 implement logging properly 2024-05-01 13:06:27 +02:00
David Ventura 9c19c05274 include files at build time 2024-05-01 12:44:52 +02:00
David Ventura a547546055 add slow-startup flag 2024-05-01 11:56:17 +02:00
David Ventura c4007961c7 fix makefile target 2024-05-01 11:23:32 +02:00
David Ventura 15f70b55af run tsc in ci 2024-05-01 11:23:26 +02:00
David Ventura 8f4c5f09a4 tidy up dissector 2024-05-01 11:23:16 +02:00
David Ventura 06cafff105 return nil from packet lut 2024-05-01 11:23:05 +02:00
David Ventura e2a4352659 add heuristic packet matcher 2024-05-01 10:47:00 +02:00
David Ventura 5bb0038d9b send P2PAlive immediately after P2pRdy 2024-04-30 23:16:24 +02:00
David Ventura b653368d89 fix test 2024-04-30 23:11:27 +02:00
Adriano Cunha 690c1b4146 More features in the HTTP server (#16)
* Added more features to HTTP server

* Added a title to the multi-camera view page.
* Support naming cameras through a simple mapping using a text file.
* Updated some logging messages to be more verbose (and match those in pair.ts).
* Confirmed the charging status and battery level values reported back by DevStatusAck.
* Log camera status (charging, battery, Wifi strength) when connected.

* More updates to HTTP server

* Deal with Web Audio autoplay policy by starting camera on mute and adding a button to toggle the audio stream.
* Add a favicon so the server tan can be easily spotted.

The favicon file is a 48x48 32bpp ICO file, gzipped.

* Update README.md

* Describe flags and enable camera audio by default.

* Update audio button based on state change

* Update description of discovery_ip in bin.ts

* Update session start message in http_server.ts

* Update README.md

Update flags and note about --discovery_ip.
2024-04-30 16:18:10 +02:00
David Ventura 66796267f2 add serial to punchpkt/p2prdy dissector 2024-04-30 12:00:37 +02:00
David Ventura 11ab1597f6 test punchpkt parsing 2024-04-30 12:00:37 +02:00
Adriano CunhaandDavid Ventura 3edccdc4ab Update "Spyware" section in README (#15)
* Update "Spyware" section in README

Added a paragraph mentioning the possible reason and clarifying that blocking outgoing traffic doesn't affect the HTTP server.

* Update README.md

---------

Co-authored-by: David Ventura <davidventura27@gmail.com>
2024-04-28 00:53:04 +02:00
David Ventura c308307fea Merge pull request #11 from adrcunha/patch-3
Trigger debug output in handlers.ts according to --debug
2024-04-28 00:51:31 +02:00
Adriano Cunha ca6928aa7c Merge branch 'master' into patch-3 2024-04-27 13:01:17 -07:00
David Ventura 632308a50e Merge pull request #13 from adrcunha/patch-1
Add a few user-friendly messages to pair.ts for first-timers
2024-04-27 13:32:18 +02:00
Adriano Cunha 40bb81f22e Add a few user-friendly messages to pair.ts for first-timers 2024-04-26 17:29:53 -07:00
Adriano Cunha e9487f9603 Add a few user-friendly messages to pair.ts for first-timers
Also fix the session handling, allowing a camera to be reconnected and recognized again.
2024-04-26 08:51:06 -07:00
Adriano Cunha 82d9115ae6 Merge branch 'DavidVentura:master' into master 2024-04-26 08:47:36 -07:00
Adriano Cunha 31baea2415 Trigger debug output in handlers.ts according to --debug 2024-04-26 08:42:42 -07:00
David Ventura da867078b1 Merge pull request #10 from adrcunha/patch-3
Remove the `pair` target
2024-04-26 17:41:39 +02:00
David Ventura dc75519f3d Merge pull request #9 from adrcunha/patch-2
Drop newline in the activity indicator
2024-04-26 17:41:14 +02:00
David Ventura 8246ea52ef Merge pull request #8 from adrcunha/patch-1
Update instructions in README
2024-04-26 17:40:19 +02:00
Adriano Cunha 72de55b26f Remove the pair target
Now that `pair` requires parameters and SSID/password have no defaults, this target doesn't work.
2024-04-26 08:30:33 -07:00
Adriano Cunha 6b5026dcf1 Drop newline in the activity indicator
The "." character is printed on a new line every 2s to indicate that the discovery process is running. However, using a new line unnecessary floods the output log; switch to printing "." without a newline, keeping the activity indicator while also drastically reducing the number of lines in the output log.
2024-04-26 08:24:55 -07:00
Adriano Cunha a3a5a9404c Update instructions in README
Update instructions a bit for first-time users, fix a few typos.
2024-04-26 08:18:20 -07:00
30 changed files with 1733 additions and 303 deletions
+1
View File
@@ -28,6 +28,7 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run tsc
- run: npm run build
- run: npm test
- uses: actions/upload-artifact@v4
+1
View File
@@ -3,3 +3,4 @@ venv
node_modules
build
dist/
cameras.txt
+6 -8
View File
@@ -1,4 +1,4 @@
.PHONY: run hook install-wireshark-dissector test build
.PHONY: run hook install-wireshark-dissector test build typecheck
bundle.js: frida-hooks.js func_replacements.js
~/node_modules/.bin/frida-compile -o $@ frida-hooks.js
@@ -8,15 +8,13 @@ venv: requirements.txt
./venv/bin/pip install -r requirements.txt
touch venv
build: node_modules
typecheck: node_modules
npm run tsc
build: node_modules
npm run build
run: node_modules
./node_modules/.bin/ts-node --esm cmd/bin http_server --port=1234
pair: node_modules
./node_modules/.bin/ts-node --esm cmd/bin pair
run: build
node dist/bin.cjs http_server --port=1234
hook: bundle.js venv
./venv/bin/python3 -u loader3.py
@@ -25,7 +23,7 @@ node_modules:
npm install
test: node_modules
./node_modules/.bin/mocha --require ts-node/register tests/fn.test.js
npm run test
install-wireshark-dissector:
mkdir -p ~/.local/lib/wireshark/plugins
+70 -32
View File
@@ -6,25 +6,29 @@ Re-implementation of the "iLnk"/"iLnkP2P"/"PPPP" protocol used on some cheap (\<
Per pictures of the [X5](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb.jpg?raw=true), [A9](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb_a9.jpg?raw=true) the main chip is TXW817 ([chinese](https://www.taixin-semi.com/Product/ProductDetail?productId=306), [eng, google translate](https://www-taixin--semi-com.translate.goog/Product/ProductDetail?productId=306&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp))
## Features
- Multi camera support
- Audio & video streaming
- Rotation / mirroring of video streams
- Friendly names for cameras
- Ability to configure "blank" cameras with Wifi settings
## Building
Run `make build` or `npm run build` to build the server artifact. You can also find some pre-built files [in the CI results](https://github.com/DavidVentura/cam-reverse/actions) or [in the releases](https://github.com/DavidVentura/cam-reverse/releases/)
## Pairing a new camera
Ensure your device in access point mode (the blue LED blinks slowly to indicate that); optionally, press the MODE button for 5s to switch to access point mode.
Connect to the device's access point (e.g., FTYC811847AGFDZ) and run `node dist/bin.cjs pair --ssid <SSID> --password <PASSWORD>`.
## Running
To execute the HTTP server, run `node bin.cjs`; you can access the JPEG stream at http://localhost:5000/ and (optionally) a file `audio.pcm` will be created.
```bash
$ node dist/bin.cjs http_server --help
start http server
Options:
--help Show help [boolean]
--version Show version number [boolean]
--debug [boolean] [default: false]
--ansi [boolean] [default: false]
--audio [boolean] [default: false]
--discovery_ip [default: "192.168.1.255"]
--attempt_to_fix_packet_loss [default: false]
--port HTTP Port to listen on [number] [default: 5000]
```
### HTTP Server
To execute the HTTP server, run `node dist/bin.cjs http_server`; you can access the JPEG stream at http://localhost:5000/.
The roundtrip delay when using MJPEG is [~350ms](pics/delay.jpg?raw=true).
@@ -32,29 +36,61 @@ There's a basic UI which can display multiple cameras:
![](pics/web-ui.jpg?raw=true)
The server will send a broadcast packet every few seconds to discover all the cameras available; this means that it *must* run in the same broadcast domain (VLAN) as your cameras.
Clicking on the image will take you to a page that has audio streaming. Click the button below the image to mute/unmute the audio.
Clicking on the image will take you to a page that has audio streaming enabled.
## Pairing a new camera
#### Settings
Connect to the device's access point and run `node bin.cjs pair --ssid <SSID> --password <PASSWORD>`.
```bash
$ node dist/bin.cjs pair --help
You can provide a config file in `yml` format, then pass it as an argument: `node bin.cjs http_server --config_file <your_config.yml>`
configure a camera
```yml
http_server:
port: 5000
Options:
--help Show help [boolean]
--version Show version number [boolean]
--debug [boolean] [default: false]
--ansi [boolean] [default: false]
--discovery_ip [default: "192.168.1.255"]
--attempt_to_fix_packet_loss [default: false]
--ssid [string] [required]
--password [string] [required]
logging:
level: debug
use_color: true
cameras:
FTYC477360FAWUK:
alias: "A9"
rotate: 1
mirror: false
fix_packet_loss: yes
audio: true
BATC609531EXLVS:
alias: "X5"
# If you are crossing broadcast domains (VLANs) then
# you need to specify all IPs as unicast targets
discovery_ips:
- 192.168.40.101
- 192.168.40.102
- 192.168.40.103
- 192.168.40.104
- 192.168.40.105
# If you are in the same broadcast domain, then
# it's easier to just use the broadcast address of your network
# discovery_ips:
# - 192.168.1.255
blacklisted_ips:
- 192.168.40.102
```
All keys are optional
You must restart the HTTP server for changes to the settings file to take effect.
### Single capture mode
```bash
node bin.cjs frame --discovery_ip 192.168.40.104 --out out.jpg
```
----
## Protocol
The protocol is weirdly complex, though very little communication is necessary to use the device
@@ -181,6 +217,8 @@ Every 8-10s
There are some other strings in the APK ending in `-$$` which decode to other ips/hostnames.
Most of the IPs point to AWS compute instances, and this connection is probably used to see live streams over the Internet using the app. It's fine (and recommended!) to block outgoing traffic from the cameras, as it won't affect the HTTP server.
## Other stuff
+66 -25
View File
@@ -1,41 +1,62 @@
<html>
<h2>${id}</h2><a href="/camera/${id}"><img src="/camera/${id}"/></a><hr/>
<head>
<link rel="shortcut icon" href="/favicon.ico">
<title>${name}</title>
</head><body>
<h2>${name}</h2><a href="/camera/${id}"><img style="height: 640px" src="/camera/${id}"/></a><hr/>
<button onclick="toggle_audio()" id=audio disabled=true>Audio: disabled</button>
<button onclick="fetch('/rotate/${id}')">Rotate</button>
<button onclick="fetch('/mirror/${id}')">Mirror</button>
<script>
const alaw_to_s16_table = [
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752,
-2624, -3008, -2880, -2240, -2112, -2496, -2368, -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, -22016,
-20992, -24064, -23040, -17920, -16896, -19968, -18944, -30208, -29184, -32256, -31232, -26112, -25088, -28160,
-27136, -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, -15104, -14592, -16128, -15616, -13056, -12544,
-14080, -13568, -344, -328, -376, -360, -280, -264, -312, -296, -472, -456, -504, -488, -408, -392, -440, -424, -88,
-72, -120, -104, -24, -8, -56, -40, -216, -200, -248, -232, -152, -136, -184, -168, -1376, -1312, -1504, -1440, -1120,
-1056, -1248, -1184, -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, -688, -656, -752, -720, -560, -528, -624,
-592, -944, -912, -1008, -976, -816, -784, -880, -848, 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, 7552, 7296,
8064, 7808, 6528, 6272, 7040, 6784, 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, 3776, 3648, 4032, 3904, 3264,
3136, 3520, 3392, 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, 30208, 29184, 32256, 31232, 26112, 25088,
28160, 27136, 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, 15104, 14592, 16128, 15616, 13056, 12544, 14080,
13568, 344, 328, 376, 360, 280, 264, 312, 296, 472, 456, 504, 488, 408, 392, 440, 424, 88, 72, 120, 104, 24, 8, 56,
40, 216, 200, 248, 232, 152, 136, 184, 168, 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, 1888, 1824, 2016, 1952,
1632, 1568, 1760, 1696, 688, 656, 752, 720, 560, 528, 624, 592, 944, 912, 1008, 976, 816, 784, 880, 848,
];
const alaw_to_s16_table = [
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752,
-2624, -3008, -2880, -2240, -2112, -2496, -2368, -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, -22016,
-20992, -24064, -23040, -17920, -16896, -19968, -18944, -30208, -29184, -32256, -31232, -26112, -25088, -28160,
-27136, -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, -15104, -14592, -16128, -15616, -13056, -12544,
-14080, -13568, -344, -328, -376, -360, -280, -264, -312, -296, -472, -456, -504, -488, -408, -392, -440, -424, -88,
-72, -120, -104, -24, -8, -56, -40, -216, -200, -248, -232, -152, -136, -184, -168, -1376, -1312, -1504, -1440, -1120,
-1056, -1248, -1184, -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, -688, -656, -752, -720, -560, -528, -624,
-592, -944, -912, -1008, -976, -816, -784, -880, -848, 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, 7552, 7296,
8064, 7808, 6528, 6272, 7040, 6784, 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, 3776, 3648, 4032, 3904, 3264,
3136, 3520, 3392, 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, 30208, 29184, 32256, 31232, 26112, 25088,
28160, 27136, 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, 15104, 14592, 16128, 15616, 13056, 12544, 14080,
13568, 344, 328, 376, 360, 280, 264, 312, 296, 472, 456, 504, 488, 408, 392, 440, 424, 88, 72, 120, 104, 24, 8, 56,
40, 216, 200, 248, 232, 152, 136, 184, 168, 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, 1888, 1824, 2016, 1952,
1632, 1568, 1760, 1696, 688, 656, 752, 720, 560, 528, 624, 592, 944, 912, 1008, 976, 816, 784, 880, 848,
];
const alaw_to_s16 = (a_val) => {
return alaw_to_s16_table[a_val];
};
const alaw_to_s16 = (a_val) => {
return alaw_to_s16_table[a_val];
};
const audio_context = new AudioContext();
var audio_context;
const audio_button = document.getElementById('audio');
audio_button.disabled = !${audio};
update_audio_button();
function setup_audio() {
audio_context = new AudioContext();
const gain_node = audio_context.createGain(); // Declare gain node
const channels =1;
const sample_rate = 8000;
const audioBuffer = audio_context.createBuffer(channels, 960, sample_rate); // 960??
//const audioBuffer = audio_context.createBuffer(channels, decoded.length, sample_rate);
gain_node.connect(audio_context.destination); // Connect gain node to speakers
audio_context.onstatechange = () => {
console.log("Audio state is now ", audio_context.state);
update_audio_button(audio_context.state == "running");
};
gain_node.connect(audio_context.destination); // Connect gain node to speakers
audio_context.resume();
const evtSource = new EventSource('/audio/${id}');
const evtSource = new EventSource("/audio/${id}");
evtSource.onopen = (e) => {
console.log("evtsource open");
}
evtSource.onerror = (e) => {
console.log("evtsource error", e);
}
let endsAt = 0;
let startAt = 0;
evtSource.onmessage = (e) => {
@@ -55,8 +76,28 @@
source_node.start(startAt);
endsAt = now + audioBuffer.duration * 1000;
};
evtSource.onerror = (e) => {
console.log("evtsource error", e);
}
function update_audio_button(on) {
if (${audio}) {
audio_button.innerText = "Audio: " + (on ? "on \u{1F508}" : "off \u{1F507}");
}
}
function toggle_audio() {
if (audio_context == undefined) {
setup_audio();
return;
}
if (audio_context.state == "running") {
audio_context.suspend();
return;
}
if (audio_context.state == "suspended") {
audio_context.resume();
return;
}
console.log("Unknown audio stream status");
}
</script>
</html>
BIN
View File
Binary file not shown.
+39
View File
@@ -0,0 +1,39 @@
import fs from "node:fs";
import { DevSerial } from "./impl.js";
import { RemoteInfo } from "dgram";
import { logger } from "./logger.js";
import { startVideoStream } from "./session.js";
import { discoverDevices } from "./discovery.js";
import { Session } from "./session.js";
import { Handlers, makeSession } from "./session.js";
import { config } from "./settings.js";
const sessions: Record<string, Session> = {};
export const captureSingle = ({ discovery_ip, out_file }: { discovery_ip: string; out_file: string }) => {
let devEv = discoverDevices([discovery_ip]);
const startSession = (s: Session) => {
startVideoStream(s);
logger.info(`Camera ${s.devName} is now ready to stream`);
};
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
if (dev.devId in sessions) {
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
return;
}
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
const s = makeSession(Handlers, dev, rinfo, startSession, 5000);
sessions[dev.devId] = s;
config.cameras[dev.devId] = { fix_packet_loss: false };
s.eventEmitter.on("frame", () => {
const assembled = Buffer.concat(s.curImage);
fs.writeFileSync(out_file, assembled);
logger.info(`Got frame. Exiting`);
devEv.emit("close");
s.close();
});
});
};
+64 -19
View File
@@ -1,9 +1,15 @@
import process from "node:process";
import { hideBin } from "yargs/helpers";
import yargs from "yargs/yargs";
import { captureSingle } from "../capture_single.js";
import { serveHttp } from "../http_server.js";
import { opt } from "../options.js";
import { pair } from "../pair.js";
import { loadConfig, config } from "../settings.js";
import { buildLogger, logger } from "../logger.js";
const majorVersion = process.versions.node.split(".").map(Number)[0];
yargs(hideBin(process.argv))
.command(
@@ -11,19 +17,38 @@ yargs(hideBin(process.argv))
"start http server",
(yargs) => {
return yargs
.option("debug", { default: false })
.option("ansi", { default: false })
.option("audio", { default: false })
.boolean(["debug", "ansi", "audio"])
.option("discovery_ip", { default: "192.168.1.255" })
.option("attempt_to_fix_packet_loss", { default: false })
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
.option("color", { describe: "Use color in logs" })
.boolean(["audio", "color"])
.option("config_file", { describe: "Specify config file" })
.option("log_level", { describe: "Set log level" })
.option("discovery_ip", { describe: "Camera discovery IP address" })
.option("port", { describe: "HTTP Port to listen on" })
.string(["log_level", "discovery_ip", "config_file"])
.number(["port"])
.strict();
},
(argv) => {
const opts: opt = argv as opt;
serveHttp(opts, argv.port, argv.audio || false);
if (argv.config_file !== undefined) {
loadConfig(argv.config_file);
}
if (argv.port) {
config.http_server.port = argv.port;
}
if (argv.color !== undefined) {
config.logging.use_color = argv.color;
}
if (argv.log_level !== undefined) {
config.logging.level = argv.log_level;
}
if (argv.discovery_ip !== undefined) {
config.discovery_ips = [argv.discovery_ip];
}
buildLogger(config.logging.level, config.logging.use_color);
if (majorVersion < 16) {
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
}
serveHttp(config.http_server.port);
},
)
.command(
@@ -31,18 +56,38 @@ yargs(hideBin(process.argv))
"configure a camera",
(yargs) => {
return yargs
.option("debug", { default: false })
.option("ansi", { default: false })
.boolean(["debug", "ansi"])
.option("discovery_ip", { default: "192.168.1.255" })
.option("attempt_to_fix_packet_loss", { default: false })
.demandOption("ssid")
.demandOption("password")
.option("log_level", { describe: "Set log level", default: "info" })
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
.option("ssid", { describe: "Wifi network for the camera to connect to" })
.option("password", { describe: "Wifi network password" })
.demandOption(["ssid", "password"])
.string(["ssid", "password"]);
},
(argv) => {
const opts: opt = argv as unknown as opt;
pair({ opts, ssid: argv.ssid, password: argv.password });
buildLogger(argv.log_level, undefined);
if (majorVersion < 16) {
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
}
pair({ ssid: argv.ssid, password: argv.password });
},
)
.command(
"frame",
"capture a single frame from the first discovered camera",
(yargs) => {
return yargs
.option("log_level", { describe: "Set log level", default: "info" })
.option("discovery_ip", { describe: "Camera discovery IP address", default: "192.168.1.255" })
.option("out", { describe: "Path for output file" })
.demandOption(["out"])
.string(["out", "discovery_ip"]);
},
(argv) => {
buildLogger(argv.log_level, undefined);
if (majorVersion < 16) {
logger.error(`Node version ${majorVersion} is not supported, may malfunction`);
}
captureSingle({ discovery_ip: argv.discovery_ip, out_file: argv.out });
},
)
.demandCommand()
+34
View File
@@ -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
+2
View File
@@ -46,10 +46,12 @@ export const ControlCommands = {
ListWifi: 0x0360, // CMD_NET_WIFI_SCAN
ListWifiAck: 0x0361,
StartVideo: 0x1030, // CMD_PEER_LIVEVIDEO_START
StartVideoAck: 0x1031,
StopVideo: 0x1130, // CMD_PEER_LIVEVIDEO_STOP
Shutdown: 0x1010, //CMD_SYSTEM_SHUTDOWN,
Reboot: 0x1110, //CMD_SYSTEM_REBOOT,
VideoParamSet: 0x1830, // CMD_PEER_VIDEOPARAM_SET
VideoParamSetAck: 0x1831,
VideoParamGet: 0x1930, // CMD_PEER_VIDEOPARAM_GET
IRToggle: 0x0a30, // CMD_PEER_IRCUT_ONOFF
};
+27 -15
View File
@@ -1,22 +1,27 @@
import EventEmitter from "node:events";
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
import { createSocket, RemoteInfo } from "node:dgram";
import { Commands } from "./datatypes.js";
import EventEmitter from "node:events";
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo, debug: boolean) => {
import { Commands } from "./datatypes.js";
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
import { logger } from "./logger.js";
import { config } from "./settings.js";
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo) => {
const ab = new Uint8Array(msg).buffer;
const dv = new DataView(ab);
const cmd_id = dv.readU16();
if (cmd_id != Commands.PunchPkt) {
return;
}
if (debug) {
console.log("Discovery got a PunchPkt");
if (config.blacklisted_ips.indexOf(rinfo.address) !== -1) {
logger.debug(`Dropping packet of blacklisted IP: ${rinfo.address}`);
return;
}
logger.debug("Received a PunchPkt message");
ee.emit("discover", rinfo, parse_PunchPkt(dv));
};
export const discoverDevices = (debug: boolean, discovery_ip: string): EventEmitter => {
export const discoverDevices = (discovery_ips: string[]): EventEmitter => {
const sock = createSocket("udp4");
const SEND_PORT = 32108;
const ee = new EventEmitter();
@@ -26,21 +31,28 @@ export const discoverDevices = (debug: boolean, discovery_ip: string): EventEmit
sock.close();
});
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo, debug));
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo));
let timers = [];
sock.on("listening", () => {
let ls_buf = create_LanSearch();
sock.setBroadcast(true);
console.log(`Searching for devices on ${discovery_ip}`);
discovery_ips.forEach((discovery_ip) => {
logger.info(`Searching for devices on ${discovery_ip}`);
let buf = create_LanSearch();
setInterval(() => {
console.log(".");
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
}, 2000);
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
let int = setInterval(() => {
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
}, 3000);
timers.push(int);
logger.log("trace", `>> LanSearch [${discovery_ip}]`);
sock.send(new Uint8Array(ls_buf.buffer), SEND_PORT, discovery_ip);
});
});
sock.bind();
sock.on("close", () => timers.forEach((timer) => clearInterval(timer)));
ee.on("close", () => sock.close());
return ee;
};
+386 -7
View File
@@ -18,13 +18,31 @@ ilnk_proto.fields.start = ProtoField.uint16("iLnkP2P.start", "Start", base.HE
ilnk_proto.fields.cmd_dest = ProtoField.uint16("iLnkP2P.cmd_dest", "Dest", base.HEX)
ilnk_proto.fields.auth_token = ProtoField.bytes("iLnkP2P.auth_token", "CMD auth token", base.DASH)
ilnk_proto.fields.cmd_payload = ProtoField.bytes("iLnkP2P.payload", "CMD Payload", base.DASH)
ilnk_proto.fields.warning = ProtoField.string("iLnkP2P.warning", "Warning")
--
-- jpeg | audio | continuation type?
ilnk_proto.fields.data_payload = ProtoField.bytes("iLnkP2P.data_payload", "Data Payload", base.DASH)
ilnk_proto.fields.payload_type = ProtoField.string("iLnkP2P.payload_type", "Payload type")
ilnk_proto.fields.payload_subtype = ProtoField.string("iLnkP2P.payload_type", "Payload type")
ilnk_proto.fields.payload_len = ProtoField.uint32("iLnkP2P.payload_len", "Payload len")
ilnk_proto.fields.frame_no = ProtoField.uint32("iLnkP2P.frame_no", "Frame no")
-- audio
ilnk_proto.fields.audio_header = ProtoField.bytes("iLnkP2P.audio_header", "Audio Header")
ilnk_proto.fields.hdr_type = ProtoField.uint16("iLnkP2P.hdr_type", "Header Type")
ilnk_proto.fields.hdr_streamid = ProtoField.uint16("iLnkP2P.hdr_streamid", "Header Stream ID")
ilnk_proto.fields.hdr_frameno = ProtoField.uint32("iLnkP2P.hdr_frameno", "Header Frame")
ilnk_proto.fields.hdr_len = ProtoField.uint16("iLnkP2P.hdr_len", "Header Len")
ilnk_proto.fields.hdr_ver = ProtoField.uint16("iLnkP2P.hdr_ver", "Header version")
ilnk_proto.fields.hdr_res = ProtoField.uint16("iLnkP2P.hdr_red", "Header resolution")
ilnk_proto.fields.encrypted = ProtoField.bool("iLnkP2P.encrypted", "Encrypted")
ilnk_proto.fields.cmd_type = ProtoField.string("iLnkP2P.cmd_type", "Cmd Pkt Type")
ilnk_proto.fields.cmd_name = ProtoField.string("iLnkP2P.cmd_name", "Cmd Pkt")
ilnk_proto.fields.decrypted_data = ProtoField.bytes("iLnkP2P.decrypted_data", "Decrypted data")
-- PunchPkt
ilnk_proto.fields.serial = ProtoField.string("iLnkP2P.serial", "Serial")
lut = {
[0xf1f0] = "Close",
[0xf132] = "LanSearchExt",
@@ -46,11 +64,308 @@ lut = {
[0xf169] = "ListenReqAck",
[0xf170] = "RlyHelloAck",
[0xf171] = "RlyHelloAck2",
__index = function(tbl, key)
return "UNK " .. string.format("0x%X", key)
end
}
setmetatable(lut, lut)
control_lut = {
[0x0000] = "CFGID_VERSION",
[0x0000] = "CMD_ACK_OK",
[0x0001] = "CFGID_LANGUAGE",
[0x0001] = "CMD_ACK_UNAUTH",
[0x0002] = "CFGID_PRODUCTE",
[0x0002] = "CMD_ACK_NO_PRIVILEGE",
[0x0003] = "CFGID_UPGRADE",
[0x0003] = "CMD_ACK_INVALID_PARAM",
[0x0004] = "CFGID_P2P",
[0x0004] = "CMD_ACK_CMDEXCUTE_FAILED",
[0x0005] = "CFGID_TZ",
[0x0005] = "CMD_ACK_NONE_RESULT",
[0x0006] = "CFGID_USER",
[0x0006] = "CMD_ACK_UNKNOWN",
[0x0007] = "CFGID_OPR",
[0x0008] = "CFGID_SERIAL",
[0x0009] = "CFGID_WIRED",
[0x000a] = "CFGID_WLAN",
[0x000b] = "CFGID_OSD",
[0x000c] = "CFGID_IMG",
[0x000d] = "CFGID_CMOS",
[0x000e] = "CFGID_PTZ",
[0x000f] = "CFGID_AUDIO",
[0x0010] = "CFGID_VIDEO",
[0x0011] = "CFGID_RECPOLICY",
[0x0012] = "CFGID_RESCH",
[0x0013] = "CFGID_MDALARM",
[0x0014] = "CFGID_ADCALARM",
[0x0015] = "CFGID_INPUTALARM",
[0x0016] = "CFGID_SMTP",
[0x0017] = "CFGID_FTP",
[0x0018] = "CFGID_PUSH",
[0x0019] = "CFGID_WLANPMK",
[0x00fe] = "CGICMD",
[0x00ff] = "BINCMD",
[0x03e8] = "CMD_ACK_ILLIGAL",
[0x0eff] = "CMD_DEV_BROADCAST",
[0x1000] = "CMD_SYSTEM_DFTCFG_IMPORT",
[0x1001] = "CMD_SYSTEM_DFTCFG_EXPORT",
[0x1002] = "CMD_SYSTEM_DFTCFG_RECOVERY",
[0x1003] = "CMD_SYSTEM_ITEMDFTCFG_RECOVERY",
[0x1004] = "CMD_SYSTEM_CRNCFG_EXPORT",
[0x1005] = "CMD_SYSTEM_CRNCFG_IMPORT",
[0x1006] = "CMD_SYSTEM_DFTCFG_CREATE",
[0x1007] = "CMD_SYSTEM_UPGRAD_SET",
[0x1008] = "CMD_SYSTEM_STATUS_GET",
[0x1009] = "CMD_SYSTEM_UPGRAD_GET",
[0x1010] = "CMD_SYSTEM_SHUTDOWN",
[0x1011] = "CMD_SYSTEM_REBOOT",
[0x1012] = "CMD_SYSTEM_INF_GET",
[0x1013] = "CMD_SYSTEM_ALIAS_SET",
[0x1020] = "CMD_SYSTEM_USER_CHK",
[0x1021] = "CMD_SYSTEM_USER_SET",
[0x1022] = "CMD_SYSTEM_USER_GET",
[0x1023] = "CMD_SYSTEM_USER_CHG",
[0x1030] = "CMD_SYSTEM_P2PPARAM_SET",
[0x1031] = "CMD_SYSTEM_OPRPOLICY_SET",
[0x1032] = "CMD_SYSTEM_OPRPOLICY_GET",
[0x1033] = "CMD_SYSTEM_P2PPARAM_GET",
[0x1040] = "CMD_SYSTEM_DATETIME_SET",
[0x1041] = "CMD_SYSTEM_DATETIME_GET",
[0x1051] = "CMD_NOTIFICATION",
[0x1100] = "ACK_SYSTEM_DFTCFG_IMPORT",
[0x1101] = "ACK_SYSTEM_DFTCFG_EXPORT",
[0x1102] = "ACK_SYSTEM_DFTCFG_RECOVERY",
[0x1103] = "ACK_SYSTEM_ITEMDFTCFG_RECOVERY",
[0x1104] = "ACK_SYSTEM_CRNCFG_EXPORT",
[0x1105] = "ACK_SYSTEM_CRNCFG_IMPORT",
[0x1106] = "ACK_SYSTEM_DFTCFG_CREATE",
[0x1107] = "ACK_SYSTEM_UPGRAD_SET",
[0x1108] = "ACK_SYSTEM_STATUS_GET",
[0x1109] = "ACK_SYSTEM_UPGRAD_GET",
[0x1110] = "ACK_SYSTEM_SHUTDOWN",
[0x1111] = "ACK_SYSTEM_REBOOT",
[0x1112] = "ACK_SYSTEM_INF_GET",
[0x1113] = "ACK_SYSTEM_ALIAS_SET",
[0x1120] = "ACK_SYSTEM_USER_CHK",
[0x1121] = "ACK_SYSTEM_USER_SET",
[0x1122] = "ACK_SYSTEM_USER_GET",
[0x1123] = "ACK_SYSTEM_USER_CHG",
[0x1130] = "ACK_SYSTEM_P2PPARAM_SET",
[0x1131] = "ACK_SYSTEM_OPRPOLICY_SET",
[0x1132] = "ACK_SYSTEM_OPRPOLICY_GET",
[0x1133] = "ACK_SYSTEM_P2PPARAM_GET",
[0x1140] = "ACK_SYSTEM_DATETIME_SET",
[0x1141] = "ACK_SYSTEM_DATETIME_GET",
[0x1151] = "ACK_NOTIFICATION",
[0x2000] = "CMD_SD_FORMAT",
[0x2001] = "CMD_SD_RECPOLICY_SET",
[0x2002] = "CMD_SD_RECPOLICY_GET",
[0x2003] = "CMD_SD_RECORDING_NOW",
[0x2004] = "CMD_SD_INFO_GET",
[0x2005] = "CMD_SD_RECORDFILE_GET",
[0x2006] = "CMD_SD_RECORDSCH_GET",
[0x2007] = "CMD_SD_RECORDSCH_SET",
[0x2008] = "CMD_SD_RETRIVEL",
[0x2009] = "CMD_SD_PICFILE_GET",
[0x200a] = "CMD_SD_PIC_CAPTURE",
[0x200b] = "CMD_SD_REC_DEL",
[0x200c] = "CMD_SD_PIC_DEL",
[0x200d] = "CMD_SD_SPL_DEL",
[0x2100] = "ACK_SD_FORMAT",
[0x2101] = "ACK_SD_RECPOLICY_SET",
[0x2102] = "ACK_SD_RECPOLICY_GET",
[0x2103] = "ACK_SD_RECORDING_NOW",
[0x2104] = "ACK_SD_INFO_GET",
[0x2105] = "ACK_SD_RECORDFILE_GET",
[0x2106] = "ACK_SD_RECORDSCH_GET",
[0x2107] = "ACK_SD_RECORDSCH_SET",
[0x2108] = "ACK_SD_RETRIVEL",
[0x2109] = "ACK_SD_PICFILE_GET",
[0x210a] = "ACK_SD_PIC_CAPTURE",
[0x210b] = "ACK_SD_REC_DEL",
[0x210c] = "ACK_SD_PIC_DEL",
[0x210d] = "ACK_SD_SPL_DEL",
[0x3000] = "CMD_PEER_LIVEAUDIO_START",
[0x3001] = "CMD_PEER_LIVEAUDIO_STOP",
[0x3002] = "CMD_LOCAL_LIVEAUDIO_START",
[0x3003] = "CMD_LOCAL_LIVEAUDIO_STOP",
[0x3004] = "CMD_PEER_AUDIOPARAM_SET",
[0x3005] = "CMD_PEER_AUDIOPARAM_GET",
[0x3006] = "CMD_PEER_AUDIOFILE_STARTPLAY",
[0x3007] = "CMD_PEER_AUDIOFILE_STOPPLAY",
[0x3008] = "CMD_PEER_AUDIOFILELIST_GET",
[0x300a] = "CMD_PEER_IRCUT_ONOFF",
[0x300b] = "CMD_PEER_LIGHTFILL_ONOFF",
[0x3010] = "CMD_PEER_LIVEVIDEO_START",
[0x3011] = "CMD_PEER_LIVEVIDEO_STOP",
[0x3012] = "CMD_PEER_PLAYBACK_START",
[0x3013] = "CMD_PEER_PLAYBACK_STOP",
[0x3014] = "CMD_PEER_PLAYBACK_SEEK",
[0x3015] = "CMD_PEER_PLAYBACK_SPEED",
[0x3016] = "CMD_PEER_PLAYBACK_PAUSE",
[0x3017] = "CMD_PEER_PLAYBACK_RESUME",
[0x3018] = "CMD_PEER_VIDEOPARAM_SET",
[0x3019] = "CMD_PEER_VIDEOPARAM_GET",
[0x301a] = "CMD_SNAPSHOT_GET",
[0x301b] = "CMD_PEER_PLAYBACK_END",
[0x301c] = "CMD_PEER_PLAYBACK_STEP",
[0x3020] = "CMD_DOORBELL_CALL_OPEN",
[0x3021] = "CMD_DOORBELL_CALL_CLOSE",
[0x3022] = "CMD_DOORBELL_CALL_ACCEPT",
[0x3023] = "CMD_DOORBELL_CALL_REJECT",
[0x3024] = "CMD_LOCAL_LIVEVIDIO_SEND_ON",
[0x3025] = "CMD_LOCAL_LIVEVIDIO_SEND_OFF",
[0x3026] = "CMD_LOCAL_AUDIO_STATUS_SET",
[0x3027] = "CMD_LOCAL_AUDIO_STATUS_GET",
[0x3028] = "CMD_LOCAL_AVREC_START",
[0x3029] = "CMD_LOCAL_AVREC_STOP",
[0x3030] = "CMD_LOCAL_PLAYBACK_START",
[0x3031] = "CMD_LOCAL_PLAYBACK_STOP",
[0x3032] = "CMD_LOCAL_PLAYBACK_SEEK",
[0x3033] = "CMD_LOCAL_PLAYBACK_PAUSE",
[0x3034] = "CMD_LOCAL_PLAYBACK_RESUME",
[0x3035] = "CMD_LOCAL_PLAYBACK_START1",
[0x3036] = "CMD_LOCAL_PLAYBACK_STEP",
[0x3037] = "CMD_LOCAL_PLAYBACK_START2",
[0x3040] = "CMD_LOCAL_MJREC_START",
[0x3041] = "CMD_LOCAL_MJREC_STOP",
[0x3100] = "ACK_PEER_LIVEAUDIO_START",
[0x3101] = "ACK_PEER_LIVEAUDIO_STOP",
[0x3102] = "ACK_LOCAL_LIVEAUDIO_START",
[0x3103] = "ACK_LOCAL_LIVEAUDIO_STOP",
[0x3104] = "ACK_PEER_AUDIOPARAM_SET",
[0x3105] = "ACK_PEER_AUDIOPARAM_GET",
[0x3106] = "ACK_PEER_AUDIOFILE_STARTPLAY",
[0x3107] = "ACK_PEER_AUDIOFILE_STOPPLAY",
[0x3108] = "ACK_PEER_AUDIOFILELIST_GET",
[0x310a] = "ACK_PEER_IRCUT_ONOFF",
[0x310b] = "ACK_PEER_LIGHTFILL_ONOFF",
[0x3110] = "ACK_PEER_LIVEVIDEO_START",
[0x3111] = "ACK_PEER_LIVEVIDEO_STOP",
[0x3112] = "ACK_PEER_PLAYBACK_START",
[0x3113] = "ACK_PEER_PLAYBACK_STOP",
[0x3114] = "ACK_PEER_PLAYBACK_SEEK",
[0x3115] = "ACK_PEER_PLAYBACK_SPEED",
[0x3116] = "ACK_PEER_PLAYBACK_PAUSE",
[0x3117] = "ACK_PEER_PLAYBACK_RESUME",
[0x3118] = "ACK_PEER_VIDEOPARAM_SET",
[0x3119] = "ACK_PEER_VIDEOPARAM_GET",
[0x311a] = "ACK_SNAPSHOT_GET",
[0x311b] = "ACK_PEER_PLAYBACK_END",
[0x311c] = "ACK_PEER_PLAYBACK_STEP",
[0x4000] = "CMD_FILE_CTRL",
[0x4005] = "CMD_FILETRANSFER_FILELIST_GET",
[0x4010] = "CMD_LOCALPATH",
[0x4101] = "ACK_FILE_CREATE",
[0x4102] = "ACK_FILE_RENAME",
[0x4103] = "ACK_FILE_DELETE",
[0x4104] = "ACK_FILE_MOVE",
[0x4105] = "ACK_FILE_LIST",
[0x4110] = "ACK_FILE_DOWNLOAD",
[0x4111] = "ACK_FILE_DOWNLOAD_PAUSE",
[0x4112] = "ACK_FILE_DOWNLOAD_RESUME",
[0x4113] = "ACK_FILE_DOWNLOAD_CANCEL",
[0x4120] = "ACK_FILE_UPLOAD",
[0x4121] = "ACK_FILE_UPLOAD_PAUSE",
[0x4122] = "ACK_FILE_UPLOAD_RESUME",
[0x4123] = "ACK_FILE_UPLOAD_CANCEL",
[0x50ff] = "CMD_PASSTHROUGH_STRING_PUT",
[0x51ff] = "ACK_PASSTHROUGH_STRING_PUT",
[0x55fe] = "CMD_SESSION_CHECK",
[0x6001] = "CB_IEGET_STATUS",
[0x6001] = "CMD_NET_WIFISETTING_SET",
[0x6002] = "CB_IEGET_PARAM",
[0x6002] = "CMD_NET_WIFISETTING_GET",
[0x6003] = "CB_IEGET_CAM_PARAMS",
[0x6003] = "CMD_NET_WIFI_SCAN",
[0x6004] = "CB_IEGET_LOG",
[0x6004] = "CMD_NET_WIREDSETTING_SET",
[0x6005] = "CB_IEGET_MISC",
[0x6005] = "CMD_NET_WIREDSETTING_GET",
[0x6006] = "CB_IEGET_RECORD",
[0x6007] = "CB_IEGET_RECORD_FILE",
[0x6008] = "CB_IEGET_WIFI_SCAN",
[0x6009] = "CB_IEGET_FACTORY",
[0x600a] = "CB_IESET_IR",
[0x600b] = "CB_IESET_UPNP",
[0x600c] = "CB_IESET_ALARM",
[0x600d] = "CB_IESET_LOG",
[0x600e] = "CB_IESET_USER",
[0x600f] = "CB_IESET_ALIAS",
[0x6010] = "CB_IESET_MAIL",
[0x6011] = "CB_IESET_WIFI",
[0x6012] = "CB_CAM_CONTROL",
[0x6013] = "CB_IESET_DATE",
[0x6014] = "CB_IESET_MEDIA",
[0x6015] = "CB_IESET_SNAPSHOT",
[0x6016] = "CB_IESET_DDNS",
[0x6017] = "CB_IESET_MISC",
[0x6018] = "CB_IEGET_FTPTEST",
[0x6019] = "CB_DECODER_CONTROL",
[0x601a] = "CB_IESET_DEFAULT",
[0x601b] = "CB_IESET_MOTO",
[0x601c] = "CB_IEGET_MAILTEST",
[0x601d] = "CB_IESET_MAILTEST",
[0x601e] = "CB_IEDEL_FILE",
[0x601f] = "CB_IELOGIN",
[0x6020] = "CB_IESET_DEVICE",
[0x6021] = "CB_IESET_NETWORK",
[0x6022] = "CB_IESET_FTPTEST",
[0x6023] = "CB_IESET_DNS",
[0x6024] = "CB_IESET_OSD",
[0x6025] = "CB_IESET_FACTORY",
[0x6026] = "CB_IESET_PPPOE",
[0x6027] = "CB_IEREBOOT",
[0x6028] = "CB_IEFORMATSD",
[0x6029] = "CB_IESET_RECORDSCH",
[0x602a] = "CB_IESET_WIFISCAN",
[0x602b] = "CB_IERESTORE",
[0x602c] = "CB_IESET_FTP",
[0x602d] = "CB_IESET_RTSP",
[0x602e] = "CB_IEGET_VIDEOSTREAM",
[0x602f] = "CB_UPGRADE_APP",
[0x6030] = "CB_UPGRADE_SYS",
[0x6031] = "CB_SET_IIC",
[0x6032] = "CB_GET_IIC",
[0x6033] = "CB_IEGET_ALARMLOG",
[0x6034] = "CB_IESET_ALARMLOGCLR",
[0x6035] = "CB_IEGET_SYSWIFI",
[0x6036] = "CB_IESET_SYSWIFI",
[0x6037] = "CB_IEGET_LIVESTREAM",
[0x6040] = "CB_NOTIFICATION",
[0x6053] = "CB_IEGET_BILL",
[0x6054] = "CB_APP_VERSION",
[0x60a0] = "CB_CHECK_USER",
[0x60a1] = "CB_IESET_BILL",
[0x6101] = "ACK_NET_WIFISETTING_SET",
[0x6102] = "ACK_NET_WIFISETTING_GET",
[0x6103] = "ACK_NET_WIFI_SCAN",
[0x6104] = "ACK_NET_WIREDSETTING_SET",
[0x6105] = "ACK_NET_WIREDSETTING_GET",
[0x7000] = "CMD_FRIEND_MSG",
[0x99f0] = "CB_SET_P2PPARAM",
[0x99fe] = "CB_GET_SYSOPR",
[0x99ff] = "CB_SET_SYSOPR",
[0xf000] = "CMD_LOCAL_SESSION_INF",
[0xf001] = "CMD_LOCAL_SESSION_CHECK",
[0xf002] = "CMD_LOCAL_SESSION_GET",
[0xf003] = "CMD_LOCAL_SESSION_CTRL",
[0xf004] = "CMD_LOCAL_REC_START",
[0xf005] = "CMD_LOCAL_REC_STOP",
[0xf006] = "CMD_LOCAL_REC_MERGECTRL",
[0xf007] = "CMD_LOCAL_P2P_START",
[0xf008] = "CMD_LOCAL_P2P_STOP",
[0xf00f] = "CMD_SESSION_CLOSE",
[0xf010] = "CMD_LOCAL_PUSH_STRING",
[0xf011] = "CMD_LOCAL_PUSH_CFG",
[0xf012] = "CMD_LOCAL_RCVVID_DEC",
[0xf021] = "CMD_LOCAL_LAPSED",
[0xff01] = "CB_SET_SINGLE_SETTING_DEFAULT",
[0xff10] = "CB_GET_FILE",
[0xff11] = "CB_PUT_FILE",
[0xff12] = "CB_SET_FILE",
[0xff13] = "CB_GET_FILELIST",
[0xff14] = "CB_SET_GPIO",
[0xff15] = "CB_GET_GPIO",
[0xff16] = "CB_GET_ADC",
}
-- Define a function to dissect the packets
function ilnk_proto.dissector(buffer, pinfo, tree)
@@ -59,11 +374,22 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
local subtree = tree:add(ilnk_proto, buffer(), "iLnkP2P")
-- Add the entire packet as a field
local packetname = lut[buffer(0, 2):uint()]
subtree:add(ilnk_proto.fields.type, packetname)
local packettype = buffer(0, 2):uint()
local packetname = lut[packettype]
packetname = packetname or "UNK " .. string.format("0x%X", packettype)
subtree:add(ilnk_proto.fields.type, buffer(0, 2), packetname)
-- Set the protocol description in the packet list
pinfo.cols.protocol:set("iLnkP2P")
pinfo.cols.info:set(packetname)
if packetname == "PunchPkt" or packetname == "P2pRdy" then
local len = buffer(2, 2)
subtree:add(ilnk_proto.fields.len, len)
local serial_prefix = buffer(4, 4):string()
local serial_no = UInt64(buffer(12, 4):uint(), buffer(8, 4):uint())
local serial_suffix = buffer(16, 5):string()
subtree:add(ilnk_proto.fields.serial, buffer(4, len:uint()-3), serial_prefix..serial_no..serial_suffix)
end
if packetname == "DrwAck" then
subtree:add(ilnk_proto.fields.len, buffer(2, 2))
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
@@ -78,13 +404,23 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
subtree:add(ilnk_proto.fields.len, b_pkt_len)
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
subtree:add(ilnk_proto.fields.m_stream_id, buffer(5, 1))
if pkt_len < 12 then
subtree:add(ilnk_proto.fields.warning, "Short read"):set_generated()
return
end
subtree:add(ilnk_proto.fields.pkt_seq, buffer(6, 2))
local b_payload_len = buffer(0xc, 2)
local payload_len = buffer(0xc, 2):le_uint()
if payload_len > pkt_len then
payload_len = pkt_len - 12
end
if not is_data_packet then
subtree:add_le(ilnk_proto.fields.start, buffer(8, 2))
subtree:add_le(ilnk_proto.fields.cmd, buffer(0xa, 2))
local cmdname = control_lut[buffer(0xa, 2):le_uint()] or "UNK"
pinfo.cols.info:set(cmdname)
subtree:add(ilnk_proto.fields.cmd_name, cmdname):set_generated()
subtree:add_le(ilnk_proto.fields.cmd_payload_len, b_payload_len)
subtree:add_le(ilnk_proto.fields.cmd_dest, buffer(0xe, 2))
-- inline value for short-payload bytes
@@ -127,7 +463,39 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
subtree:add(ilnk_proto.fields.cmd_payload, payload_tvb:range(0, payload_len -4))
end
else
local payload_type
local payload_subtype
local payload_tvb = ByteArray.tvb(buffer(8, packet_length-8):bytes(), "Data Payload")
if payload_tvb:range(0, 4):uint() == 0xffd8ffdb then
payload_subtype = "new frame"
-- start of new frame
end
if payload_tvb:range(0, 4):uint() == 0x55aa15a8 then
payload_type = "audio"
subtree:add(ilnk_proto.fields.audio_header, buffer(8, 32))
subtree:add(ilnk_proto.fields.hdr_type, buffer(12, 1))
subtree:add(ilnk_proto.fields.hdr_streamid, buffer(13, 1))
subtree:add(ilnk_proto.fields.hdr_frameno, buffer(20, 4), buffer(20, 4):le_uint())
subtree:add(ilnk_proto.fields.hdr_len, buffer(24, 4), buffer(24, 4):le_uint())
subtree:add(ilnk_proto.fields.hdr_ver, buffer(28, 1), buffer(28, 1):le_uint())
subtree:add(ilnk_proto.fields.hdr_res, buffer(29, 1), buffer(29, 1):le_uint())
subtree:add(ilnk_proto.fields.payload_len, buffer(24, 4), buffer(24, 4):le_uint())
subtree:add(ilnk_proto.fields.frame_no, buffer(20, 4), buffer(20, 4):le_uint())
if payload_tvb:range(4, 1):uint() == 0x06 then
payload_subtype = "audio data"
elseif payload_tvb:range(4, 1):uint() == 0x03 then
payload_subtype = "maybe audio metadata"
else
payload_subtype = "REALLY not sure audio data"
end
else
payload_type = "jpeg"
payload_subtype = "jpeg continuation"
end
subtree:add(ilnk_proto.fields.payload_type, buffer(8, 4), payload_type)
subtree:add(ilnk_proto.fields.payload_subtype, buffer(12, 1), payload_subtype)
subtree:add(ilnk_proto.fields.data_payload, payload_tvb:range(0, packet_length-8))
end
end
@@ -140,4 +508,15 @@ function ilnk_proto.dissector(buffer, pinfo, tree)
-- subtree:add(ilnk_proto.fields.payload, buffer(2, packet_length-2))
end
udp_table = DissectorTable.get("udp.port"):add(49512, ilnk_proto)
local function heuristic_checker(buffer, pinfo, tree)
length = buffer:len()
if length < 2 then return false end
local packetname = lut[buffer(0, 2):uint()]
if packetname ~= nil then
ilnk_proto.dissector(buffer, pinfo, tree)
return true
end
return false
end
udp_table2 = DissectorTable.get("udp.port"):add(32108, ilnk_proto)
h = ilnk_proto:register_heuristic("udp", heuristic_checker)
+28
View File
@@ -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;
};
+214 -102
View File
@@ -1,14 +1,14 @@
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
import { XqBytesDec } from "./func_replacements.js";
import { hexdump } from "./hexdump.js";
import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
import { create_P2pRdy, DevSerial, SendListWifi, SendUsrChk } from "./impl.js";
import { logger } from "./logger.js";
import { Session } from "./session.js";
import { u16_swap, u32_swap } from "./utils.js";
import { config } from "./settings.js";
export const notImpl = (_: Session, dv: DataView) => {
export const notImpl = (session: Session, dv: DataView) => {
const raw = dv.readU16();
const cmd = CommandsByValue[raw];
console.log(`^^ ${cmd} (${raw.toString(16)}) and it's not implemented yet`);
logger.debug(`^^ ${cmd} (${raw.toString(16)}) and it's not implemented yet`);
};
export const noop = (_: Session, __: DataView) => {};
@@ -25,141 +25,231 @@ export const handle_P2PAlive = (session: Session, _: DataView) => {
};
export const handle_P2PRdy = (session: Session, _: DataView) => {
// TODO - config
const b = SendUsrChk(session, "admin", "admin");
session.send(b);
};
export const makePunchPkt = (dev: DevSerial): DataView => {
const len = dev.prefix.length + dev.suffix.length + 8;
export const makeP2pRdy = (dev: DevSerial): DataView => {
const outbuf = new DataView(new Uint8Array(0x14).buffer); // 8 = serial u64
// The protocol seems to expect 4 bytes -- check the regression test
// `replies properly to PunchPkt with 3-letters-long prefix` for a case with a
// real device
const devPrefixLength = 4;
outbuf.add(0).writeString(dev.prefix);
outbuf.add(4).writeU64(dev.serialU64);
outbuf.add(8 + dev.prefix.length).writeString(dev.suffix);
outbuf.add(8 + devPrefixLength).writeString(dev.suffix);
return create_P2pRdy(outbuf);
};
export const swVerToString = (swver: number): string => {
return (
((swver >> 24) & 255).toString() +
"." +
((swver >> 16) & 255).toString() +
"." +
((swver >> 8) & 255).toString() +
"." +
(swver & 255).toString()
);
};
export type DevStatus = {
charging: boolean;
battery_mV: number;
dbm: number;
swver: string;
};
export const parseDevStatusAck = (dv: DataView): DevStatus => {
const charging = dv.add(0x28).readU32LE() & 1; // 0x14000101 v 0x14000100
const power = dv.add(0x18).readU16LE(); // '3730' or '3765', milliVolts
const dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
const n_swver = dv.add(0x14).readU32LE();
const swver = swVerToString(n_swver);
return {
charging: charging > 0,
battery_mV: power,
dbm,
swver,
};
};
export const createResponseForControlCommand = (session: Session, dv: DataView): DataView[] => {
const start_type = dv.add(8).readU16(); // 0xa11 on control; data starts here on DATA pkt
const cmd_id = dv.add(10).readU16(); // 0x1120
const payload_len = u16_swap(dv.add(0xc).readU16());
let payload_len = dv.add(0xc).readU16LE();
if (dv.byteLength > 20 && payload_len > dv.byteLength) {
logger.warning(`Received a cropped payload: ${payload_len} when packet is ${dv.byteLength}`);
payload_len = dv.byteLength - 20;
}
if (start_type != 0x110a) {
console.error(`Expected start_type to be 0xa11, got 0x${start_type.toString(16)}`);
logger.error(`Expected start_type to be 0xa11, got 0x${start_type.toString(16)}`);
return [];
}
const rotate_chr = 4;
if (payload_len > rotate_chr) {
// 20 = 16 (header) + 4 (??)
XqBytesDec(dv.add(20), payload_len - 4, rotate_chr);
console.log("Decrypted");
console.log(hexdump(dv));
}
if (cmd_id == ControlCommands.ConnectUserAck) {
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
session.ticket = [...c];
session.eventEmitter.emit("login");
return [];
}
// the first 20 bytes are header
if (cmd_id == ControlCommands.DevStatusAck) {
// ParseDevStatus -> offset relevant?
let charging = u32_swap(dv.add(0x28).readU32()) & 1; // 0x14000101 v 0x14000100
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts?
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
switch (cmd_id) {
case ControlCommands.ConnectUserAck:
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
session.ticket = [...c];
session.eventEmitter.emit("login");
return [];
console.log(`charging? ${charging}, batlevel? ${power}, wifi dbm: ${dbm}`);
}
case ControlCommands.DevStatusAck:
// ParseDevStatus -> offset relevant?
const status = parseDevStatusAck(dv);
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
if (cmd_id == ControlCommands.WifiSettingsAck) {
const wifiSettings = {
enable: dv.add(0x14).readU32(),
status: dv.add(0x18).readU32(),
mode: dv.add(0x1c).readU32LE(),
channel: dv.add(0x20).readU32(),
authtype: dv.add(0x24).readU32(),
dhcp: dv.add(0x28).readU32(),
ssid: dv.add(0x2c).readString(0x20),
psk: dv.add(0x4c).readString(0x80),
ip: dv.add(0xcc).readString(0x10),
mask: dv.add(0xdc).readString(0x10),
gw: dv.add(0xec).readString(0x10),
dns1: dv.add(0xfc).readString(0x10),
dns2: dv.add(0x10c).readString(0x10),
};
const buf = SendListWifi(session);
console.log(`Current wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
return [buf];
}
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.ListWifiAck) {
let startat = 0x10;
let msg_len = 91;
console.log("payload len", payload_len);
let msg_count = (payload_len - 9) / msg_len;
let remote_msg_count = dv.add(startat).readU32LE();
console.log("should get messages:", msg_count, "in payload: ", remote_msg_count);
startat += 4;
let items = [];
for (let i = 0; i < msg_count; i++) {
const wifiListItem = {
// startat = msg_len * i + 0x14;
ssid: dv.add(startat).readString(0x40),
mac: dv.add(startat + 0x40).readByteArray(8),
security: dv.add(startat + 0x48).readU32LE(),
dbm0: dv.add(startat + 0x4c).readU32LE(),
dbm1: dv.add(startat + 0x50).readU32LE(),
mode: dv.add(startat + 0x54).readU32LE(),
channel: dv.add(startat + 0x58).readU32LE(),
case ControlCommands.WifiSettingsAck:
const wifiSettings = {
enable: dv.add(0x14).readU32(),
status: dv.add(0x18).readU32(),
mode: dv.add(0x1c).readU32LE(),
channel: dv.add(0x20).readU32(),
authtype: dv.add(0x24).readU32(),
dhcp: dv.add(0x28).readU32(),
ssid: dv.add(0x2c).readString(0x20),
psk: dv.add(0x4c).readString(0x80),
ip: dv.add(0xcc).readString(0x10),
mask: dv.add(0xdc).readString(0x10),
gw: dv.add(0xec).readString(0x10),
dns1: dv.add(0xfc).readString(0x10),
dns2: dv.add(0x10c).readString(0x10),
};
console.log(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
startat += msg_len;
console.log("ended at", startat);
items.push(wifiListItem);
}
const buf = SendListWifi(session);
logger.info(`Current Wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
return [buf];
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;
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 = {
ssid: dv.add(startat).readString(0x40),
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(),
};
startat += msg_len;
items.push(wifiListItem);
}
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 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(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);
if (is_new_image) {
if (session.curImage.length > 0 && !session.frame_is_bad) {
session.eventEmitter.emit("frame");
}
// this only happens on un-framed-cameras, which start the JPEG image
// directly
let is_new_image = m_hdr.startsWith(JPEG_HEADER);
session.frame_was_fixed = false;
session.frame_is_bad = false;
session.curImage = [Buffer.from(data.buffer)];
session.rcvSeqId = pkt_id;
if (is_new_image) {
startNewFrame(data.buffer);
} else {
if (pkt_id <= session.rcvSeqId) {
// retransmit
@@ -169,14 +259,17 @@ const deal_with_data = (session: Session, dv: DataView) => {
let b = Buffer.from(data.buffer);
if (pkt_id > session.rcvSeqId + 1) {
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) {
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 than just missing some frames
if (!config.cameras[session.devName]?.fix_packet_loss) {
return;
}
if (session.curImage.length == 1) return; // header does not have markers
if (session.curImage.length <= 1) return; // header does not have markers
let lastFrameSlice = session.curImage[session.curImage.length - 1];
const lastResetMarker = findAllResetMarkers(lastFrameSlice).pop();
@@ -235,6 +328,17 @@ const makeDrwAck = (dv: DataView): DataView => {
}
return outbuf;
};
export const handle_DrwAck = (session: Session, dv: DataView) => {
const packetlen = dv.add(2).readU16();
const str_type = dv.add(4).readU8();
const str_id = dv.add(5).readU8();
const ack_count = dv.add(6).readU16();
for (let i = 0; i < ack_count; i++) {
const ack_id = dv.add(8 + i * 2).readU16();
session.ackDrw(ack_id);
}
};
export const handle_Drw = (session: Session, dv: DataView) => {
const ack = makeDrwAck(dv);
session.send(ack);
@@ -242,8 +346,16 @@ export const handle_Drw = (session: Session, dv: DataView) => {
const m_stream = dv.add(5).readU8(); // data = 1, control = 0
if (m_stream == 1) {
deal_with_data(session, dv);
} else {
} else if (m_stream == 0) {
const b = createResponseForControlCommand(session, dv);
b.forEach(session.send);
} else {
logger.warning(`Received a Drw packet with stream tag: ${m_stream}, which is not implemented`);
}
};
export const handle_Close = (session: Session, dv: DataView) => {
const ack = makeDrwAck(dv);
session.send(ack);
logger.info("Requested to close connection");
session.close();
};
+93 -29
View File
@@ -1,18 +1,37 @@
import { RemoteInfo } from "dgram";
import { readFileSync } from "node:fs";
import http from "node:http";
import { opt } from "./options.js";
import { logger } from "./logger.js";
import { config } from "./settings.js";
import { discoverDevices } from "./discovery.js";
import { DevSerial } from "./impl.js";
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
import { addExifToJpeg, createExifOrientation } from "./exif.js";
let BOUNDARY = "a very good boundary line";
let responses: Record<string, http.ServerResponse[]> = {};
let audioResponses: Record<string, http.ServerResponse[]> = {};
let sessions: Record<string, Session> = {};
// @ts-expect-error TS2307
import favicon from "./cam.ico.gz";
// @ts-expect-error TS2307
import html_template from "./asd.html";
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
const BOUNDARY = "a very good boundary line";
const responses: Record<string, http.ServerResponse[]> = {};
const audioResponses: Record<string, http.ServerResponse[]> = {};
const sessions: Record<string, Session> = {};
// https://sirv.com/help/articles/rotate-photos-to-be-upright/
const oMap = [1, 8, 3, 6];
const oMapMirror = [2, 7, 4, 5];
const orientations = [1, 2, 3, 4, 5, 6, 7, 8].reduce((acc, cur) => {
return { [cur]: createExifOrientation(cur), ...acc };
}, {});
// Reads the mapping of serial numbers to camera names from the text file.
// Returns the camera name (custom name, if it exists, otherwise its ID).
const cameraName = (id: string): string => config.cameras[id].alias || id;
// The HTTP server.
export const serveHttp = (port: number) => {
const server = http.createServer((req, res) => {
if (req.url.startsWith("/ui/")) {
let devId = req.url.split("/")[2];
@@ -27,8 +46,12 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
res.end("Nothing online");
return;
}
const ui = readFileSync("asd.html").toString();
res.end(ui.replace(/\${id}/g, devId));
const ui = html_template
.toString()
.replace(/\${id}/g, devId)
.replace(/\${name}/g, cameraName(devId))
.replace(/\${audio}/g, config.cameras[devId].audio ? "true" : "false");
res.end(ui);
return;
}
if (req.url.startsWith("/audio/")) {
@@ -46,22 +69,46 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
}
res.setHeader("Content-Type", `text/event-stream`);
audioResponses[devId].push(res);
logger.info(`Audio stream requested for camera ${devId}`);
return;
}
if (req.url.startsWith("/camera/")) {
if (req.url.startsWith("/favicon.ico")) {
res.setHeader("Content-Type", "image/x-icon");
res.setHeader("Content-Encoding", "gzip");
res.end(Buffer.from(favicon));
return;
}
if (req.url.startsWith("/rotate/")) {
let devId = req.url.split("/")[2];
console.log("requested for", devId);
let curPos = config.cameras[devId]?.rotate || 0;
let nextPos = (curPos + 1) % 4;
logger.debug(`Rotating ${devId} to ${nextPos}`);
config.cameras[devId].rotate = nextPos;
res.writeHead(204);
res.end();
return;
} else if (req.url.startsWith("/mirror/")) {
let devId = req.url.split("/")[2];
logger.debug(`Mirroring ${devId}`);
config.cameras[devId].mirror = !config.cameras[devId].mirror;
res.writeHead(204);
res.end();
return;
} else if (req.url.startsWith("/camera/")) {
let devId = req.url.split("/")[2];
logger.info(`Video stream requested for camera ${devId}`);
let s = sessions[devId];
if (s === undefined) {
res.writeHead(400);
res.end("invalid ID");
res.end(`Camera ${devId} not discovered`);
return;
}
if (!s.connected) {
res.writeHead(400);
res.end("Nothing online");
res.end(`Camera ${devId} offline`);
return;
}
@@ -69,35 +116,53 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
responses[devId].push(res);
res.on("close", () => {
responses[devId] = responses[devId].filter((r) => r !== res);
console.log("Conn closed, kicked");
logger.info(`Video stream closed for camera ${devId}`);
});
} else {
res.write(`<html>`);
res.write("<html>");
res.write("<head>");
res.write(`<link rel="shortcut icon" href="/favicon.ico">`);
res.write("<title>All cameras</title>");
res.write("</head>");
res.write("<body>");
res.write("<h1>All cameras</h1><hr/>");
Object.keys(sessions).forEach((id) =>
res.write(`<h2>${id}</h2><a href="/ui/${id}"><img src="/camera/${id}"/></a><hr/>`),
res.write(`<h2>${cameraName(id)}</h2><a href="/ui/${id}"><img src="/camera/${id}"/></a><hr/>`),
);
res.write(`</html>`);
res.write("</body>");
res.write("</html>");
res.end();
}
});
let devEv = discoverDevices(opts.debug, opts.discovery_ip);
let devEv = discoverDevices(config.discovery_ips);
const startSession = (s: Session) => {
startVideoStream(s);
logger.info(`Camera ${s.devName} is now ready to stream`);
};
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
if (dev.devId in sessions) {
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
return;
}
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
responses[dev.devId] = [];
audioResponses[dev.devId] = [];
const s = makeSession(Handlers, dev, rinfo, startVideoStream, opts);
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
const s = makeSession(Handlers, dev, rinfo, startSession, 5000);
sessions[dev.devId] = s;
config.cameras[dev.devId] = { rotate: 0, mirror: false, audio: true, ...(config.cameras[dev.devId] || {}) };
s.eventEmitter.on("frame", () => {
const assembled = Buffer.concat(s.curImage);
// Add an EXIF header to indicate if the image should be rotated or mirrored
let orientation = config.cameras[dev.devId].rotate;
orientation = config.cameras[dev.devId].mirror ? oMapMirror[orientation] : oMap[orientation];
const exifSegment = orientations[orientation];
const jpegHeader = addExifToJpeg(s.curImage[0], exifSegment);
const assembled = Buffer.concat([jpegHeader, ...s.curImage.slice(1)]);
const header = Buffer.from(`\r\n--${BOUNDARY}\r\nContent-Length: ${assembled.length}\r\nContent-Type: image/jpeg\r\n\r\n`);
responses[dev.devId].forEach((res) => {
res.write(header);
res.write(assembled);
@@ -105,10 +170,10 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
});
s.eventEmitter.on("disconnect", () => {
console.log("deleting from sessions");
logger.info(`Camera ${dev.devId} disconnected`);
delete sessions[dev.devId];
});
if (with_audio) {
if (config.cameras[dev.devId].audio) {
s.eventEmitter.on("audio", ({ gap, data }) => {
// ew, maybe WS?
var b64encoded = Buffer.from(data).toString("base64");
@@ -119,9 +184,8 @@ export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
});
});
}
sessions[dev.devId] = s;
});
console.log(`Starting HTTP server on port ${port}`);
logger.info(`Starting HTTP server on port ${port}`);
server.listen(port);
};
+20 -4
View File
@@ -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);
+26
View File
@@ -0,0 +1,26 @@
import { isatty } from "node:tty";
import { addColors, config, createLogger, format, transports as wtransports } from "winston";
const myFormat = format.printf(({ level, message, timestamp }) => {
return `${timestamp} [${level}] ${message}`;
});
const transports = {
console: new wtransports.Console(),
};
export let logger = undefined;
export const buildLogger = (level: string, colorize: boolean | undefined) => {
let use_color = colorize === undefined ? isatty(1) : colorize;
const fmt = use_color
? format.combine(format.colorize(), format.timestamp(), myFormat)
: format.combine(format.timestamp(), myFormat);
logger = createLogger({
levels: { ...config.syslog.levels, trace: 10 },
level,
format: fmt,
transports: [transports.console],
});
addColors({ trace: "white" });
};
+14
View File
@@ -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;
};
-2
View File
@@ -1,6 +1,4 @@
export type opt = {
debug: boolean;
ansi: boolean;
discovery_ip: string;
attempt_to_fix_packet_loss: boolean;
};
+268 -6
View File
@@ -5,14 +5,27 @@
"packages": {
"": {
"dependencies": {
"esbuild": "^0.20.2",
"winston": "^3.13.0",
"yaml": "^2.4.2",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/yargs": "^17.0.32",
"esbuild": "^0.20.2",
"mocha": "^10.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=16.0"
}
},
"node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@cspotcode/source-map-support": {
@@ -27,6 +40,16 @@
"node": ">=12"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@@ -34,6 +57,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
@@ -49,6 +73,7 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
@@ -64,6 +89,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
@@ -79,6 +105,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
@@ -94,6 +121,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@@ -109,6 +137,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@@ -124,6 +153,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@@ -139,6 +169,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@@ -154,6 +185,7 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -169,6 +201,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -184,6 +217,7 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -199,6 +233,7 @@
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -214,6 +249,7 @@
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -229,6 +265,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -244,6 +281,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -259,6 +297,7 @@
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -274,6 +313,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -289,6 +329,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
@@ -304,6 +345,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
@@ -319,6 +361,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
@@ -334,6 +377,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@@ -349,6 +393,7 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
@@ -364,6 +409,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@@ -431,6 +477,11 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="
},
"node_modules/@types/yargs": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
@@ -523,6 +574,11 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/async": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz",
"integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -645,6 +701,15 @@
"node": ">=12"
}
},
"node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -661,6 +726,37 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"dependencies": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -722,10 +818,16 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@@ -779,6 +881,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -816,6 +923,11 @@
"flat": "cli.js"
}
},
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -929,8 +1041,12 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
@@ -991,6 +1107,17 @@
"node": ">=8"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@@ -1015,6 +1142,11 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -1046,6 +1178,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/logform": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz",
"integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==",
"dependencies": {
"@colors/colors": "1.6.0",
"@types/triple-beam": "^1.3.2",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -1136,8 +1284,7 @@
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/nanoid": {
"version": "3.3.3",
@@ -1169,6 +1316,14 @@
"wrappy": "1"
}
},
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -1238,6 +1393,19 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -1262,7 +1430,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [
{
"type": "github",
@@ -1278,6 +1445,14 @@
}
]
},
"node_modules/safe-stable-stringify": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
"integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
"engines": {
"node": ">=10"
}
},
"node_modules/serialize-javascript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
@@ -1287,6 +1462,30 @@
"randombytes": "^2.1.0"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
"engines": {
"node": "*"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -1338,6 +1537,11 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1350,6 +1554,14 @@
"node": ">=8.0"
}
},
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
@@ -1422,12 +1634,51 @@
"dev": true,
"peer": true
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true
},
"node_modules/winston": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz",
"integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==",
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.4.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.7.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz",
"integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==",
"dependencies": {
"logform": "^2.3.2",
"readable-stream": "^3.6.0",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/workerpool": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
@@ -1464,6 +1715,17 @@
"node": ">=10"
}
},
"node_modules/yaml": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+7 -2
View File
@@ -3,16 +3,21 @@
"scripts": {
"test": "mocha tests",
"tsc": "tsc",
"build": "esbuild cmd/bin.ts --bundle --platform=node --outfile=dist/bin.cjs --target=node12"
"build": "esbuild cmd/bin.ts --bundle --platform=node --outfile=dist/bin.cjs --target=node12 --loader:.gz=binary --loader:.html=text"
},
"devDependencies": {
"@types/yargs": "^17.0.32",
"esbuild": "^0.20.2",
"mocha": "^10.2.0",
"ts-node": "^10.9.2",
"esbuild": "^0.20.2",
"typescript": "^5.3.3"
},
"dependencies": {
"winston": "^3.13.0",
"yaml": "^2.4.2",
"yargs": "^17.7.2"
},
"engines": {
"node": ">=16.0"
}
}
+40 -10
View File
@@ -1,33 +1,63 @@
import { RemoteInfo } from "dgram";
import { opt } from "./options.js";
import { config } from "./settings.js";
import { discoverDevices } from "./discovery.js";
import { DevSerial } from "./impl.js";
import { WifiListItem } from "./handlers.js";
import { DevSerial, SendListWifi } from "./impl.js";
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
import { logger } from "./logger.js";
export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; password: string }) => {
console.log(`Will configure any devices found to join ${ssid}`);
export const pair = ({ ssid, password }: { ssid: string; password: string }) => {
logger.info(`Will configure any devices found to join ${ssid}`);
let sessions: Record<string, Session> = {};
let devEv = discoverDevices(opts.debug, opts.discovery_ip);
let devEv = discoverDevices(config.discovery_ips);
if (password == "") {
throw new Error("You must set a non-zero-length password");
}
const onLogin = configureWifi(ssid, password);
const onLogin = (s: Session) => {
logger.info(`Scanning for Wifi networks on ${s.devName} -- this may time out`);
// configureWifi(ssid, password, 0)(s);
s.send(SendListWifi(s));
};
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
if (dev.devId in sessions) {
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
logger.info(`Camera ${dev.devId} at ${rinfo.address} already discovered, ignoring`);
return;
}
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
const s = makeSession(Handlers, dev, rinfo, onLogin, opts);
logger.info(`Discovered camera ${dev.devId} at ${rinfo.address}`);
const s = makeSession(Handlers, dev, rinfo, onLogin, 10000);
let configured = {};
s.eventEmitter.on("disconnect", () => {
console.log("deleting from sessions");
logger.info(`Camera ${dev.devId} disconnected`);
if (configured[dev.devId]) {
logger.info("Press CONTROL+C if you're done setting up your cameras");
}
delete sessions[dev.devId];
delete configured[dev.devId];
});
sessions[dev.devId] = s;
s.eventEmitter.on("ListWifi", (items: WifiListItem[]) => {
const matches = items.filter((i) => i.ssid == ssid);
if (matches.length == 0) {
logger.error(`Camera could not find SSID '${ssid}'`);
return;
}
if (configured[dev.devId]) {
logger.info(`Got two answers from camera, ignoring second`);
return;
}
const match = matches[0];
logger.info(`Configuring camera ${s.devName} on ${JSON.stringify(match)}`);
configureWifi(ssid, password, match.channel)(s);
configured[dev.devId] = true;
logger.info(`WiFi config for camera ${s.devName} is done`);
logger.info(`Camera should reboot now`);
});
});
};
+3
View File
@@ -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
+74 -34
View File
@@ -2,13 +2,23 @@ import { createSocket, RemoteInfo } from "node:dgram";
import EventEmitter from "node:events";
import { Commands, CommandsByValue } from "./datatypes.js";
import { handle_Drw, handle_P2PAlive, handle_P2PRdy, makePunchPkt, noop, notImpl } from "./handlers.js";
import { hexdump } from "./hexdump.js";
import {
handle_Close,
handle_Drw,
handle_DrwAck,
handle_P2PAlive,
handle_P2PRdy,
makeP2pRdy,
notImpl,
noop,
} from "./handlers.js";
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
import { opt } from "./options.js";
import { logger } from "./logger.js";
export type Session = {
send: (msg: DataView) => void;
ackDrw: (id: number) => void;
unackedDrw: { [id: number]: { sent_ts: number; data: DataView } };
outgoingCommandId: number;
ticket: number[];
eventEmitter: EventEmitter;
@@ -21,7 +31,8 @@ export type Session = {
rcvSeqId: number;
frame_is_bad: boolean;
frame_was_fixed: boolean;
options: opt;
started: boolean;
close: () => void;
};
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
@@ -36,13 +47,13 @@ type msgCb = (
const handleIncoming: msgCb = (session, handlers, msg, rinfo) => {
const ab = new Uint8Array(msg).buffer;
const dv = new DataView(ab);
const cmd = CommandsByValue[dv.readU16()];
if (session.options.debug) {
console.log(`<< ${cmd}`);
console.log(hexdump(msg.buffer, { ansi: session.options.ansi, ansiColor: 1 }));
}
const raw = dv.readU16();
const cmd = CommandsByValue[raw];
logger.log("trace", `<< ${cmd}`);
handlers[cmd](session, dv, rinfo);
session.lastReceivedPacket = Date.now();
if (raw != Commands.P2PAlive && raw != Commands.P2PAliveAck) {
session.lastReceivedPacket = Date.now();
}
};
export const makeSession = (
@@ -50,8 +61,9 @@ export const makeSession = (
dev: DevSerial,
ra: RemoteInfo,
onLogin: (s: Session) => void,
options: opt,
timeoutMs: number,
): Session => {
let unackedDrw = {};
const sock = createSocket("udp4");
sock.on("error", (err) => {
@@ -62,68 +74,96 @@ export const makeSession = (
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
sock.on("listening", () => {
const buf = makePunchPkt(dev);
const buf = makeP2pRdy(dev);
session.send(buf);
session.started = true;
});
const SEND_PORT = 32108;
sock.bind();
const sessTimer = setInterval(() => {
const delta = Date.now() - session.lastReceivedPacket;
if (delta > 600) {
let buf = create_P2pAlive();
session.send(buf);
}
if (delta > 8000) {
session.eventEmitter.emit("disconnect");
if (session.started) {
if (delta > 600) {
let buf = create_P2pAlive();
session.send(buf);
}
if (delta > timeoutMs) {
logger.warning(`Camera ${session.devName} timed out`);
session.eventEmitter.emit("disconnect");
}
}
}, 400);
const resendTimer = setInterval(() => {
const now = Date.now();
for (const [key, value] of Object.entries(session.unackedDrw)) {
const { sent_ts, data } = value;
if (now - sent_ts > 100) {
const pkt_id = data.add(6).readU16();
logger.debug(`Resending packet ${pkt_id} as ${session.outgoingCommandId}`);
data.add(6).writeU16(session.outgoingCommandId);
session.outgoingCommandId++;
delete session.unackedDrw[key];
session.send(data);
}
}
}, 500);
const session: Session = {
outgoingCommandId: 0,
ticket: [0, 0, 0, 0],
lastReceivedPacket: 0,
eventEmitter: new EventEmitter(),
connected: true,
timers: [sessTimer],
timers: [sessTimer, resendTimer],
devName: dev.devId,
started: false,
send: (msg: DataView) => {
const raw = msg.readU16();
const cmd = CommandsByValue[raw];
if (options.debug) {
console.log(`>> ${cmd}`);
if (raw != Commands.P2PAlive) {
console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 0 }));
}
// send command
if (raw == 0xf1d0 && msg.add(4).readU8() == 0xd1) {
const packet_id = msg.add(6).readU16();
logger.debug(`Sending Drw Packet with id ${packet_id}`);
unackedDrw[packet_id] = { sent_ts: Date.now(), data: msg };
}
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
logger.log("trace", `>> ${cmd}`);
sock.send(new Uint8Array(msg.buffer), ra.port, session.dst_ip);
},
ackDrw: (id: number) => {
logger.debug(`Removing ${id} from pending`);
delete unackedDrw[id];
},
dst_ip: ra.address,
curImage: [],
rcvSeqId: 0,
frame_is_bad: false,
frame_was_fixed: false,
options: options,
unackedDrw,
close: () => {
session.eventEmitter.emit("disconnect");
},
};
session.eventEmitter.on("disconnect", () => {
console.log(`Disconnected from ${session.devName} - ${session.dst_ip}`);
logger.info(`Disconnected from camera ${session.devName} at ${session.dst_ip}`);
session.dst_ip = "0.0.0.0";
session.connected = false;
session.timers.forEach((x) => clearInterval(x));
session.timers = [];
sock.close();
});
session.eventEmitter.on("login", () => {
console.log(`Logged in - ${session.devName}`);
logger.info(`Logging in to camera ${session.devName}`);
onLogin(session);
});
return session;
};
export const configureWifi = (ssid: string, password: string) => {
export const configureWifi = (ssid: string, password: string, channel: number) => {
return (s: Session) => {
[SendWifiDetails(s, ssid, password, true)].forEach(s.send);
[SendWifiDetails(s, ssid, password, channel, true)].forEach(s.send);
};
};
@@ -138,13 +178,13 @@ export const Handlers: Record<keyof typeof Commands, PacketHandler> = {
PunchPkt: notImpl,
P2PAlive: handle_P2PAlive,
P2pRdy: handle_P2PRdy,
DrwAck: noop,
DrwAck: handle_DrwAck,
Drw: handle_Drw,
Close: handle_Close,
Close: notImpl,
P2PAliveAck: noop,
LanSearchExt: notImpl,
LanSearch: notImpl,
P2PAliveAck: notImpl,
Hello: notImpl,
P2pReq: notImpl,
LstReq: notImpl,
+45
View File
@@ -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 };
+12
View File
@@ -59,6 +59,17 @@ DataView.prototype.writeString = function (str) {
const bytes = [...str].map((_, i) => str.charCodeAt(i));
return this.writeByteArray(bytes);
};
DataView.prototype.startsWith = function (arr) {
if (this.byteLength < arr.length) {
return false;
}
for (let i = 0; i < arr.length; i++) {
if (this.add(i).readU8() != arr[i]) {
return false;
}
}
return true;
};
declare global {
interface DataView {
@@ -77,6 +88,7 @@ declare global {
writeU32(n: number): void;
writeU64(n: bigint): void;
writeU8(n: number): void;
startsWith(arr: number[]): boolean;
}
}
export default global;
+136 -7
View File
@@ -1,11 +1,13 @@
// vim: nowrap
import "../shim.ts";
import assert from "assert";
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
import { createResponseForControlCommand } from "../handlers.js";
import { makeP2pRdy, parseDevStatusAck, parseListWifi } from "../handlers.js";
import { hexdump } from "../hexdump.js";
import { SendStartVideo, SendDevStatus, SendUsrChk, SendWifiDetails } from "../impl.ts";
import { parse_PunchPkt, SendDevStatus, SendStartVideo, SendUsrChk, SendWifiDetails } from "../impl.ts";
import { buildLogger } from "../logger.js";
import { placeholderTypes, sprintf } from "../utils.js";
describe("debug_tools", () => {
@@ -123,9 +125,99 @@ describe("module", () => {
});
const hstrToBA = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16))).buffer;
describe("events", () => {
it("emits login event upon logging in", () => {
// TODO
const BATohstr = (ba) => [...new Uint8Array(ba.buffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
describe("parse packet", () => {
it("parses PunchPkt", () => {
const in_pkt_str = "f14100144241544400000000000262ca574f4e4a4d000000";
const pkt = new DataView(hstrToBA(in_pkt_str));
const expected = {
prefix: "BATD",
serial: "156362",
suffix: "WONJM",
serialU64: BigInt(156362),
devId: "BATD156362WONJM",
};
assert.deepEqual(parse_PunchPkt(pkt), expected);
});
{
const in_pkt_str = "f14100145848410000000000000003e24b4d4d4542000000";
const pkt = new DataView(hstrToBA(in_pkt_str));
it("parses PunchPkt when prefix is 3 letters long", () => {
const expected = {
prefix: "XHA",
serial: "994",
suffix: "KMMEB",
serialU64: BigInt(994),
devId: "XHA994KMMEB",
};
assert.deepEqual(parse_PunchPkt(pkt), expected);
});
// https://github.com/DavidVentura/cam-reverse/issues/17#issuecomment-2094819873
it("replies properly to PunchPkt with 3-letters-long prefix", () => {
const dev = parse_PunchPkt(pkt);
const p2prdy = makeP2pRdy(dev);
let p2pstr = BATohstr(p2prdy);
assert.deepEqual(in_pkt_str.slice(8), p2pstr.slice(8));
});
}
it("parses wifiscan chan0", () => {
buildLogger("warning");
const in_pkt_str =
"f1d00238d1000009110a03612c020100060000002f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101a741a15822a1010101010101b2fefefe650101010101010101010101" +
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101ab41a158605c010101010101b6fefefe650101010101010101010101" +
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101a741a158605c010101010101c3fefefe650101010101010101010101" +
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101af41a15822a1010101010101c8fefefe650101010101010101010101" +
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101933aac0b5330010101010101b4fefefe650101010101010101010101" +
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101973aac0b5330010101010101b4fefefe650101010101010101010101" +
"40425342"; // incomplete entry
let pkt = new DataView(hstrToBA(in_pkt_str));
let payload_len = pkt.add(0xc).readU16LE();
XqBytesDec(pkt.add(20), payload_len - 4, 4); // this mutates pkt
const expected = [
{ channel: 0, dbm0: 4294967219, dbm1: 100, mac: "a6:40:a0:59:23:a0", mode: 0, security: 0, ssid: "ACRC.NET" },
{ channel: 0, dbm0: 4294967223, dbm1: 100, mac: "aa:40:a0:59:61:5d", mode: 0, security: 0, ssid: "ACRC.Guest" },
{ channel: 0, dbm0: 4294967234, dbm1: 100, mac: "a6:40:a0:59:61:5d", mode: 0, security: 0, ssid: "ACRC.NET" },
{ channel: 0, dbm0: 4294967241, dbm1: 100, mac: "ae:40:a0:59:23:a0", mode: 0, security: 0, ssid: "ACRC.Guest" },
{ channel: 0, dbm0: 4294967221, dbm1: 100, mac: "92:3b:ad:0a:52:31", mode: 0, security: 0, ssid: "ACRC.NET" },
{ channel: 0, dbm0: 4294967221, dbm1: 100, mac: "96:3b:ad:0a:52:31", mode: 0, security: 0, ssid: "ACRC.Guest" },
];
assert.deepEqual(parseListWifi(pkt), expected);
});
it("parses wifiscan chan2", () => {
buildLogger("warning");
const in_pkt_str =
"f1d00404d100000d110a0361f80300000b0000002f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101603735316031340101010101c701010165010101010101010301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101606035316031340101010101c7010101650101010101010103010101" + // frame
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101603735316031340101010101c2010101650101010101010103010101" +
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101676460373736320101010101b1010101650101010101010103010101" +
"404253422f4f44550101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101383332636065310101010101af010101650101010101010103010101" +
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101393764606465350101010101af010101650101010101010103010101" +
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101326460603964670101010101ac010101650101010101010103010101" +
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101606435316031340101010101c7010101650101010101010103010101" +
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101606035316031340101010101c2010101650101010101010103010101" +
"01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101643562323360620101010101b3010101650101010101010103010101" +
"404253422f4674647275010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101383732636065310101010101af010101650101010101010103010101" +
"40425342"; // incomplete
let pkt = new DataView(hstrToBA(in_pkt_str));
let payload_len = pkt.add(0xc).readU16LE();
XqBytesDec(pkt.add(20), payload_len - 4, 4); // this mutates pkt
const expected = [
{ ssid: "ACRC.NET", mac: "61:36:34:30:61:30", security: 0, dbm0: 198, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "", mac: "61:61:34:30:61:30", security: 0, dbm0: 198, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "ACRC.NET", mac: "61:36:34:30:61:30", security: 0, dbm0: 195, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "", mac: "66:65:61:36:36:37", security: 0, dbm0: 176, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "ACRC.NET", mac: "39:32:33:62:61:64", security: 0, dbm0: 174, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "", mac: "38:36:65:61:65:64", security: 0, dbm0: 174, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "", mac: "33:65:61:61:38:65", security: 0, dbm0: 173, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "ACRC.Guest", mac: "61:65:34:30:61:30", security: 0, dbm0: 198, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "ACRC.Guest", mac: "61:61:34:30:61:30", security: 0, dbm0: 195, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "", mac: "65:34:63:33:32:61", security: 0, dbm0: 178, dbm1: 100, mode: 0, channel: 2 },
{ ssid: "ACRC.Guest", mac: "39:36:33:62:61:64", security: 0, dbm0: 174, dbm1: 100, mode: 0, channel: 2 },
];
assert.deepEqual(parseListWifi(pkt), expected);
});
});
describe("make packet", () => {
@@ -155,10 +247,47 @@ describe("make packet", () => {
it("builds a good WifiSettingsSet", () => {
const sess = { outgoingCommandId: 2, ticket: [1, 2, 3, 4] };
const _expected_str =
"f1d00118d1000002110a01600c010000010203040101010101010101010101010101010100010101726a786f647501010101010101010101010101010101010101010101010101017274716473627360710101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101312f3334342f3334342f333434010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f3101010101010101010101010101";
"f1d00118d1000002110a01600c01000001020304" + // drw header + ticket
"0101010101010101010101010101010100010101" + // all zeroes, but DHCP u32
// set to 1
"726a786f64750101010101010101010101010101010101010101010101010101" + // ssid
"7274716473627360710101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" + // pass
"312f312f312f31010101010101010101" + // 0.0.0.0
"312f3334342f3334342f333434010101" + // 0.255.255.255
"312f312f312f31010101010101010101" + // 0.0.0.0
"312f312f312f31010101010101010101" + // 0.0.0.0
"312f312f312f31010101010101010101" + // 0.0.0.0
"01010101"; // 0000
const expected = hstrToBA(_expected_str);
const got = SendWifiDetails(sess, "skynet", "supercrap", true);
const got = SendWifiDetails(sess, "skynet", "supercrap", 0, true);
assert.deepEqual(got.buffer, expected);
});
it("builds a good WifiSettingsSet - with channel", () => {
const sess = { outgoingCommandId: 0xc, ticket: [0x30, 0x35, 0x74, 0x72] };
const _expected_str =
"f1d00118d100000c110a01600c010000303574720101010101010101030101010101010100010101404253422f4f445501010101010101010101010101010101010101010101010167606a6471607272010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101312f312f312f31010101010101010101312f3334342f3334342f333434010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f3101010101010101010101010101";
const expected = hstrToBA(_expected_str);
const got = SendWifiDetails(sess, "ACRC.NET", "fakepass", 2, true);
assert.deepEqual(got.buffer, expected);
});
it("parses devstatusack", () => {
const pkt = new DataView(
hstrToBA(
"f1d0008cd1000009110a08118000000000000000190e010101010101fefefefebefefefe01000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100010101010001030101010101010101010101010101fefefefefe010101010101010134030300",
),
);
let payload_len = pkt.add(0xc).readU16LE();
XqBytesDec(pkt.add(20), payload_len - 4, 0); // this mutates pkt
const got = parseDevStatusAck(pkt);
const expected = {
battery_mV: 0,
charging: false,
dbm: -256,
swver: "0.0.15.24",
};
assert.deepEqual(expected, got);
});
});
+37
View File
@@ -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
View File
@@ -7,7 +7,7 @@
"moduleResolution": "node",
"esModuleInterop": true,
"preserveConstEnums": true,
"outDir": "./build/",
"noEmit": true,
"rootDir": "./",
"strict": false,
"sourceMap": false
+19
View File
@@ -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];
};