# Add Probe Detector Strix

> Add a new device type detector to the Strix probe system. Covers adding new probers, result types, and detector functions.

- Skill: `eduard256/add-probe-detector-strix` (Agent Skill)
- Install (CLI): `npx skillmds@latest add eduard256/add-probe-detector-strix`
- Raw SKILL.md: https://api.skillmd.com/api/skills/eduard256/add-probe-detector-strix/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: eduard256 (https://skillmd.com/u/eduard256)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/eduard256/add-probe-detector-strix

---


# Add Probe Detector to Strix

You are adding a new device type detector to the Strix probe system. The probe system runs when a user enters an IP address -- it discovers what's at that IP and determines the device type. The device type drives the frontend flow.

The detector name is provided as argument (e.g. `/add_probe_detector_strix onvif`). If no argument, use AskUserQuestion to ask which detector to add.

## Repository

- Strix: `/home/user/Strix`
- go2rtc (reference): `/home/user/go2rtc`

---

## STEP 0: Understand the probe system

Before writing anything, read these files COMPLETELY:

```
/home/user/Strix/internal/probe/probe.go   -- glue: Init(), runProbe(), detectors, API handler
/home/user/Strix/pkg/probe/models.go       -- all data structures (Response, Probes, result types)
/home/user/Strix/pkg/probe/ping.go         -- prober example: ICMP ping
/home/user/Strix/pkg/probe/ports.go        -- prober example: TCP port scan
/home/user/Strix/pkg/probe/arp.go          -- prober example: ARP lookup
/home/user/Strix/pkg/probe/dns.go          -- prober example: reverse DNS
/home/user/Strix/pkg/probe/http.go         -- prober example: HTTP HEAD request
/home/user/Strix/pkg/probe/mdns.go         -- prober example: HomeKit mDNS query
/home/user/Strix/pkg/probe/oui.go          -- prober example: OUI vendor lookup
```

Read ALL of them. Every prober is different. Understand the full picture before proceeding.

### How the probe system works

The probe has three layers:

**Layer 1: Probers** (`pkg/probe/`)

Pure functions that gather raw data about an IP address. Each runs in parallel with a shared 100ms timeout context. They do NOT interpret results -- just collect facts.

Current probers:
- `Ping()` -- ICMP echo, returns latency
- `ScanPorts()` -- TCP connect to all known camera ports, returns open ports
- `ReverseDNS()` -- reverse DNS lookup, returns hostname
- `LookupARP()` -- reads /proc/net/arp, returns MAC address
- `LookupOUI()` -- looks up MAC prefix in SQLite, returns vendor name
- `ProbeHTTP()` -- HTTP HEAD to ports 80/8080, returns status + server header
- `QueryHAP()` -- mDNS query for HomeKit Accessory Protocol, returns device info

Every prober writes its result into `resp.Probes.{Name}` via mutex.

**Layer 2: Detectors** (`internal/probe/probe.go`)

Functions registered in the `detectors` slice. They run AFTER all probers complete. Each detector receives the full `*probe.Response` with all probe results and returns a device type string (or empty string to pass).

```go
var detectors []func(*probe.Response) string
```

Detectors are checked in order. First non-empty result wins and sets `resp.Type`.

Default type is `"standard"`. If device is unreachable, type is `"unreachable"`.

**Layer 3: API** (`internal/probe/probe.go`)

`GET /api/probe?ip=192.168.1.100` returns the full Response JSON. The frontend uses `type` field to decide which UI flow to show.

### Data flow

```
IP address
  |
  v
[All probers run in parallel, 100ms timeout]
  |
  v
probe.Response filled with results
  |
  v
[Detectors run in order on the Response]
  |
  v
resp.Type = "homekit" | "standard" | "unreachable" | ...
  |
  v
JSON response to frontend
```

### API response example

```json
{
  "ip": "192.168.1.100",
  "reachable": true,
  "latency_ms": 2.5,
  "type": "homekit",
  "probes": {
    "ping": {"latency_ms": 2.5},
    "ports": {"open": [80, 554, 5353]},
    "dns": {"hostname": "camera.local"},
    "arp": {"mac": "C0:56:E3:AA:BB:CC", "vendor": "Hikvision"},
    "mdns": {
      "name": "My Camera",
      "device_id": "AA:BB:CC:DD:EE:FF",
      "model": "Camera 1080p",
      "category": "camera",
      "paired": false,
      "port": 80
    },
    "http": {"port": 80, "status_code": 200, "server": "nginx"}
  }
}
```

---

## STEP 1: Determine what you need

Use AskUserQuestion to discuss with the user. There are two scenarios:

### Scenario A: Detector only (using existing probe data)

The detector can determine device type from data already collected by existing probers. No new prober needed.

Examples:
- Detect ONVIF cameras by checking if port 80 is open and HTTP server header contains "onvif" or specific vendor strings
- Detect specific brands by ARP vendor name
- Detect UPnP devices by checking specific open ports

In this case: skip to STEP 3.

### Scenario B: New prober + detector

Need to collect new data that existing probers don't provide. Requires adding a new prober to `pkg/probe/` and a new result type to `models.go`.

Examples:
- ONVIF discovery (send ONVIF GetCapabilities request)
- UPnP SSDP discovery
- Specific protocol handshake

In this case: proceed to STEP 2.

---

## STEP 2: Add new prober (Scenario B only)

### 2a: Add result type to models.go

Edit `/home/user/Strix/pkg/probe/models.go`:

1. Add new result struct:
```go
type {Name}Result struct {
    // fields specific to this probe
}
```

2. Add field to `Probes` struct:
```go
type Probes struct {
    Ping  *PingResult  `json:"ping"`
    Ports *PortsResult `json:"ports"`
    DNS   *DNSResult   `json:"dns"`
    ARP   *ARPResult   `json:"arp"`
    MDNS  *MDNSResult  `json:"mdns"`
    HTTP  *HTTPResult  `json:"http"`
    {Name} *{Name}Result `json:"{name}"`  // add here
}
```

### 2b: Write prober function

Create `/home/user/Strix/pkg/probe/{name}.go`.

Rules:
- Pure function, no app/api imports
- Takes `context.Context` and `ip string` as first params
- Returns `(*{Name}Result, error)`
- Respects context deadline (timeout comes from runProbe)
- Returns `nil, nil` when device doesn't support this (NOT an error)
- Keep it simple -- one file, one function

Pattern:
```go
package probe

import "context"

func Probe{Name}(ctx context.Context, ip string) (*{Name}Result, error) {
    // respect context deadline
    deadline, ok := ctx.Deadline()
    if !ok {
        // set sensible default
    }

    // do the probe work...

    // not supported = nil, nil (not an error)
    // found = &{Name}Result{...}, nil
    // actual error = nil, err
}
```

### 2c: Wire prober into runProbe

Edit `/home/user/Strix/internal/probe/probe.go`, add to `runProbe()` alongside other probers:

```go
run(func() {
    r, _ := probe.Probe{Name}(ctx, ip)
    mu.Lock()
    resp.Probes.{Name} = r
    mu.Unlock()
})
```

All probers run in parallel inside the same `run()` pattern. The mutex protects writes to `resp.Probes`.

---

## STEP 3: Add detector function

Edit `/home/user/Strix/internal/probe/probe.go`, add detector in `Init()`:

```go
// {Name} detector
detectors = append(detectors, func(r *probe.Response) string {
    // check probe results to determine device type
    // return type string or "" to pass
    if r.Probes.{Something} != nil && {condition} {
        return "{type_name}"
    }
    return ""
})
```

### Detector rules

1. Return a SHORT type string: `"homekit"`, `"onvif"`, `"tapo"`, etc.
2. Return `""` (empty) to pass to the next detector
3. Detectors run in order -- put more specific detectors BEFORE generic ones
4. A detector can use ANY combination of probe results (ports, HTTP, ARP, mDNS, custom)
5. Don't do network I/O in detectors -- all data should come from probers

### Type string convention

The type string is used by the frontend to select UI flow:
- `"unreachable"` -- device not found (set automatically, don't return this)
- `"standard"` -- default, normal camera (set automatically if no detector matches)
- `"homekit"` -- Apple HomeKit device
- Custom types: lowercase, one word, matches the protocol/brand name

---

## STEP 4: Build and test

```bash
cd /home/user/Strix
go build ./...
```

If it compiles, rebuild Docker and test:

```bash
docker build -t strix:test .
docker rm -f strix
docker run -d --name strix --network host --restart unless-stopped strix:test
sleep 2

# test probe on a known device
curl -s "http://localhost:4567/api/probe?ip={DEVICE_IP}" | python3 -m json.tool
```

Verify:
1. New probe data appears in `probes` object (if new prober added)
2. `type` field correctly identifies the device
3. No errors in `docker logs strix`

---

## STEP 5: Commit and push

```bash
cd /home/user/Strix
git add -A
git commit -m "Add {name} probe detector"
git push origin develop
```

---

## CODE STYLE

### pkg/probe/ files
- One file per prober
- Pure functions, no globals, no app imports
- `context.Context` as first param for anything with I/O
- Return `nil, nil` for "not applicable" (not an error)
- Short names: `conn`, `resp`, `buf`

### internal/probe/probe.go
- Detectors are inline anonymous functions in Init()
- Keep detector logic minimal -- just check fields and return type
- If detector logic is complex (>10 lines), extract to a named function in the same file

### models.go
- All result structs in one file
- JSON tags use lowercase with underscores
- Optional fields use `omitempty`
- Pointer types for probe results (nil = not collected)

