Documentation
¶
Overview ¶
Package emberplus implements an Ember+ consumer (client). It does not implement a provider.
A Client connects to a provider, keeps a mirror of the provider tree and offers the operations of Ember+: browsing the tree, reading and setting parameter values, invoking functions, connecting matrix crosspoints and observing changes.
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
return err
}
defer c.Close()
el, err := c.Get(ctx, "root/settings/info/version")
Paths ¶
Elements are addressed by path strings:
- identifier paths separated by "/": "root/settings/info"
- numeric paths separated by ".": "1.100.3", as printed by glow.Path
- mixed paths separated by "/": "root/100/info"
In "/" separated paths, segments consisting of digits only are numbers, since identifiers must begin with a letter or "_". Identifiers may contain ".", as in "dsp/gain.left"; "." only separates purely numeric paths.
The client resolves every path itself and loads missing parts of the tree with GetDirectory as needed; later accesses are served from the mirror. Numbers are only valid for one session, so applications should store identifier paths. IdentifierPath converts a path to its identifier form.
Observing changes ¶
A provider reports changes of all elements below a node once their directory has been loaded. Watch registers a callback for an element and loads what is needed for its changes to be reported.
Stream parameters, typically audio level meters, are different: they only report values while subscribed, and usually do so every 50 to 80 ms. Subscribing is therefore never implicit. Use Subscribe to start a stream and Watch to receive its values; glow.Parameter.IsStream reports whether a parameter is a stream.
Concurrency ¶
All methods of Client are safe for concurrent use. The client reads from the connection in its own goroutine, answers keep-alive requests and delivers Watch callbacks from a separate goroutine, so a slow callback does not stall the connection.
Ember+ has no request identifiers except for function invocations. Responses are therefore matched by path: a GetDirectory is answered by the first message reporting children of the requested node, a SetValue by the next value of the parameter. Requests that a provider never answers end when their context ends, or after the timeout set with WithRequestTimeout.
Packages ¶
The client builds on three packages that can also be used on their own: s101 for message framing, ber for the encoding and glow for the data schema.
References such as (spec: "Keep-Alive mechanism") name chapters of the Ember+ specification ("Ember+ Documentation.pdf"), published by Lawo GmbH at https://github.com/Lawo/ember-plus/tree/master/documentation.
Example ¶
package main
import (
"context"
"fmt"
"log"
emberplus "github.com/lukirs95/goemberplusclient"
"github.com/lukirs95/goemberplusclient/pkg/glow"
)
func main() {
ctx := context.Background()
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
el, err := c.Get(ctx, "root/settings/info/version")
if err != nil {
log.Fatal(err)
}
fmt.Println(el.(*glow.Parameter).Value)
}
Output:
Index ¶
- Variables
- type Client
- func (c *Client) Children(ctx context.Context, path string) ([]glow.Element, error)
- func (c *Client) Close() error
- func (c *Client) Connect(ctx context.Context, path string, target uint32, sources []uint32, ...) (glow.Connection, error)
- func (c *Client) Done() <-chan struct{}
- func (c *Client) Err() error
- func (c *Client) Get(ctx context.Context, path string) (glow.Element, error)
- func (c *Client) GetDirectory(ctx context.Context, path string) ([]glow.Element, error)
- func (c *Client) IdentifierPath(ctx context.Context, path string) (string, error)
- func (c *Client) Invoke(ctx context.Context, path string, args ...glow.Value) (*glow.InvocationResult, error)
- func (c *Client) IsOnline(ctx context.Context, path string) (bool, error)
- func (c *Client) SetValue(ctx context.Context, path string, v glow.Value) (glow.Value, error)
- func (c *Client) Subscribe(ctx context.Context, path string) (unsubscribe func() error, err error)
- func (c *Client) Watch(ctx context.Context, path string, fn func(Event)) (stop func(), err error)
- type Event
- type Option
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrClosed = errors.New("emberplus: client closed") ErrNotFound = errors.New("emberplus: element not found") ErrKeepAliveTimeout = errors.New("emberplus: keep-alive timeout") )
var ErrNotStream = errors.New("emberplus: parameter is not a stream")
var ErrWrongType = errors.New("emberplus: wrong element type")
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is an Ember+ consumer. Its methods are safe for concurrent use.
func NewClient ¶
func NewClient(conn io.ReadWriteCloser, opts ...Option) *Client
NewClient starts a client on an established connection. The client owns conn and closes it on Close.
func (*Client) Children ¶
Children returns snapshots of the children of the element at path. Unlike GetDirectory, it only sends a request if the children have not been loaded yet.
Example ¶
This example prints the provider tree, loading every directory.
package main
import (
"context"
"fmt"
"log"
emberplus "github.com/lukirs95/goemberplusclient"
"github.com/lukirs95/goemberplusclient/pkg/glow"
)
func main() {
ctx := context.Background()
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
var walk func(path string, depth int)
walk = func(path string, depth int) {
children, err := c.Children(ctx, path)
if err != nil {
log.Fatal(err)
}
for _, child := range children {
id, _ := c.IdentifierPath(ctx, child.ElementPath().String())
fmt.Printf("%*s%s\n", 2*depth, "", id)
if n, ok := child.(*glow.Node); ok && (n.IsOnline == nil || *n.IsOnline) {
walk(child.ElementPath().String(), depth+1)
}
}
}
walk("", 0)
}
Output:
func (*Client) Connect ¶
func (c *Client) Connect(ctx context.Context, path string, target uint32, sources []uint32, op glow.ConnectionOperation) (glow.Connection, error)
Connect changes the sources connected to a target of a matrix and returns the connection the provider reports. Its Disposition tells whether the change was applied (Modified), queued (Pending) or refused because the target is locked (Locked). With glow.OpAbsolute, sources replaces all connected sources; an empty list disconnects the target.
Example ¶
package main
import (
"context"
"fmt"
"log"
emberplus "github.com/lukirs95/goemberplusclient"
"github.com/lukirs95/goemberplusclient/pkg/glow"
)
func main() {
ctx := context.Background()
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
// Connect source 3 to target 0, replacing its current sources.
conn, err := c.Connect(ctx, "root/router/matrix", 0, []uint32{3}, glow.OpAbsolute)
if err != nil {
log.Fatal(err)
}
if conn.Disposition == glow.Locked {
fmt.Println("target 0 is locked")
}
}
Output:
func (*Client) Done ¶
func (c *Client) Done() <-chan struct{}
Done is closed when the connection is closed, by Close or because of an error.
func (*Client) Err ¶
Err returns nil while the connection is open. Afterwards it returns ErrClosed after Close, or the error that closed the connection.
func (*Client) Get ¶
Get returns a snapshot of the element at path. Elements that are not in the mirror yet are loaded with GetDirectory on their ancestors; afterwards Get needs no network access.
The value of a stream parameter is only kept current while it is subscribed; otherwise Get returns the last value received.
func (*Client) GetDirectory ¶
GetDirectory requests the children of a node, or the element itself for parameters, matrices and functions, and returns a snapshot from the mirror. For the root, path is "".
The request is answered by the first message that reports a child of a node (or the node without properties, meaning it has no children). If a provider spreads the children of a node over several messages, the snapshot may be incomplete; later messages still update the mirror. A matrix is answered once a connection is known for every target.
func (*Client) IdentifierPath ¶
IdentifierPath returns the identifier path of the element at path, such as "root/settings/info", which stays valid across sessions.
func (*Client) Invoke ¶
func (c *Client) Invoke(ctx context.Context, path string, args ...glow.Value) (*glow.InvocationResult, error)
Invoke calls a function and waits for its result. The result reports whether the provider executed the function successfully. A function may be invoked more than once by redundant consumers, so functions should not change state relatively (spec: "Ember+ 1.2: Function Extensions").
Example ¶
package main
import (
"context"
"fmt"
"log"
emberplus "github.com/lukirs95/goemberplusclient"
"github.com/lukirs95/goemberplusclient/pkg/glow"
)
func main() {
ctx := context.Background()
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
res, err := c.Invoke(ctx, "root/presets/recall", glow.Int(4))
if err != nil {
log.Fatal(err)
}
if !res.Success {
fmt.Println("recall failed")
}
}
Output:
func (*Client) IsOnline ¶
IsOnline reports whether the element at path is online: an element is offline if it or one of its ancestors is marked offline (spec: "Glow specific properties").
func (*Client) SetValue ¶
SetValue requests a new value for a parameter and returns the value the provider confirms. A provider answers an invalid value with the unchanged current value (spec: "Value change requests"), so the result may differ from v.
Another value update of the parameter that arrives before the provider's answer, e.g. caused by a front panel, is taken as the answer.
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
emberplus "github.com/lukirs95/goemberplusclient"
"github.com/lukirs95/goemberplusclient/pkg/glow"
)
func main() {
ctx := context.Background()
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
v, err := c.SetValue(ctx, "root/dsp/gain", glow.Real(-3))
if err != nil {
log.Fatal(err)
}
if v != glow.Real(-3) {
fmt.Println("the provider kept", v)
}
}
Output:
func (*Client) Subscribe ¶
Subscribe requests the values of a stream parameter, such as audio level meters (spec: "Application defined commands"). Streams typically report values every 50 to 80 ms, so they should only be subscribed while needed. Use Watch to receive the values.
Parameters that are not streams return ErrNotStream without sending anything; glow.Parameter.IsStream reports whether a parameter is a stream. Subscriptions are counted: the provider is unsubscribed when the last unsubscribe function is called.
Example ¶
Stream parameters such as meters only report values while subscribed. This example checks that a parameter is a stream before subscribing.
package main
import (
"context"
"fmt"
"log"
"time"
emberplus "github.com/lukirs95/goemberplusclient"
"github.com/lukirs95/goemberplusclient/pkg/glow"
)
func main() {
ctx := context.Background()
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
const meter = "root/mixer/ch1/level"
el, err := c.Get(ctx, meter)
if err != nil {
log.Fatal(err)
}
if !el.(*glow.Parameter).IsStream() {
log.Fatal(meter, " is not a stream")
}
unsubscribe, err := c.Subscribe(ctx, meter)
if err != nil {
log.Fatal(err)
}
defer unsubscribe()
stop, err := c.Watch(ctx, meter, func(e emberplus.Event) {
fmt.Println(e.Update.(*glow.Parameter).Value)
})
if err != nil {
log.Fatal(err)
}
defer stop()
time.Sleep(10 * time.Second)
}
Output:
func (*Client) Watch ¶
Watch calls fn for every change of the element at path. Watching a parameter or node loads the directory of its parent if needed, which makes the provider report changes (spec: "Application defined commands"). Watching a matrix additionally loads the matrix itself, which subscribes to its connection changes (spec: "Ember+ 1.1: Matrix Extensions").
Watch never subscribes to streams: a stream parameter only reports values while it is subscribed with Subscribe.
fn is called from a single goroutine in the order the changes arrived. A slow fn delays later events but never the connection itself; events are queued without limit until fn catches up. After stop returns, fn is not called again.
Example ¶
package main
import (
"context"
"fmt"
"log"
emberplus "github.com/lukirs95/goemberplusclient"
"github.com/lukirs95/goemberplusclient/pkg/glow"
)
func main() {
ctx := context.Background()
c, err := emberplus.Dial(ctx, "192.168.1.10:9000")
if err != nil {
log.Fatal(err)
}
defer c.Close()
stop, err := c.Watch(ctx, "root/dsp/gain", func(e emberplus.Event) {
if v := e.Update.(*glow.Parameter).Value; v != nil {
fmt.Println("gain changed to", v)
}
})
if err != nil {
log.Fatal(err)
}
defer stop()
<-c.Done()
}
Output:
type Event ¶
type Event struct {
// Element is a snapshot of the element after the change was applied.
Element glow.Element
// Update holds only the fields the provider transmitted.
Update glow.Element
}
Event reports a change of a watched element.
type Option ¶
type Option func(*config)
Option configures a Client.
func WithKeepAlive ¶
WithKeepAlive sets how long the connection may be idle before the client sends a keep-alive request, and how long it waits for any data afterwards before closing the connection. The defaults are 5 s each (spec: "Keep-Alive mechanism"). An idle duration of 0 disables keep-alive requests; requests from the provider are always answered.
func WithLogger ¶
WithLogger sets the logger for protocol errors that do not close the connection, such as undecodable messages. By default nothing is logged.
func WithRequestTimeout ¶
WithRequestTimeout sets the timeout for requests whose context has no deadline. Ember+ does not define a response to requests on unknown elements, so without a timeout such requests could wait forever. The default is 10 s.
Directories
¶
| Path | Synopsis |
|---|---|
|
pkg
|
|
|
ber
Package ber implements the subset of the Basic Encoding Rules (X.690) used by Ember+ (EmBER).
|
Package ber implements the subset of the Basic Encoding Rules (X.690) used by Ember+ (EmBER). |
|
glow
Package glow implements the Glow data schema of Ember+ (Glow DTD).
|
Package glow implements the Glow data schema of Ember+ (Glow DTD). |
|
s101
Package s101 implements S101, the framing protocol that transports Ember+ messages (spec: "Message Framing").
|
Package s101 implements S101, the framing protocol that transports Ember+ messages (spec: "Message Framing"). |