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:
co-authored by
Claude Opus 4.8
moontwister
parent
21de4f7b92
commit
e6d8822517
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
@@ -0,0 +1,12 @@
|
||||
CC ?= gcc
|
||||
CFLAGS ?= -O2
|
||||
|
||||
all: ptz
|
||||
|
||||
ptz: ptz.c
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f ptz
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user