Modular Capability Detection System
The capability detection system uses a pluggable architecture where each capability is a self-contained module implementing the CapabilityDetector interface.
Architecture
Core Interface
type CapabilityDetector interface {
Name() string // "color", "copier", "duplex", etc.
Detect(evidence) float64 // Returns confidence 0.0-1.0
Threshold() float64 // Minimum confidence (default 0.7)
}
Registry Pattern
registry := NewCapabilityRegistry()
// Built-in detectors auto-registered:
// - PrinterDetector
// - ColorDetector
// - MonoDetector
// - CopierDetector
// - ScannerDetector
// - FaxDetector
// - DuplexDetector
// Add custom detector
registry.Register(&CustomDetector{})
// Detect all capabilities
caps := registry.DetectAll(evidence)
Capability Modules
Each capability is in its own file: capability_<name>.go
1. Printer (capability_printer.go)
- Evidence: Serial number, Printer-MIB OIDs, printer ports, vendor
- Strong: Serial (0.5), Printer-MIB (0.3)
- Weak: Open ports (0.1), vendor match (0.1)
2. Color (capability_color.go)
- Evidence: Colorant names, color page counter, model keywords, consumable count
- Strong: CMY colorants (0.9), color pages > 0 (0.8)
- Medium: Model has “color” (0.6)
- Weak: 4+ consumables (0.3)
3. Mono (capability_mono.go)
- Evidence: Only black colorants, model keywords, low consumable count
- Strong: Only black colorant (0.9)
- Medium: Model has “mono” (0.7)
- Weak: 1-2 consumables (0.3)
- Note: Mutually exclusive with color
4. Copier (capability_copier.go)
- Evidence: Copy page counter, scan counters, MFP keywords, ADF
- Strong: Copy counter > 0 (0.9), counter exists (0.7)
- Medium: Has scan counters (0.5), “MFP” in model (0.6)
- Weak: Has ADF (0.3)
5. Scanner (capability_scanner.go)
- Evidence: Scan counters, scanner OID, model keywords, ADF
- Strong: Scan counter > 0 (0.9), counter exists (0.6)
- Medium: Scanner OID (0.5), “scanner” in model (0.6)
- Special: Boost confidence if printer_confidence < 0.3 (standalone scanner)
6. Fax (capability_fax.go)
- Evidence: Fax page counter, fax scan counters, model keywords, modem interface
- Strong: Fax counter > 0 (0.9), counter exists (0.6)
- Medium: Fax scan counters (0.5), “fax” in model (0.4)
- Weak: Modem/PSTN interface (0.3)
7. Duplex (capability_duplex.go)
- Evidence: Duplex counter, duplex unit, model suffix, keywords
- Strong: Duplex counter > 0 (0.9), counter exists (0.6)
- Medium: Duplex unit (0.7), “dn”/“dw” suffix (0.6)
- Weak: “duplex” keyword (0.5)
Usage Example
// Prepare evidence
evidence := &DetectionEvidence{
PDUs: snmpResults,
SysDescr: "HP LaserJet Pro M479fdw",
SysOID: "1.3.6.1.4.1.11.2.3.9.1",
Vendor: "HP",
Model: "LaserJet Pro M479fdw",
Serial: "JPBHM12345",
OpenPorts: []int{9100, 80, 443, 631},
}
// Detect capabilities
registry := NewCapabilityRegistry()
caps := registry.DetectAll(evidence)
// Results
fmt.Printf("Printer: %.2f (%v)\n", caps.Scores["printer"], caps.IsPrinter)
fmt.Printf("Color: %.2f (%v)\n", caps.Scores["color"], caps.IsColor)
fmt.Printf("Copier: %.2f (%v)\n", caps.Scores["copier"], caps.IsCopier)
fmt.Printf("Device Type: %s\n", caps.DeviceType)
// Output:
// Printer: 1.00 (true)
// Color: 0.95 (true)
// Copier: 0.90 (true)
// Device Type: Color MFP
Adding Custom Detectors
Example: Network Fax Detector
type NetworkFaxDetector struct{}
func (d *NetworkFaxDetector) Name() string {
return "network_fax"
}
func (d *NetworkFaxDetector) Threshold() float64 {
return 0.6 // Lower threshold
}
func (d *NetworkFaxDetector) Detect(evidence *DetectionEvidence) float64 {
score := 0.0
// Check if regular fax capability exists
if faxScore, exists := evidence.Capabilities["fax"]; exists && faxScore > 0.5 {
score += 0.5
}
// Check for network fax OIDs (HP Network Fax)
networkFaxOIDs := []string{
"1.3.6.1.4.1.11.2.4.3.3.0", // HP Network Fax enabled
}
if HasAnyOID(evidence.PDUs, networkFaxOIDs) {
score += 0.7
}
// Check model for "network fax" keyword
if ContainsAny(evidence.Model, []string{"network fax", "lan fax"}) {
score += 0.4
}
return Min(score, 1.0)
}
// Register custom detector
registry := NewCapabilityRegistry()
registry.Register(&NetworkFaxDetector{})
Benefits of Modular Design
1. Testability
Each detector can be unit tested independently:
func TestColorDetector(t *testing.T) {
detector := &ColorDetector{}
evidence := &DetectionEvidence{
Model: "HP Color LaserJet Pro M479fdw",
PDUs: mockCMYKColorants(),
}
score := detector.Detect(evidence)
if score < 0.9 {
t.Errorf("Expected high confidence for color device")
}
}
2. Extensibility
Add new capabilities without modifying core code:
WirelessDetector- WiFi capabilityNFC- Near-field communicationCloudPrintDetector- Cloud printing supportSecurePrintDetector- PIN/badge release printing
3. Maintainability
Each file is focused on one capability (~100 lines):
- Easy to understand
- Clear responsibility
- Independent changes
- No side effects
4. Customization
Users can override thresholds or add vendor-specific detectors:
// Lower threshold for duplex (more lenient)
type CustomDuplexDetector struct {
DuplexDetector
}
func (d *CustomDuplexDetector) Threshold() float64 {
return 0.5 // Lower than default 0.7
}
registry.Register(&CustomDuplexDetector{})
5. Cross-Referencing
Detectors can use results from other detectors:
// In ScannerDetector:
if printerScore, exists := evidence.Capabilities["printer"]; exists {
if printerScore < 0.3 && score > 0.5 {
score += 0.2 // Likely standalone scanner
}
}
Testing Strategy
Unit Tests (per detector)
// capability_color_test.go
func TestColorDetector_CMYKColorants(t *testing.T) { }
func TestColorDetector_ColorPageCounter(t *testing.T) { }
func TestColorDetector_ModelKeywords(t *testing.T) { }
func TestColorDetector_MonoDevice(t *testing.T) { } // Should score low
Integration Tests
// capabilities_test.go
func TestCapabilityRegistry_HPColorMFP(t *testing.T) {
evidence := loadRealDeviceData("hp_m479fdw.json")
registry := NewCapabilityRegistry()
caps := registry.DetectAll(evidence)
assert.True(t, caps.IsPrinter)
assert.True(t, caps.IsColor)
assert.True(t, caps.IsCopier)
assert.Equal(t, "Color MFP", caps.DeviceType)
}
Regression Tests
func TestCapabilityDetection_BackwardCompatibility(t *testing.T) {
// Ensure existing printer detection still works
// after capability system addition
}
File Structure
scanner/
├── capabilities.go # Core interface & registry
├── capability_printer.go # Printer detection
├── capability_color.go # Color detection
├── capability_mono.go # Monochrome detection
├── capability_copier.go # Copier detection
├── capability_scanner.go # Scanner detection
├── capability_fax.go # Fax detection
├── capability_duplex.go # Duplex detection
├── capabilities_test.go # Integration tests
├── capability_printer_test.go # Printer unit tests
├── capability_color_test.go # Color unit tests
└── ... (one test file per detector)
Performance
Detection Cost
- Per detector: 10-50μs (mostly map lookups)
- All 7 detectors: < 500μs per device
- Overhead: Negligible compared to SNMP query time (100ms-2s)
Optimization
Detectors run sequentially, allowing cross-referencing. Could parallelize if needed:
func (r *CapabilityRegistry) DetectAllParallel(evidence) DeviceCapabilities {
results := make(chan result, len(r.detectors))
for _, detector := range r.detectors {
go func(d CapabilityDetector) {
results <- result{d.Name(), d.Detect(evidence)}
}(detector)
}
// Collect results...
}
Next Steps
- Create unit tests for each detector
- Gather real-world data from various devices
- Tune confidence scores based on test results
- Add vendor-specific detectors (HP-specific, Canon-specific, etc.)
- Integrate with storage (add capabilities column to devices table)
- Update UI (show capability badges, conditional metrics)
Metrics Filtering System
The capability detection system integrates with metrics filtering to control which metrics are queried, parsed, and displayed based on device capabilities.
Three-Layer Optimization
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Scanner OID Selection │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ QueryDeviceWithCapabilities() │ │
│ │ • Detects capabilities during QueryFull │ │
│ │ • Filters OIDs using GetCapabilityAwareMetricsOIDs()│ │
│ │ • 30-71% fewer SNMP queries on targeted devices │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Metrics Parsing (GetRelevantMetrics) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GetRelevantMetrics(caps) │ │
│ │ • Filters 40+ metric definitions │ │
│ │ • Checks RequiresAll/RequiresAny/ExcludesAny │ │
│ │ • Returns only metrics applicable to device │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Layer 3: UI Display (GetRelevantMetricsByCategory) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ GetRelevantMetricsByCategory(caps, category) │ │
│ │ • Groups metrics by category (Page, Supplies, etc.) │ │
│ │ • Hides irrelevant metrics from user interface │ │
│ │ • Shows capability-appropriate data only │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Metric Definition Structure
Each metric is defined with capability requirements:
type MetricDefinition struct {
Name string // "color_pages", "toner_cyan", etc.
Category string // "PageCounters", "Supplies", etc.
RequiresAll []string // All must be true: ["printer", "color"]
RequiresAny []string // At least one true: ["copier", "scanner"]
ExcludesAny []string // None can be true: ["mono"]
}
Filtering Logic
// Example: Color-specific metrics
{
Name: "color_pages",
Category: "PageCounters",
RequiresAll: []string{"printer", "color"}, // Must be color printer
ExcludesAny: []string{"mono"}, // NOT mono
}
// Example: MFP-specific metrics
{
Name: "copy_total",
Category: "PageCounters",
RequiresAny: []string{"copier"}, // Must have copier
}
// Example: Supply metrics
{
Name: "toner_cyan",
Category: "Supplies",
RequiresAll: []string{"color"}, // Must be color
ExcludesAny: []string{"mono"}, // NOT mono
}
Usage Example
Basic Filtering
// After capability detection
caps := registry.DetectAll(evidence)
// Get all relevant metrics
relevant := GetRelevantMetrics(caps)
fmt.Printf("Device has %d relevant metrics\n", len(relevant))
// Filter by category
pageMetrics := GetRelevantMetricsByCategory(caps, "PageCounters")
supplyMetrics := GetRelevantMetricsByCategory(caps, "Supplies")
// Check individual metric
if IsMetricRelevant("color_pages", caps) {
// Parse and display color page counter
}
Mono Printer Example
caps := DeviceCapabilities{
IsPrinter: true,
IsMono: true,
IsColor: false,
HasDuplex: true,
}
relevant := GetRelevantMetrics(caps)
// Returns: total_pages, mono_pages, duplex_pages, toner_black, drum_black
// Excludes: color_pages, toner_cyan, toner_magenta, toner_yellow
Color MFP Example
caps := DeviceCapabilities{
IsPrinter: true,
IsColor: true,
IsCopier: true,
IsScanner: true,
}
relevant := GetRelevantMetrics(caps)
// Returns: total_pages, color_pages, copy_total, scan_total,
// toner_cyan, toner_magenta, toner_yellow, toner_black
Metric Categories
The system defines metrics across 4 categories:
1. PageCounters (20 metrics)
- Total:
total_pages,total_impressions - Color/Mono:
color_pages,mono_pages - Function:
copy_total,scan_total,fax_total - Sided:
simplex_pages,duplex_pages - Color Detail:
color_copy,mono_copy,color_print,mono_print
2. Supplies (9 metrics)
- Toner:
toner_black,toner_cyan,toner_magenta,toner_yellow - Drums:
drum_black,drum_cyan,drum_magenta,drum_yellow - Maintenance:
maintenance_kit
3. Usage (3 metrics)
- Utilization:
uptime_hours,energy_kwh,duty_cycle_percent
4. Status (2 metrics)
- State:
device_status,alert_count
Benefits
1. Performance
// Without capabilities:
oids := []string{...100 OIDs...}
// Query all, parse all, store all
// With capabilities:
oids := vendor.GetCapabilityAwareMetricsOIDs(caps)
// Query 30-40 OIDs (30-71% reduction)
// Parse only relevant metrics
// Store only applicable data
2. User Experience
// UI displays only relevant metrics
if IsMetricRelevant("color_pages", caps) {
renderMetric("Color Pages", colorPages)
}
// Group by category for clean layout
pageMetrics := GetRelevantMetricsByCategory(caps, "PageCounters")
for _, metric := range pageMetrics {
renderMetric(metric.Name, values[metric.Name])
}
3. Data Quality
- No confusing zero values for non-existent features
- Accurate device representation
- Cleaner database (only store applicable metrics)
Integration Example
// In vendor's ExtractMetrics method:
func (h *HPModule) ExtractMetrics(snmpData, caps) Metrics {
relevant := GetRelevantMetrics(caps)
metrics := Metrics{}
for _, metric := range relevant {
switch metric.Name {
case "color_pages":
if caps.IsColor {
metrics.ColorPages = extractColorPages(snmpData)
}
case "toner_cyan":
if caps.IsColor {
metrics.TonerCyan = extractTonerLevel(snmpData, "cyan")
}
// ... only parse relevant metrics
}
}
return metrics
}
Testing
All metric filtering logic is tested in capabilities_test.go:
// Test mono printer filtering
func TestGetRelevantMetrics_MonoPrinter(t *testing.T) {
caps := DeviceCapabilities{IsPrinter: true, IsMono: true}
metrics := GetRelevantMetrics(caps)
// Should have mono metrics
assertContains(t, metrics, "mono_pages", "toner_black")
// Should NOT have color metrics
assertNotContains(t, metrics, "color_pages", "toner_cyan")
}
// Test color MFP filtering
func TestGetRelevantMetrics_ColorMFP(t *testing.T) {
caps := DeviceCapabilities{IsPrinter: true, IsColor: true, IsCopier: true}
metrics := GetRelevantMetrics(caps)
// Should have printer, color, and copier metrics
assertContains(t, metrics, "total_pages", "color_pages", "copy_total")
}
Related Documentation
- Scanner Module - SNMP querying and vendor profiles
- Capability Integration Guide (legacy document unavailable) - Usage examples
- Storage Module - Database persistence