Documentation
¶
Index ¶
- Constants
- Variables
- func DefaultScanTargets() []byte
- func MethodRoutableOf(method Method) bool
- func ReadVaillantScanID(ctx context.Context, bus ScanBus, source byte, target byte) (string, bool, error)
- type AddressSlot
- func (s *AddressSlot) AddressByRole(role SlotRole) (byte, bool)
- func (s *AddressSlot) Addresses() []byte
- func (s *AddressSlot) DeviceID() string
- func (s *AddressSlot) HardwareVersion() string
- func (s *AddressSlot) MacAddress() string
- func (s *AddressSlot) Manufacturer() string
- func (s *AddressSlot) Planes() []Plane
- func (s *AddressSlot) PrimaryDisplayAddress() byte
- func (s *AddressSlot) Projections() []Projection
- func (s *AddressSlot) SerialNumber() string
- func (s *AddressSlot) SoftwareVersion() string
- type AddressSlotSnapshot
- type BusFace
- type CanonicalIndex
- type DeviceEntry
- type DeviceEntrySnapshot
- type DeviceInfo
- type DeviceRegistry
- func (r *DeviceRegistry) AliasAddresses(a, b byte) error
- func (r *DeviceRegistry) Iterate(fn func(DeviceEntry) bool)
- func (r *DeviceRegistry) IterateSnapshots(fn func(DeviceEntrySnapshot) bool)
- func (r *DeviceRegistry) Lookup(address byte) (DeviceEntry, bool)
- func (r *DeviceRegistry) LookupEntrySnapshot(address byte) (DeviceEntrySnapshot, bool)
- func (r *DeviceRegistry) LookupSlot(address byte) (*AddressSlot, bool)
- func (r *DeviceRegistry) LookupSlotSnapshot(address byte) (AddressSlotSnapshot, bool)
- func (r *DeviceRegistry) MarkSlotPassiveObserved(address byte, role SlotRole, observedAt time.Time)
- func (r *DeviceRegistry) MarkSlotStaticSeed(address byte, role SlotRole, seededAt time.Time)
- func (r *DeviceRegistry) Register(info DeviceInfo) DeviceEntry
- func (r *DeviceRegistry) RegisterPassiveObserved(info DeviceInfo, role SlotRole, observedAt time.Time) DeviceEntry
- func (r *DeviceRegistry) RegisterProvider(provider PlaneProvider)
- func (r *DeviceRegistry) RegisterStaticSeed(info DeviceInfo, role SlotRole, seededAt time.Time) DeviceEntry
- func (r *DeviceRegistry) WithObservationGeneration(fn func(uint64)) bool
- type DiscoverySource
- type Edge
- type EdgeID
- type EntryIterator
- type FrameTemplate
- type Method
- type MethodDanger
- type MethodDangerProvider
- type MethodMetadata
- type MethodMutability
- type MethodMutabilityProvider
- type MethodRoutableProvider
- type Node
- type NodeID
- type PathSegment
- type Plane
- type PlaneIndex
- func (index *PlaneIndex) EdgeByID(id EdgeID) (Edge, bool)
- func (index *PlaneIndex) EdgeCount() int
- func (index *PlaneIndex) EdgesFrom(id NodeID) []Edge
- func (index *PlaneIndex) EdgesTo(id NodeID) []Edge
- func (index *PlaneIndex) NodeByCanonical(canonical ProjectionPath) (Node, bool, error)
- func (index *PlaneIndex) NodeByID(id NodeID) (Node, bool)
- func (index *PlaneIndex) NodeByPath(path ProjectionPath) (Node, bool, error)
- func (index *PlaneIndex) Plane() string
- type PlaneProvider
- type PortalIndex
- type Projection
- type ProjectionPath
- type ProjectionProvider
- type ScanBus
- type ServiceDeviceView
- type ServiceMethodView
- type ServicePlaneView
- type SlotRole
- type VerificationState
Constants ¶
const ServicePlane = "Service"
Variables ¶
var ( ErrPortalDuplicatePlane = errors.New("portal: duplicate plane") ErrPortalDuplicateNode = errors.New("portal: duplicate node") ErrPortalDuplicateEdge = errors.New("portal: duplicate edge") ErrPortalInvalidEntry = errors.New("portal: invalid entry") )
var ( ErrProjectionInvalidPlane = errors.New("projection: invalid plane") ErrProjectionInvalidSegment = errors.New("projection: invalid path segment") ErrProjectionInvalidPath = errors.New("projection: invalid path") ErrProjectionInvalidNode = errors.New("projection: invalid node") ErrProjectionInvalidEdge = errors.New("projection: invalid edge") )
var ErrScanDirectedEmptyTargets = errors.New("scan: directed scan requires explicit non-empty targets")
ErrScanDirectedEmptyTargets is returned by ScanDirected when it is invoked with a nil or empty targets slice. ScanDirected requires an explicit, bounded target list; the historical full-range fall-through that Scan performs when targets is nil is deliberately not available via this API.
See AD10 in helianthus-execution-plans/startup-admission-discovery-w17-26.locked/ 12-decision-matrix.md for the directed-scan contract.
Functions ¶
func DefaultScanTargets ¶
func DefaultScanTargets() []byte
DefaultScanTargets returns the default address range for scanning.
func MethodRoutableOf ¶
MethodRoutableOf resolves routability with legacy default=true behavior.
func ReadVaillantScanID ¶
func ReadVaillantScanID(ctx context.Context, bus ScanBus, source byte, target byte) (string, bool, error)
ReadVaillantScanID sends B5.09 identity reads (chunks 0x24..0x27) and returns the formatted Vaillant serial number. It is the exported twin of the internal readVaillantScanID so that callers outside this package (e.g. the gateway's ebusd-tcp preload enrichment) can invoke it.
Types ¶
type AddressSlot ¶
type AddressSlot struct {
Addr byte
Role SlotRole
DiscoverySource DiscoverySource
VerificationState VerificationState
Device *deviceEntry
FirstObservedAt time.Time
LastObservedAt time.Time
}
func (*AddressSlot) AddressByRole ¶
func (s *AddressSlot) AddressByRole(role SlotRole) (byte, bool)
AddressByRole forwards to the wrapped Device's AddressByRole; falls back to s.Addr matching when Device is nil and the slot's own Role matches the requested role.
func (*AddressSlot) Addresses ¶
func (s *AddressSlot) Addresses() []byte
func (*AddressSlot) DeviceID ¶
func (s *AddressSlot) DeviceID() string
func (*AddressSlot) HardwareVersion ¶
func (s *AddressSlot) HardwareVersion() string
func (*AddressSlot) MacAddress ¶
func (s *AddressSlot) MacAddress() string
func (*AddressSlot) Manufacturer ¶
func (s *AddressSlot) Manufacturer() string
func (*AddressSlot) Planes ¶
func (s *AddressSlot) Planes() []Plane
func (*AddressSlot) PrimaryDisplayAddress ¶
func (s *AddressSlot) PrimaryDisplayAddress() byte
PrimaryDisplayAddress returns the slot's display address — the wrapped Device's PrimaryDisplayAddress when present, otherwise the slot's own Addr. Phase C M-C6c: replaces the removed AddressSlot.Address() method (which had identical semantics but conflated display vs. routing intent).
func (*AddressSlot) Projections ¶
func (s *AddressSlot) Projections() []Projection
func (*AddressSlot) SerialNumber ¶
func (s *AddressSlot) SerialNumber() string
func (*AddressSlot) SoftwareVersion ¶
func (s *AddressSlot) SoftwareVersion() string
type AddressSlotSnapshot ¶
type AddressSlotSnapshot struct {
Addr byte
Role SlotRole
DiscoverySource DiscoverySource
VerificationState VerificationState
FirstObservedAt time.Time
LastObservedAt time.Time
DeviceAttached bool
}
AddressSlotSnapshot is a value-typed copy of an AddressSlot's observable fields. Snapshots are taken under r.mu.RLock so callers can read the fields without holding any registry lock and without risking torn reads from concurrent writers.
P8.1 — addresses the lock-free read advisory raised by the GitHub Codex bot on helianthus-ebusgateway PR #589: the gateway's AddressTable previously dereferenced a live AddressSlot pointer (returned by LookupSlot) outside the registry's RLock to read DiscoverySource / VerificationState. AddressSlotSnapshot eliminates that race surface — the value copy is immune to concurrent mutations because the writer must acquire r.mu.Lock() (which blocks behind the RLock taken in LookupSlotSnapshot below) before changing the underlying slot.
Note: Device is reduced to a boolean (DeviceAttached) because returning the *deviceEntry pointer would re-introduce lock-free dereferencing of mutable identity fields downstream. Callers that need the entry's identity fields can fetch the entry via Lookup, but should be aware that the returned DeviceEntry interface still reads through to the registry's internal entry struct — those reads are not snapshot-isolated. A future entry-snapshot API may be added if the same pattern proves desirable for identity reads.
type BusFace ¶
type BusFace struct {
Addr byte
Role SlotRole
DiscoverySource DiscoverySource
VerificationState VerificationState
AccessProtocols []string
}
type CanonicalIndex ¶
type CanonicalIndex struct {
// contains filtered or unexported fields
}
func BuildCanonicalIndex ¶
func BuildCanonicalIndex(projections []Projection) (CanonicalIndex, error)
func CanonicalIndexForEntry ¶
func CanonicalIndexForEntry(entry DeviceEntry) (CanonicalIndex, error)
func (CanonicalIndex) Canonical ¶
func (index CanonicalIndex) Canonical(id NodeID) (ProjectionPath, bool)
func (CanonicalIndex) PlanePath ¶
func (index CanonicalIndex) PlanePath(plane string, id NodeID) (ProjectionPath, bool)
func (CanonicalIndex) PlanePaths ¶
func (index CanonicalIndex) PlanePaths(id NodeID) map[string]ProjectionPath
type DeviceEntry ¶
type DeviceEntry interface {
// AddressByRole returns the first BusFace address whose Role
// matches the requested SlotRole. Returns (0, false) when no face
// matches. Used by routing code to address the correct byte for
// the intended frame type (per AddressClass taxonomy).
//
// Phase C M-C6c: replaces the previous ambiguous Address() method,
// which conflated the "show me a representative byte" use case
// (now PrimaryDisplayAddress) with the "give me the routing-
// correct byte for this frame type" use case (this method). The
// removed method silently returned the initiator byte for an
// aliased canonical pair (e.g. BAI 0x03↔0x08), causing M2S writes
// to mis-route to the initiator side. AddressByRole forces
// callers to declare their intent.
AddressByRole(role SlotRole) (byte, bool)
// PrimaryDisplayAddress returns a representative address for log /
// UI display. May be initiator OR target for aliased pairs; do
// NOT use for wire routing. For routing, use AddressByRole.
PrimaryDisplayAddress() byte
Addresses() []byte
Manufacturer() string
DeviceID() string
SerialNumber() string
MacAddress() string
SoftwareVersion() string
HardwareVersion() string
Planes() []Plane
Projections() []Projection
}
func Scan ¶
func Scan(ctx context.Context, bus ScanBus, registry *DeviceRegistry, source byte, targets []byte) ([]DeviceEntry, error)
Scan performs a 07 04 identification scan over the provided targets.
func ScanDirected ¶
func ScanDirected(ctx context.Context, bus ScanBus, registry *DeviceRegistry, source byte, targets []byte) ([]DeviceEntry, error)
ScanDirected performs a 07 04 identification scan over an explicit, bounded set of targets. Unlike Scan, it refuses to fall through to DefaultScanTargets: callers MUST supply a non-empty targets slice. Nil or empty input returns ErrScanDirectedEmptyTargets and the bus is not touched.
Behaviour when targets is non-empty is identical to Scan. The same target-capability rules apply (FrameTypeForTarget, initiator-capable exclusion, SYN/ESC via FrameTypeUnknown), dedupe is consistent, and the collision-retry loop is shared.
This API is intended for startup admission on non-ebusd-tcp direct transports where the gateway MUST probe only promoted suspects — never the full 0x01..0xFD address range. Callers on the ebusd-tcp sanctioned bounded-retry path should continue to use Scan directly so that Scan(ctx, bus, registry, source, nil) retains its fall-through semantics.
See helianthus-execution-plans/startup-admission-discovery-w17-26.locked/ 12-decision-matrix.md#ad10 for the directed-scan contract.
type DeviceEntrySnapshot ¶
type DeviceEntrySnapshot struct {
PrimaryAddress byte
Addresses []byte
Faces []BusFace
Manufacturer string
DeviceID string
SerialNumber string
MacAddress string
SoftwareVersion string
HardwareVersion string
// Planes + Projections — slice headers captured under RLock at
// snapshot time. The interface elements (Plane, Method, etc.) are
// immutable after PlaneProvider.CreatePlanes returns; safe to
// read after the lock has been released.
Planes []Plane
Projections []Projection
}
DeviceEntrySnapshot is a value-typed copy of a DeviceEntry's observable identity fields. Snapshots are taken under r.mu.RLock so callers can read the fields without holding any registry lock and without risking torn reads from concurrent writers (Register / RegisterStaticSeed / RegisterPassiveObserved / AliasAddresses / detachAddressLocked).
P9 — addresses Codex post-P8.3 audit: the DeviceEntry interface methods (Manufacturer / DeviceID / SerialNumber / etc.) read `d.info.<Field>` lock-free. Concurrent Register replaces `entry.info` with a new DeviceInfo struct (line 240 `entry.info = storedInfo`); a reader holding the *deviceEntry pointer can observe a torn read of the string fields (string is a 16-byte ptr+len header).
DeviceEntrySnapshot copies all string and slice fields under RLock; the snapshot is disconnected from registry storage and safe to read concurrently. Slice copies (Addresses, Faces) prevent callers from mutating registry state through the snapshot.
SCOPE (P9.x): Planes and Projections were originally omitted on the theory that their interface trees transitively exposed registry- mutable state. In practice the *plane and *method implementations (vaillant providers + similar) are constructed once in PlaneProvider.CreatePlanes / ProjectionProvider.CreateProjections and never mutated afterward; the only registry-side write to the `entry.planes` / `entry.projections` slice headers happens during the identity-merge path (mergeEntries dst.planes = src.planes / dst.projections = src.projections). Capturing those slice headers under RLock therefore produces a stable view: readers iterating the snapshot's Planes / Projections see element references that remain valid for the lifetime of the snapshot, with no risk of a mid-iteration slice reassignment.
P9.x adds Planes + Projections to DeviceEntrySnapshot so the graphql.BuildSchema hot path can drop its live-pointer Iterate usage. Callers that mutate the registry (registerLocked path) must continue to use the live `*deviceEntry` pointer; the snapshot is READ-ONLY by construction.
func (DeviceEntrySnapshot) AddressByRole ¶
func (s DeviceEntrySnapshot) AddressByRole(role SlotRole) (byte, bool)
AddressByRole mirrors deviceEntry.AddressByRole using the snapshot's Faces slice. Returns (0, false) when no face matches the requested role (after the same role-class fallback rules used by the live implementation).
func (DeviceEntrySnapshot) PrimaryDisplayAddress ¶
func (s DeviceEntrySnapshot) PrimaryDisplayAddress() byte
PrimaryDisplayAddress mirrors deviceEntry.PrimaryDisplayAddress for callers that work with the value-typed snapshot.
type DeviceInfo ¶
type DeviceRegistry ¶
type DeviceRegistry struct {
// contains filtered or unexported fields
}
func NewDeviceRegistry ¶
func NewDeviceRegistry(providers []PlaneProvider) *DeviceRegistry
func (*DeviceRegistry) AliasAddresses ¶
func (r *DeviceRegistry) AliasAddresses(a, b byte) error
Lookup returns the canonical *DeviceEntry for the given address, or (nil, false) if no device occupies that slot. Preserved (signature unchanged) so existing callers continue to compile.
func (*DeviceRegistry) Iterate ¶
func (r *DeviceRegistry) Iterate(fn func(DeviceEntry) bool)
func (*DeviceRegistry) IterateSnapshots ¶
func (r *DeviceRegistry) IterateSnapshots(fn func(DeviceEntrySnapshot) bool)
IterateSnapshots visits each registered device entry as a value-typed snapshot. Snapshots are built under r.mu.RLock; the lock is released BEFORE the callback runs. Callers can therefore safely call any DeviceRegistry method from within the callback (no deadlock risk).
This matches the existing Iterate API's lock-then-snapshot-then- unlock contract. The only behavioural difference vs Iterate is the value-typed snapshot vs live entry pointer (Codex P9 review pass 1 MINOR FINDING_2).
P9 — Iterate remains for callers that need live entry pointers (or Planes / Projections, which the snapshot intentionally omits). New consumers SHOULD prefer IterateSnapshots.
func (*DeviceRegistry) Lookup ¶
func (r *DeviceRegistry) Lookup(address byte) (DeviceEntry, bool)
func (*DeviceRegistry) LookupEntrySnapshot ¶
func (r *DeviceRegistry) LookupEntrySnapshot(address byte) (DeviceEntrySnapshot, bool)
LookupEntrySnapshot returns a value-typed snapshot of the DeviceEntry registered for addr, taken under r.mu.RLock. The race-free counterpart to Lookup for callers that only need the entry's observable identity fields (gateway MCP / GraphQL / observability projections). Returns (zero, false) when no entry exists for the address.
P9 — Lookup remains for callers that need the live entry pointer (e.g. registry-internal mutation paths or callers that need Planes/Projections).
func (*DeviceRegistry) LookupSlot ¶
func (r *DeviceRegistry) LookupSlot(address byte) (*AddressSlot, bool)
LookupSlot returns the AddressSlot for the requested address (M1 address-table accessor), with the slot's own role/source/confidence metadata. When the address is aliased to a multi-address device, the returned slot.Device pointer is shared with the primary slot, but slot.Addr/Role/DiscoverySource/VerificationState describe the REQUESTED address — callers inspecting per-address metadata get the per-slot view (Codex P2: return-the-requested-address-slot).
func (*DeviceRegistry) LookupSlotSnapshot ¶
func (r *DeviceRegistry) LookupSlotSnapshot(address byte) (AddressSlotSnapshot, bool)
LookupSlotSnapshot returns a value-typed snapshot of the AddressSlot at addr, taken under r.mu.RLock. Callers can read the snapshot fields without any registry-lock concerns. Returns (zero, false) when no slot exists for the address.
P8.1 — the race-free counterpart to LookupSlot for callers that only need the slot's observable fields (gateway address-table projection, MCP/GraphQL surfaces). LookupSlot remains for callers that need the live pointer (e.g. registry-internal mutation paths holding the appropriate lock externally).
func (*DeviceRegistry) MarkSlotPassiveObserved ¶
func (r *DeviceRegistry) MarkSlotPassiveObserved(address byte, role SlotRole, observedAt time.Time)
MarkSlotPassiveObserved updates an AddressSlot for an address that was passively observed by the gateway (e.g. by AddressTableInserter on positive ACK following a complete request). Writes Role / Discovery Source / VerificationState / FirstObservedAt / LastObservedAt under the registry write lock so concurrent readers via LookupSlot / Lookup do not see torn state.
This API replaces direct *AddressSlot field mutation by the gateway inserter, which was racy with other readers (Codex P2 follow-up from PR #565). Idempotent: re-marking the same slot only advances DiscoverySource / VerificationState monotonically (the slot retains the higher of the existing and new value, matching observeAddressSlotLocked's monotonic semantics).
SCOPE: this API only mutates the AddressSlot. It does NOT attach the slot to a device entry. To plant a NEW passively-observed address with identity attached AND label it correctly in a single critical section, use RegisterPassiveObserved (which composes registerLocked + this primitive). Calling Register followed by MarkSlotPassiveObserved produces a label-misorder because Register stamps DiscoverySourceActiveConfirmed and the monotonic guard then refuses to downgrade — that was the P8 bug fixed in RegisterPassiveObserved.
func (*DeviceRegistry) MarkSlotStaticSeed ¶
func (r *DeviceRegistry) MarkSlotStaticSeed(address byte, role SlotRole, seededAt time.Time)
MarkSlotStaticSeed updates an AddressSlot for an address known from a product taxonomy seed table, mirroring MarkSlotPassiveObserved (lock discipline, monotonic upgrade semantics, idempotence) but stamping DiscoverySourceStaticSeed / VerificationStateCandidate.
SCOPE: this API only mutates the AddressSlot. It does NOT attach the slot to a device entry; if `slot.Device` is nil at call time it stays nil, the address is NOT added to `r.entries`, and the device's `addresses` / `Faces` lists are NOT updated. Therefore:
To plant a NEW seeded address with identity attached, use RegisterStaticSeed (which composes registerLocked + this stamp). Each face that should appear in `Lookup` / `AddressByRole` needs its own RegisterStaticSeed call; the identity-merge in registerLocked joins them when DeviceInfo identity matches, or they can be aliased post-hoc via AliasAddresses.
The use case for MarkSlotStaticSeed in isolation is updating an AddressSlot that was already attached to a device by some prior path (Register / RegisterStaticSeed / AliasAddresses) to upgrade its discovery_source / verification labels — for example, marking a slot newly populated by passive observation as "now also seeded from the static table" so the operator surface reflects that the addresses are pre-known.
Idempotent. Re-calling on a slot already at higher DiscoverySource (e.g. ActiveConfirmed) is a no-op for the discovery label, though it may still upgrade VerificationState if the existing state is below Candidate.
func (*DeviceRegistry) Register ¶
func (r *DeviceRegistry) Register(info DeviceInfo) DeviceEntry
func (*DeviceRegistry) RegisterPassiveObserved ¶
func (r *DeviceRegistry) RegisterPassiveObserved(info DeviceInfo, role SlotRole, observedAt time.Time) DeviceEntry
RegisterPassiveObserved plants identity for an address newly observed on the wire by the gateway's passive inserter. Mirrors Register's identity-merge behaviour but stamps the AddressSlot with DiscoverySourcePassiveObserved / VerificationStateCorroborated so the observability surface (`/metrics`, MCP `bus.summary.get`, address-table snapshots) correctly shows the slot's provenance as passive observation rather than active confirmation.
P8 fix: previously the gateway inserter called Register (which stamps ActiveConfirmed/IdentityConfirmed) followed by MarkSlotPassiveObserved. The monotonic ladder (PassiveObserved < ActiveConfirmed) made the second call a no-op, so passively-observed slots were misreported as `active_confirmed`. RegisterPassiveObserved performs the identity-merge AND the passive-label stamping atomically under a single lock acquisition, avoiding the misorder.
Subsequent label progression (after RegisterPassiveObserved):
- An active confirmation (e.g. directed scan) DOES advance the DiscoverySource to ActiveConfirmed (PassiveObserved < ActiveConfirmed) AND VerificationState to IdentityConfirmed.
- A static-seed mark on a passively-observed slot DOES advance DiscoverySource to StaticSeed (PassiveObserved < StaticSeed) — pre-known taxonomy outranks wire-only inference.
Single lock acquisition — composes registerLocked, then the shared passive-observation primitive, then syncEntryFacesLocked.
func (*DeviceRegistry) RegisterProvider ¶
func (r *DeviceRegistry) RegisterProvider(provider PlaneProvider)
func (*DeviceRegistry) RegisterStaticSeed ¶
func (r *DeviceRegistry) RegisterStaticSeed(info DeviceInfo, role SlotRole, seededAt time.Time) DeviceEntry
RegisterStaticSeed plants identity for an address known from a product taxonomy table BEFORE any wire traffic has been observed. Mirrors Register's identity-merge behavior but stamps the AddressSlot with DiscoverySourceStaticSeed / VerificationStateCandidate so the observability surface (`/metrics`, MCP `bus.summary.get`, address-table snapshots) correctly shows the slot's provenance as a pre-known seed rather than active confirmation.
On a clean cold boot a static-seeded slot subsequently observed passively will: NOT advance DiscoverySource (PassiveObserved < StaticSeed in the monotonic enum order), WILL advance VerificationState from Candidate to Corroborated. An active confirmation (e.g. directed scan) DOES advance DiscoverySource to ActiveConfirmed (StaticSeed < ActiveConfirmed) AND VerificationState to IdentityConfirmed.
Single lock acquisition — composes registerLocked, then the shared static-seed stamping primitive, then syncEntryFacesLocked.
func (*DeviceRegistry) WithObservationGeneration ¶
func (r *DeviceRegistry) WithObservationGeneration(fn func(uint64)) bool
WithObservationGeneration executes fn while holding the registry read lock and supplies the current monotonic observation generation. Public registry mutations advance the generation under the corresponding write lock, so a writer linearizes either before the callback begins or after it returns.
fn must remain bounded and must not call another DeviceRegistry method: the callback deliberately executes inside the read critical section so callers can atomically compare a captured generation and commit derived state before a relevant registry mutation can interleave.
The method returns false without invoking fn when either receiver or callback is nil.
type DiscoverySource ¶
type DiscoverySource int
const ( DiscoverySourceUnknown DiscoverySource = iota DiscoverySourcePassiveObserved DiscoverySourceStaticSeed DiscoverySourceActiveConfirmed )
type EntryIterator ¶
type EntryIterator interface {
Iterate(func(DeviceEntry) bool)
}
EntryIterator is the minimal registry surface required by projection helpers.
type FrameTemplate ¶
type Method ¶
type Method interface {
Name() string
ReadOnly() bool
Template() FrameTemplate
ResponseSchema() schema.SchemaSelector
}
type MethodDanger ¶
type MethodDanger string
MethodDanger describes risk level for invoke safety controls.
const ( MethodDangerUnknown MethodDanger = "unknown" MethodDangerSafe MethodDanger = "safe" MethodDangerDangerous MethodDanger = "dangerous" )
func MethodDangerOf ¶
func MethodDangerOf(method Method) MethodDanger
MethodDangerOf resolves danger with mutability-based fallback.
type MethodDangerProvider ¶
type MethodDangerProvider interface {
Danger() MethodDanger
}
MethodDangerProvider optionally overrides default danger inference.
type MethodMetadata ¶
type MethodMetadata struct {
Mutability MethodMutability
Danger MethodDanger
Routable bool
}
MethodMetadata is the normalized method safety/routing contract.
Backward compatibility defaults: - Mutability: derived from Method.ReadOnly() when not explicitly provided. - Danger: safe for read_only, dangerous for mutating/unknown. - Routable: true when not explicitly provided.
func ResolveMethodMetadata ¶
func ResolveMethodMetadata(method Method) MethodMetadata
ResolveMethodMetadata returns normalized metadata for a method with backward-compatible defaults for legacy Method implementations.
type MethodMutability ¶
type MethodMutability string
MethodMutability describes whether a method has side effects.
const ( MethodMutabilityUnknown MethodMutability = "unknown" MethodMutabilityReadOnly MethodMutability = "read_only" MethodMutabilityMutating MethodMutability = "mutating" )
func MethodMutabilityOf ¶
func MethodMutabilityOf(method Method) MethodMutability
MethodMutabilityOf resolves mutability with legacy ReadOnly fallback.
type MethodMutabilityProvider ¶
type MethodMutabilityProvider interface {
Mutability() MethodMutability
}
MethodMutabilityProvider optionally overrides default mutability inference.
type MethodRoutableProvider ¶
type MethodRoutableProvider interface {
Routable() bool
}
MethodRoutableProvider optionally overrides default routability.
type Node ¶
type Node struct {
ID NodeID
Path ProjectionPath
CanonicalPath ProjectionPath
}
func NewNode ¶
func NewNode(path ProjectionPath, canonical ProjectionPath) (Node, error)
type NodeID ¶
type NodeID string
func StableNodeID ¶
func StableNodeID(canonical ProjectionPath) (NodeID, error)
type PathSegment ¶
func (PathSegment) String ¶
func (segment PathSegment) String() string
type PlaneIndex ¶
type PlaneIndex struct {
// contains filtered or unexported fields
}
PlaneIndex provides indexed access to nodes and edges within a projection plane.
Example:
plane, ok := portal.PlaneIndex("Observability")
if ok {
node, ok, err := plane.NodeByCanonical(ProjectionPath{
Plane: ServicePlane,
Segments: []PathSegment{
{Name: "devices"},
{Name: "boiler"},
},
})
_ = node
_ = err
}
func NewPlaneIndex ¶
func NewPlaneIndex(projection Projection) (PlaneIndex, error)
NewPlaneIndex builds an index from a single projection.
func (*PlaneIndex) EdgeByID ¶
func (index *PlaneIndex) EdgeByID(id EdgeID) (Edge, bool)
EdgeByID looks up an edge by its stable ID.
func (*PlaneIndex) EdgeCount ¶
func (index *PlaneIndex) EdgeCount() int
EdgeCount returns the number of edges indexed for the plane.
func (*PlaneIndex) EdgesFrom ¶
func (index *PlaneIndex) EdgesFrom(id NodeID) []Edge
EdgesFrom returns edges originating from the provided node ID.
func (*PlaneIndex) EdgesTo ¶
func (index *PlaneIndex) EdgesTo(id NodeID) []Edge
EdgesTo returns edges targeting the provided node ID.
func (*PlaneIndex) NodeByCanonical ¶
func (index *PlaneIndex) NodeByCanonical(canonical ProjectionPath) (Node, bool, error)
NodeByCanonical looks up a node by canonical Service path.
func (*PlaneIndex) NodeByID ¶
func (index *PlaneIndex) NodeByID(id NodeID) (Node, bool)
NodeByID looks up a node by stable node ID.
func (*PlaneIndex) NodeByPath ¶
func (index *PlaneIndex) NodeByPath(path ProjectionPath) (Node, bool, error)
NodeByPath looks up a node by its plane-specific path.
func (*PlaneIndex) Plane ¶
func (index *PlaneIndex) Plane() string
Plane returns the plane name for this index.
type PlaneProvider ¶
type PlaneProvider interface {
Name() string
Match(info DeviceInfo) bool
CreatePlanes(info DeviceInfo) []Plane
}
type PortalIndex ¶
type PortalIndex struct {
// contains filtered or unexported fields
}
PortalIndex provides plane-scoped projection lookups for a single device entry.
Example:
portal, err := NewPortalIndex(entry.Projections())
if err != nil {
return err
}
node, ok, err := portal.NodeByCanonical("Observability", ProjectionPath{
Plane: ServicePlane,
Segments: []PathSegment{
{Name: "devices"},
{Name: "boiler"},
},
})
if err != nil {
return err
}
if ok {
_ = node.Path
}
func NewPortalIndex ¶
func NewPortalIndex(projections []Projection) (PortalIndex, error)
NewPortalIndex builds a read-only portal index from projections.
Example:
portal, err := NewPortalIndex(entry.Projections())
if err != nil {
return err
}
func PortalIndexForEntry ¶
func PortalIndexForEntry(entry DeviceEntry) (PortalIndex, error)
PortalIndexForEntry builds a portal index from the entry projections.
func (PortalIndex) EdgeByID ¶
func (portal PortalIndex) EdgeByID(plane string, edgeID EdgeID) (Edge, bool)
EdgeByID looks up an edge by plane and edge ID.
func (PortalIndex) NodeByCanonical ¶
func (portal PortalIndex) NodeByCanonical(plane string, canonical ProjectionPath) (Node, bool, error)
NodeByCanonical looks up a node by plane and canonical Service path.
Example:
node, ok, err := portal.NodeByCanonical("Observability", ProjectionPath{
Plane: ServicePlane,
Segments: []PathSegment{
{Name: "devices"},
{Name: "boiler"},
},
})
func (PortalIndex) PlaneIndex ¶
func (portal PortalIndex) PlaneIndex(plane string) (*PlaneIndex, bool)
PlaneIndex returns the plane-specific index if present.
Example:
plane, ok := portal.PlaneIndex("Observability")
if ok {
_ = plane.Plane()
}
type Projection ¶
func NewProjection ¶
func NewProjection(plane string, nodes []Node, edges []Edge) (Projection, error)
func (Projection) Validate ¶
func (projection Projection) Validate() error
type ProjectionPath ¶
type ProjectionPath struct {
Plane string
Segments []PathSegment
}
func (ProjectionPath) String ¶
func (path ProjectionPath) String() string
func (ProjectionPath) Validate ¶
func (path ProjectionPath) Validate() error
type ProjectionProvider ¶
type ProjectionProvider interface {
CreateProjections(info DeviceInfo, planes []Plane) []Projection
}
type ServiceDeviceView ¶
type ServiceDeviceView struct {
Address byte
Addresses []byte
Manufacturer string
DeviceID string
SerialNumber string
MacAddress string
SoftwareVersion string
HardwareVersion string
Planes []ServicePlaneView
}
ServiceDeviceView is a normalized device projection for shared service-layer use.
func ProjectDeviceEntry ¶
func ProjectDeviceEntry(entry DeviceEntry) (ServiceDeviceView, error)
func ProjectRegistryDevices ¶
func ProjectRegistryDevices(iter EntryIterator) ([]ServiceDeviceView, error)
type ServiceMethodView ¶
type ServiceMethodView struct {
Name string
ReadOnly bool
Primary byte
Secondary byte
Metadata MethodMetadata
ResponseSelector schema.SchemaSelector
}
ServiceMethodView is a normalized method projection for shared service-layer use.
func ProjectMethod ¶
func ProjectMethod(method Method) (ServiceMethodView, error)
type ServicePlaneView ¶
type ServicePlaneView struct {
Name string
Methods []ServiceMethodView
}
ServicePlaneView is a normalized plane projection for shared service-layer use.
func ProjectPlane ¶
func ProjectPlane(plane Plane) (ServicePlaneView, error)
type VerificationState ¶
type VerificationState int
const ( VerificationStateUnknown VerificationState = iota VerificationStateCandidate VerificationStateCorroborated VerificationStateIdentityConfirmed )