Files
openipc-builder/package/kc110-board-support/src/ptz.c
T
e6d8822517 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>
2026-06-13 22:37:11 +03:00

282 lines
9.0 KiB
C

/* 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;
}