Skip to content

Commit 0202eee

Browse files
committed
Add camhi (Hi3516CV610 / Hichip PIHC) target
Adds a coupler build target for CamHi-app cameras on the Hisilicon Hi3516CV610 SoC, which ship firmware in the proprietary "PIHC" container accepted by the vendor's admin upgrade.cgi endpoint. Reverse-engineered from the vendor ipc_server binary and one live reference device (MPP HI3516CV610_MPP_V1.0.1.0 B040): - pihc_pack.py: PIHC container builder. 512-byte header (PIHC magic, type 4098, six component lengths, six MD5(component||"IPCAM") integrity strings), concatenated components, PK-signature mangle on the zip slot. The Hichip block cipher on the zip component is NOT AES and is left unreversed; the empty-zip path stays under the cipher's 1024-byte threshold so it is never needed. - uboot_env.py: pure-Python mkenvimage fallback (CRC32 + KEY=VALUE env). - _camhi2oipc.sh: wraps an OpenIPC hi3516cv6xx release into a .pkg. Builds a u-boot env that switches init=/bin/sh -> init=/init and relabels the trailing partition to rootfs_data for OpenIPC's overlay; omits boot.img to preserve vendor u-boot (keeps TFTP recovery); aborts if the rootfs exceeds the 3456K partition. - camhi.yml: workflow that self-tests the packer then builds one generic image (sensor left to OpenIPC autodetect / fw_setenv). - tests/test_pihc_pack.py: 19 unit tests mirroring the vendor's header validator (sub_36A70). - tests/test_build_e2e.sh: end-to-end build smoke test against a synthetic release tarball, validating the produced .pkg. - tests.yml: runs both test suites on push/pull_request. - camhi-NOTES.md: verified facts (partition geometry, flash path) vs the open questions. STATUS: UNTESTED ON HARDWARE. The format and partition map are verified by reverse engineering, the packer is unit-tested, but no end-to-end conversion flash has been performed. README marks the target (Untested!). No sensor or root password is asserted — neither was verified on the reference unit.
1 parent 65c18dd commit 0202eee

10 files changed

Lines changed: 987 additions & 0 deletions

File tree

.github/workflows/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
__pycache__/
2+
dl/
3+
out/
4+
workdir/
5+
*.pkg

.github/workflows/_camhi2oipc.sh

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/bin/bash
2+
3+
#####
4+
## Build a PIHC-format firmware that the camhi (Hi3516CV610) vendor's
5+
## `upgrade.cgi` admin endpoint accepts as a normal firmware update.
6+
##
7+
## STATUS: UNTESTED ON HARDWARE. The container format is reverse-engineered
8+
## and the packer is unit-tested, but no end-to-end flash has been performed.
9+
## See camhi-NOTES.md for what is verified vs assumed.
10+
##
11+
## Inputs (env vars):
12+
## SOC — OpenIPC SoC tag (default hi3516cv6xx — the only Hi3516CV610
13+
## build OpenIPC currently publishes).
14+
## RELEASE — OpenIPC flavor (default ultimate; lite is not published for
15+
## this SoC, and may be required if rootfs does not fit — see the
16+
## size check below).
17+
## HARDWARE — Hardware name stamped into u-boot env (cosmetic).
18+
## OSMEM — kernel "mem=" value (default 46400KB — the vendor's own value).
19+
## TOTALMEM — totalmem env (default 64M).
20+
##
21+
## Dependencies: u-boot-tools (mkenvimage) OR the bundled uboot_env.py fallback;
22+
## python3 (>= 3.8); tar.
23+
#####
24+
25+
set -euo pipefail
26+
27+
SOC="${SOC:-hi3516cv6xx}"
28+
RELEASE="${RELEASE:-ultimate}"
29+
HARDWARE="${HARDWARE:-CAMHI_CV610}"
30+
OSMEM="${OSMEM:-46400KB}"
31+
TOTALMEM="${TOTALMEM:-64M}"
32+
33+
WORKDIR="${WORKDIR:-workdir}"
34+
OUTPUTDIR="${OUTPUTDIR:-..}"
35+
TARBALL="${TARBALL:-openipc.${SOC}-nor-${RELEASE}.tgz}"
36+
HERE="$(cd "$(dirname "$0")" && pwd)"
37+
38+
# Partition geometry for the Hi3516CV610 camhi reference device. Verified from
39+
# the live device on 2026-06-06 (/proc/mtd + /proc/cmdline):
40+
# mtdparts=sfc:192K(boot),64K(env),2112K(kernel),3456K(rootfs),10560K(ipc)
41+
# 16 MB SPI NOR, 64 KB erase blocks.
42+
ADDR_KERNEL_START="0x00040000"
43+
ADDR_ROOTFS_START="0x00250000"
44+
ENV_SIZE="0x10000" # 64 KB env partition (mtd1)
45+
KERNEL_PART_SIZE=$((0x250000 - 0x40000)) # 2,162,688 bytes (mtd2)
46+
ROOTFS_PART_SIZE=$((0x5B0000 - 0x250000)) # 3,538,944 bytes (mtd3)
47+
48+
mkdir -p "${WORKDIR}" "${OUTPUTDIR}"
49+
50+
tar -xvz -f "${TARBALL}" -C "${WORKDIR}/" --exclude "*.md5sum" || {
51+
echo "Error: cannot untar ${TARBALL}." >&2
52+
exit 1
53+
}
54+
55+
KERNEL_SRC=$(ls "${WORKDIR}"/uImage* 2>/dev/null | head -n1 || true)
56+
ROOTFS_SRC=$(ls "${WORKDIR}"/rootfs.squashfs* 2>/dev/null | head -n1 || true)
57+
[[ -z "${KERNEL_SRC}" || -z "${ROOTFS_SRC}" ]] && {
58+
echo "Error: ${TARBALL} missing uImage* or rootfs.squashfs*." >&2
59+
exit 1
60+
}
61+
62+
# Size guard. The reference vendor rootfs already fills the 3456 KB partition
63+
# (df reported /dev/root 3.3M used 3.3M 100%), so OpenIPC's squashfs has very
64+
# little headroom here. If the ultimate build overflows, a lite build or a
65+
# repartition is required — fail loudly rather than produce a brick.
66+
KERNEL_SIZE=$(wc -c <"${KERNEL_SRC}")
67+
ROOTFS_SIZE=$(wc -c <"${ROOTFS_SRC}")
68+
if (( KERNEL_SIZE > KERNEL_PART_SIZE )); then
69+
echo "Error: kernel ${KERNEL_SIZE} > kernel partition ${KERNEL_PART_SIZE}." >&2
70+
exit 1
71+
fi
72+
if (( ROOTFS_SIZE > ROOTFS_PART_SIZE )); then
73+
echo "Error: rootfs ${ROOTFS_SIZE} > rootfs partition ${ROOTFS_PART_SIZE}." >&2
74+
echo " Try RELEASE=lite, or repartition (out of scope for this build)." >&2
75+
exit 1
76+
fi
77+
78+
# u-boot env (flashed to mtd1). Two deliberate changes from the vendor env
79+
# (verified vendor /proc/cmdline shown for reference):
80+
# 1. init=/bin/sh -> init=/init (OpenIPC runs /init as PID 1)
81+
# 2. last partition renamed "ipc" -> "rootfs_data" so OpenIPC's overlay
82+
# mechanism finds a writable data partition where it expects one. The
83+
# offset/size are unchanged; only the mtdparts label differs. OpenIPC's
84+
# `firstboot` is expected to format this overlay on first boot. UNTESTED.
85+
# Everything else (mem, console, mtdparts geometry) is kept as the vendor set
86+
# it, to stay in a configuration the SoC is known to boot.
87+
cat >"${WORKDIR}/u-boot.env.txt" <<EOF
88+
bootdelay=1
89+
baudrate=115200
90+
bootcmd=sf probe 0; sf read 0x42000000 ${ADDR_KERNEL_START} 0x200000; bootm 0x42000000
91+
bootargs=mem=${OSMEM} earlycon=pl011,0x11040000 console=ttyAMA0,115200 panic=20 rw root=/dev/mtdblock3 rootfstype=squashfs init=/init mtdparts=sfc:192K(boot),64K(env),2112K(kernel),3456K(rootfs),10560K(rootfs_data)
92+
osmem=${OSMEM}
93+
totalmem=${TOTALMEM}
94+
soc=${SOC}
95+
hardware=${HARDWARE}
96+
stdin=serial
97+
stdout=serial
98+
stderr=serial
99+
EOF
100+
101+
if command -v mkenvimage >/dev/null 2>&1; then
102+
mkenvimage -s "${ENV_SIZE}" -o "${WORKDIR}/bootarg.img" "${WORKDIR}/u-boot.env.txt"
103+
else
104+
python3 "${HERE}/uboot_env.py" -s "${ENV_SIZE}" -o "${WORKDIR}/bootarg.img" "${WORKDIR}/u-boot.env.txt"
105+
fi
106+
107+
# Pad kernel/rootfs to their partition sizes with 0xFF (NOR erased state).
108+
# LC_ALL=C forces tr into byte mode; without it the macOS BSD tr treats 0xFF
109+
# as a multibyte UTF-8 sequence and doubles the output.
110+
pad_to() {
111+
local src="$1" dst="$2" size="$3" cur need
112+
cur=$(wc -c <"${src}")
113+
cp "${src}" "${dst}"
114+
if (( cur < size )); then
115+
need=$((size - cur))
116+
dd if=/dev/zero bs=1 count="${need}" 2>/dev/null | LC_ALL=C tr '\000' '\377' >>"${dst}"
117+
fi
118+
}
119+
pad_to "${KERNEL_SRC}" "${WORKDIR}/kernel.img" "${KERNEL_PART_SIZE}"
120+
pad_to "${ROOTFS_SRC}" "${WORKDIR}/rootfs.img" "${ROOTFS_PART_SIZE}"
121+
122+
# boot.img is intentionally omitted: leaving the vendor u-boot in place keeps
123+
# TFTP recovery available if the conversion goes wrong.
124+
# ipc.img is intentionally omitted: the vendor app store (mtd4) is not written;
125+
# OpenIPC reformats that space as its overlay on first boot.
126+
# upgrade.zip stays empty (pihc_pack.py supplies a 22-byte EOCD-only ZIP,
127+
# below the 1024-byte Hichip-cipher threshold).
128+
129+
OUTPUT="${OUTPUTDIR}/openipc.${SOC}.${HARDWARE}.pkg"
130+
python3 "${HERE}/pihc_pack.py" \
131+
--bootarg "${WORKDIR}/bootarg.img" \
132+
--kernel "${WORKDIR}/kernel.img" \
133+
--rootfs "${WORKDIR}/rootfs.img" \
134+
--filename "openipc.pkg" \
135+
-o "${OUTPUT}"
136+
137+
echo "Built ${OUTPUT}"
138+
ls -la "${OUTPUT}"

.github/workflows/camhi-NOTES.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# camhi (Hi3516CV610) — engineering notes
2+
3+
> **Status: UNTESTED on hardware.** The container format and partition
4+
> geometry below are reverse-engineered from the vendor `ipc_server` binary
5+
> and read from one live reference device. The packer is unit-tested. **No
6+
> end-to-end conversion flash has been performed.** Treat the produced `.pkg`
7+
> as a candidate to validate on a spare unit with UART, not a finished
8+
> product.
9+
10+
## Verified (vendor binary + live device, 2026-06-06)
11+
12+
Reference device: CamHi-app camera, web banner `Server: Hipcam`,
13+
`getsysinfo.cgi` returns `devid="IPCAM"`, CGI prefix `/cgi-bin/hi3510/`.
14+
15+
* **SoC:** Hisilicon **Hi3516CV610**. Vendor MPP banner
16+
`HI3516CV610_MPP_V1.0.1.0 B040 Release`; loader script `load3516cv610`.
17+
* **Kernel:** Linux 5.10.221, musl, dual ARMv7 Cortex-A7.
18+
* **Flash:** 16 MB SPI NOR (`sfc`), 64 KB erase blocks. From `/proc/mtd` and
19+
`/proc/cmdline`:
20+
21+
| mtd | name | size | offset |
22+
|-----|--------|-----------------|-------------|
23+
| 0 | boot | 0x030000 (192K) | 0x000000 |
24+
| 1 | env | 0x010000 (64K) | 0x030000 |
25+
| 2 | kernel | 0x210000 (2112K)| 0x040000 |
26+
| 3 | rootfs | 0x360000 (3456K)| 0x250000 |
27+
| 4 | ipc | 0xA50000 (10560K)| 0x5B0000 |
28+
29+
* **Vendor bootargs:** `... root=/dev/mtdblock3 rootfstype=squashfs
30+
mtdparts=sfc:192K(boot),64K(env),2112K(kernel),3456K(rootfs),10560K(ipc)
31+
init=/bin/sh`. The `init=/bin/sh` is why OpenIPC needs an env override to
32+
`init=/init`.
33+
* **Root filesystem:** squashfs on mtd3, mounted read-only; `df` shows it
34+
**100% full at 3.3 M** — i.e. almost no headroom in the 3456 K partition.
35+
* **Upload path:** `upgrade.cgi` (admin auth) accepts the PIHC `.pkg`. The web
36+
realm default is `admin`/`admin` (confirmed: `getsysinfo.cgi` answered with
37+
those). The vendor flow is `upgrade.cgi` → in-binary decrypt+stage
38+
(`SysUpdateEx`) → `/mnt/mtd/ipc/upgrade` (a 9484-byte ARM ELF, not a script)
39+
→ `/mnt/mtd/ipc/flash_upg.sh boot.img bootarg.img kernel.img rootfs.img
40+
ipc.img upgrade.zip`.
41+
* **PIHC container format:** 512-byte header (`PIHC` magic 0x43484950, type
42+
4098, six component lengths, six `MD5(component||"IPCAM")` hex strings),
43+
then concatenated component bytes. Integrity is MD5-only, no signature.
44+
Encoded in `pihc_pack.py` with unit tests.
45+
46+
## Assumed / NOT verified — validate before trusting
47+
48+
* **That the `.pkg` flashes and boots OpenIPC.** No hardware flash done.
49+
* **`flash_upg.sh` exact behaviour.** It is 416 bytes; we did not capture its
50+
contents (telnet on the reference unit was unstable). The component→mtd
51+
mapping (boot→mtd0 … ipc→mtd4) is inferred from the argument order and the
52+
staged filenames, not confirmed by reading the script.
53+
* **Overlay partition.** The build relabels mtd4 `ipc``rootfs_data` in the
54+
new env so OpenIPC's overlay finds a writable area. Whether OpenIPC's
55+
`firstboot`/overlay actually adopts it (and formats the old jffs2) is
56+
untested.
57+
* **rootfs fits.** OpenIPC's `hi3516cv6xx` ultimate squashfs may exceed the
58+
3456 K rootfs partition (the vendor's own rootfs already fills it). The
59+
build script aborts if it overflows; a lite build or repartition may be
60+
needed. OpenIPC currently publishes only the ultimate flavor for this SoC.
61+
* **Sensor.** Unknown — `sensor.conf` on the reference unit was not read.
62+
No sensor is baked into the build; set it post-flash with
63+
`fw_setenv sensor <name>`.
64+
* **Root shell password.** Not publicly known and not a fixed vendor default;
65+
obtaining a shell on a stock unit needs UART or a u-boot env override.
66+
67+
## Recovery
68+
69+
The build omits the `boot.img` component, so the vendor u-boot is preserved
70+
and TFTP recovery via UART remains available. This is the intended safety net
71+
while the conversion is still unverified.

.github/workflows/camhi.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: camhi to OpenIPC
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
build:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: setenv
11+
run: |
12+
TAG_NAME="camhi"
13+
RELEASE_NAME="OpenIPC Firmware"
14+
PRERELEASE=true
15+
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
16+
echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_ENV
17+
echo "PRERELEASE=$PRERELEASE" >> $GITHUB_ENV
18+
19+
- name: Install dependencies
20+
run: |
21+
sudo apt-get update
22+
sudo apt-get install -y u-boot-tools python3 zip
23+
24+
- name: Checkout
25+
uses: actions/checkout@v4
26+
27+
- name: Self-test PIHC packer
28+
run: |
29+
cd "$GITHUB_WORKSPACE"
30+
python3 tests/test_pihc_pack.py
31+
32+
- name: Fetch OpenIPC release
33+
run: |
34+
mkdir -p "$GITHUB_WORKSPACE/.github/workflows/dl" && cd "$_"
35+
# Hi3516CV610 is published under the cv6xx tag in OpenIPC/firmware's
36+
# `latest` release. Only the `ultimate` flavor is currently built.
37+
wget -q https://github.com/OpenIPC/firmware/releases/download/latest/openipc.hi3516cv6xx-nor-ultimate.tgz
38+
39+
- name: Build firmware
40+
run: |
41+
cd "$GITHUB_WORKSPACE/.github/workflows/dl"
42+
export OUTPUTDIR="$GITHUB_WORKSPACE/out"
43+
mkdir -p "$OUTPUTDIR"
44+
# A single generic build. Sensor identity is left to OpenIPC's
45+
# boot-time autodetection or a manual post-flash `fw_setenv sensor`.
46+
SOC=hi3516cv6xx RELEASE=ultimate HARDWARE=CAMHI_CV610 \
47+
bash "$GITHUB_WORKSPACE/.github/workflows/_camhi2oipc.sh"
48+
49+
- name: Upload Release
50+
uses: softprops/action-gh-release@v1
51+
with:
52+
tag_name: ${{ env.TAG_NAME }}
53+
name: ${{ env.RELEASE_NAME }}
54+
prerelease: ${{ env.PRERELEASE }}
55+
files: out/*
56+
env:
57+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)