You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

273 lines
10 KiB

oc-basecontrol
==============
`oc-basecontrol` is a library for distributed base control systems in
OpenComputers.
Concept
-------
Each node in the base network exports a set of _nouns_ and _verbs_.
- _Nouns_ are values which can be queried by other nodes and which
can change over time. Other nodes can also request to be notified
about updates of nouns. This is called _listening_. Examples: The power
a capacitor has stored or the fluid level in a tank.
- _Verbs_ are functions that can be triggered remotely. There are two
ways this can be done:
- _Asynchroneously_, which is "fire-and-forget". There is no checking that
the call succeeded and no way to pass back a return value.
- _Synchroneous_ verb calling, where the caller will block until a response
with a return value is returned or the call timed out.
- _Listening_: Nodes can request a notification on a noun change event.
There are a few different queries that can be used here:
- `Query.Change`: Notify whenever the noun changes
- `Query.Rising`: Notify whenever the noun's value increases
- `Query.Falling`: Notify whenever the noun's value decreases
- `Query.Equals(v)`: Notify whenever the noun's value equals a given value
- `Query.Above(v)`: Notify whenever the noun's value changes and is above
a given limit
- `Query.Below(v)`: Notify whenever the noun's value changes and is below
a given limit
Basic API
---------
### `BaseControl:new([modem])`
There are a few different ways to construct the base-controller. The easiest
is to just use the object created during loading the library:
```lua
local bc = require("bc"):new()
bc:register("foo_noun", 1234)
bc:finalize()
```
If you need finer grained control, you can also use the initial object as a
class to create base-controllers:
```lua
local BaseControl = require("bc")
local bc = BaseControl()
-- or
local bc = BaseControl:new()
```
The main job of the constructor is to initialize the network interface.
This means as soon as the constructor is called, our node will go "online".
You can give a custom network to `new` if you do not want to use the default
one. More info under _Network Configuration_.
### `BaseControl:register(name, value)`
Registers a new noun or verb named `name` with the initial value of `value`.
If `value` is a _function_, it will be a verb. `register` will fail if `name`
is already known as a local noun or verb. Note that locals always take
precedence over remotes so `register` will happily add a local which a remote
also exports.
```lua
local bc = require("bc")
-- Register a noun
bc:register("some_noun", 12345)
-- Register a verb
bc:register("some_verb", function(param_a, param_b)
print("Verb called with "..param_a.." and "..param_b..".")
end)
bc:finalize()
```
You can also batch-register multiple nouns/verbs:
```lua
local bc = require("bc")
bc:register{
some_noun=1234,
other_noun=4321,
a_verb=function(param)
print("Param: ", param)
end,
}
```
### `BaseControl:finalize([waits], [timeout])`
After creating a new base-controller, it will not immediately announce its data
into the network. It will first acumulate local nouns and verbs and then send
them out in one batch. This happens when you call `finalize`.
Finalize takes two optional parameters:
- `waits`: A list of remote nouns/verbs that this node requires to be available.
`finalize` will only return once all of them have been registered by remote
nodes.
- `timeout`: Return early if timeout was reached before all waits were found.
If finalize is called again, it will start waiting once more, but it will not
reannounce its own presence. `finalize` can also be used as a constructor,
like this:
```lua
local bc = require("bc"):finalize{"some_noun"}
-- With timeout
local bc = require("bc"):finalize({"some_noun"}, 10)
```
### `BaseControl:set(name, value)`
Set a noun or verb to a new value. This only works if `name` is local.
You can not set remote nouns. If `name` is a verb, value **must** be a function
which will be the new callback.
```lua
local bc = require("bc"):new()
bc:register("some_noun", 1234)
bc:finalize()
-- Later ...
bc:set("some_noun", 4321)
```
### `BaseControl:get(noun, [timeout])`
Get the value of a noun named `noun`. `get` can not be called for verbs.
If `timeout` is supplied, the call will return early if no response came before
the timeout expired. `get` will return `nil` if a name is not known.
```lua
local bc = require("bc"):finalize{"some_noun"}
print("Value: ", bc:get("some_noun"))
```
### `BaseControl:call(verb, ...)`
Call a verb **asynchroneously**. All parameters following `verb` will be given
to the remote function. There is no guarantee that the verb was actually run.
`call` will return `true` if an attempt was made to run the verb and `false`
otherwise. `call` specifically does **not** error, because no guarantees
could be made anyway.
_Note_: For local verbs, the call will still be synchroneous.
```lua
local bc = require("bc")
bc:register("some_verb", function(param_a, param_b)
print("Verb called with "..param_a.." and "..param_b..".")
end)
bc:finalize()
-- This call can either happen locally or on another node
bc:call("some_verb", "param_a_value", "param_b_value")
```
### `BaseControl:call_sync(verb, timeout, ...)`
Call a verb **synchroneously**. All parameters following `verb` will be given
to the remote function. `call_sync` will return the remote function's return
value. If timeout is not `nil` or `0`, `call_sync` will error upon
the timeout expiring.
_Note_: For local verbs, the timeout can not be adhered to.
```lua
local bc = require("bc")
bc:register("stupid_add", function(a, b)
return a + b
end)
bc:finalize()
-- This call can either happen locally or on another node
local res = bc:call_sync("stupid_add", nil, 12, 34)
print("12 + 34 = "..res)
```
### `BaseControl:listen(noun, query, callback)`
Install a listener on changes of remote nouns. `noun` must be a remote noun.
If it is not known at this time, the listener is still installed in hope that
the node will come online later. `query` must be a valid query, chosen from
`BaseControl.Query`. A full list of currently supported queries was shown above.
The `callback` function will get one argument: The new value of the noun that
triggered the event. `listen` returns a unique id for the installed listener
that could later be used to cancel it. `listen` also returns a boolean as the
second value: It indicates whether an attempt was made at installing the
listener. If the noun was not yet known (`false`), the listener is still kept
and will be installed as soon as the noun is registered.
```lua
bc = require("bc"):finalize{"some_noun"}
bc:listen("some_noun", bc.Query.Change, function(value)
print("'some_noun' is now '"..value.."'")
end)
bc:listen("some_noun", bc.Query.Below(0), function(value)
print("'some_noun' is negative! ("..value..")")
end)
```
### `BaseControl:cancel(noun, id)`
Cancel a previously installed listener. The id will be invalid after this call.
```lua
local listenid = bc:listen("some_noun", bc.Query.Change, function(value)
print("'some_noun' is now '"..value.."'")
end)
-- Later ...
bc:cancel("some_noun", listenid)
```
### `BaseControl:close()`
End this base-controller and notify others of the local nouns and verbs going
offline. Close will also cancel all installed listeners.
Introspection API
-----------------
The following methods give you insight into the network:
### `BaseControl:has_noun(noun)`
Whether `noun` is known at this time (either local or remote). Note that even if
a noun is known, it might still not be available if the remote node exited
without deregistering.
### `BaseControl:has_verb(verb)`
Whether `verb` is known at this time (either local or remote). Note that even if
a verb is known, calling it might still not succeed if the remote node exited
without deregistering. If you need to know for sure, call using `call_sync`.
### `BaseControl:nouns([local_only])`
Returns a list of all known nouns (either local or remote). If `local_only` is
boolean true, only a list of local nouns will be returned.
### `BaseControl:verbs([local_only])`
Returns a list of all known verbs (either local or remote). If `local_only` is
boolean true, only a list of local verbs will be returned.
Network Configuration
---------------------
_oc-basecontrol_ uses the raw [Modem API](https://ocdoc.cil.li/component:modem) with the default modem
(`component.modem`), if no alternative network is supplied to `BaseControl:new`.
If you have multiple modems and want to base-control to use a specific one,
you can set it like this:
```lua
local BaseControl = require("bc")
local bc = BaseControl:new(BaseControl.Network:new(my_moden))
```
The Network constructor looks like this: `function Network:new([modem], [port])`.
You can leave out the modem (it will use the default) and just specify a port,
if you just don't like the default port. If you leave out the port or give no
custom network at all, _oc-basecontrol_ will use a port that is derived from the
version number. This prevents different versions of _oc-basecontrol_ clashing.
Alternatively, you can use a completely different networking stack by writing
your own implementation of `Network`. Take a look at the sources to see how
the default implementation works. A custom network needs to have the following
methods:
### `Network:start(callback)`
Start this network connection (eg. open the port) and install `callback` as a
listener for incoming messages. `callback` has the following signature:
`function callback(remote_addr, msg_tbl)` where `remote_addr` is the address of
the other side (any string works here) and `msg_tbl` is the unserialized message.
For serialization, please use `OpenOS`'s `serialization` library.
### `Network:send(addr, msg_tbl)`
Send a message to the remote party identified by `addr`. `msg_tbl` should be
serialized using `OpenOS`'s `serialization` library.
### `Network:broadcast(msg_tbl)`
Send a message to all connected nodes on the network.
### `Network:pull(filter_func, timeout)`
Block until a message arrives for which `filter_func` returns `true`. The signature
of `filter_func` is `function(addr, msg_tbl)`, similar to the callback given to
`Network:start`. If `timeout` is not `nil`, `pull` should return after the
timeout expired as well.
**Important**: When pulling on an event, the message handler given to `start`
**must** have run before pull returns! Make sure your implementation upholds
this guarantee!
### `Network:stop()`
Close this connection and uninstall the message handler.