8.9 KiB
name, description, disable-model-invocation, argument-hint
| name | description | disable-model-invocation | argument-hint | |
|---|---|---|---|---|
| add_probe_detector_strix | Add a new device type detector to the Strix probe system. Covers adding new probers, result types, and detector functions. | true |
|
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 latencyScanPorts()-- TCP connect to all known camera ports, returns open portsReverseDNS()-- reverse DNS lookup, returns hostnameLookupARP()-- reads /proc/net/arp, returns MAC addressLookupOUI()-- looks up MAC prefix in SQLite, returns vendor nameProbeHTTP()-- HTTP HEAD to ports 80/8080, returns status + server headerQueryHAP()-- 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).
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
{
"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:
- Add new result struct:
type {Name}Result struct {
// fields specific to this probe
}
- Add field to
Probesstruct:
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.Contextandip stringas first params - Returns
(*{Name}Result, error) - Respects context deadline (timeout comes from runProbe)
- Returns
nil, nilwhen device doesn't support this (NOT an error) - Keep it simple -- one file, one function
Pattern:
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:
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():
// {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
- Return a SHORT type string:
"homekit","onvif","tapo", etc. - Return
""(empty) to pass to the next detector - Detectors run in order -- put more specific detectors BEFORE generic ones
- A detector can use ANY combination of probe results (ports, HTTP, ARP, mDNS, custom)
- 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
cd /home/user/Strix
go build ./...
If it compiles, rebuild Docker and test:
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:
- New probe data appears in
probesobject (if new prober added) typefield correctly identifies the device- No errors in
docker logs strix
STEP 5: Commit and push
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.Contextas first param for anything with I/O- Return
nil, nilfor "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)