Functionality can be added to Weber via extensions. You can add multiple extensions to the same experiment. The reference provides a list of available extensions. Here we'll cover how to create new extensions.
Extensions can create new methods of existing Weber functions on custom types, just like any Julia package, and this may be all that's necessary to extend Weber.
However, extensions also have several ways to insert additional behavior into a number of methods via special extension machinery.
To extend one of these functions you first define an extension type. For example:
type MyExtension <: Weber.Extension
my_value::String
end
For all of the public functions above (everything but poll_events
), you can then define a new method of these functions that includes one additional argument beyond that listed in its documentation, located before all other arguments. This argument should be of type ExtendedExperiment{MyExtension}
. To extend the private poll_events
function, replace the Experiment
argument with an ExtendedExperiment{MyExtension}
argument.
These functions have specific machinery setup to make extension possible. Don't use this same approach with other functions and expect your extension to work.
As an example, record
could be extended as follows.
function record(experiment::ExtendedExperiment{MyExtension},code;keys...)
record(next(experiment),code;my_extension=extension(experiment).my_value,keys...)
end
There are a few things to note about this implementation. First, the extension object is accessed using extension
.
Second, record
is called on the next
extension. All extended functions should follow this pattern. Each experiment can have multiple extensions, and each pairing of an experiment with a particular extension is called an experiment version. These are ordered from top-most to bottom-most version. The top-most version is paired with the first extension in the list specified during the call to Experiment
. Subsequent versions are accessed in this same order, using next
, until the bottom-most version, which is the experiment without any paired extension.
For the extension to record
to actually work, setup
must also be extended to add the column :my_extension
to the data file.
function setup(fn::Function,experiment::ExtendedExperiment{MyExtension})
setup(next(experiment)) do
addcolumn(top(experiment),:my_extension)
fn()
end
end
This demonstrates one last important concept. When calling addcolumn
, the function top
is called on the experiment to get the top-most version of the experiment. This is done so that any functionality of versions above the current one will be utilized in the call to addcolumn
.
As a general rule, inside an extended method, when you dispatch over the same function which that method implements, you should pass it next(experiment)
while all other functions taking an experiment argument should be passed top(experiment)
.
The private interface of run-time objects.
Most of the functionality above is for the extension of setup-time behavior. However, there are two ways to implement new run-time behavior: the generation of custom events and custom moments.
Custom Events
Extensions to poll_events
can be used to notify watcher functions of new kinds of events. An event is an object that inherits from Weber.ExpEvent
and which is tagged with the @event
macro. Custom events can implement new methods for the existing public functions on events or their own new functions.
If you define new functions, instead of leveraging the existing ones, they should generally have some default behavior for all ExpEvent
objects, so it is easy to call the method on any event a watcher moment receives.
Event Timing
To specify event timing, you must define a time
method for your custom event. You can simply store the time passed to poll_events
in your custom event, or, if you have more precise timing information for your hardware you can store it here. Internally, the value returend by time
is used to determine when to run the next moment when a prior moment triggers on the event.
Custom Key Events
One approach, if you are implementing events for a hardware input device, is to implement methods for iskeydown
. You can define your own type of keycode (which should be of some new custom type <: Weber.Key
). Then, you can make use of the @key_str
macro by adding entries to the Weber.str_to_code
dictionary (a private global constant). So for example, you could add the following to the module implementing your extension.
Weber.str_to_code["my_button1"] = MyHardwareKey(1)
Weber.str_to_code["my_button1"] = MyHardwareKey(2)
Such key types should implement ==
, hash
and isless
so that key events can be ordered. This allows them to be displayed in an organized fashion when printed using listkeys
.
Once these events are defined you can extend poll_events
so that it generates events that return true for iskeydown(myevent,key"my_button1")
(and a corresponding method for iskeyup
). How this happens will depend on the specific hardware you are supporting. These new events could then be used in an experiment as follows.
response(key"my_button1" => "button1_pressed",
key"my_button2" => "button2_pressed")
Custom Moments
You can create your own moment types, which must be children of Weber.SimpleMoment
. These new moments will have to be generated using some newly defined function, or added automatically by extending addtrial
. Once created, and added to trials, these moments will be processed at run-time using the function handle
, which should define the moment's run-time behavior. Such a moment must also define moment_trace
.
A moment can also define delta_t
–to define when it occurs–or prepare!
–to have some kind of initialization occur before its onset–but these both have default implementations.
Methods of handle
should not make use of the extension machinery described above. What this means is that methods of handle
should never dispatch on an extended experiment, and no calls to top
, next
or extension
should occur on the experiment object. Further, each moment should belong to one specific extension, in which all functionality for that custom moment should be implemented.
Registering Your Extension
Optionally, you can make it possible for users to extend Weber without ever having to manually download or import your extension.
To do so you register your extension using the @Weber.extension
macro. This macro is not exported and should not be called within your extensions module. Instead you should submit a pull request to Weber with your new extension defintion added to extensions.jl
. Once your extension is also a registered package with METADATA.jl it can be downloaded the first time a user initializes your extension using its corresponding macro.