Add TP-Link Kasa KC110 (HI3518EV200 + OV2735) (#99)

* Add TP-Link Kasa KC110 (HI3518EV200 + OV2735)

New device variant: hi3518ev200_ultimate_tplink-kasa-kc110.

Contents:
- kc110-board-support package: PTZ binary (mmap'd PL061), gpio-motors
  shim for the openipc web GUI, IR control script, and a boot-time
  pinmux init that writes 0x1 to the chip7/chip8 pads (0x200f00e8/ec/
  f0/fc/100/104). The OpenIPC kernel pinctrl driver does not auto-set
  these for the KC110's pan motor / IR-cut / IR-LED pads — the vendor
  firmware writes them explicitly in hn_config.sh. Without this,
  PL061 GPIODATA writes succeed silently but the pads never drive
  the ULN2803 / solenoid / LED.
- Device overlay:
    * etc/sensors/ov2735_i2c_1080p.ini — input_mode=0 (MIPI), 1-lane
      (was input_mode=4 / 4-lane in upstream cv200 osdrv, which is
      wrong for OV2735).
    * usr/bin/load_hisilicon — adds an "ov2735" case that mirrors
      ov2710_mipi (same MIPI pinmux + clock).
    * usr/share/openipc/customizer.sh — first-boot configuration:
      RTL8188FU wlandev, sensor config path, IR-cut pin 64, backlight
      pin 63, lightMonitor true (AE recovery after IR-cut toggle).
- README.md row.

Hardware notes:
- SoC: Hi3518EV200; Sensor: OV2735 (MIPI 1-lane, RAW10);
  Wi-Fi: RTL8188FU (USB); Flash: W25Q128 16 MB NOR.
- PTZ: 4-phase unipolar steppers via ULN2803 darlington array.
  Pan = vendor base-10 GPIO 81/74/73/72 = OpenIPC PL061 65/60/59/58;
  tilt = 0/1/2/44 = 0/1/2/36 (vendor's "44" is bank 4 pin 4 — i.e.
  pin 36 in OpenIPC's PL061 numbering, NOT 44).
- IR-cut filter: GPIO 64 (vendor 80) active-low; IR-LED: GPIO 63
  (vendor 77).

Status: PTZ + GPIO + IR fully functional. Video pipeline currently
needs an LD_PRELOAD shim around HI_MIPI_SET_DEV_ATTR because upstream
majestic builds combo_dev_attr_t with enIntfMode=DC / raw_data_type=0
for MIPI sensors that need RAW10. The shim is downstream-only pending
a real fix to majestic upstream; this PR delivers the rest of the
KC110 board support and will produce a fully-functional camera once
the majestic fix lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* tplink-kasa-kc110: use ov2735_mipi_1080p.ini

Follow OpenIPC/firmware #2155, which ships the OV2735 MIPI config as a
separate ov2735_mipi_1080p.ini and leaves ov2735_i2c_1080p.ini (the
parallel/DVP config) untouched.

- Rename the device's bundled sensor .ini to ov2735_mipi_1080p.ini and
  carry the full corrected MIPI config: input_mode=0 (MIPI),
  data_type=2 (RAW10), lane_id 2-lane, Input_mod=5 (MIPI),
  Mask_0=0xFFF00000. The previous bundled copy only had input_mode
  fixed (data_type/Input_mod/Mask_0 were still the wrong DVP defaults).
- Point customizer.sh's isp.sensorConfig at the new file.
- Exclude the unused parallel ov2735_i2c_1080p.ini from the image.

Verified on a TP-Link Kasa KC110 (OV2735 2-lane MIPI RAW10): clean
image, correct AE, RTSP H.264 1080p.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: moontwister <moontwister@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
moontwister
2026-06-13 22:37:11 +03:00
committed by GitHub
co-authored by Claude Opus 4.8 moontwister
parent 21de4f7b92
commit e6d8822517
14 changed files with 1366 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
config BR2_PACKAGE_KC110_BOARD_SUPPORT
bool "kc110-board-support"
depends on BR2_arm
help
Board support for the TP-Link Kasa KC110 PTZ camera (HiSilicon
Hi3518EV200 + OV2735 MIPI sensor + 24BYJ48 stepper motors via
ULN2803 darlington array).
Installs:
- ptz: C binary driving pan/tilt steppers (mmap PL061) and IR-cut /
IR-LED via vendor-base-10 GPIO numbering. Replaces the upstream
gpio-motors sysfs approach which uses different pin numbers.
- gpio-motors: small shell shim that forwards web-GUI calls
(gpio-motors X Y N) to the ptz CLI.
- ir-control.sh: convenience wrapper for IR-LED + IR-cut filter.
- S94pinmux-pan-ir: boot-time init that writes 0x1 to pinmux
registers 0x200f00e8/ec/f0/fc/100/104 for chip7/8 pads. OpenIPC
kernel pinctrl doesn't auto-set this for the KC110's pan + IR-cut
+ IR-LED pads (vendor's hn_config.sh writes them explicitly).
KC110 GPIO map (vendor base-10 numbering = OpenIPC PL061 pins):
pan: 65 / 60 / 59 / 58 (vendor 81/74/73/72)
tilt: 0 / 1 / 2 / 36 (vendor 0/1/2/44)
ir-cut: 64 (vendor 80)
ir-led: 63 (vendor 77)
Conflicts with the upstream gpio-motors package — this provides a
drop-in replacement at /usr/bin/gpio-motors that calls ptz.
+59
View File
@@ -0,0 +1,59 @@
# kc110-board-support
Buildroot package providing KC110-specific hardware support for OpenIPC. Delivers:
| File | Purpose |
|---|---|
| `/usr/bin/ptz` | C binary (mmap PL061) for pan/tilt + IR-cut + IR-LED via vendor-base-10 GPIO numbering |
| `/usr/bin/gpio-motors` | 451-byte shell shim that forwards calls to `ptz pan/tilt` (web-GUI compatible) |
| `/usr/bin/ir-control.sh` | Wrapper for IR-LED + IR-cut filter control |
| `/etc/init.d/S94pinmux-pan-ir` | Boot-time init that writes 0x1 to pinmux registers 0x200f00e8/ec/f0/fc/100/104 (KC110 chip7/8 pads) |
## Layout
```
kc110-board-support/
├── Config.in # BR2_PACKAGE_KC110_BOARD_SUPPORT
├── kc110-board-support.mk # generic-package build + install rules
├── src/ # source tree (compiled with target toolchain)
│ ├── Makefile
│ └── ptz.c # ARM-static-stripped via cross-compile
└── files/ # installed as-is (no build step)
├── etc/init.d/S94pinmux-pan-ir
└── usr/bin/{gpio-motors,ir-control.sh}
```
## Use from upstream openipc-firmware
The package lives at the repo root (`kc110-board-support/`) but must be discoverable by Buildroot inside the upstream tree via a symlink:
```
openipc/openipc-firmware/general/package/kc110-board-support → ../../../../kc110-board-support
```
The symlink is reconstructed by the setup step (below).
## Setup after `git pull` of upstream
Because `openipc/` is gitignored, the wire-in edits in the upstream tree are lost on each pull. Re-apply them from `patches/openipc-wire-in-kc110.patch`:
```bash
cd openipc/openipc-firmware
git apply ../../patches/openipc-wire-in-kc110.patch
# Re-create the symlink if it disappeared:
[ -L general/package/kc110-board-support ] || \
ln -s ../../../../kc110-board-support general/package/kc110-board-support
```
The patch adds:
- A `source` statement for the package in `general/package/Config.in`
- `BR2_PACKAGE_KC110_BOARD_SUPPORT=y` to `hi3518ev200_ultimate_defconfig` and removes `BR2_PACKAGE_GPIO_MOTORS=y` (our shim replaces upstream's gpio-motors binary)
## Build
Picked up automatically by `BOARD=hi3518ev200_ultimate make all` once the wire-in is applied. The `ptz` binary builds reproducibly — md5 `e70f78d73c91248ba044082e3f21c54b` across rebuilds.
## Conflict with upstream gpio-motors
`BR2_PACKAGE_GPIO_MOTORS` (upstream) and `BR2_PACKAGE_KC110_BOARD_SUPPORT` both install `/usr/bin/gpio-motors` and cannot be enabled simultaneously. The wire-in patch removes `BR2_PACKAGE_GPIO_MOTORS=y` from the defconfig to avoid the collision.
@@ -0,0 +1,26 @@
#!/bin/sh
# KC110 pad-pinmux: chip7/8 GPIOs used by pan motor + IR-cut + IR-LED
# need function=0x1 (per vendor hn_config.sh comments "GPIO8_0", "GPIO8_1").
# OpenIPC kernel pinctrl leaves them at default (0x0 = alternate peripheral)
# which makes PL061 GPIODATA writes succeed silently — pad never toggles.
# Stock-OpenIPC builds (and prior boot residue) can also leave wrong state
# across soft-reboot, so we force-write here regardless of current value.
start() {
# pan motor coils B/C/D (chip7 pin 2/3/4) + IR-LED (chip7 pin 7)
devmem 0x200f00e8 32 0x1
devmem 0x200f00ec 32 0x1
devmem 0x200f00f0 32 0x1
devmem 0x200f00fc 32 0x1
# IR-cut solenoid (chip8 pin 0) + pan motor coil A (chip8 pin 1)
devmem 0x200f0100 32 0x1
devmem 0x200f0104 32 0x1
}
case "$1" in
start|"") start ;;
stop) ;;
restart|reload) start ;;
*) echo "usage: $0 {start|stop|restart}"; exit 1 ;;
esac
exit 0
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
# Shim: OpenIPC motor.cgi calls "gpio-motors <pan> <tilt> <speed>"
# Translate to our ptz binary which knows the correct PL061 mapping.
pan="${1:-0}"
tilt="${2:-0}"
phase="${3:-3}"
log() { logger -t gpio-motors "$@"; }
log "called pan=$pan tilt=$tilt phase=$phase"
[ "$pan" != "0" ] && /usr/bin/ptz pan "$pan" "$phase" 2>&1 | logger -t gpio-motors
[ "$tilt" != "0" ] && /usr/bin/ptz tilt "$tilt" "$phase" 2>&1 | logger -t gpio-motors
exit 0
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
# KC110 IR-LED + IR-cut master via sysfs gpio export.
# GPIO 77 = IR-LED (active-high, vendor UCI: ledOnValue=1, ledOffValue=0)
# GPIO 80 = IR-cut master (active-low, vendor: set_ir_cut(1) writes 0)
# Modes:
# day - LED off, IR-cut filter IN (sees IR as colour-cast — bad, so cut blocks it)
# night - LED on, IR-cut filter OUT (lets IR illuminate the scene)
GPIO_LED=77
GPIO_CUT=80
export_pin() {
[ -d /sys/class/gpio/gpio$1 ] || echo $1 > /sys/class/gpio/export 2>/dev/null
echo out > /sys/class/gpio/gpio$1/direction 2>/dev/null
}
set_pin() {
echo $2 > /sys/class/gpio/gpio$1/value
}
case "$1" in
day)
export_pin $GPIO_LED
export_pin $GPIO_CUT
set_pin $GPIO_LED 0
set_pin $GPIO_CUT 0 # active-low: 0 = filter IN
;;
night)
export_pin $GPIO_LED
export_pin $GPIO_CUT
set_pin $GPIO_CUT 1 # active-low: 1 = filter OUT
sleep 1 # let mechanical filter settle
set_pin $GPIO_LED 1
;;
off)
set_pin $GPIO_LED 0 2>/dev/null
set_pin $GPIO_CUT 0 2>/dev/null
;;
status)
echo "led: $(cat /sys/class/gpio/gpio$GPIO_LED/value 2>/dev/null || echo unset)"
echo "ircut: $(cat /sys/class/gpio/gpio$GPIO_CUT/value 2>/dev/null || echo unset)"
;;
*)
echo "usage: $0 {day|night|off|status}" >&2
exit 1
;;
esac
@@ -0,0 +1,19 @@
################################################################################
#
# kc110-board-support
#
################################################################################
KC110_BOARD_SUPPORT_SITE_METHOD = local
KC110_BOARD_SUPPORT_SITE = $(KC110_BOARD_SUPPORT_PKGDIR)/src
define KC110_BOARD_SUPPORT_BUILD_CMDS
$(MAKE) CC="$(TARGET_CC)" CFLAGS="$(TARGET_CFLAGS)" -C $(@D)
endef
define KC110_BOARD_SUPPORT_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/ptz $(TARGET_DIR)/usr/bin/ptz
cp -a $(KC110_BOARD_SUPPORT_PKGDIR)/files/. $(TARGET_DIR)/
endef
$(eval $(generic-package))
+12
View File
@@ -0,0 +1,12 @@
CC ?= gcc
CFLAGS ?= -O2
all: ptz
ptz: ptz.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f ptz
.PHONY: all clean
+281
View File
@@ -0,0 +1,281 @@
/* ptz — KC110 pan/tilt/IR controller via PL061 /dev/mem (no sysfs overhead).
*
* Pin map (OpenIPC numbering on Hi3518EV200 PL061 chips 0-8).
* Derived from vendor kernel disasm of gpio_set_value @ 0xc01b854c:
* bank = vendor_gpio / 10, pin = vendor_gpio % 10
* PL061_phys = 0x20140000 + (bank << 16) + (1 << (pin + 2))
*
* pan A=8/1 B=7/4 C=7/3 D=7/2 (vendor 81 / 74 / 73 / 72)
* tilt A=0/0 B=0/1 C=0/2 D=4/4 (vendor 0 / 1 / 2 / 44)
* IR-LED bank 7 pin 7 (vendor 77, active-high)
* IR-cut bank 8 pin 0 (vendor 80, edge-triggers solenoid)
*
* State persisted in /etc/ptz_state across invocations so phase continuity
* across CLI calls keeps the motor smooth.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <errno.h>
#define PL061_BASE 0x20140000u
#define PL061_STRIDE 0x10000u
#define GPIODIR_OFF 0x400u
#define STATE_PATH "/etc/ptz_state"
#define PAN_STEPS_MAX 1934
#define PAN_DEG 170
#define TILT_STEPS_MAX 387
#define TILT_DEG 68
struct pin { uint8_t bank, pin; };
static const struct pin PAN[4] = { {8,1}, {7,4}, {7,3}, {7,2} }; /* A B C D */
static const struct pin TILT[4] = { {0,0}, {0,1}, {0,2}, {4,4} };
static const struct pin IRLED = {7,7};
static const struct pin IRCUT = {8,0};
/* Full-step (2-phase-on) sequence: AB, BC, CD, DA. */
static const uint8_t SEQ[4][4] = {
{1,1,0,0}, {0,1,1,0}, {0,0,1,1}, {1,0,0,1},
};
static void *bank_map[9];
static void *map_bank(uint8_t bank) {
if (bank_map[bank]) return bank_map[bank];
int fd = open("/dev/mem", O_RDWR | O_SYNC);
if (fd < 0) { perror("/dev/mem"); exit(1); }
off_t phys = PL061_BASE + (off_t)bank * PL061_STRIDE;
void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, phys);
close(fd);
if (p == MAP_FAILED) { fprintf(stderr,"mmap bank %u: %s\n",bank,strerror(errno)); exit(1); }
bank_map[bank] = p;
return p;
}
static void pin_set_output(struct pin p) {
volatile uint32_t *dir = (uint32_t *)((uint8_t *)map_bank(p.bank) + GPIODIR_OFF);
*dir |= (1u << p.pin);
}
static void pin_write(struct pin p, int v) {
/* PL061 GPIODATA: address = base + (mask << 2). Only bits in mask change. */
uint8_t *base = (uint8_t *)map_bank(p.bank);
uint32_t mask = 1u << p.pin;
volatile uint32_t *reg = (uint32_t *)(base + (mask << 2));
*reg = v ? mask : 0;
}
static void msleep(long ms) {
struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
/* State -------------------------------------------------------------------- */
struct state {
int pan_pos; /* 0 .. PAN_STEPS_MAX */
int tilt_pos; /* 0 .. TILT_STEPS_MAX */
int pan_phase; /* 0..3 — last SEQ index applied */
int tilt_phase;
int ircut; /* 0 or 1 */
int irled; /* 0 or 1 */
};
static void state_load(struct state *s) {
memset(s, 0, sizeof(*s));
FILE *f = fopen(STATE_PATH, "r");
if (!f) return;
fscanf(f, "pan_pos=%d tilt_pos=%d pan_phase=%d tilt_phase=%d ircut=%d irled=%d",
&s->pan_pos, &s->tilt_pos, &s->pan_phase, &s->tilt_phase,
&s->ircut, &s->irled);
fclose(f);
}
static void state_save(const struct state *s) {
FILE *f = fopen(STATE_PATH, "w");
if (!f) { perror(STATE_PATH); return; }
fprintf(f, "pan_pos=%d tilt_pos=%d pan_phase=%d tilt_phase=%d ircut=%d irled=%d\n",
s->pan_pos, s->tilt_pos, s->pan_phase, s->tilt_phase, s->ircut, s->irled);
fclose(f);
}
/* Motor drive -------------------------------------------------------------- */
static void apply_phase(const struct pin coils[4], int phase) {
const uint8_t *row = SEQ[phase & 3];
for (int i = 0; i < 4; i++) pin_write(coils[i], row[i]);
}
static void park(const struct pin coils[4]) {
for (int i = 0; i < 4; i++) pin_write(coils[i], 0);
}
static void prep_motor(const struct pin coils[4]) {
for (int i = 0; i < 4; i++) pin_set_output(coils[i]);
}
/* Drive `delta` steps. Positive = forward sequence direction. Returns new phase. */
static int drive(const struct pin coils[4], int start_phase, int delta, long phase_ms) {
int phase = start_phase;
int dir = (delta > 0) ? 1 : -1;
int n = delta * dir;
while (n--) {
phase = (phase + dir + 4) & 3;
apply_phase(coils, phase);
msleep(phase_ms);
}
return phase;
}
/* Commands ----------------------------------------------------------------- */
static int clamp(int v, int lo, int hi) {
return v < lo ? lo : (v > hi ? hi : v);
}
static int cmd_pan(struct state *s, int delta, long phase_ms) {
int target = clamp(s->pan_pos + delta, 0, PAN_STEPS_MAX);
int real = target - s->pan_pos;
prep_motor(PAN);
s->pan_phase = drive(PAN, s->pan_phase, real, phase_ms);
park(PAN);
s->pan_pos = target;
return real;
}
static int cmd_tilt(struct state *s, int delta, long phase_ms) {
int target = clamp(s->tilt_pos + delta, 0, TILT_STEPS_MAX);
int real = target - s->tilt_pos;
prep_motor(TILT);
s->tilt_phase = drive(TILT, s->tilt_phase, real, phase_ms);
park(TILT);
s->tilt_pos = target;
return real;
}
/* Home: drive backwards beyond max range, hit mechanical stop, mark as 0. */
static void cmd_home(struct state *s, long phase_ms) {
prep_motor(PAN);
drive(PAN, s->pan_phase, -(PAN_STEPS_MAX + 200), phase_ms);
park(PAN);
s->pan_phase = 0;
s->pan_pos = 0;
prep_motor(TILT);
drive(TILT, s->tilt_phase, -(TILT_STEPS_MAX + 50), phase_ms);
park(TILT);
s->tilt_phase = 0;
s->tilt_pos = 0;
}
static void cmd_irled(struct state *s, int on) {
pin_set_output(IRLED);
pin_write(IRLED, on ? 1 : 0);
s->irled = on ? 1 : 0;
}
/* IR-cut: edge-triggered solenoid. Pulse the master GPIO to flip filter state. */
static void cmd_daynight(struct state *s) {
pin_set_output(IRCUT);
int new_state = !s->ircut;
pin_write(IRCUT, new_state);
/* hold long enough for solenoid to actuate */
msleep(80);
/* leave the pin in the new state; vendor pattern is to keep the bit set */
s->ircut = new_state;
}
static void cmd_status(const struct state *s) {
printf("pan_pos=%d/%d tilt_pos=%d/%d ircut=%d irled=%d\n",
s->pan_pos, PAN_STEPS_MAX, s->tilt_pos, TILT_STEPS_MAX,
s->ircut, s->irled);
}
/* Argument parsing --------------------------------------------------------- */
static int parse_int(const char *s) {
char *end; long v = strtol(s, &end, 10);
if (*end) { fprintf(stderr,"bad integer: %s\n",s); exit(2); }
return (int)v;
}
static void usage(void) {
fprintf(stderr,
"usage:\n"
" ptz pan <+N|-N|abs N> [phase_ms]\n"
" ptz tilt <+N|-N|abs N> [phase_ms]\n"
" ptz home [phase_ms]\n"
" ptz ir on|off\n"
" ptz daynight\n"
" ptz status\n"
" ptz raw set <bank> <pin> <0|1> (debug)\n"
"default phase_ms = 3\n");
exit(2);
}
int main(int argc, char **argv) {
if (argc < 2) usage();
struct state s; state_load(&s);
if (!strcmp(argv[1], "pan")) {
if (argc < 3) usage();
long pms = (argc >= 4 && strcmp(argv[2],"abs")) ? parse_int(argv[3]) : 3;
int delta;
if (!strcmp(argv[2], "abs")) {
if (argc < 4) usage();
delta = parse_int(argv[3]) - s.pan_pos;
pms = (argc >= 5) ? parse_int(argv[4]) : 3;
} else delta = parse_int(argv[2]);
int real = cmd_pan(&s, delta, pms);
printf("pan: %d steps (requested %d, clamped) → pos=%d\n", real, delta, s.pan_pos);
}
else if (!strcmp(argv[1], "tilt")) {
if (argc < 3) usage();
long pms = (argc >= 4 && strcmp(argv[2],"abs")) ? parse_int(argv[3]) : 3;
int delta;
if (!strcmp(argv[2], "abs")) {
if (argc < 4) usage();
delta = parse_int(argv[3]) - s.tilt_pos;
pms = (argc >= 5) ? parse_int(argv[4]) : 3;
} else delta = parse_int(argv[2]);
int real = cmd_tilt(&s, delta, pms);
printf("tilt: %d steps (requested %d, clamped) → pos=%d\n", real, delta, s.tilt_pos);
}
else if (!strcmp(argv[1], "home")) {
long pms = (argc >= 3) ? parse_int(argv[2]) : 4;
cmd_home(&s, pms);
puts("home: reset to (0,0)");
}
else if (!strcmp(argv[1], "ir")) {
if (argc < 3) usage();
cmd_irled(&s, !strcmp(argv[2], "on"));
printf("ir: %s\n", s.irled ? "on" : "off");
}
else if (!strcmp(argv[1], "daynight")) {
cmd_daynight(&s);
printf("daynight: ircut=%d\n", s.ircut);
}
else if (!strcmp(argv[1], "status")) {
cmd_status(&s);
}
else if (!strcmp(argv[1], "raw") && argc >= 6 && !strcmp(argv[2],"set")) {
struct pin p = { (uint8_t)parse_int(argv[3]), (uint8_t)parse_int(argv[4]) };
pin_set_output(p);
pin_write(p, parse_int(argv[5]));
printf("raw: bank=%u pin=%u val=%s\n", p.bank, p.pin, argv[5]);
}
else usage();
state_save(&s);
return 0;
}