Add probe detector skill
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
---
|
||||
name: add_probe_detector_strix
|
||||
description: Add a new device type detector to the Strix probe system. Covers adding new probers, result types, and detector functions.
|
||||
disable-model-invocation: true
|
||||
argument-hint: [detector-name]
|
||||
---
|
||||
|
||||
# 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)
|
||||
Reference in New Issue
Block a user