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.

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:

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. 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:

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.

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.