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. _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 return early upon the timeout expiring. ```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. ```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(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(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.