agnoio

package module
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2023 License: MIT Imports: 16 Imported by: 1

README

Agno(stic) IO

Package agnoio provides interfaces and structures that allows for data streaming, and command & control interfaces.

License

MIT License

Copyright (c) 2015-2017 University Corporation for Atmospheric Research

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Documentation

Overview

Package agnoio provides and interface and some implementers that allow easier use of low level IO in a way that is mostly agnostic to the actual IO transport. This was conceived of in order to abstract out many hardware dependant stream IO oriented hardware protocols in an agnostic manner. This package attempts to answer the question of how to treat a network socket, serial port, I2C, SPI, and any other common streaming systems the same. All these transports have the following in common: they read & write bytes, can be closed, occasionally screw up and need to be re-opened (and that is what the IDoIO interface tries to provide).

Purpose

Have you every wanted to do something bizarre as open a serial port, socket, zmq, UDP datastream, i2c or spi, or named pipe as agnostic as possible? If so, then this package is for you. This package attempts to be a thin veneer over several different io packages so callers can be mostly agnostic to what the underlying mechanics are.

Interfaces

This package provides two different, but related, interfaces: IDoIO (eye-do-eye-oh) and an Arbiter. An IDoIO is the basic read-write-open-close item that can read and write some bytes off of. Examples of an IDoIO include a serial port connected to a GPS NMEA (stream of location information) or some a free running sampling instrument (temperature, distance, speed, height, motor speed, etc.) running over a tcp socket. An Arbiter is similar in concept to an IDoIO, but it provides more of a command & control interface: You send it commands, the remote side does something with the command, and provides a response. The Arbiter pattern might be found in sending commands to a servo to move to a different position, move the center frequency of a piece of test equipment, or send some sort of power on/off sequence to a PDU in a data center. IDoIOs usually need some sort of parser where Arbiters need to be instructed what to do.

Dial Strings and Implementations

Although you can write your own IDoIO (and I welcome patches!), this package provides IDoIOs for the following transports, which are selected via a URI dial string. The schema (eg tcp:// or serial://) portion determines the backend for which IDoIO implementation is selected. Hereafter, the individual format for the remaining portion of the string is implementation specific, but should be transparent enough that someone with a crude understanding would know what to make of the parameters. The following schemas are provided by this package, and can be generically returned via the NewIDoIO() function:

tcp://<host:port> - Outgoing Sockets of type tcp (either v4 or v6)
tcp4://<host:port> - Outgoing Sockets of type tcp v4
tcp6://<host:port> - Outgoing Sockets of type tcp v6
udp://<host:port> - Outgoing Sockets of type udp (either v4 or v6)
udp4://<host:port> - Outgoing Sockets of type udp v4
udp6://<host:port> - Outgoing Sockets of type udp v6
serial://<device>:<baud> - Serial connection
rs232://<device>:<baud> - Serial connection

Context Usage

This package makes use of the context package. The passed context is used to derive child contexts and a cancel function. If .Stop() is called, the cancel function will be called, and any further IO using the structure will end up in context errors. This is helpful as it forces connection hangup and known exit behaviour.

Error Handling

All errors returned from this package either implicitly or explicitly conform to net.Error, which is to say after a cast, you have access to two additional func receivers: .Timeout() and .Temporary(). Timeout() returns true if the error was due to a timeout of some variety, and the transport is still opened. Temporary() returns true if the error is a temporary error, and true if the connection is closed and will need to be opened.

It is preferred that no structures provided by this package attempt to maintain a constant connection, but rather that when the connection dies / is killed / fails / returns errors, the caller should have a bit of knowledge as to what to do with these errors, such as reconnect, panic, stick a finger in a light socket, etc. Generally each transport will have some sort of unique errors that might need special handling.

Index

Constants

This section is empty.

Variables

View Source
var (
	//ErrBytesArgs is returned when calling Bytes if any of the following occur:
	// - Wrong Number of args (too few / many)
	// - Wrong order (ie Command.Prototype is "%s %d" and provided args are '24, "string"”)
	// - Wrong types (ie Command.Prototype is "%s" and provided arg is '25')
	ErrBytesArgs = errors.Errorf("Proper arguments not provided to expand command into bytes")

	//ErrBytesFormat is returned when the args used to populate the command forms
	//a byte[] that does not match the Validating regexp (.CommandRegexp)
	ErrBytesFormat = errors.Errorf("Formed command does not match allowable format for outgoing commands")

	// ErrErrorResponse is returned when the response to a command matches the failure
	// or error criterial criteria.  It has the following properties:
	// - IsTemporary(ErrErrorResponse) = false
	// - IsTimeout(ErrErrorResponse) == false
	// This error is intended to be used to compare against when checking errors
	ErrErrorResponse = newErr(false, false, errors.New("Command received error response"))
)

Functions

func IsTemporary

func IsTemporary(err error) bool

IsTemporary is a shorthand way to check if a returned error is temporary. Dont pass nil errors here, the desired behaviour is not defined, and will panic

func IsTimeout

func IsTimeout(err error) bool

IsTimeout is a shorthand way to check if a returned error is a timeout. Dont pass nil errors here, the desired behaviour is not defined, and will panic

Types

type Arb

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

Arb is a wrapper over a IDoIO, but it locks the IDoIO under a mutex to serialize access.

func (*Arb) Close

func (a *Arb) Close() error

Close conforms to IDoIO and io.Closer, but for an Arbiter. Unlike a regular IDoIO, access is locked within a mutex, and the read and write channels are linked / bonded

func (*Arb) Control

func (a *Arb) Control(cmd Command, args ...interface{}) (rsp Response)

Control conforms to Arbiter interface, but this implementation uses a IDoIO to handles the data. Control is the reason that serialized access is required: when Commands are sent, Control needs to read all the incoming data while Checking for a valid Response.

If .CommandRegexp is nil, whatever command is formed is not checked for completeness (see Command.Bytes) If .Error is nil (not set), then the output is not compared for an error condition, and the command will only succeed or timeout. If .Response is nil (not set), then the output is not compared for a positive response, and Command will only fail or timeout. If both .Error and .Response are nil, this command will only time out. The response.Error will be the package ErrErrorResponse if the Error condition is matched

func (*Arb) Open

func (a *Arb) Open() error

Open conforms to IDoIO, but for an Arbiter. Unlike a regular IDoIO, access is locked within a mutex, and the read and write channels are linked / bonded

func (*Arb) Read

func (a *Arb) Read(b []byte) (int, error)

Read conforms to IDoIO, io.Reader, but for an Arbiter. Unlike a regular IDoIO, access is locked within a mutex, and the read and write channels are linked / bonded

func (*Arb) Simple

func (a *Arb) Simple(cmd, success, failure []byte, duration time.Duration) (rsp Response)

Simple is a very dumb control IO Method. It blindly sends the 'cmd' byte[], and waits up to duration before giving up with an error where IsTimeout() returns true. The success and failure criteria use bytes.Contains to evaluate the success / failure criteria, with the following exceptions. If success is nil (or []byte{}), then there is no success criteria, and the returned response.Error is guaranteed to be ErrErrorResponse (if the failure criteria is met), an error where IsTimeout() returns true, or some other underlying connection error. Similarly, if failure is nil (or []byte{}) then there is no error criteria, and the only possible error types are nil (for a successful response), an error where IsTimeout() returns true, or some underlying connection error. If both success and failure are nil, Response.Error will be either a timeout condition, or some underlying connection error. There are corner cases where allowing for nil criteria is helpful, assuming that the caller is aware of the behaviour

Access is serialized, and takes over control of the arbiter. EG:

a, _ := agnoio.NewArbiter(...)
a.Simple(nil, nil, nil, 1 * time.Hour) //Blocks other a.* calls for an hour, sans connection faults

func (*Arb) String

func (a *Arb) String() string

String conforms to IDoIO, but for an Arbiter. It usually returns something like "Arbiter over <idoio>", where <idoio> is the Stringer variant of the underlying IDoIO

func (*Arb) Write

func (a *Arb) Write(b []byte) (int, error)

Write conforms to IDoIO, io.Writer, but for an Arbiter. Unlike a regular IDoIO, access is locked within a mutex, and the read and write channels are linked / bonded

type Arbiter

type Arbiter interface {
	IDoIO //I Do Too

	/*Simple is a very simple form of command and control.  It sends out cmd,
	  making sure all the bytes get pushed out, and then constantly reads the incoming
	  data for any a sequence that contains either 'ok' or 'failure' before timing
	  out at the passed duration. The returned response contains the duration,
	  the bytes received, and an error, which is nil if the ok sequence was
	  detected, or a non-nil error*/
	Simple(cmd, ok, failure []byte, duration time.Duration) Response

	/*Control forms a byte slice to write out on the wire by combining cmd with
	  args, and sans error, will write the formed byte slice out on the wire. It
	  should block until either its internal buffer matches cmd.Response, cmd.Error,
	  or the process takes longer than cmd.Timeout. The returned Response should be
	  populated correctly as described in the Response docstring*/
	Control(cmd Command, args ...interface{}) Response
}

Arbiter provides a command and control interface to []byte streams. Original design intentions were to provide a way to communicate to devices that respond to 'commands' sent over the wire. Functionally, this can be seen as a socket or generic IO wrapper to provide a way to read and write commands and data. As a sanity, there can only be one caller, as this is purposefully not safe from multiple callers via the standard "go <func>" syntax. Any errors that are not ErrTimeout or ErrBusy are errors coming from the underlying layers and are to be dealt with

func Arbitrate

func Arbitrate(ctx context.Context, idoio IDoIO) (Arbiter, context.CancelFunc)

Arbitrate returns an Arbiter and a context.CancelFunc. This is meant to be a temporary solution, where the arbiter is meant to be used for a short duration and then revert to using the IDoIO. The CancelFunc should be called whenever the caller is done using the Arbiter functionally (eg, .Control).

func NewArbiter

func NewArbiter(ctx context.Context, timeout time.Duration, dial string) (Arbiter, error)

NewArbiter returns an opened Arbiter from the passed dial string, ctx, and timeout. dial will need to match a known dial format, timeout will be used during the connection process, and the ctx will be used to ensure the operation will cease if the ctx is stopped.

type CheckFunc

type CheckFunc func([]byte) ExitCriteria

CheckFunc is used to determine if the passed bytes match some success, failure, or insufficient data to determine exit criteria. Only the defined ExitCriteria may be used - any other return value will panic. If the CheckFunc returns Insufficient, it is assumed that more incoming data is required before a success or failure criteria can be established. If Failure is returned, it is assumed that the sum of the bytes demarcates a failure condition, and the calling process should cease reading data. Likewise, a Success condition indicates a successful exit criteria, and the calling process should cease reading data and return a nil error.

type Command

type Command struct {
	/*Name is the human name of command, typically without any arguments. E.g. if
	  the Prototype is something like "Floor\x55\x32", the name should be something
	  that make sense for your average human being:  like "Go to Floor 55"*/
	Name string

	/*Timeout is the max time allowed before the command should be forced to return
	  a failed-because-it-took-too-long response. If the command take longer
	  than this timeout, the command is to be understood to have failed*/
	Timeout time.Duration

	/*Prototype is the command prototype that is fed, with any arguments, to fmt.Sprintf
	  and converted to bytes to shovel to a IDoIO.  That is,
	      fmt(.Prototype, args...)
	  is sent down the line.*/
	Prototype string

	/*CommandRegexp is the regex that the final command must match before being
	  returned by Bytes(). This works in conjunction with the .Prototype in the
	  following way such that c, defined by the following:
	       c := fmt.Sprintf(.Prototype, v ... interface{})
	  must not contain %!, (a sign of too many/few/wrong parameters), and
	       CommandRegexp.MatchString(c)
	  must be true.*/
	CommandRegexp *regexp.Regexp

	//Response is a regexp that should match good/positive/affirmative responses.
	Response *regexp.Regexp

	//Error is a regexp that should match bad/negative/failure responses
	Error *regexp.Regexp

	//Description is a human-readable string of a brief explanation of the commands purpose
	Description string
}

Command represents a command represents the Command portion of a Command-Response operation.

func (Command) Bytes

func (c Command) Bytes(v ...interface{}) ([]byte, error)

Bytes returns the raw bytes that should be sent to the interface based on the Command.Prototype and any optional arguments passed to it via

fmt.Sprintf(.Prototype, v...)

If the resulting string formed by above contains any "%!" sequences, then this assumes that the formed command was not properly fed through fmt.Sprintf, and will return the package error ErrBytesArgs. This currently does not allow for embedded "#!" sequences, which should be fixed via lexical analysis

If .CommandRegexp is nil, it is assumed that any command formed (sans the above rule) is acceptable. If not, the formed command is compared against CommandRegexp. If the formed command does not match, the package error ErrBytesFormat is returned.

If all goes well, a byte slice to be sent down the line and a nil error is returned.

BUG: Current implementation disallows handling of commands with "%!" sequences

func (Command) String

func (c Command) String() string

String implements the Stringer interface

type Commands

type Commands map[string]Command

Commands is map of Command structure where the key should be Command.Name

func Merge

func Merge(cmds ...Commands) Commands

Merge takes multiple command sets and returns a single command set

func (Commands) Clone

func (c Commands) Clone() Commands

Clone returns a deep copy of the Commands

func (Commands) Contains

func (c Commands) Contains(named ...string) bool

Contains returns true if the command set contains any of the passed named commands. It checks the key values, not the embedded Command.Name values

func (Commands) JSONLabels

func (c Commands) JSONLabels() (r string)

JSONLabels returns a json array of the stored commands

func (Commands) String

func (c Commands) String() (r string)

String implements the Stringer() interface

type ExitCriteria

type ExitCriteria int

ExitCriteria is a set of defined success criteria that CheckFunc must return

const (
	//Insufficient should be returned if more bytes are required in order to determine success or failure
	Insufficient ExitCriteria = 1 + iota

	//Failure indicates the current set of bytes indicates an error condition
	Failure

	//Success indicates the current set of bytes indicates an accepted condition.
	Success
)

type IDoIO

type IDoIO interface {
	fmt.Stringer
	io.ReadWriter
	io.Closer
	Open() error
}

IDoIO is a generic interface agnostic io devices should conform to. An IDoIO should be able to tell others in some human-readable string form what the transport actually is (fmt.Stringer). An IDoIO should be able to read, and write byte slices (io.ReadWriter), and also should be able to Open and Close the device as will. This does mean that once created, an IDoIO needs to cache and properly deal with opening criteria.

Any error returned must be castable to net.Error

func NewIDoIO

func NewIDoIO(ctx context.Context, timeout time.Duration, dial string) (IDoIO, error)

NewIDoIO returns a struct the conforms to the IOStreamer interface

type InvalidIO

type InvalidIO string

InvalidIO conforms to IDoIO, but returns an error for every operation.

If invalid Dial strings are passed, you received one of these

func (InvalidIO) Close

func (i InvalidIO) Close() error

Close always returns an error

func (InvalidIO) Open

func (i InvalidIO) Open() error

Open always returns an error

func (InvalidIO) Read

func (i InvalidIO) Read([]byte) (int, error)

Read always returns an error

func (InvalidIO) String

func (i InvalidIO) String() string

String is less than helpful

func (InvalidIO) Write

func (i InvalidIO) Write([]byte) (int, error)

Write always returns an error

type NetClient

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

NetClient provides an implementer of the IDoIO interface. It provides access under the following URI Regimes:

tcp://
tcp4://
tcp6://
udp://
udp4://
udp6://

func NewNetClient

func NewNetClient(ctx context.Context, timeout time.Duration, dial string) (*NetClient, error)

NewNetClient opens a connection to remote tcpv4 host. dial should be in the form of: 'tcp|udp[46]{0,1}://<host>:<port>'

Timeout is used a read/write timeout at the socket level. If timeout is zero, timeouts are not used nor applied, and any errors are due to normal socket behaviour. If timeout is greater than zero, a deadline is set on every Read() and Write() function. In this case, Read() and Write() will return a timeout error that should be checked via something like the following:

io := NewNetClient(ctx, 100 * time.Millisecond, "tcp://localhost:4242")
...
n, e := io.Write(b)
switch e {
case io.EOF: // Broken socket
  ...
default:
  if nerr, ok := e.(net.Error); ok {
    if nerr.Temporary() { //Temporary error
      ...
    }
    if nerr.Timeout() { //Timeout error from enforced deadline
      ...
    }
  }
}

The caller is responsible for handling errors. This pkg just propagates any error encountered.

func (*NetClient) Close

func (nc *NetClient) Close() error

Close conforms to io.Closer, but immediately returns upon ctx destruction after closing the underlying transport

func (*NetClient) Open

func (nc *NetClient) Open() (err error)

Open forcible disconnects (ignore errors) the network connection and attempts the connect process again. It returns an error if it was unable to start

func (*NetClient) Read

func (nc *NetClient) Read(b []byte) (int, error)

Read conforms to io.Writer, but immediately returns upon ctx destruction after closing the underlying transport

func (*NetClient) String

func (nc *NetClient) String() string

String conforms to the fmt.Stringer interface. Prints something like

tcp6 connection to tcp6://localhost::8080

which meant as a human comprehendable explanation of the connection

func (*NetClient) Write

func (nc *NetClient) Write(b []byte) (int, error)

Write conforms to io.Writer, but immediately returns upon ctx destruction after closing the underlying transport

type Response

type Response struct {
	Bytes    []byte        //Raw bytes read or received.  In Control funcs, this is the raw value that matched the 'match' clause
	Error    error         //any non-nil errors
	Duration time.Duration //how long did the request take
}

Response is what is returns from Command requests.

Bytes is a copy of the []byte read while waiting for a timeout or matching response. Error is one of:

  • nil if the bytes received match the ingoing Command.Response regexp
  • ErrTimeout if a timeout was received
  • Some other error on other low level issues.

Duration is the duration the command took before it succeeded (or failed).

func (Response) String

func (r Response) String() string

String implements the Stringer interface

type SerialClient

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

SerialClient wraps around a serial port

func NewSerialClient

func NewSerialClient(ctx context.Context, timeout time.Duration, dial string) (*SerialClient, error)

NewSerialClient opens a connection to a serial device in 8N1 mode. Dial should be in the form of "serial://<device>:<baud>

func (*SerialClient) Close

func (sc *SerialClient) Close() error

Close conforms to io.Closer, but immediately returns upon ctx destruction after closing the underlying transport

func (*SerialClient) Open

func (sc *SerialClient) Open() (err error)

Open forcible closes any previously open ports (ignore errors) the network connection and attempts the connect process again. It returns an error if it was unable to start

func (*SerialClient) Read

func (sc *SerialClient) Read(b []byte) (int, error)

Read conforms to io.Writer, but immediately returns upon ctx destruction after closing the underlying transport

func (*SerialClient) String

func (sc *SerialClient) String() string

String conforms to the fmt.Stringer interface

func (*SerialClient) Write

func (sc *SerialClient) Write(b []byte) (int, error)

Write conforms to io.Writer, but immediately returns upon ctx destruction after closing the underlying transport

Jump to

Keyboard shortcuts

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