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 changesQuery.Rising: Notify whenever the noun's value increasesQuery.Falling: Notify whenever the noun's value decreasesQuery.Equals(v): Notify whenever the noun's value equals a given valueQuery.Above(v): Notify whenever the noun's value changes and is above a given limitQuery.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:
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:
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.
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:
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.finalizewill 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:
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.
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.
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.
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.
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.
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.
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 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:
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.