diff --git a/README.md b/README.md index 6cd9176..4bb947f 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,39 @@ # aiopppp -**aiopppp** is an asynchronous Python library designed to simplify connecting to and interacting with cameras that -utilize the Peer-to-Peer Protocol (PPPP) which is implemented in some cheap cameras (A9, X5, etc.) +**aiopppp** is an asynchronous Python library designed to simplify connecting to and interacting with cameras that +utilize the Peer-to-Peer Protocol (PPPP) which is implemented in some cheap cameras (A9, X5, etc.) This library enables seamless communication with compatible cameras for live video streaming, -capturing snapshots, or configuring camera settings, all using asyncio for efficient performance. +audio, capturing snapshots, or configuring camera settings, all using asyncio for efficient performance. ## Features -- Initial camera discovery (plain and encoded (not all keys)) +- Camera discovery: both `LanSearch` and `LanSearchExt` probes, plain and + encoded (not all keys) β€” some firmwares only answer the extended variant - Asynchronous peer-to-peer connections with PPPP-enabled cameras using both JSON and binary control protocols -- Stream live video feeds directly from the camera. -- Remote camera rotation -- (TBD) Capture snapshots and save them locally. -- (TBD) Configure and manage camera settings. -- Lightweight and easy to integrate into Python applications. +- Live MJPEG video streaming, including cameras that mux audio into the video channel (FTYC) +- **Two-way audio** on binary cameras: G.711 (A-law/Β΅-law) listening and talk-back to the camera speaker +- PTZ movement (up/down/left/right, step and continuous) +- White light / IR light control, image flip/mirror (camera-dependent) +- Video parameters: resolution, bitrate, etc. β€” set and read-back +- Snapshots (via `CMD_SNAPSHOT_GET` where supported, otherwise from the live video frame buffer) +- System commands: reboot, device status (battery, power source, Wi-Fi signal), date/time sync + (both known firmware layouts), device info, Wi-Fi settings read-out +- Automatic reconnection with backoff (`Device` high-level API) +- SD card listing and playback control *(implemented, untested on hardware)* +- Test web server with a per-camera control page, plus a protocol simulator and a + transparent DID-rewriting proxy for debugging +- Lightweight: Python 3.7+, depends only on `aiohttp` ## Tested Devices -| Prefix | Protocol | Video | [Audio*](https://github.com/devbis/aiopppp/issues/6) | PTZ | White Light | IR Light | Reboot | Resolution | -|:---------|:---------|:-----:|:---------------------------------------------------------------:|:---:|:-----------:|:--------:|:------:|:----------:| -| **DGOK** | πŸ“œ JSON | βœ… | βœ–οΈ | βœ… | βœ… | βœ… | βœ… | βœ–οΈ | -| **PTZA** | πŸ”’ Binary| βœ… | βœ–οΈ | βœ… | βœ… | 🚫 | βœ… | βœ… | -| **FTYC** | πŸ”’ Binary| [❌*](https://github.com/devbis/aiopppp/issues/8)| βœ–οΈ | 🚫 | 🚫 | βœ… | βœ… | βœ… | -| [**BATE***](https://github.com/devbis/pppp_camera/issues/4) | πŸ”’ Binary|❔ |βœ–οΈ | ❔ | ❔ | ❔ | ❔ | ❔ | -| [**DGB***](https://github.com/devbis/pppp_camera/issues/2) | πŸ“œ JSON |⚠️ |βœ–οΈ | ❔ | ❔ | ❔ | ❔ | ❔ | -| [**ACCQ***](https://github.com/devbis/pppp_camera/issues/1) | ❔ Unknown|βœ–οΈ|βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | +| Prefix | Protocol | Video | Audio (listen) | Talk | PTZ | White Light | IR Light | Reboot | Resolution | Flip/Mirror | Time sync | +|:---------|:---------|:-----:|:--------------:|:----:|:---:|:-----------:|:--------:|:------:|:----------:|:-----------:|:---------:| +| **DGOK** | πŸ“œ JSON | βœ… | βœ–οΈ | βœ–οΈ | βœ… | βœ… | βœ… | βœ… | βœ–οΈ | ❔ | βœ–οΈ | +| **PTZA** | πŸ”’ Binary| βœ… | βœ… | βœ… | βœ… | βœ… | 🚫 | βœ… | βœ… | 🚫 | βœ… | +| **FTYC** | πŸ”’ Binary| βœ… | βœ… | 🚫 | 🚫 | 🚫 | βœ… | βœ… | βœ… | βœ… | ⚠️ | +| [**BATE***](https://github.com/devbis/pppp_camera/issues/4) | πŸ”’ Binary|❔ |❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | +| [**DGB***](https://github.com/devbis/pppp_camera/issues/2) | πŸ“œ JSON |⚠️ |βœ–οΈ | βœ–οΈ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | βœ–οΈ | +| [**ACCQ***](https://github.com/devbis/pppp_camera/issues/1) | ❔ Unknown|βœ–οΈ|βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | βœ–οΈ | **Legend:** -  βœ…  **Working**: Feature is fully functional. @@ -34,6 +43,83 @@ capturing snapshots, or configuring camera settings, all using asyncio for effic -  πŸš«  **Not supported**: Feature is not supported by the device. -  ❔   **Not tested**: Feature has not been tested on the device. +Notes: FTYC has no speaker, hence no talk-back. Time sync sets the clock on +both binary firmwares (verified within a couple of seconds of the host), but +FTYC has no timezone field to write, so its offset stays whatever the vendor +app configured β€” hence ⚠️ rather than βœ…. JSON cameras expose no set-time +command at all. Device alias is not supported +by the tested firmwares (the vendor app doesn't implement it either). +Flip/mirror (`rotate` video param) works on FTYC, is ACKed but ignored by +PTZA. PTZ presets are implemented with the PREFAB scheme found in YsxLite +(`ptz_set_preset` / `ptz_goto_preset` / `ptz_delete_preset` / +`ptz_query_presets`) but appear **not supported by the tested cameras** β€” the +camera does not act on them. Left in as best-effort in case other firmwares +honor it. + +## Hardware-confirmed protocol notes + +These were established against real PTZA/FTYC cameras and the decompiled +vendor apps, and are encoded in the library: + +- **FTYC muxes audio into the video DRW channel.** Both share the + `55 aa 15 a8` stream header; byte 4 is the stream type (`0x03` = JPEG frame + header packet, `0x06` = audio). Video frames arrive as + `[audio pkt][32-byte frame-header pkt][raw JPEG chunks…]`. The library + demuxes automatically; FTYC audio flows whenever video streams. +- **Talk-back audio must be framed** with the same 32-byte stream header + (type `0x06`, payload length at offset 16) β€” bare G.711 is ignored. +- **`VIDEOPARAM_GET` returns a table** of all params 1..12 (u32 each) + regardless of the requested id; the value is at `table[param_id - 1]`. +- **Two DATETIME layouts** exist: PTZA stores `(UTC epoch, tz seconds west of + UTC, ntp[64])`; FTYC has no tz field and stores a timestamp that renders as + local time (it adds its own configured offset to the UTC epoch you set). + `parse_datetime_block` auto-detects the layout; `set_datetime()` works on + both (send UTC, preserve the NTP server). +- **Status block semantics** (per the vendor SDK parser): `batLevel` is the + battery voltage in mV, and bit 0 of `powerSupply` is `externalPower`. +- **The field the SDK calls `sysUptime` is actually the Wi-Fi RSSI**, exposed + as `dbm`. The name is the only thing about it that suggests a duration: in + both vendor apps every read of it goes straight to `setWifidbm()` and is + rendered by `wlanSigGet()`, whose buckets are RSSI ranges + (βˆ’100/βˆ’85/βˆ’70/βˆ’55). There is no uptime display anywhere in either app. + Values outside a plausible βˆ’127β€¦βˆ’1 are reported as `None`. No `uptime` key + is published. +- **`batPercent` is derived from a single-cell LiPo discharge curve**, not + from the vendor thresholds. Those thresholds only choose one of five + battery *icons* (β‰₯4350/4200/4100/3950/3900 mV); read as percentages they + pin a fully-charged camera resting at 4195 mV to "60%" indefinitely. + `None` is returned when the field isn't a battery reading at all β€” + mains-only cameras park it at 8000. +- **`CMD_SNAPSHOT_GET` is not answered** by any tested camera; use the video + frame buffer for stills (the test web server does this automatically). + +## Known issues + +- **`start_video()` always requests HD, ignoring any resolution set + beforehand.** `_request_video(1)` sends a hardcoded HD parameter and then + re-asserts it ~5 s later, because the cameras self-downgrade and ignore the + value set at stream start. The re-assert is what makes a resolution chosen + *while streaming* stick β€” but it also means a resolution set while idle is + discarded, and the stall-recovery path (`_request_video(1)` again after + `VIDEO_REREQUEST_SEC` without frames) can revert a running stream to HD. + + The fix is a per-session preferred resolution that `set_resolution()` + records and `_request_video()` prefers over the constant, falling back to + today's behaviour when unset. Not implemented yet. + +## Untested / experimental + +- **SD card & playback** (`get_sd_info`, `list_recordings`, `playback_*`): + implemented from the decompiled apps, not yet verified on hardware. +- **Wi-Fi scan / device users** (`scan_wifi`, `get_users`): return empty data + on already-configured cameras; probably only answered in AP/setup mode. +- **`set_wifi` β€” do not use**: the write layout very likely doesn't match the + (confirmed) 264-byte read layout and could mis-provision the camera. +- **CGI command vocabulary** (`send_cgi_command`): experimental hook for + firmwares speaking the CB_* command set; untested. +- **JSON-protocol cameras** (DGOK): functional but none of the recent fixes + were exercised against one. + ## Installation To install the library, run: @@ -54,14 +140,17 @@ pip install aiopppp ### Prerequisites -The camera must be connected to WiFi using its mobile app. On the first start the camera creates WiFi access -point with the name like `DGXX-XXXX` or a different name. And it should be used for configuring WiFi settings. +The camera must be connected to WiFi using its mobile app. On the first start the camera creates WiFi access +point with the name like `DGXX-XXXX` or a different name. And it should be used for configuring WiFi settings. After it is connected to you network you can use its IP address to connect to it. -The camera should use UDP port 32108 for discovery. -There are cameras with the same form-factor with open port 20190 which is not supported. +The camera should use UDP port 32108 for discovery. +There are cameras with the same form-factor with open port 20190 which is not supported. It uses either a different protocol or a different encryption. +Only one client can talk to a camera at a time β€” close the vendor app before +connecting. + ### Usage Here’s an example of how to use the library: @@ -79,15 +168,15 @@ async def main(): await asyncio.sleep(10) await device.stop_video() print("Disconnected from the device") - - # or - + + # or + device = Device("192.168.1.2") await device.connect() print("Device info:", device.properties) await device.close() - - + + asyncio.run(main()) ``` @@ -108,16 +197,16 @@ async def main(): await asyncio.wait([session.device_is_ready.wait(), session.main_task], return_when=asyncio.FIRST_COMPLETED) if session.main_task.done(): await session.main_task - return + return print("Connected to the device") print("Device info:", session.dev_properties) session.stop() with suppress(asyncio.CancelledError): await session.main_task print("Disconnected from the device") - - - + + + asyncio.run(main()) ``` @@ -138,28 +227,59 @@ async def main(): discovery = Discovery(remote_addr='255.255.255.255') await discovery.discover(on_device_found) - + asyncio.run(main()) ``` -## Running test web server +## Running the test web server -To test the library, you can run a simple web server that streams the camera feed. -The server will automatically discover the camera and start streaming the video feed. +The bundled web server discovers cameras and gives each one a full control page. ```bash -python -m aiopppp -u admin -p 6666 +python -m aiopppp -u admin -p admin # binary cameras (PTZA/FTYC default creds) +python -m aiopppp -u admin -p 6666 # JSON cameras +python -m aiopppp -a 192.168.1.255 # directed broadcast for your camera LAN ``` -Then, visit `http://localhost:4000` in your browser to view the camera feed. +Visit `http://localhost:4000` β€” the index lists discovered cameras; each links +to `/camera/{dev_id}` with: + +- live MJPEG stream, start/stop, snapshot button +- PTZ arrows (+ preset buttons β€” unsupported by tested firmwares) +- white light / IR buttons +- video parameters with read-back (current values pre-select the dropdowns; + raw payload in the tooltip) +- audio: low-latency listen (Web Audio, ~0.1–0.5 s behind live), a buffered + `