devices

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 22, 2026 License: MIT Imports: 6 Imported by: 0

README

devices-common

Shared foundation for the jarvisfriends devices-* family — devices-cpu, devices-gpu, devices-memory, devices-drive, devices-bios, devices-monitor, devices-npu. Each category repo implements one small interface; this module turns that into four consumption surfaces:

  1. Library — typed snapshots from Go code.
  2. CLI — one shared flag set for every devices-* binary.
  3. TUI — a live dashboard built on snap (gradient gauges, sparklines, sortable tables), mountable as a tui-base router page.
  4. Web — a JSON + SSE API and an embeddable <jf-devices> web component.

The model

Values split along two axes:

  • KindStatic values are one-time lookups (model numbers, serial numbers, specs); the engine caches them after the first read. Dynamic values change over time (usage, temperatures, clocks, power) and are re-collected on every request.
  • Levelbasic (default) is the cheap common set every platform can produce. detailed and full progressively add values that cost more to retrieve: vendor CLIs, WMI queries, privileged files. You opt into the latency instead of paying it by default.

Everything works without cgo. Builds with cgo can add more (Snapshot.CGO says which build you got); values that cannot be read are reported as Gaps with reasons, never silently dropped.

Library

import (
    devices "github.com/jarvisfriends/devices-common"
    cpu "github.com/jarvisfriends/devices-cpu"
)

e := devices.New(cpu.New())

// On demand (static answers are cached after the first call).
snap, err := e.Snapshot(ctx, devices.Detailed)

// Async one-shot.
res := <-e.Request(ctx, devices.Basic)

// Every 0.25 seconds until ctx is canceled.
for res := range e.Stream(ctx, devices.Basic, devices.Every(0.25)) {
    fmt.Println(res.Snapshot.Taken, res.Snapshot.Fields)
}

CLI

Every devices-* binary speaks the same grammar — report by default, with tui and web subcommands:

$ devices-cpu                       # one snapshot, aligned text
$ devices-cpu --level full          # everything we know how to read
$ devices-cpu --static --json       # specs/serials as JSON, once
$ devices-cpu --every 0.25          # four samples a second until Ctrl-C
$ devices-cpu --json --every 1 --count 10   # NDJSON, ten samples
$ devices-cpu tui                   # live dashboard
$ devices-cpu tui --level detailed --every 0.5
$ devices-cpu web :8080             # HTTP API + web component

TUI

tui.Run(engine, level, every) runs the standalone dashboard. The main view stays readable by design: category-wide values render as gauge and sparkline rows, and devices render as sortable tables carrying only their Basic fields — grouped by field shape, so disks and volumes get separate, cleanly aligned tables instead of one sparse union. Everything deeper is one interaction away: Enter or double-click a row to open a live detail popup that collects at Full and shows every value for that one device, tagged by level.

Mouse works throughout: click a header to sort that column, click a row to select it, double-click to open details, wheel to scroll the table under the pointer. tab moves keyboard focus between tables, / filters, l cycles the level, p pauses, esc closes the popup, q quits.

The same model embeds snap/page.Base, so it drops straight into a tui-base app:

router.NewWithOptions(router.Options{
    AppName:    "hw",
    ExtraPages: []router.RegisteredPage{{Title: "CPU", Model: tui.NewModel(e, devices.Basic, time.Second)}},
})

Web

web.New(collectors...) hosts any number of categories:

  • GET /api/devices — hosted categories
  • GET /api/devices/{category}?level=basic&kind=all|static|dynamic
  • GET /api/devices/{category}/stream?level=basic&every=0.25 — SSE
  • GET /devices.js — the web component
  • GET / — a demo page with one component per category

Embed on any site:

<script src="http://host:8080/devices.js"></script>
<jf-devices category="cpu" every="0.5" level="detailed"></jf-devices>

web.Server is an http.Handler, so it also mounts inside a larger mux.

Implementing a category

type Collector interface {
    Category() string
    Static(ctx context.Context, level devices.Level) (devices.Snapshot, error)
    Dynamic(ctx context.Context, level devices.Level) (devices.Snapshot, error)
}

Build fields with the helpers (devices.Str, devices.Pct, devices.Bytes, …), put per-instance values on Device entries (stable IDs), report unreadable values as Gaps, and prefer well-maintained open source sources (gopsutil, ghw, cpuid, vendor CLIs) over hand-rolled platform code.

License

MIT — see LICENSE.

Documentation

Overview

Package devices is the shared vocabulary for the jarvisfriends devices-* family (devices-cpu, devices-gpu, devices-memory, …). Each category repo implements Collector; everything else here — the Engine, the cli, tui, and web packages — works against that one interface, so a new category gets all four consumption surfaces (library, CLI, TUI, web component) for free.

The model splits along two axes:

  • Kind: Static values are one-time lookups (model numbers, serials, specs) that the Engine caches after the first read. Dynamic values change over time (temperatures, usage, clocks, power) and are re-collected on every request.
  • Level: Basic is the cheap common set every platform can produce. Detailed and Full add values that cost more to retrieve — extra subprocesses, WMI queries, privileged files — so callers opt into the latency instead of paying it by default.

Index

Constants

View Source
const CGOEnabled = true

CGOEnabled reports whether the binary was built with cgo. Collectors use it to advertise (via Snapshot.CGO and gaps) why cgo-only fields are absent from a pure-Go build rather than leaving them silently missing.

Variables

This section is empty.

Functions

func Every

func Every(seconds float64) time.Duration

Every converts a decimal number of seconds into a Duration, so "0.25" from a flag or query parameter maps directly onto an interval.

func FormatBytes

func FormatBytes(bytes uint64) string

FormatBytes renders a byte count in binary units (KiB-scale, single letter), matching how the rest of the jarvisfriends repos print sizes.

func FormatValue

func FormatValue(f Field) string

FormatValue renders one field's value with its unit, the same way on every surface: byte counts humanize, floats keep one decimal, everything else prints as-is.

Types

type Collector

type Collector interface {
	// Category is the short lowercase name: "cpu", "gpu", "drive", ….
	Category() string
	Static(ctx context.Context, level Level) (Snapshot, error)
	Dynamic(ctx context.Context, level Level) (Snapshot, error)
}

Collector is what each devices-* repo implements. Static and Dynamic are separate so the Engine can cache one and re-collect the other; both honor the level by not collecting values above it (Trim exists for the cheap cases where gating isn't worth the branching).

Collectors must be safe for concurrent use: the Engine may serve an on-demand request while a stream is mid-collection.

type Device

type Device struct {
	// ID is stable across snapshots of the same boot ("nvme0n1", "gpu0",
	// "DIMM_A1") so streams can be joined over time.
	ID     string  `json:"id"`
	Name   string  `json:"name,omitempty"`
	Fields []Field `json:"fields,omitempty"`
}

Device is one physical instance in a category: one GPU, one DIMM, one disk, one connected monitor. Category-wide values live on the Snapshot itself; per-instance values live here.

func (Device) Field

func (d Device) Field(key string) (Field, bool)

Field returns the device field with the given key.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine wraps a Collector with the three request shapes every surface needs: blocking on-demand (Snapshot), async one-shot (Request), and a periodic stream (Stream). It also owns the static cache, so serial numbers and model names are read once per process, not once per refresh.

func New

func New(c Collector, opts ...Option) *Engine

New builds an Engine around a collector.

func (*Engine) Collector

func (e *Engine) Collector() Collector

Collector returns the wrapped collector.

func (*Engine) Dynamic

func (e *Engine) Dynamic(ctx context.Context, level Level) (Snapshot, error)

Dynamic collects the live values; nothing is cached.

func (*Engine) InvalidateStatic

func (e *Engine) InvalidateStatic()

InvalidateStatic drops the static cache, forcing the next read to hit the collector again.

func (*Engine) Request

func (e *Engine) Request(ctx context.Context, level Level) <-chan Result

Request is the async form of Snapshot: it returns immediately and delivers exactly one Result on the channel, then closes it.

func (*Engine) Snapshot

func (e *Engine) Snapshot(ctx context.Context, level Level) (Snapshot, error)

Snapshot returns static (cached) and dynamic values merged into one snapshot. A dynamic failure fails the call; a static failure is reported as a gap so a permissions problem on serial numbers doesn't take the live readings down with it.

func (*Engine) Static

func (e *Engine) Static(ctx context.Context, level Level) (Snapshot, error)

Static returns the collector's static snapshot, cached per level.

func (*Engine) Stream

func (e *Engine) Stream(ctx context.Context, level Level, interval time.Duration) <-chan Result

Stream emits a merged snapshot immediately and then every interval until ctx is canceled, closing the channel on the way out. Intervals accept fractions of a second (Every(0.25)). Sends block, so a slow consumer slows the stream rather than piling snapshots up; if collection itself takes longer than the interval, ticks are dropped, not queued.

type Field

type Field struct {
	// Key is the stable machine-readable identity ("usage_percent",
	// "serial_number"). Consumers select and chart by Key; Name is what
	// humans see.
	Key   string `json:"key"`
	Name  string `json:"name"`
	Value any    `json:"value"`
	// Unit is the display unit: "%", "°C", "MHz", "W", "B", "B/s". A Unit of
	// "B" tells renderers to humanize (GiB/MiB); empty means unitless.
	Unit  string `json:"unit,omitempty"`
	Kind  Kind   `json:"kind"`
	Level Level  `json:"level"`
	// Min and Max bound gauge-like values so gradients and bars can be drawn
	// without per-field knowledge. Both zero means no range is known.
	Min float64 `json:"min,omitempty"`
	Max float64 `json:"max,omitempty"`
}

Field is one reported value. Value is one of: string, bool, int64, uint64, float64 — nothing richer, so every field survives a JSON round trip and any renderer (table cell, gauge, chart point) knows what it is holding.

func Bool

func Bool(key, name string, value bool, kind Kind, level Level) Field

Bool builds a boolean field.

func Bytes

func Bytes(key, name string, value uint64, kind Kind, level Level) Field

Bytes builds a byte-count field; renderers humanize it (GiB, MiB, …).

func Int

func Int(key, name string, value int64, unit string, kind Kind, level Level) Field

Int builds an integer field.

func Num

func Num(key, name string, value float64, unit string, kind Kind, level Level) Field

Num builds a numeric field with a display unit.

func Pct

func Pct(key, name string, value float64, kind Kind, level Level) Field

Pct builds a 0–100 percentage field, ranged so gauges and gradients work.

func Str

func Str(key, name, value string, kind Kind, level Level) Field

Str builds a string field.

func (Field) Float

func (f Field) Float() (float64, bool)

Float returns the field's value as a float64 for charting. The second result is false for non-numeric fields.

type Gap

type Gap struct {
	Field  string `json:"field"`
	Reason string `json:"reason"`
}

Gap records a value that could not be read and why — permissions, missing vendor tool, unsupported platform. Reported instead of silently dropped so an empty list is distinguishable from an unreadable one.

type Kind

type Kind uint8

Kind says whether a field is a one-time lookup or a live reading.

const (
	// Static values do not change while the machine is up: model numbers,
	// serial numbers, capacities, firmware versions. The Engine caches them.
	Static Kind = iota
	// Dynamic values move over time: utilization, temperature, clock speed,
	// power draw. They are re-collected on every request.
	Dynamic
)

func (Kind) MarshalText

func (k Kind) MarshalText() ([]byte, error)

MarshalText makes Kind render as its name in JSON output.

func (Kind) String

func (k Kind) String() string

String returns "static" or "dynamic".

func (*Kind) UnmarshalText

func (k *Kind) UnmarshalText(b []byte) error

UnmarshalText accepts "static" or "dynamic".

type Level

type Level uint8

Level selects how much a collection is allowed to cost. Levels are cumulative: Full includes everything Detailed does, which includes everything Basic does.

const (
	// Basic is the default: the common values every platform can produce
	// quickly, with no subprocesses and no privileged reads.
	Basic Level = iota
	// Detailed adds values that take noticeably longer or need extra
	// sources — vendor CLIs, WMI queries, wider sysfs walks.
	Detailed
	// Full adds everything else we know how to read, however slow: SMART
	// polls, per-process counters, exhaustive enumeration.
	Full
)

func ParseLevel

func ParseLevel(s string) (Level, error)

ParseLevel maps a level name to its Level. It exists for flag and query parameter parsing, so unknown names are an error rather than a default.

func (Level) MarshalText

func (l Level) MarshalText() ([]byte, error)

MarshalText makes Level render as its name in JSON output.

func (Level) String

func (l Level) String() string

String returns "basic", "detailed", or "full".

func (*Level) UnmarshalText

func (l *Level) UnmarshalText(b []byte) error

UnmarshalText parses a level name.

type Option

type Option func(*Engine)

Option configures an Engine.

func WithStaticTTL

func WithStaticTTL(d time.Duration) Option

WithStaticTTL re-reads static values after d instead of caching them forever. Useful for long-lived daemons where hot-pluggable hardware (monitors, drives) can change under them.

type Result

type Result struct {
	Snapshot Snapshot
	Err      error
}

Result is one delivery from Request or Stream.

type Snapshot

type Snapshot struct {
	// Category is the collector's name: "cpu", "gpu", "memory", ….
	Category string    `json:"category"`
	Taken    time.Time `json:"taken"`
	Level    Level     `json:"level"`
	// CGO reports whether the binary was built with cgo, which some
	// categories use to unlock extra fields.
	CGO     bool     `json:"cgo"`
	Fields  []Field  `json:"fields,omitempty"`
	Devices []Device `json:"devices,omitempty"`
	Gaps    []Gap    `json:"gaps,omitempty"`
}

Snapshot is one collection result: category-wide fields, per-instance devices, and the gaps hit along the way.

func Merge

func Merge(static, dynamic Snapshot) Snapshot

Merge combines a static and a dynamic snapshot: category-wide fields concatenate (static first) and devices join by ID, so a GPU's model number and its live temperature end up on the same Device entry.

func (Snapshot) Device

func (s Snapshot) Device(id string) (Device, bool)

Device returns the device with the given ID.

func (Snapshot) Field

func (s Snapshot) Field(key string) (Field, bool)

Field returns the first category-wide field with the given key.

func (Snapshot) Trim

func (s Snapshot) Trim(level Level) Snapshot

Trim drops every field above the requested level, keeping collectors free to build their cheap fields unconditionally and gate only the expensive ones. Devices left with no fields are dropped with the fields.

Directories

Path Synopsis
Package cli is the shared command line for every devices-* tool.
Package cli is the shared command line for every devices-* tool.
Package tui renders any devices.Collector as a live full-screen dashboard built from the snap component set.
Package tui renders any devices.Collector as a live full-screen dashboard built from the snap component set.
Package web serves device snapshots over HTTP three ways: a JSON API for programs, a Server-Sent Events stream for live consumers, and an embeddable web component (<jf-devices>) that any site can drop in to get live tables and gauges.
Package web serves device snapshots over HTTP three ways: a JSON API for programs, a Server-Sent Events stream for live consumers, and an embeddable web component (<jf-devices>) that any site can drop in to get live tables and gauges.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL