tirtc-device-builder 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/skills/tirtc-esp32-builder/SKILL.md +9 -0
- package/skills/tirtc-esp32-builder/VERSION +1 -1
- package/skills/tirtc-esp32-builder/assets/lckfb-szpi-esp32s3-portable-prompt.md +4 -4
- package/skills/tirtc-esp32-builder/knowledge/board-registry.json +37 -0
- package/skills/tirtc-esp32-builder/knowledge/waveshare-esp32-p4-wifi6-touch-lcd-3.5/knowledge.md +146 -0
- package/skills/tirtc-esp32-builder/references/board-knowledge.md +7 -0
- package/skills/tirtc-esp32-builder/references/runtime-resource-review.md +67 -0
- package/skills/tirtc-esp32-builder/references/tirtc-platform.md +4 -0
- package/skills/tirtc-esp32-builder/references/video-orientation.md +117 -0
- package/skills/tirtc-esp32-builder/scripts/board_registry.py +19 -3
- package/skills/tirtc-esp32-builder/scripts/capture_uplink.py +220 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
This project follows Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.9.5
|
|
6
|
+
|
|
7
|
+
- Add focused video-orientation and WeChat UI-profile guidance distinguishing
|
|
8
|
+
encoded pixels, display size, rotation conventions and endpoint verification.
|
|
9
|
+
- Package the bounded XiaoTai serial capture/export helper with offline tests
|
|
10
|
+
for ANSI queries, evidence integrity and retained/empty capture recovery.
|
|
11
|
+
- Pin Waveshare product knowledge to a95f368 and document the watchpoint-proven
|
|
12
|
+
LVGL callback-count overwrite; preserve pending HIL and non-default UI angles.
|
|
13
|
+
|
|
14
|
+
## 0.9.4
|
|
15
|
+
|
|
16
|
+
- Add a knowledge-only Waveshare ESP32-P4-WIFI6-Touch-LCD-3.5 package with
|
|
17
|
+
pinned source maps, media/resource lessons and explicit hardware/HIL limits.
|
|
18
|
+
- Expose validated knowledge references for model candidates without granting
|
|
19
|
+
adapter reuse; allow unknown PCB revisions only in knowledge-only packages.
|
|
20
|
+
- Add focused media/resource diagnosis guidance covering directional stream
|
|
21
|
+
contracts, WithCaps task cleanup, reserved codec ownership and UI cadence.
|
|
22
|
+
- Test candidate discovery, unknown revisions and reference path boundaries.
|
|
23
|
+
|
|
5
24
|
## 0.9.3
|
|
6
25
|
|
|
7
26
|
- Pin ESP32 Device Kit 1.1.4 and use wildcard DNS plus HTTP probe redirects for
|
package/README.md
CHANGED
|
@@ -961,8 +961,8 @@ metadata 中的版本、标签、上游 commit 和期望 SHA-256 必须与本地
|
|
|
961
961
|
|
|
962
962
|
```bash
|
|
963
963
|
npm test
|
|
964
|
-
git tag -a v0.9.
|
|
965
|
-
git push origin v0.9.
|
|
964
|
+
git tag -a v0.9.5 -m "v0.9.5"
|
|
965
|
+
git push origin v0.9.5
|
|
966
966
|
```
|
|
967
967
|
|
|
968
968
|
不要重复发布已经存在的 npm 版本。版本变化同步更新 `package.json`、`.codex-plugin/plugin.json` 和发布说明。
|
package/package.json
CHANGED
|
@@ -43,6 +43,15 @@ A probable match supplies hypotheses; a component match supplies only component
|
|
|
43
43
|
lessons. Any identity conflict creates a new variant and keeps concrete GPIO,
|
|
44
44
|
clock, DMA and task values unresolved.
|
|
45
45
|
|
|
46
|
+
Read a match's `knowledge_refs` before scanning reference repositories. These
|
|
47
|
+
are source maps and evidence, not permission to install a candidate adapter.
|
|
48
|
+
For an existing project's media, memory or scheduling bug, read
|
|
49
|
+
[runtime-resource-review.md](references/runtime-resource-review.md); preserve
|
|
50
|
+
the user's business baseline separately from the hardware reference.
|
|
51
|
+
For video orientation, unexpected cropping, or WeChat display-profile changes,
|
|
52
|
+
read [video-orientation.md](references/video-orientation.md) before changing
|
|
53
|
+
pixel transforms or UI angles.
|
|
54
|
+
|
|
46
55
|
## 3. Prove the business flows before board porting
|
|
47
56
|
|
|
48
57
|
When device-call or WeChat VoIP is requested, first build and run the pinned
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.5
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
export TIRTC_AGENT_CLIENT=codex
|
|
11
|
-
npx --yes tirtc-device-builder@0.9.
|
|
11
|
+
npx --yes tirtc-device-builder@0.9.5 setup esp32 --install \
|
|
12
12
|
--client "$TIRTC_AGENT_CLIENT" --force-skill
|
|
13
13
|
```
|
|
14
14
|
|
|
@@ -17,11 +17,11 @@ npx --yes tirtc-device-builder@0.9.3 setup esp32 --install \
|
|
|
17
17
|
本轮第一步先只读运行:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npx --yes tirtc-device-builder@0.9.
|
|
21
|
-
npx --yes tirtc-device-builder@0.9.
|
|
20
|
+
npx --yes tirtc-device-builder@0.9.5 --version
|
|
21
|
+
npx --yes tirtc-device-builder@0.9.5 setup esp32 --client "$TIRTC_AGENT_CLIENT"
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
必须根据命令的实际输出和本机文件确认:npm 包为 0.9.
|
|
24
|
+
必须根据命令的实际输出和本机文件确认:npm 包为 0.9.5、已安装 Skill 的 `VERSION` 为 0.9.5、所选 Device Kit 的 `manifest.json` 中 `kit_version` 为 1.1.4,并且 Doctor 对 `--expected-kit 1.1.4` 输出 `OVERALL: PASS`。Plugin manifest 不属于这种 npm 安装方式的运行时前置条件,不得把不可访问的 Plugin 版本当作阻塞项。如果版本不一致、Skill 是在当前会话启动后才安装,或环境检查未通过,停止并报告前置条件不成立;不要在当前会话中替换 Skill 后继续生成工程。
|
|
25
25
|
|
|
26
26
|
工作区与 clean-room 边界:
|
|
27
27
|
- 将启动 Agent 客户端时的当前目录定义为 `WORKSPACE_ROOT`。
|
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
3
|
"boards": [
|
|
4
|
+
{
|
|
5
|
+
"package_id": "waveshare_esp32p4_wifi6_touch_lcd_35_unknown_rev",
|
|
6
|
+
"status": "knowledge_only",
|
|
7
|
+
"identity": {
|
|
8
|
+
"vendor": "Waveshare",
|
|
9
|
+
"model": "ESP32-P4-WIFI6-Touch-LCD-3.5",
|
|
10
|
+
"aliases": ["微雪ESP32-P4-WIFI6-Touch-LCD-3.5", "Waveshare ESP32-P4-WIFI6-Touch-LCD-3.5"],
|
|
11
|
+
"hardware_revision": null,
|
|
12
|
+
"target": "esp32p4",
|
|
13
|
+
"module": null,
|
|
14
|
+
"flash_mb": 16,
|
|
15
|
+
"psram_mb": 32,
|
|
16
|
+
"components": [
|
|
17
|
+
{"kind": "camera", "model": "OV5647"},
|
|
18
|
+
{"kind": "audio_codec", "model": "ES8311"},
|
|
19
|
+
{"kind": "display", "model": "ST7796"},
|
|
20
|
+
{"kind": "touch", "model": "FT6336"},
|
|
21
|
+
{"kind": "network", "model": "ESP32-C6"}
|
|
22
|
+
],
|
|
23
|
+
"probes": []
|
|
24
|
+
},
|
|
25
|
+
"compatibility": {"idf": "5.5.4", "tirtc_sdk": "2.3.0"},
|
|
26
|
+
"artifacts": {},
|
|
27
|
+
"knowledge_refs": ["waveshare-esp32-p4-wifi6-touch-lcd-3.5/knowledge.md"],
|
|
28
|
+
"lessons": [
|
|
29
|
+
{
|
|
30
|
+
"id": "waveshare_p4_media_receive_identity",
|
|
31
|
+
"scope": "board",
|
|
32
|
+
"subject": "VoIP receive admission",
|
|
33
|
+
"guidance": "Observed WeChat JPEG stream1 differs from subscribe/uplink stream11. Check active connection, generation and codec; reproduce with actual ingress before editing filters. See knowledge_refs for evidence and limitations.",
|
|
34
|
+
"action": "warn",
|
|
35
|
+
"verification": "corroborated",
|
|
36
|
+
"applies_to": {"package_id": "waveshare_esp32p4_wifi6_touch_lcd_35_unknown_rev"},
|
|
37
|
+
"evidence": [{"board_package_id": "waveshare_esp32p4_wifi6_touch_lcd_35_unknown_rev", "source_ref": "waveshare-esp32-p4-wifi6-touch-lcd-3.5/knowledge.md#media-and-failures-worth-remembering"}]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
4
41
|
{"package_id":"lckfb_szpi_esp32s3_v1_0_1","status":"knowledge_only","identity":{"vendor":"深圳嘉立创科技集团股份有限公司(立创开发板)","model":"立创·实战派 ESP32-S3 开发板","aliases":["LCKFB SZPI ESP32-S3"],"hardware_revision":"V1.0.1","target":"esp32s3","module":"ESP32-S3 module","flash_mb":16,"psram_mb":8,"components":[{"kind":"camera","model":"GC2145"},{"kind":"aec_reference","model":"ES7210 MIC3 hardware reference"}],"probes":[]},"compatibility":{"idf":"5.5.4","tirtc_sdk":"2.3.0"},"artifacts":{},"lessons":[]},
|
|
5
42
|
{"package_id":"alientek_atk_dnesp32s3_v1","status":"knowledge_only","identity":{"vendor":"正点原子 (ALIENTEK)","model":"ATK-DNESP32S3","aliases":[],"hardware_revision":"V1.0","target":"esp32s3","module":"ATK-MWS3S","flash_mb":16,"psram_mb":8,"components":[{"kind":"camera","model":"OV5640 PID 0x5640"},{"kind":"audio_codec","model":"ES8388"}],"probes":[]},"compatibility":{"idf":"5.5.4","tirtc_sdk":"2.3.0"},"artifacts":{},"lessons":[]}
|
|
6
43
|
]
|
package/skills/tirtc-esp32-builder/knowledge/waveshare-esp32-p4-wifi6-touch-lcd-3.5/knowledge.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Waveshare ESP32-P4-WIFI6-Touch-LCD-3.5
|
|
2
|
+
|
|
3
|
+
2026-09-07. Knowledge only; PCB and silicon revisions unresolved. This package
|
|
4
|
+
contains source-correlated observations and targeted regressions, not an
|
|
5
|
+
installable adapter or complete Hardware IR. A matching sales name does not
|
|
6
|
+
authorize pin reuse. No credentials, raw call logs or user media are retained.
|
|
7
|
+
|
|
8
|
+
## Read only the needed source
|
|
9
|
+
|
|
10
|
+
| Baseline | Pinned source | Locate next |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| Product business | xiaotai `a95f368a037eb3ebe88d8564e1cb2f85e369f525` | `lckfb-szpi-esp32s3-tirtc/components/starter_runtime/src/starter_runtime.c`, shared `starter_product`, `starter_tirtc` |
|
|
13
|
+
| Hardware/media | tirtc-device-example `cad4cbe58c3ff451906322930ceede19e6bf6a07`, app 1.5.3 | `complete-applications/esp32-p4/device-monitor/main/` |
|
|
14
|
+
| Vendor | waveshareteam/ESP32-P4-WIFI6-Touch-LCD-3.5 `870588e62daaf723f3c52f0060d8dc053665ed30` | `examples/esp-idf/06_I2SCodec`, `docs/revisions.md`, schematic |
|
|
15
|
+
| Cross-check | xiaozhi-esp32 `1ce658bcb9ce9aac69d3f87af9894a715b7175f6` | `main/boards/waveshare/esp32-p4-wifi6-touch-lcd-3.5/` |
|
|
16
|
+
|
|
17
|
+
Source paths are repository-relative, not prerequisites for a particular home
|
|
18
|
+
directory. Product HEAD alone does not identify dirty source; retain the local
|
|
19
|
+
diff and BIN/ELF identity when promoting this package. Project source maps live
|
|
20
|
+
under `docs/boards/waveshare-esp32-p4-wifi6-touch-lcd-3.5/`; current build status
|
|
21
|
+
is in `waveshare-esp32p4-xiaotai/PORT_STATUS.md` and `BUILD_IDENTITY.md`.
|
|
22
|
+
|
|
23
|
+
## Hardware candidates (source-correlated, re-probe on new hardware)
|
|
24
|
+
|
|
25
|
+
- P4, 32 MiB PSRAM, 16 MiB flash; ESP32-C6 over Hosted SDIO. P4 requires its own
|
|
26
|
+
RISC-V SDK; the tested IDF is 5.5.4-dirty, TiRTC 2.3.0.
|
|
27
|
+
- ST7796 **SPI**, physical 320x480 / landscape 480x320; FT6336 uses the FT5x06
|
|
28
|
+
driver family. CSI camera does not imply a DSI display.
|
|
29
|
+
- No onboard IMU is documented for this model: vendor hardware inventory omits
|
|
30
|
+
one, pinned monitor BSP declares `BSP_CAPS_IMU=0`, and vendor BSP 2.0.1 lists
|
|
31
|
+
IMU unavailable. External sensing is needed for motion-based rotation; BSP
|
|
32
|
+
support flags alone are not proof of hardware absence. Sources checked
|
|
33
|
+
2026-09-07: https://docs.waveshare.com/ESP32-P4-WIFI6-Touch-LCD-3.5 and
|
|
34
|
+
https://components.espressif.com/components/waveshare/esp32_p4_wifi6_touch_lcd_3_5/versions/2.0.1/readme
|
|
35
|
+
- Shared I2C1 SDA7/SCL8: pass the existing bus to camera SCCB (`init_sccb=false`),
|
|
36
|
+
codec and touch; an unused I2C0 macro is not evidence for a second owner.
|
|
37
|
+
- ES8311 I2S1 MCLK13/BCLK12/WS10/DOUT9/DIN11; PA53 high. Shared TX/RX,
|
|
38
|
+
16 kHz stereo 16-bit, left MIC/right DAC reference. Monitor uses
|
|
39
|
+
`.no_dac_ref=false`, codec register 0x44=0x58. Validate reference energy and
|
|
40
|
+
double-talk; a board-level reference macro in Xiaozhi does not prove its
|
|
41
|
+
actual codec object enables reference input.
|
|
42
|
+
- LCD MOSI20/CLK21/CS23/DC26/RST27/BL28; touch RST29/INT50; BOOT35.
|
|
43
|
+
Verify combined rotation/mirror and all four touch corners. BOOT is distinct
|
|
44
|
+
from AXP2101 power controls; PMIC power sequencing remains unverified.
|
|
45
|
+
- C6 SDIO CLK18/CMD19/D0..3=14..17/RST54. Read host/slave versions; STA working
|
|
46
|
+
does not verify SoftAP/captive portal. Read the remote STA MAC after got-IP.
|
|
47
|
+
- OV5647 source: `main/drivers/camera/camera_driver.c`. Sensor mode may run
|
|
48
|
+
faster than requested output: observed 800x640@50 sensor paced to 15 fps.
|
|
49
|
+
Verify PID and actual silicon revision before selecting rev<3 defaults.
|
|
50
|
+
|
|
51
|
+
## Media and failures worth remembering
|
|
52
|
+
|
|
53
|
+
| Symptom | Evidence and correction | Regression / next source |
|
|
54
|
+
| --- | --- | --- |
|
|
55
|
+
| VoIP audio but no remote image | Callback metadata stream=1/media=65 (JPEG), rx=37 and decoded=0; adapter only allowed stream11. Receive by active connection/generation/codec, not uplink ID. User later reported image visible. | P4 `tools/test_video_ingress.py`, actual `p4_video_submit`/`drain_video`; monitor `tirtc_session_on_video` |
|
|
56
|
+
| Remote producer not requested | Monitor subscribes then sends its version-specific video-enable command `(SN<<16)\u007c0x1105`, byte1. Added missing request, but this alone was not proven to fix image. | `tools/test_p4_video.py`; monitor `tirtc_session.c`, `tirtc_commands.c` |
|
|
57
|
+
| H264 open fails with >8 MiB free | Singleton output workspace remained reserved for old profile. Stop worker, apply profile, reconcile reservation, restart. | `tools/test_p4_profile_switch.py`; `camera_pipeline_on_rtc_video_config_changed` |
|
|
58
|
+
| Repeated WHIP attempts leak | Shared worker used WithCaps creation and ordinary deletion. Fixed to paired deletion; 24 KiB stack plus TCB per completed attempt was retained. | S3 `tools/test_voip_task_cleanup.py`; installed IDF `esp_additions/idf_additions.c` |
|
|
59
|
+
| UI slow / video not smooth | Product UI tick100ms limits presentation near10fps; renderer input24x256KiB plus output20x480x320x2 dominate pools. Retained pools are intentional, not proven optimal for this product. | `call_video_renderer_config.h`, `p4_video_ui_tick`; measure before tuning |
|
|
60
|
+
|
|
61
|
+
Product-specific media: device CALL H264 both directions; WeChat VOIP H264 up /
|
|
62
|
+
MJPEG down, profile video enabled with actual screen 480x320, 8k mono A-law. Preserve explicit
|
|
63
|
+
voice calls. Camera privacy disables local capture/send while keeping remote
|
|
64
|
+
decode/audio; microphone is independent. Profile declaration is not proof of
|
|
65
|
+
actual packets or display. P4 H264 encode is hardware; downstream H264 here is
|
|
66
|
+
software, MJPEG uses hardware JPEG plus scaling.
|
|
67
|
+
|
|
68
|
+
## Evidence boundary
|
|
69
|
+
|
|
70
|
+
P4 `xiaotai-p4-voip.8` compiled with host regressions including 100 simulated
|
|
71
|
+
worker exits; no repeated-call heap HIL yet.
|
|
72
|
+
|
|
73
|
+
- BIN SHA256: `8bb2e46f3ce68318ebdd544c6cff979e6885e1c40b70ff0ee1731342b07c55e1`
|
|
74
|
+
- ELF SHA256: `6a138dede2fe20e7980937801c48a0206b0a78a3634a49a9ef3e31f9bce811aa`
|
|
75
|
+
- P4 SDK archive SHA256: `a7a01ffd496a55364c7e4d665ff3884d078147bba96752a965d97befca12e451`
|
|
76
|
+
|
|
77
|
+
These identify historical local artifacts, not bundled binaries or a reproducible
|
|
78
|
+
release. User reports video visible after the stream-filter correction, without
|
|
79
|
+
a complete artifact-bound capture. Startup logs prove one successful media
|
|
80
|
+
start, not long-run stability. Earlier HMAC signing -> invalid LVGL event crash
|
|
81
|
+
was subsequently traced to an LVGL overwrite, as detailed below; task cleanup
|
|
82
|
+
was a separate fix. AEC/double-talk, weak
|
|
83
|
+
network, repeated calls, PMIC controls and full product parity remain pending.
|
|
84
|
+
Promote only after exact identity, portable adapter/IR/contracts and per-flow
|
|
85
|
+
artifact-bound acceptance are retained.
|
|
86
|
+
|
|
87
|
+
## Later findings at the committed product baseline
|
|
88
|
+
|
|
89
|
+
### LVGL callback counter overwrites an unrelated allocator hook
|
|
90
|
+
|
|
91
|
+
The diagnostic `diag.9` hardware watchpoint caught `lv_obj_add_event_cb` writing
|
|
92
|
+
the mbedTLS calloc function pointer while `render_page` registered the screen
|
|
93
|
+
event again. `lv_obj_clean(screen)` removed children, not screen callbacks.
|
|
94
|
+
The selected LVGL descriptor count was six bits: registration 64 wrapped it,
|
|
95
|
+
zero-sized realloc returned the allocator's sentinel, and descriptor index -1
|
|
96
|
+
wrote 12 bytes before that sentinel. An ensuing HMAC allocation jumped into
|
|
97
|
+
`on_screen_event(e=1)`. The apparent crypto/LVGL call stack was a consequence,
|
|
98
|
+
not an allocator ABI diagnosis or proof of stack exhaustion.
|
|
99
|
+
|
|
100
|
+
Correction: register once when the persistent screen is initialized, not on
|
|
101
|
+
every page rebuild; remove temporary watchpoints after capturing the writer.
|
|
102
|
+
`tools/test_screen_event_lifetime.py` in the S3 project runs 512 actual setup
|
|
103
|
+
paths against the relevant LVGL counter logic. Source/build regression passed;
|
|
104
|
+
post-fix sustained HIL is not established by that test. Watchpoint addresses
|
|
105
|
+
belonged to that ELF and must never become hardcoded protection logic.
|
|
106
|
+
|
|
107
|
+
### Orientation, aspect ratio and profile experiments
|
|
108
|
+
|
|
109
|
+
Read [video-orientation.md](../../references/video-orientation.md) for the
|
|
110
|
+
general procedure and protocol fields. The capture.13 pre-SDK PNG showed the
|
|
111
|
+
upright test card clockwise90; thus the device's encoded pixels were already
|
|
112
|
+
sideways, not merely H5 CSS. The original H264 was not retained at inspection,
|
|
113
|
+
so 1280x960 is corroborated by PNG/capture metadata, not a fresh SPS inspection.
|
|
114
|
+
|
|
115
|
+
The upright.14 / voipdir.15 implementation captures native1280x960 and uses
|
|
116
|
+
PPA CCW90 before H264 encode, producing960x1280@15 / target2Mbps without crop
|
|
117
|
+
or spatial scale. It adds about1,843,200 bytes of rotation-output PSRAM and one
|
|
118
|
+
PPA transaction. The source geometry and submission tests cover exchanged axes,
|
|
119
|
+
zero crop offsets, 1:1 scale and rotation-aware resource matching. CALL's
|
|
120
|
+
decoder-limited profile remains separate; these values are not maximum ratings.
|
|
121
|
+
|
|
122
|
+
The user reported H5 orientation correct and P4's MJPEG CW90 display correct
|
|
123
|
+
with voipdir.15, but mini-program rendering still wrong. These are user-reported
|
|
124
|
+
per-direction observations, not a complete artifact-bound acceptance bundle.
|
|
125
|
+
voipui.16 added UI fields with rotation0 / aspect0.75 / mirrorsfalse / contain.
|
|
126
|
+
The subsequent request selected **additional UI rotation270** in voipui.17;
|
|
127
|
+
that latest setting compiled but has **no confirming post-change HIL**. Do not
|
|
128
|
+
promote270 into the board registry's default or claim it fixes every endpoint.
|
|
129
|
+
|
|
130
|
+
- Latest compiled BIN SHA256: `8de6f919acd120fe283db17ffd653d347a5850df715e1f3879e7910b3b3df29c`
|
|
131
|
+
- Latest compiled ELF SHA256: `2aecb8fb9e81347ef9aed76371c5f0404364b687f393df8fb4ae2099edfddc1c`
|
|
132
|
+
- Source: `waveshare-esp32p4-xiaotai/main/media/{camera_pipeline,video_yuv420_scaler}.c`,
|
|
133
|
+
`main/services/call_video_renderer.c`; shared `request_voip_profile`.
|
|
134
|
+
- Tests: P4 `tools/test_full_frame_uplink.py`, `test_uplink_rotation.py`,
|
|
135
|
+
`test_voip_profile.py`. Local SDK PPA enums are CCW, UI angles CW.
|
|
136
|
+
|
|
137
|
+
### Reusable capture tool boundary
|
|
138
|
+
|
|
139
|
+
The packaged `scripts/capture_uplink.py` is derived from the committed P4
|
|
140
|
+
`tools/capture_uplink.py`. Its device counterpart is
|
|
141
|
+
`components/p4_hardware/p4_video_capture.c` plus the pre-SDK hook in `p4_video.c`.
|
|
142
|
+
Device firmware is not shipped by this knowledge-only package. Read product
|
|
143
|
+
`VIDEO_CAPTURE.md` and confirm compatible commands before running the helper.
|
|
144
|
+
The helper's actual dump parsing, fragmented ANSI query handling, stopped/empty
|
|
145
|
+
recovery and disconnect classification have packaged host regressions. They do
|
|
146
|
+
not establish unattended operation on an untested USB/terminal combination.
|
|
@@ -51,6 +51,13 @@ Registry package status controls reuse:
|
|
|
51
51
|
|
|
52
52
|
An older HIL result is provenance, not proof for a newly built artifact.
|
|
53
53
|
|
|
54
|
+
Knowledge-only packages may record an unknown PCB revision as `null`; verified
|
|
55
|
+
packages still require an explicit revision. `match` returns registry-relative
|
|
56
|
+
`knowledge_refs` for exact/model candidates, including knowledge-only packages.
|
|
57
|
+
Read that source map first and inspect only relevant source deltas. A probable
|
|
58
|
+
match provides investigation leads, not validated pins or an installable adapter.
|
|
59
|
+
The Waveshare ESP32-P4-WIFI6-Touch-LCD-3.5 package follows this boundary.
|
|
60
|
+
|
|
54
61
|
## Learning loop
|
|
55
62
|
|
|
56
63
|
After the final assessment, create a project-local candidate:
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Existing-project media and resource review
|
|
2
|
+
|
|
3
|
+
Use for missing remote video, allocation failures, task leaks, slow UI or call
|
|
4
|
+
switching. This is a focused diagnostic route, not a requirement to redo board
|
|
5
|
+
intake, replay every simulator flow or regenerate a working product.
|
|
6
|
+
|
|
7
|
+
## Establish a narrow baseline
|
|
8
|
+
|
|
9
|
+
Record business baseline and hardware reference separately. Keep the user's
|
|
10
|
+
contacts, call arbitration, onboarding and UI semantics in the former; borrow
|
|
11
|
+
drivers, capture and codec resource ownership from the latter. Read the matched
|
|
12
|
+
knowledge package's source map, compare pinned commits and relevant dirty files,
|
|
13
|
+
then inspect only the path under investigation. Reuse prior build evidence only
|
|
14
|
+
when artifact identity matches; otherwise label it historical.
|
|
15
|
+
|
|
16
|
+
## Receive-to-display contract
|
|
17
|
+
|
|
18
|
+
For each active mode record uplink codec, subscribe ID, downlink codec and
|
|
19
|
+
observed receive metadata independently. H5 video-up stream 11 remains valid;
|
|
20
|
+
it does not establish a universal receive stream ID. Authenticate the active
|
|
21
|
+
connection, validate generation and negotiated media, and test actual ingress
|
|
22
|
+
with captured non-sensitive frame metadata before changing filters.
|
|
23
|
+
|
|
24
|
+
Use bounded counters at callback, admission, queue, decoder, converter and
|
|
25
|
+
presentation boundaries. Count admission rejection separately from overflow.
|
|
26
|
+
`rx>0, decoded=0` is not automatically a codec failure. Test enabling remote
|
|
27
|
+
production separately from subscription; protocol commands are SDK/reference
|
|
28
|
+
specific, not generic magic constants.
|
|
29
|
+
|
|
30
|
+
## Resource lifecycle
|
|
31
|
+
|
|
32
|
+
- A callback reached from an unrelated allocator/crypto call can be the victim
|
|
33
|
+
of an earlier overwrite. Validate the exact ELF, inspect the indirect-call
|
|
34
|
+
target and use a narrowly scoped watchpoint to catch the writer. In LVGL,
|
|
35
|
+
persistent screen callbacks belong to screen creation, not every child-page
|
|
36
|
+
rebuild: cleaning children does not remove callbacks on their parent. Check
|
|
37
|
+
the actual version's descriptor counter width. See the Waveshare knowledge
|
|
38
|
+
package for the captured six-bit overflow case; numeric addresses are not
|
|
39
|
+
portable fixes.
|
|
40
|
+
|
|
41
|
+
- Inventory task creation and deletion pairs. ESP-IDF `xTaskCreate*WithCaps`
|
|
42
|
+
requires `vTaskDeleteWithCaps`; ordinary deletion retains its statically
|
|
43
|
+
registered stack/TCB. Confirm the installed IDF implementation. Self-deletion
|
|
44
|
+
may allocate a helper task; under tight internal RAM prefer owner-driven
|
|
45
|
+
cleanup after the worker has stopped. A mock API-pair test is not heap HIL.
|
|
46
|
+
- Distinguish free capacity, largest contiguous block, DMA capability and
|
|
47
|
+
reserved-workspace ownership. Stop the old codec owner before reconfiguring
|
|
48
|
+
or acquiring a singleton workspace. Retry cannot repair stale ownership.
|
|
49
|
+
- Budget internal RAM and PSRAM independently for idle, handshake, duplex media
|
|
50
|
+
and stop/reconnect overlap. Include codec pools, ingress copies, UI surfaces,
|
|
51
|
+
worker stacks and SDK queues; a retained pool is not a per-call leak.
|
|
52
|
+
- Measure receive, decode and presentation cadence independently. A 100 ms UI
|
|
53
|
+
timer caps presentation near 10 fps even if decode runs at 15 fps. Evaluate
|
|
54
|
+
dedicated video presentation and smaller MJPEG latest-frame pools against
|
|
55
|
+
audio deadlines; H264 recovery buffers have different dependencies.
|
|
56
|
+
|
|
57
|
+
## Completion evidence
|
|
58
|
+
|
|
59
|
+
Run an actual-code regression for the failing seam. Report build/host tests
|
|
60
|
+
separately from hardware. For resource changes, compare post-warmup baselines
|
|
61
|
+
over repeated connect/cancel/hangup cycles and a sustained duplex call: heap
|
|
62
|
+
free/largest/minimum by capability, task stack low-water marks, queue drops,
|
|
63
|
+
decode/present rates and audio starvation. Sample cheaply outside realtime
|
|
64
|
+
callbacks; choose intervals for the experiment, not permanent noisy polling.
|
|
65
|
+
Unreproduced crashes remain unresolved even if a later call succeeds. Change
|
|
66
|
+
one high-risk variable per hardware comparison and retain redacted artifact-
|
|
67
|
+
bound results; user-reported video is evidence of video, not AEC or stability.
|
|
@@ -68,6 +68,10 @@ authorized contact list, route WeChat MQTT events, and use `POST
|
|
|
68
68
|
selects an authorized contact; it is not a direct SDK API. Mini-program
|
|
69
69
|
authorization and plugin behavior are separate platform acceptance evidence.
|
|
70
70
|
|
|
71
|
+
For video profile fields and orientation, use
|
|
72
|
+
[video-orientation.md](video-orientation.md). Screen dimensions, encoded-frame
|
|
73
|
+
aspect ratio and mini-program display transforms describe different boundaries.
|
|
74
|
+
|
|
71
75
|
## Simulator before hardware
|
|
72
76
|
|
|
73
77
|
For the four-feature portfolio, use the pinned Linux C reference implementation
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Video geometry, orientation and WeChat UI
|
|
2
|
+
|
|
3
|
+
Use when a video appears sideways, mirrored, cropped/zoomed, or differs between
|
|
4
|
+
H5 and WeChat. Keep four independent facts: sensor output, encoded pixels,
|
|
5
|
+
device display viewport, and remote player's UI transform. An absent rotate
|
|
6
|
+
call does not prove upright pixels: camera mounting can already introduce an
|
|
7
|
+
offset. H5 working does not prove the VoIP-mode encoded pixels are identical.
|
|
8
|
+
|
|
9
|
+
## Locate the transform
|
|
10
|
+
|
|
11
|
+
Keep the board and an asymmetric upright test card stationary. Use readable
|
|
12
|
+
text plus top/left/right marks; rotation alone and rotation plus reflection
|
|
13
|
+
are distinct. State only what the screenshot establishes, not where the
|
|
14
|
+
transform happened. Avoid repeatedly asking for placement once confirmed.
|
|
15
|
+
|
|
16
|
+
Compare short **pre-SDK** H264 clips from each affected mode, with the exact
|
|
17
|
+
firmware identity and active generation. Start at SPS/PPS plus IDR, decode
|
|
18
|
+
without automatic rotation, inspect pixels, and read coded width/height from
|
|
19
|
+
SPS (for example ffprobe). Callback dimensions are useful corroboration, not
|
|
20
|
+
a substitute for SPS. A truncated printed ELF hash is not a full identity.
|
|
21
|
+
|
|
22
|
+
- Matching source clips but different displays: inspect SDK/bridge and player
|
|
23
|
+
UI settings rather than globally rotating the camera again.
|
|
24
|
+
- Different source clips: inspect mode-specific capture, crop, encoder input
|
|
25
|
+
and profile transitions first.
|
|
26
|
+
- One correctly oriented receive direction says nothing about the opposite
|
|
27
|
+
direction. Apply and test corrections at the intended boundary only.
|
|
28
|
+
|
|
29
|
+
Retain separate claims for observed pixels, verified source configuration,
|
|
30
|
+
successful compilation, and post-change hardware observations.
|
|
31
|
+
|
|
32
|
+
## Geometry and resources
|
|
33
|
+
|
|
34
|
+
For a full-frame 90-degree rotation, exchange width/height. A 1280x960 sensor
|
|
35
|
+
frame becomes 960x1280; preserving complete pixels is not the same as forcing
|
|
36
|
+
a landscape aspect ratio. Validate encoder width AND height limits, alignment,
|
|
37
|
+
crop coordinates in input axes, scale factors, clamp logic, direct-input
|
|
38
|
+
bypasses, and reserved-resource matching including rotation. Budget an extra
|
|
39
|
+
output surface and conversion time. PPA angles in the ESP-IDF P4 driver are
|
|
40
|
+
counterclockwise; check the selected header instead of sharing enum values
|
|
41
|
+
with a clockwise UI API.
|
|
42
|
+
|
|
43
|
+
Treat a conservative reference profile as policy, not silicon capacity. Verify
|
|
44
|
+
the installed sensor's native modes and actual output; selecting an 800x640
|
|
45
|
+
mode and taking its central 640x480 narrows field of view even without a
|
|
46
|
+
digital zoom setting. Disable or expose automatic resolution fallback when
|
|
47
|
+
the product requires an exact output. Keep H5/phone encode capability separate
|
|
48
|
+
from a peer P4's software H264 decode limits.
|
|
49
|
+
|
|
50
|
+
## WeChat profile contract
|
|
51
|
+
|
|
52
|
+
Checked 2026-09-07 against the primary
|
|
53
|
+
[profile API](https://github.com/tangeai/tirtc-server-example/blob/main/thing-connect/api-reference.md#post-v1voipdeviceprofile)
|
|
54
|
+
and [VoIP guide](https://github.com/tangeai/tirtc-server-example/blob/main/thing-connect/device-voip.md).
|
|
55
|
+
These URLs track main: pin/recheck the selected platform revision before a
|
|
56
|
+
new integration. Do not infer protocol absence from an older board example.
|
|
57
|
+
|
|
58
|
+
| Field | Boundary / constraint |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `screen_width`, `screen_height` | Actual device display region, not uplink coded dimensions |
|
|
61
|
+
| `camera_rotation` | Additional clockwise mini-program UI angle, one of 0/90/180/270; default 0 |
|
|
62
|
+
| `aspect_ratio` | Device video's width/height, positive; default 4/3 can be wrong for portrait output |
|
|
63
|
+
| `hor_mirror`, `vert_mirror` | Mini-program horizontal/vertical reflection; defaults false |
|
|
64
|
+
| `object_fit` | Mini-program `fill` or `contain`; default fill |
|
|
65
|
+
| `video_res_mode` | Downlink `auto`, `fit_screen`, or `fill_screen`; does not rotate |
|
|
66
|
+
|
|
67
|
+
The five UI fields do not configure TiRTC encoding. `fit_screen` bounds MJPEG
|
|
68
|
+
inside the screen without cropping/upscaling; `fill_screen` can upscale and
|
|
69
|
+
center-crop. Both need valid screen sizes; fill requires even dimensions.
|
|
70
|
+
Do not silently change a working downlink mode while investigating uplink UI.
|
|
71
|
+
The profile JSON must fit 512 bytes. Test actual serialized fields, types and
|
|
72
|
+
size, not only log text. Re-report after changes and start a fresh call so the
|
|
73
|
+
mini-program consumes updated configuration.
|
|
74
|
+
|
|
75
|
+
Choose UI angle relative to the already-transformed encoded pixels; avoid
|
|
76
|
+
blindly reporting a physical mounting angle. If pixel correction is already
|
|
77
|
+
upright, zero additional UI rotation is a hypothesis to test, not an absolute
|
|
78
|
+
rule. A observed need for another angle is endpoint-specific evidence: retain
|
|
79
|
+
H5/VoIP captures and test the new call before calling it verified. In particular,
|
|
80
|
+
the Waveshare experiment's requested 270 is not a universal board default.
|
|
81
|
+
|
|
82
|
+
## Bounded capture and serial recovery
|
|
83
|
+
|
|
84
|
+
The packaged `scripts/capture_uplink.py` supports the **XiaoTai video-capture
|
|
85
|
+
console protocol**, not arbitrary TiRTC firmware. It does not flash or install
|
|
86
|
+
firmware. Source and device-side integration are pinned in the Waveshare
|
|
87
|
+
knowledge package; verify `video-capture` exists before use. Host requirements:
|
|
88
|
+
Python plus pyserial on POSIX; ffprobe/ffmpeg are optional for decoded PNG.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python3 <skill-dir>/scripts/capture_uplink.py --port <exact-port> --output <new-path>.h264
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use test-card media only, with explicit serial/capture authorization. The
|
|
95
|
+
firmware allocates at most 512 KiB PSRAM on demand and copies complete frames
|
|
96
|
+
without blocking for a lock or doing file/serial I/O in the encoder callback.
|
|
97
|
+
It stops on capacity, generation/size change, or contention. Approximate
|
|
98
|
+
two-second duration is not guaranteed; missing SPS/PPS/IDR can leave it empty.
|
|
99
|
+
Export is console-task work after stop and disconnect. No network upload.
|
|
100
|
+
|
|
101
|
+
The helper answers ANSI status/cursor queries (including queries without a
|
|
102
|
+
newline), preserves partial serial lines, uses CR and clears stale command
|
|
103
|
+
input. It opens the port exclusively where supported, but a read interruption
|
|
104
|
+
alone does not distinguish another reader from USB disconnect or reboot.
|
|
105
|
+
Check process ownership and USB logs without killing unrelated processes.
|
|
106
|
+
Display safe panic/reboot markers rather than dumping credential-bearing logs.
|
|
107
|
+
|
|
108
|
+
Recovery is explicit: use `--dump-only` to stop then inspect retained capture;
|
|
109
|
+
keep nonempty media, release only an inactive empty buffer, and ask the user
|
|
110
|
+
to close the H5 page or hang up before exporting. Terminal Enter is confirmation,
|
|
111
|
+
not a remote hangup command. Check offsets, length and checksum before writing
|
|
112
|
+
new files. Existing H264/PNG/TXT files can indicate a successful prior export:
|
|
113
|
+
inspect them or select a new basename instead of deleting them blindly.
|
|
114
|
+
|
|
115
|
+
Completion: accepted serialized profile, identified coded dimensions and
|
|
116
|
+
pre-SDK pixels, plus separate H5 and WeChat display checks. Tool/host tests alone
|
|
117
|
+
do not prove camera orientation, serial HIL or call stability.
|
|
@@ -95,12 +95,16 @@ def validate_probe(item: Any, path: str, errors: list[str]) -> None:
|
|
|
95
95
|
errors.append(f"{path}.required_for_exact must be true or false")
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def validate_identity(identity: Any, path: str, errors: list[str]) -> None:
|
|
98
|
+
def validate_identity(identity: Any, path: str, errors: list[str], *, allow_unknown_revision: bool = False) -> None:
|
|
99
99
|
if not isinstance(identity, dict):
|
|
100
100
|
errors.append(f"{path} must be an object")
|
|
101
101
|
return
|
|
102
|
-
for field in ("vendor", "model", "
|
|
102
|
+
for field in ("vendor", "model", "target"):
|
|
103
103
|
nonempty(identity.get(field), f"{path}.{field}", errors)
|
|
104
|
+
if allow_unknown_revision:
|
|
105
|
+
nullable_nonempty(identity.get("hardware_revision"), f"{path}.hardware_revision", errors)
|
|
106
|
+
else:
|
|
107
|
+
nonempty(identity.get("hardware_revision"), f"{path}.hardware_revision", errors)
|
|
104
108
|
aliases = string_array(identity.get("aliases", []), f"{path}.aliases", errors)
|
|
105
109
|
names = [identity.get("model"), *aliases]
|
|
106
110
|
normalized_names = [normalized(value) for value in names if normalized(value)]
|
|
@@ -244,7 +248,15 @@ def validate_registry(data: dict[str, Any], registry_path: Path) -> list[str]:
|
|
|
244
248
|
errors.append(
|
|
245
249
|
f"{prefix}.status must be one of {', '.join(sorted(PACKAGE_STATUSES))}"
|
|
246
250
|
)
|
|
247
|
-
validate_identity(board.get("identity"), f"{prefix}.identity", errors
|
|
251
|
+
validate_identity(board.get("identity"), f"{prefix}.identity", errors,
|
|
252
|
+
allow_unknown_revision=status == "knowledge_only")
|
|
253
|
+
for ref_index, value in enumerate(string_array(
|
|
254
|
+
board.get("knowledge_refs", []), f"{prefix}.knowledge_refs", errors
|
|
255
|
+
)):
|
|
256
|
+
resolved = safe_registry_path(registry_path, value,
|
|
257
|
+
f"{prefix}.knowledge_refs[{ref_index}]", errors)
|
|
258
|
+
if resolved is not None and not resolved.is_file():
|
|
259
|
+
errors.append(f"{prefix}.knowledge_refs[{ref_index}] does not exist")
|
|
248
260
|
compatibility = board.get("compatibility")
|
|
249
261
|
if not isinstance(compatibility, dict):
|
|
250
262
|
errors.append(f"{prefix}.compatibility must be an object")
|
|
@@ -453,6 +465,10 @@ def match_board(board: dict[str, Any], query: dict[str, Any]) -> dict[str, Any]
|
|
|
453
465
|
{"kind": kind, "model": model} for kind, model in shared_components
|
|
454
466
|
],
|
|
455
467
|
"applicable_lessons": applicable_lessons,
|
|
468
|
+
# A model candidate can read its evidence without acquiring GPIO/adapter
|
|
469
|
+
# authority. Component-only matches must not inherit board-specific docs.
|
|
470
|
+
"knowledge_refs": board.get("knowledge_refs", [])
|
|
471
|
+
if classification in {"exact", "probable"} else [],
|
|
456
472
|
"artifacts": board.get("artifacts", {}) if reuse == "registered_board" else {},
|
|
457
473
|
}
|
|
458
474
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Opt-in serial capture of pre-SDK H264. Close idf.py monitor before use."""
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Terminal:
|
|
12
|
+
"""Minimal ANSI responder; retain partial lines across serial read timeouts."""
|
|
13
|
+
def __init__(self, port):
|
|
14
|
+
self.port = port
|
|
15
|
+
self.pending = bytearray()
|
|
16
|
+
self.tail = bytearray()
|
|
17
|
+
self.received = 0
|
|
18
|
+
self.signals = []
|
|
19
|
+
|
|
20
|
+
def write(self, data):
|
|
21
|
+
return self.port.write(data)
|
|
22
|
+
|
|
23
|
+
def readline(self):
|
|
24
|
+
deadline = time.monotonic() + 0.2
|
|
25
|
+
while time.monotonic() < deadline:
|
|
26
|
+
if b"\n" in self.pending:
|
|
27
|
+
line, _, rest = self.pending.partition(b"\n")
|
|
28
|
+
self.pending = bytearray(rest)
|
|
29
|
+
for signature in (b"Guru Meditation", b"panic'ed", b"assert failed",
|
|
30
|
+
b"CORRUPT HEAP", b"Stack smashing", b"Brownout",
|
|
31
|
+
b"Rebooting", b"ESP-ROM:"):
|
|
32
|
+
if signature in line and signature.decode() not in self.signals:
|
|
33
|
+
self.signals.append(signature.decode())
|
|
34
|
+
# Do not print unrelated runtime logs or credentials.
|
|
35
|
+
print(f"设备诊断标记:{signature.decode()}", flush=True)
|
|
36
|
+
return bytes(line) + b"\n"
|
|
37
|
+
try:
|
|
38
|
+
chunk = self.port.read(min(self.port.in_waiting or 1, 4096))
|
|
39
|
+
except OSError as error:
|
|
40
|
+
raise RuntimeError(
|
|
41
|
+
f"串口读取中断(已收到{self.received}字节;"
|
|
42
|
+
f"已观测异常标记={','.join(self.signals) or '无,不能排除重启'})。"
|
|
43
|
+
"请检查串口占用及USB断连/设备重启;未自动重连或清除设备抓流。"
|
|
44
|
+
) from error
|
|
45
|
+
self.received += len(chunk)
|
|
46
|
+
for byte in chunk:
|
|
47
|
+
self.tail.append(byte)
|
|
48
|
+
self.tail = self.tail[-8:]
|
|
49
|
+
if self.tail.endswith(b"\x1b[6n"):
|
|
50
|
+
self.write(b"\x1b[1;80R")
|
|
51
|
+
elif self.tail.endswith(b"\x1b[5n"):
|
|
52
|
+
self.write(b"\x1b[0n")
|
|
53
|
+
self.pending.extend(chunk)
|
|
54
|
+
if len(self.pending) > 16384 and b"\n" not in self.pending:
|
|
55
|
+
raise RuntimeError("console line too long; check serial port/baud")
|
|
56
|
+
return b""
|
|
57
|
+
|
|
58
|
+
def settle(self):
|
|
59
|
+
deadline = time.monotonic() + 0.6
|
|
60
|
+
while time.monotonic() < deadline:
|
|
61
|
+
self.readline()
|
|
62
|
+
|
|
63
|
+
def command(self, text):
|
|
64
|
+
self.settle()
|
|
65
|
+
# Ctrl-U clears a stale partially typed command; REPL expects CR.
|
|
66
|
+
self.write(b"\x15" + text.encode("ascii") + b"\r")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def firmware_identity(port):
|
|
70
|
+
port.settle()
|
|
71
|
+
port.command("version")
|
|
72
|
+
try:
|
|
73
|
+
return until(port, "Firmware:", timeout=5)
|
|
74
|
+
except TimeoutError:
|
|
75
|
+
# An earlier monitor may have consumed the cursor query without replying.
|
|
76
|
+
# Complete that outstanding read, then clear the input and retry once.
|
|
77
|
+
port.write(b"\x1b[1;80R")
|
|
78
|
+
port.settle()
|
|
79
|
+
port.command("version")
|
|
80
|
+
return until(port, "Firmware:", timeout=10)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fnv(data):
|
|
84
|
+
value = 2166136261
|
|
85
|
+
for byte in data:
|
|
86
|
+
value = ((value ^ byte) * 16777619) & 0xffffffff
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Dump:
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.data = bytearray()
|
|
93
|
+
self.meta = None
|
|
94
|
+
|
|
95
|
+
def feed(self, line):
|
|
96
|
+
# Ignore unrelated logs, but reject holes, duplicates and corrupted data.
|
|
97
|
+
start = re.search(r"VCAP BEGIN bytes=(\d+) fnv=([0-9a-f]{8}) mode=(\d+) "
|
|
98
|
+
r"generation=(\d+) width=(\d+) height=(\d+) frames=(\d+)", line)
|
|
99
|
+
if start:
|
|
100
|
+
if self.meta is not None:
|
|
101
|
+
raise ValueError("duplicate dump header")
|
|
102
|
+
self.meta = [int(start[1]), int(start[2], 16)] + [int(start[i]) for i in range(3, 8)]
|
|
103
|
+
if not 0 < self.meta[0] <= 512 * 1024:
|
|
104
|
+
raise ValueError("invalid dump size")
|
|
105
|
+
elif "VCAP DATA " in line:
|
|
106
|
+
match = re.search(r"VCAP DATA ([0-9a-f]{8}) ([0-9a-f]+)\s*$", line)
|
|
107
|
+
if self.meta is None or not match or int(match[1], 16) != len(self.data):
|
|
108
|
+
raise ValueError("missing/corrupted/out-of-order dump line; retry dump")
|
|
109
|
+
chunk = bytes.fromhex(match[2])
|
|
110
|
+
if not 0 < len(chunk) <= 64 or len(self.data) + len(chunk) > self.meta[0]:
|
|
111
|
+
raise ValueError("invalid chunk size")
|
|
112
|
+
self.data.extend(chunk)
|
|
113
|
+
elif "VCAP END" in line:
|
|
114
|
+
if self.meta is None or len(self.data) != self.meta[0] or fnv(self.data) != self.meta[1]:
|
|
115
|
+
raise ValueError("incomplete dump or checksum mismatch; retry dump")
|
|
116
|
+
return True
|
|
117
|
+
elif "VCAP ERROR" in line:
|
|
118
|
+
raise RuntimeError(line[line.index("VCAP ERROR"):].strip())
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def until(port, marker, timeout=15):
|
|
123
|
+
deadline = time.monotonic() + timeout
|
|
124
|
+
while time.monotonic() < deadline:
|
|
125
|
+
line = port.readline().decode("ascii", errors="replace")
|
|
126
|
+
if "VCAP ERROR" in line:
|
|
127
|
+
raise RuntimeError(line[line.index("VCAP ERROR"):].strip())
|
|
128
|
+
if marker in line:
|
|
129
|
+
return line[line.index(marker):].strip()
|
|
130
|
+
raise TimeoutError(f"did not receive {marker}; check port/firmware")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def capture_status(port, command="status"):
|
|
134
|
+
port.command("video-capture " + command)
|
|
135
|
+
line = until(port, "VCAP STATUS")
|
|
136
|
+
print(line, flush=True)
|
|
137
|
+
match = re.search(r"active=(\d+) bytes=(\d+) frames=(\d+) width=(\d+) height=(\d+) reason=(\S+)", line)
|
|
138
|
+
if not match:
|
|
139
|
+
raise RuntimeError("抓流状态不完整,请保留上述状态输出")
|
|
140
|
+
return dict(zip(("active", "bytes", "frames", "width", "height", "reason"),
|
|
141
|
+
[int(match[i]) for i in range(1, 6)] + [match[6]]))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def stop_for_export(port):
|
|
145
|
+
status = capture_status(port, "stop")
|
|
146
|
+
if status["active"]:
|
|
147
|
+
raise RuntimeError("设备尚未停止抓流,未尝试导出")
|
|
148
|
+
if not status["bytes"]:
|
|
149
|
+
# No media to preserve. Release the allocation that otherwise blocks start.
|
|
150
|
+
capture_status(port, "clear")
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
f"缓冲为0字节,没有可导出的H264(reason={status['reason']});"
|
|
153
|
+
"已释放空缓冲。请保留上面的STATUS输出,重新建立视频后运行不带--dump-only的命令。"
|
|
154
|
+
)
|
|
155
|
+
return status
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
160
|
+
parser.add_argument("--port", required=True)
|
|
161
|
+
parser.add_argument("--output", required=True, type=Path)
|
|
162
|
+
parser.add_argument("--dump-only", action="store_true", help="retry exporting retained capture after hangup")
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
if args.output.exists() or args.output.with_suffix(".png").exists() or args.output.with_suffix(".txt").exists():
|
|
165
|
+
parser.error("output or companion file exists; choose a new name")
|
|
166
|
+
import serial
|
|
167
|
+
# POSIX advisory exclusive access: prevents cooperating serial tools from
|
|
168
|
+
# taking the same port; cannot evict/detect every already-open reader.
|
|
169
|
+
with serial.Serial(args.port, 115200, timeout=0.1, write_timeout=5, exclusive=True) as raw_port:
|
|
170
|
+
port = Terminal(raw_port)
|
|
171
|
+
if not args.dump_only:
|
|
172
|
+
input("保持标识纸不动,进入 H5 实时查看或微信视频通话,出图后按 Enter:")
|
|
173
|
+
identity = firmware_identity(port)
|
|
174
|
+
print(identity)
|
|
175
|
+
status = capture_status(port)
|
|
176
|
+
if status["active"] or status["bytes"]:
|
|
177
|
+
raise RuntimeError("存在正在进行或已保存的抓流,未覆盖。请退出查看后用--dump-only导出")
|
|
178
|
+
if status["reason"] != "empty":
|
|
179
|
+
capture_status(port, "clear") # inactive, zero-byte allocation only
|
|
180
|
+
port.command("video-capture start")
|
|
181
|
+
print(until(port, "VCAP ARMED"))
|
|
182
|
+
# Drain unrelated logs while allowing up to 5s for IDR + 2s capture.
|
|
183
|
+
deadline = time.monotonic() + 7
|
|
184
|
+
while time.monotonic() < deadline:
|
|
185
|
+
port.readline()
|
|
186
|
+
else:
|
|
187
|
+
identity = firmware_identity(port)
|
|
188
|
+
print(identity)
|
|
189
|
+
stop_for_export(port)
|
|
190
|
+
input("现在退出所有 H5 查看页面/挂断通话;确认连接断开后按 Enter 导出(不要只最小化页面):")
|
|
191
|
+
port.command("video-capture dump")
|
|
192
|
+
dump = Dump()
|
|
193
|
+
deadline = time.monotonic() + 240
|
|
194
|
+
while time.monotonic() < deadline:
|
|
195
|
+
if dump.feed(port.readline().decode("ascii", errors="replace")):
|
|
196
|
+
break
|
|
197
|
+
else:
|
|
198
|
+
raise TimeoutError("dump timed out; retained on device, retry with --dump-only")
|
|
199
|
+
with args.output.open("xb") as file:
|
|
200
|
+
file.write(dump.data)
|
|
201
|
+
with args.output.with_suffix(".txt").open("x") as file:
|
|
202
|
+
file.write(identity + "\n")
|
|
203
|
+
file.write(f"bytes,fnv,mode,generation,width,height,frames={dump.meta}\n")
|
|
204
|
+
port.command("video-capture clear")
|
|
205
|
+
until(port, "VCAP STATUS")
|
|
206
|
+
print(f"校验通过,已保存 {args.output};设备抓流缓冲已释放。")
|
|
207
|
+
if shutil.which("ffprobe"):
|
|
208
|
+
subprocess.run(["ffprobe", "-v", "error", "-f", "h264", "-show_entries",
|
|
209
|
+
"stream=codec_name,width,height", "-of", "default=nw=1", str(args.output)], check=True)
|
|
210
|
+
if shutil.which("ffmpeg"):
|
|
211
|
+
subprocess.run(["ffmpeg", "-v", "error", "-n", "-noautorotate", "-f", "h264", "-i",
|
|
212
|
+
str(args.output), "-frames:v", "1", str(args.output.with_suffix(".png"))], check=True)
|
|
213
|
+
print(f"发送前画面(未自动旋转):{args.output.with_suffix('.png')}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
if __name__ == "__main__":
|
|
217
|
+
try:
|
|
218
|
+
main()
|
|
219
|
+
except (TimeoutError, RuntimeError, OSError, ValueError) as error:
|
|
220
|
+
raise SystemExit(f"抓流未完成:{error}") from None
|