Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
__pycache__/
dl/
out/
workdir/
*.pkg
138 changes: 138 additions & 0 deletions .github/workflows/_camhi2oipc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#!/bin/bash

#####
## Build a PIHC-format firmware that the camhi (Hi3516CV610) vendor's
## `upgrade.cgi` admin endpoint accepts as a normal firmware update.
##
## STATUS: UNTESTED ON HARDWARE. The container format is reverse-engineered
## and the packer is unit-tested, but no end-to-end flash has been performed.
## See camhi-NOTES.md for what is verified vs assumed.
##
## Inputs (env vars):
## SOC — OpenIPC SoC tag (default hi3516cv6xx — the only Hi3516CV610
## build OpenIPC currently publishes).
## RELEASE — OpenIPC flavor (default ultimate; lite is not published for
## this SoC, and may be required if rootfs does not fit — see the
## size check below).
## HARDWARE — Hardware name stamped into u-boot env (cosmetic).
## OSMEM — kernel "mem=" value (default 46400KB — the vendor's own value).
## TOTALMEM — totalmem env (default 64M).
##
## Dependencies: u-boot-tools (mkenvimage) OR the bundled uboot_env.py fallback;
## python3 (>= 3.8); tar.
#####

set -euo pipefail

SOC="${SOC:-hi3516cv6xx}"
RELEASE="${RELEASE:-ultimate}"
HARDWARE="${HARDWARE:-CAMHI_CV610}"
OSMEM="${OSMEM:-46400KB}"
TOTALMEM="${TOTALMEM:-64M}"

WORKDIR="${WORKDIR:-workdir}"
OUTPUTDIR="${OUTPUTDIR:-..}"
TARBALL="${TARBALL:-openipc.${SOC}-nor-${RELEASE}.tgz}"
HERE="$(cd "$(dirname "$0")" && pwd)"

# Partition geometry for the Hi3516CV610 camhi reference device. Verified from
# the live device on 2026-06-06 (/proc/mtd + /proc/cmdline):
# mtdparts=sfc:192K(boot),64K(env),2112K(kernel),3456K(rootfs),10560K(ipc)
# 16 MB SPI NOR, 64 KB erase blocks.
ADDR_KERNEL_START="0x00040000"
ADDR_ROOTFS_START="0x00250000"
ENV_SIZE="0x10000" # 64 KB env partition (mtd1)
KERNEL_PART_SIZE=$((0x250000 - 0x40000)) # 2,162,688 bytes (mtd2)
ROOTFS_PART_SIZE=$((0x5B0000 - 0x250000)) # 3,538,944 bytes (mtd3)

mkdir -p "${WORKDIR}" "${OUTPUTDIR}"

tar -xvz -f "${TARBALL}" -C "${WORKDIR}/" --exclude "*.md5sum" || {
echo "Error: cannot untar ${TARBALL}." >&2
exit 1
}

KERNEL_SRC=$(ls "${WORKDIR}"/uImage* 2>/dev/null | head -n1 || true)
ROOTFS_SRC=$(ls "${WORKDIR}"/rootfs.squashfs* 2>/dev/null | head -n1 || true)
[[ -z "${KERNEL_SRC}" || -z "${ROOTFS_SRC}" ]] && {
echo "Error: ${TARBALL} missing uImage* or rootfs.squashfs*." >&2
exit 1
}

# Size guard. The reference vendor rootfs already fills the 3456 KB partition
# (df reported /dev/root 3.3M used 3.3M 100%), so OpenIPC's squashfs has very
# little headroom here. If the ultimate build overflows, a lite build or a
# repartition is required — fail loudly rather than produce a brick.
KERNEL_SIZE=$(wc -c <"${KERNEL_SRC}")
ROOTFS_SIZE=$(wc -c <"${ROOTFS_SRC}")
if (( KERNEL_SIZE > KERNEL_PART_SIZE )); then
echo "Error: kernel ${KERNEL_SIZE} > kernel partition ${KERNEL_PART_SIZE}." >&2
exit 1
fi
if (( ROOTFS_SIZE > ROOTFS_PART_SIZE )); then
echo "Error: rootfs ${ROOTFS_SIZE} > rootfs partition ${ROOTFS_PART_SIZE}." >&2
echo " Try RELEASE=lite, or repartition (out of scope for this build)." >&2
exit 1
fi

# u-boot env (flashed to mtd1). Two deliberate changes from the vendor env
# (verified vendor /proc/cmdline shown for reference):
# 1. init=/bin/sh -> init=/init (OpenIPC runs /init as PID 1)
# 2. last partition renamed "ipc" -> "rootfs_data" so OpenIPC's overlay
# mechanism finds a writable data partition where it expects one. The
# offset/size are unchanged; only the mtdparts label differs. OpenIPC's
# `firstboot` is expected to format this overlay on first boot. UNTESTED.
# Everything else (mem, console, mtdparts geometry) is kept as the vendor set
# it, to stay in a configuration the SoC is known to boot.
cat >"${WORKDIR}/u-boot.env.txt" <<EOF
bootdelay=1
baudrate=115200
bootcmd=sf probe 0; sf read 0x42000000 ${ADDR_KERNEL_START} 0x200000; bootm 0x42000000
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)
osmem=${OSMEM}
totalmem=${TOTALMEM}
soc=${SOC}
hardware=${HARDWARE}
stdin=serial
stdout=serial
stderr=serial
EOF

if command -v mkenvimage >/dev/null 2>&1; then
mkenvimage -s "${ENV_SIZE}" -o "${WORKDIR}/bootarg.img" "${WORKDIR}/u-boot.env.txt"
else
python3 "${HERE}/uboot_env.py" -s "${ENV_SIZE}" -o "${WORKDIR}/bootarg.img" "${WORKDIR}/u-boot.env.txt"
fi

# Pad kernel/rootfs to their partition sizes with 0xFF (NOR erased state).
# LC_ALL=C forces tr into byte mode; without it the macOS BSD tr treats 0xFF
# as a multibyte UTF-8 sequence and doubles the output.
pad_to() {
local src="$1" dst="$2" size="$3" cur need
cur=$(wc -c <"${src}")
cp "${src}" "${dst}"
if (( cur < size )); then
need=$((size - cur))
dd if=/dev/zero bs=1 count="${need}" 2>/dev/null | LC_ALL=C tr '\000' '\377' >>"${dst}"
fi
}
pad_to "${KERNEL_SRC}" "${WORKDIR}/kernel.img" "${KERNEL_PART_SIZE}"
pad_to "${ROOTFS_SRC}" "${WORKDIR}/rootfs.img" "${ROOTFS_PART_SIZE}"

# boot.img is intentionally omitted: leaving the vendor u-boot in place keeps
# TFTP recovery available if the conversion goes wrong.
# ipc.img is intentionally omitted: the vendor app store (mtd4) is not written;
# OpenIPC reformats that space as its overlay on first boot.
# upgrade.zip stays empty (pihc_pack.py supplies a 22-byte EOCD-only ZIP,
# below the 1024-byte Hichip-cipher threshold).

OUTPUT="${OUTPUTDIR}/openipc.${SOC}.${HARDWARE}.pkg"
python3 "${HERE}/pihc_pack.py" \
--bootarg "${WORKDIR}/bootarg.img" \
--kernel "${WORKDIR}/kernel.img" \
--rootfs "${WORKDIR}/rootfs.img" \
--filename "openipc.pkg" \
-o "${OUTPUT}"

echo "Built ${OUTPUT}"
ls -la "${OUTPUT}"
71 changes: 71 additions & 0 deletions .github/workflows/camhi-NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# camhi (Hi3516CV610) — engineering notes

> **Status: UNTESTED on hardware.** The container format and partition
> geometry below are reverse-engineered from the vendor `ipc_server` binary
> and read from one live reference device. The packer is unit-tested. **No
> end-to-end conversion flash has been performed.** Treat the produced `.pkg`
> as a candidate to validate on a spare unit with UART, not a finished
> product.

## Verified (vendor binary + live device, 2026-06-06)

Reference device: CamHi-app camera, web banner `Server: Hipcam`,
`getsysinfo.cgi` returns `devid="IPCAM"`, CGI prefix `/cgi-bin/hi3510/`.

* **SoC:** Hisilicon **Hi3516CV610**. Vendor MPP banner
`HI3516CV610_MPP_V1.0.1.0 B040 Release`; loader script `load3516cv610`.
* **Kernel:** Linux 5.10.221, musl, dual ARMv7 Cortex-A7.
* **Flash:** 16 MB SPI NOR (`sfc`), 64 KB erase blocks. From `/proc/mtd` and
`/proc/cmdline`:

| mtd | name | size | offset |
|-----|--------|-----------------|-------------|
| 0 | boot | 0x030000 (192K) | 0x000000 |
| 1 | env | 0x010000 (64K) | 0x030000 |
| 2 | kernel | 0x210000 (2112K)| 0x040000 |
| 3 | rootfs | 0x360000 (3456K)| 0x250000 |
| 4 | ipc | 0xA50000 (10560K)| 0x5B0000 |

* **Vendor bootargs:** `... root=/dev/mtdblock3 rootfstype=squashfs
mtdparts=sfc:192K(boot),64K(env),2112K(kernel),3456K(rootfs),10560K(ipc)
init=/bin/sh`. The `init=/bin/sh` is why OpenIPC needs an env override to
`init=/init`.
* **Root filesystem:** squashfs on mtd3, mounted read-only; `df` shows it
**100% full at 3.3 M** — i.e. almost no headroom in the 3456 K partition.
* **Upload path:** `upgrade.cgi` (admin auth) accepts the PIHC `.pkg`. The web
realm default is `admin`/`admin` (confirmed: `getsysinfo.cgi` answered with
those). The vendor flow is `upgrade.cgi` → in-binary decrypt+stage
(`SysUpdateEx`) → `/mnt/mtd/ipc/upgrade` (a 9484-byte ARM ELF, not a script)
→ `/mnt/mtd/ipc/flash_upg.sh boot.img bootarg.img kernel.img rootfs.img
ipc.img upgrade.zip`.
* **PIHC container format:** 512-byte header (`PIHC` magic 0x43484950, type
4098, six component lengths, six `MD5(component||"IPCAM")` hex strings),
then concatenated component bytes. Integrity is MD5-only, no signature.
Encoded in `pihc_pack.py` with unit tests.

## Assumed / NOT verified — validate before trusting

* **That the `.pkg` flashes and boots OpenIPC.** No hardware flash done.
* **`flash_upg.sh` exact behaviour.** It is 416 bytes; we did not capture its
contents (telnet on the reference unit was unstable). The component→mtd
mapping (boot→mtd0 … ipc→mtd4) is inferred from the argument order and the
staged filenames, not confirmed by reading the script.
* **Overlay partition.** The build relabels mtd4 `ipc` → `rootfs_data` in the
new env so OpenIPC's overlay finds a writable area. Whether OpenIPC's
`firstboot`/overlay actually adopts it (and formats the old jffs2) is
untested.
* **rootfs fits.** OpenIPC's `hi3516cv6xx` ultimate squashfs may exceed the
3456 K rootfs partition (the vendor's own rootfs already fills it). The
build script aborts if it overflows; a lite build or repartition may be
needed. OpenIPC currently publishes only the ultimate flavor for this SoC.
* **Sensor.** Unknown — `sensor.conf` on the reference unit was not read.
No sensor is baked into the build; set it post-flash with
`fw_setenv sensor <name>`.
* **Root shell password.** Not publicly known and not a fixed vendor default;
obtaining a shell on a stock unit needs UART or a u-boot env override.

## Recovery

The build omits the `boot.img` component, so the vendor u-boot is preserved
and TFTP recovery via UART remains available. This is the intended safety net
while the conversion is still unverified.
57 changes: 57 additions & 0 deletions .github/workflows/camhi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: camhi to OpenIPC

on:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: setenv
run: |
TAG_NAME="camhi"
RELEASE_NAME="OpenIPC Firmware"
PRERELEASE=true
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
echo "RELEASE_NAME=$RELEASE_NAME" >> $GITHUB_ENV
echo "PRERELEASE=$PRERELEASE" >> $GITHUB_ENV

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y u-boot-tools python3 zip

- name: Checkout
uses: actions/checkout@v4

- name: Self-test PIHC packer
run: |
cd "$GITHUB_WORKSPACE"
python3 tests/test_pihc_pack.py

- name: Fetch OpenIPC release
run: |
mkdir -p "$GITHUB_WORKSPACE/.github/workflows/dl" && cd "$_"
# Hi3516CV610 is published under the cv6xx tag in OpenIPC/firmware's
# `latest` release. Only the `ultimate` flavor is currently built.
wget -q https://github.com/OpenIPC/firmware/releases/download/latest/openipc.hi3516cv6xx-nor-ultimate.tgz

- name: Build firmware
run: |
cd "$GITHUB_WORKSPACE/.github/workflows/dl"
export OUTPUTDIR="$GITHUB_WORKSPACE/out"
mkdir -p "$OUTPUTDIR"
# A single generic build. Sensor identity is left to OpenIPC's
# boot-time autodetection or a manual post-flash `fw_setenv sensor`.
SOC=hi3516cv6xx RELEASE=ultimate HARDWARE=CAMHI_CV610 \
bash "$GITHUB_WORKSPACE/.github/workflows/_camhi2oipc.sh"

- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ env.TAG_NAME }}
name: ${{ env.RELEASE_NAME }}
prerelease: ${{ env.PRERELEASE }}
files: out/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Loading
Loading