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 ¶
- Variables
- func IsTemporary(err error) bool
- func IsTimeout(err error) bool
- type Arb
- func (a *Arb) Close() error
- func (a *Arb) Control(cmd Command, args ...interface{}) (rsp Response)
- func (a *Arb) Open() error
- func (a *Arb) Read(b []byte) (int, error)
- func (a *Arb) Simple(cmd, success, failure []byte, duration time.Duration) (rsp Response)
- func (a *Arb) String() string
- func (a *Arb) Write(b []byte) (int, error)
- type Arbiter
- type CheckFunc
- type Command
- type Commands
- type ExitCriteria
- type IDoIO
- type InvalidIO
- type NetClient
- type Response
- type SerialClient
Constants ¶
This section is empty.
Variables ¶
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 ¶
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
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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 ¶
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 ¶
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 ¶
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
type Commands ¶
Commands is map of Command structure where the key should be Command.Name
func (Commands) Contains ¶
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 ¶
JSONLabels returns a json array of the stored commands
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 ¶
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
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
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 ¶
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 ¶
Close conforms to io.Closer, but immediately returns upon ctx destruction after closing the underlying transport
func (*NetClient) Open ¶
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 ¶
Read 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).
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