DEVICE
COMMUNICATION LIBRARY
INTRODUCTION
The purpose of this document is to help programmers to develop
Matrix Devices. In case you want to support other operating
systems than Linux, you may either use our jdevcom API or ask
us for detailed information about the Matrix Protocol. This
Manual provides you with an overview of the device architecture
and hands-on guides (called how-to's) on the most important
points in a device file.
If you want to know details on E scripting, see the Scripting
Manual. Some additional information on using techniques like select
may be found in the forthcoming libvoc documentation.
As development distributions we used Slackware and RedHat systems,
although the Matrix is supposed to run on any Linux distribution.
Details about this may be found in other Manuals.
OVERVIEW
GENERAL
OVERVIEW
The Matrix
communicates with devices using the devcom API (device communication
application programming interface) on the device side. Since
the Matrix is one entity (although there may be many of it as
discussed later) and the devices are many, it may be thought
of as a Server - Client relation. In contrast to this view is
the fact, that the Kernel uses services of the devices. So we
have no clear Master - Slave relationship. The Kernel may be
thought of as a scriptable name server that receives, interprets
and distributes messages from devices. Messages sent from a
device to the Kernel are called Events, if they are transferred
from the Kernel to a device, they are called Actions.
For definitions of the Kernel, the Devices and Plug-ins please
see the Matrix Scripting Manual.
It is best to think of the device as an object, that may send
and receive actions in an object oriented view.
A change
of the environment is sensed by a sensor. This is detected by
the (unix) device driver, which either distributes this to the
Matrix device (if this is e.g. listening to a socket, where
new data is written by the device driver as seen in the console
device) or saves the changes, making it possible for the Matrix
device to read them (polling). The Matrix device driver reads
the new data and forms a Message that is dispatched to the Kernel
through devcom facilities. These form an ASCII message that
is sent over sockets and received by the Kernel. The Kernel
reacts to the incoming Event and may answer with Actions, sent
to this or other devices. These follow the above rules the other
way round.
POSSIBILITIES
>>> grafik
mehrere matrizen - mehrere aliases - mehrere devices
At start-up,
the Kernel contacts all hosts that run devices as scripted.
On any of these a so-called Portmapper is running on a well-known
port (735), that tells the Kernel on which port the actual devices
can be reached. To be able to tell this, each device has to
be running and known by the Portmapper (registration is part
of the devcom). Then the Kernel registers one or many alias(es)
for each device with a initialisation string that may be handled
by the device driver. The DMX (light system) device for example
may be initialized with a channellist supplied, allowing the
Kernel to access one or many (disjunct) channels of the same
DMX card with the same scripting command. Every command executed
on the device "knows", which alias is used to call it, provided
by the devcom lib.
There may be more than one Matrix running in the same network,
using the same devices. Although this functionality is
in early beta stadium, it will be fully functional in future
versions. ´No changes need to be made by the device programmer.
MATRIX
COMMUNICATION
DEVICES
A device
may be any program that incorporates with the devcom api. After
it has called the register_device function, any program can
be part of the Matrix network. By using the event dispatching
functions and registering actions, it may communicate with the
Kernel.
THE PORTMAPPER
The Portmapper provides Location transparency to the Matrix.
It is the first program that has to be started on a computer
that is assigned to supply devices. It must be started with
root privileges, since it runs on a port number smaller than
1024. Every device registers itself (by device name and port)
at the Portmapper at startup. If a script is executed by the
Kernel, it contacts the Portmapper of the scripted host (see
the Scripting Manual for details) at the well-known port 735
and retrieves the port number of the requested device. If a
machine only runs the Kernel, it does not need to run any Portmapper.
STARTUP
PRECEDENCE
As stated above, the Portmapper has to be started before any
Matrix device. Devices may executed in any order with any privileges
(even by different users). If a device is executed twice, the
first is removed from the Portmappers queue and its process
is killed, so it can not be contacted by any Matrix script executed
afterwards. A device shutdown (either by dying or by manual
shutdown) is automatically detected by the Portmapper and also
leads to deleting it from its queue.
ALIAS
REGISTRATION
If the Kernel
starts a script, it registers one or more aliases for every
device. These are sent to the devices before any incoming event
may be handled. Along with the alias name goes an initialization
string. The device may interpret this string and set its state
according to it. Then it sends the names and argument specifications
of its Events and Actions. These are compared to those mentioned
in the script and if any scripted Action/Event is not supplied
by the device, the Kernel fails to start up and returns with
an error message that specifies line and error code. The Kernel
also quits immediately, if it cannot contact any device, or the
Portmapper is not running.
EVENTS
AND ACTIONS
Events come from devices and go to the Kernel,
where they are interpreted. Actions come from the Kernel and
are interpreted (executed) by the device. The initialization
string may be parsed by the initialization function and result
in different Actions/Events for the same device and different
aliases.
CONVENTIONS
AND RESTRICTIONS
NAME
AND CODING CONVENTIONS
Since the Matrix only supports 7-bit ASCII chars, there should
not be sent any other characters using the devcom. The device
name should be human readable as well as all registered Event/Action
names, since these are used for scripting, too. There should
be no spaces (but you may use underscores if absolutely necessary)
in any names. It is good practice to separate procedures that
are directly executed by the Kernel (Actions) from library and/or
hardware-communication functions. Our recommended order of the
different parts is the following:
1.
Author/Copyright/... notice
2. Imports and defines
3. Global declarations (structs/variables)
4. Utility functions
5. Library functions
6. Polling/Timed function(s) (if necessary)
7. Action functions
8. Exit functions
9. Init functions
10. Main function
We advice
you to use this order so that readability and clarity of device
codes is maintained. All this parts of the device code will
be discussed later. If no device name is specified at the appropriate
function call ("register_device"), the file name is chosen to
be the device name, so keep this one readable, too.
You may write as many Actions and Events as you want, but there
may be only one initialization and one exit function!
LIMITATIONS
There are some limitations of the devcom API, that result either
from platform independence, system restrictions or its pre-beta
state.
-
The
most important are: The protocol only transports 7-bit ASCII
strings correctly, so be sure not to allow any device to transmit
data that consists of characters not in the first 128 characters
of the ASCII table. Also, escaped double-quotes are not working
correctly yet, so use single quotes, where possible.
-
For performance reasons do not transfer empty strings.
-
If registering a function that gets/returns no data, define
it as returning void ("v").
-
Always use the normal exit()
function. Never use _exit(),
atexit()
or on_exit()
to manipulate the shutdown behavior.
-
It may happen, that the Kernel needs some time to find a device
(this comes from the name resolution, we think). Be patient.
We are working on it.
-
Buffer overflows may happen in different parts of the system,
so do not excessively send messages. We have no performance
estimations, but the Kernel queues incoming events and handles
them one by one, so it needs its time. Again: be patient (for
at least some microticks).
-
Message length (overall) is limited to about 4k since the
message queues do not allow bigger messages on the developers
systems (linux). This may be changed in future versions (especially
if these run on other platforms).
-
If you use the threaded version, you may not use SIGUSR1 and
SIGUSR2 since Linux Threads use this signals to communicate.
Signal handling is discussed in depth later. If possible,
use the single-threaded version, since it is more stable and
easier to program (and thorough tested).
-
Message size is limited to 4k for any message passed between
the Kernel and their devices.
-
Field size (the size, data may get) is limited to 1k. Strings
are automatically truncated to this size and no error message
is returned!
-
Look at our Website to get newest information about Limits/Bugs/Fixes
and post us your experiences!
ARCHITECTURE
OF THE DEVCOM
GENERAL
ARCHITECTURE
Generally, a device may be started as a multi-threaded or a
single-threaded program. Only the "run"
function differs, but this has much impact on the general possibilities
of a device.
There are four kinds of functions that may be implemented:
1. Initialization
function : it is automatically triggered when a Kernel connects
2.
Actions : they are automatically triggered if an Action is
received
3. Events : the device may send an Event to the Kernel
4. Exit function : is triggered if an alias deregisters
All these
functions need to be registered in order to be executable by
the devcom API. This is made through the use of function pointers,
a method that strictly limits the syntax of a function head
to conform one specified prototype. Events are not implemented
as functions. They are just registered to enable the Kernel
to record their names for syntax checking at registration time.
As mentioned in the Name and coding Conventions section, a Matrix
device consists of the following parts:
1.
Author/Copyright/... notice
2.
Imports and defines
3.
Global declarations (structs/variables)
4. Utility functions
5. Library functions
6. Polling/Timed function(s) (if necessary)
7. Action functions
8. Event functions
9. Init functions
10. Exit functions
11. Main function
We normally
use one file per device, but you may separate Library and/or
Utility functions in another file as well as declarations in
an additional header file. Library functions are device-specific
utility functions (like wrappers for ioctl()
calls, ...) while normal utility functions are more general.
Ad 1:
All devices have to conform to our license concept.
Ad 2: All devices have to import "devcom.h",
"sys.h"
and "dev.h".
This files may be subject to change in future versions, so do
not rely on this information. You may import any other libraries
in any order. Keep defines readable.
Ad 3: Global declarations go here. Each alias gets a
"state"
that is stored in a memory portion and linked to the alias list.
You have to allocate the memory (using malloc) in the initialization
function. The you have to set a pointer to point on the specific
data structure. This state is accessible to all triggered functions
since it is delivered at the function call.
Ad 4: Implement any utility functions here (list handling,
sorting, ...)
Ad
5: Implement any device dependent utility functions here.
This may be calls of ioctl() or (as seen in the console) hardware
initialization functions.
Ad 6: If you need to implement polling (details later),
you will need a timer and a timed function (handler) that goes
here.
Ad 7,8: Actions and Events are described above, below
and anywhere else in this manual.
Ad 9,10: Since you need to know the names of the Actions
and Events for registering them, you are advised to write the
init function after these.
Ad 11: The main() function consists of the following
part (in this order): device registration/initialization, init
function registration, exit function registration (if needed),
arbitrary initialization stuff done by you, run function call
(either threaded or not).
The program
has two ways to exit: Either it may exit using the normal exit()
function call (for example in a shutdown action triggered by
the Kernel) or it may receive a signal (mostly SIGSEGV) that
leads to an immediate exit (but most signals are handled by
the devcom, so the shutdown is safe in most cases).
Details about the different parts of a device file can be found
in the sample device section of this manual and in the how-to's.
SINGLE-THREADED
VERSION
The term single threaded version is wrong, since it is one single
process and not one thread. We will call it this way anyway.
The motivations for creating two methods of creating matrix
devices were the following: First, we created the threaded version,
since we thought of threads of offering optimal buffering facilities.
If data arrives on a socket, the thread wakes up and handles
the data. The actual action is performed by a separate thread,
so that time-consuming tasks may be implemented using model
without blocking incoming further actions or even the execution
of the Kernel. Also, since threads are a bit nifty to program,
we decided to implement a second version of the continuously
running code that does not use those.
In this model, a select statement is used for waiting for data
arriving from the Kernel. This data is handled as soon as it
arrives and buffering is handled by the socket implementation
of the operating system (buffer size can be adjusted in a linux
header file, if necessary, but this needs recompilation of the
kernel to be activated). If events can be triggered by data
availability at file descriptors (e.g. console/stdin), this
should be done using the same select statement (and the libvoc).
See the sample device console and the select-how-to for details.
MULTI-THREADED
VERSION
There are certain limitations for working with this version
and several limitations of the protocol arise from it. First,
signal handling is more complex: Every signal received by the
process is distributed among all threads. This implicates no
problem, since signal handlers are inherited from the parent
process/thread. But you should be careful, when working with
signals. Second, do not use SIGUSR1 and SIGUSR2. This
signals confuse the threads because they are used in the pthread
implementation we used for programming the matrix devcom: LinuxThreads.
You may use any other thread library (if it complies to POSIX
Threads standards), if you want.
Also, it is even more important than in the single-threaded
case, that any device exits using the normal exit() function.
Never use _exit(),
atexit()
or on_exit()
to manipulate the shutdown behavior. Otherwise, threads may
be left running by the system and only the currently executed
thread finishes execution (only a real kill (no term) works
from this point on).
Experiments showed performance advantages of the threaded version
and it is also more resistible to buffer overflows (although
none should happen in both versions).
DATA
TYPES
Data is transferred in ASCII format in our protocol. The following
Data Types are used by the Matrix and our Linux devices:
Data
Type |
C
Type |
Size |
Format
Character |
Integer
Values |
long
int |
4
Bytes |
d |
Floating
Point |
double |
8
Bytes |
f |
Strings |
char[] |
500
Bytes max. |
s |
Void
|
- |
- |
v |
Table
1 :Data
Types
Void is
used if a function receives no parameters or returns no parameters.
Always submit a void, if no other data type is applicable.
Strings are always handled as 7-Bit ASCII conformant. Do not
use any special characters from over 127 ASCII.
The field length (for e.g. action and event names) is limited
to 100 Bytes.
PROGRAMMING
THE DEVCOM
ACTION HOW TO
As we said above, actions are triggered by the Kernel. In order
to automate this triggering, we use function pointers to manage
the actions, a device provides (you may think of them as services
e.g. in the Jini system). The format, a function thus has to
be is
long int (*func)(char *, alias_p, char *, void *)
where the
first char
* is where the parameters get, the alias_p
is a pointer to the alias structure (see the header file excerpts
below for details on data structures), the second char
* is a pointer to an allocated portion of memory where
the programmer may write return values and the last argument
is a miscellaneous data pointer that is registered with the
action in the init part and then delivered when the action is
triggered. We use the console's write function as example: The
call of this function in the Matrix Scripting Language E is
demonstrated by the following (fully functional) script:
|
use con = console@localhost("");
->con:read(^key)
{
con:write(key);
}
The syntax for the
function that is supposed to write a string to the standard ouput is:
long int con_write(char *myparams, alias_p myalias, char *msg, void *data) {
char *c;
c = dev_get_string(1,myparams);
printf(c);
fflush(stdout);
free(c);
return 0;
}
For
the registration of the function, please see the initialization
how-to below. When called through the devcom, myparams will contain
the scripted parameters, myalias will represent the alias, that
called this function, msg may be filled (use printf())
with return values and data contains device-specific data.
EVENT
HOW-TO
In the above action how-to you see the E definition of an event
(read).
This event should be triggered when a key is pressed. In order
to do so, we use the extended select functionality of the libvoc.
The event is also registered in the init function. Other ways
of generating events are Timer functions and the use of other
run modes.
The
functions that actually send the events are:
|
int single_evt(alias_p myalias, char *ename, char *params, int ack, select_p sel);
int dev_evt(alias_p myalias, char *ename, char *params, int ack);
The alias
defines the target of the event. If you provide a NULL pointer,
it will be sent to all available aliases. ename
is the name of the event as registered and scripted. params
contains the arguments sent with the event and ack defines if
the device should wait for an acknowledgement or not. Do always
set ack
to 0 (zero), since the acknowledgment function is not implemented
(in the scripting language).
Set params to NULL if you want to transmit a void event (e.g.
-> switch:on()).
PARAMETER
HANDLING HOW-TO
Parameters are sent in both directions: To and from the device.
So, the devcom provides facilities for encoding and decoding parameters
(or arguments). To get a parameter from a parameter string you
may use the dev_get_* functions that are available for all data
types supported:
|
double dev_get_float(int nr, char *params);
long int dev_get_int(int nr, char *params);
long int *dev_get_ints(int nr, char *params);
char *dev_get_string(int nr, char *params);
The Integer
value nr contains the number of the specified data type in the
parameter string, so dev_get_int(1,myparams)
searches the first integer in myparams and dev_get_string(1,myparams)
the first string (as char
*!). Also, dev_get_int(2,myparams)
returns the second Integer value. Strings acquired through this
function need to be freed when they are no longer needed. In order
to encode a parameter string you need to provide a format string.
If you want to define a void return value, you may provide a NULL
pointer as the parameter string for the event. char
*dev_create_parastr(char *fmt, ...);
See Table
1 for the format string characters.
|
Examples:
dev_create_parastr("fdd",3,3.4,6.5);
dev_create_parastr("s","test");
dev_create_parastr("ds",3,stringp);
INITIALIZATION
HOW-TO
The initialization function decides, which actions and events are available for a
specific alias on the basis of a initialization string provided.
This string is in the myparams string and may be retrieved using
the dev_get_string(1,myparams) command.
The state of the alias may be a structure defined by the device
programmer. As mentioned above, any action receives the state when
called. In our example we set the state using the following commands:
|
console_p state;
state = (console_p) malloc(sizeof(struct console_s));
state->alias = myalias;
state->sel = (select_p)data;
myalias->state = state;
After this, we register
actions and events using the following two functions:
int dev_register_action(alias_p myalias, char *name, DEVFUNCP, char *ret, char *fmt, void *data);
int dev_register_event(alias_p myalias, char *name, DEVFUNCP, char *ret, char *fmt, void *data);
#define DEVFUNCP long int (*func)(char *, alias_p, char *, void *)
The alias should be the alias of the init function. The name
is the name that is used for accessing the action/event in the
E script. Again, the fmt string consists of d, f, s and v. The
return string ret is used to define the return data. This may
either be d, f, s or v. Only one value might be returned (see
the Scripting Manual for details of using return values in scripts).
DEVFUNCP is the function to be called if an action is registered.
In case of an event registration this should be NULL (it is only
used again to maintain compatibility in call semantics). Data
may be any pointer to anything that is supplied with any succeeding
function call of DEVFUNCP.
POLLING
HOW-TO
There are two
ways of polling devices. You may either use timers and interrupt
handlers or use our run_with_me()
function (the name was chosen in a rather poetic moment). This
function just runs through a user-defined function (you are advised
to call it run_once()
for clarity) all about 10ms. This is managed by a timed select
statement, so timer interrupt calls (SIGALRM) will break it.
The run_with_me
strategy is used in the Midi device (devmidi.c) as the following
code snippet shows (please look over the fact, that this function
is called midi_run()
and not run_once()):
|
int midi_run(void *data) {
/* Cast the data-object to a select-object */
select_p sel = (select_p) data;
/*
* Listen on all file-descriptors registered in the select-objects
*/
select_select(sel, NULL);
return 0;
}
int main(int argc, char *argv[]) {
select_p sel;
/* Create the select-object used by this device */
sel = select_create();
/* Register the device midi in the matrix */
dev_register_device("midi", argc, argv);
/*
* Register the init-action - this action gets the select-object for
* further processing - so more aliases can use different midi-ports
*/
dev_register_init("midi_init", midi_init, "s", "v", sel);
/*
* Register the exit-action - this action gets the select-object for
* further processing- so the specific exited alias could be removed
* from the select-object
*/
dev_register_exit("midi_exit", midi_exit, "s", "v", sel);
/*
* Start the device and listen on all ports registered in the select-
* object by calling the function "midi_run" with the parameter "sel"
*/
dev_run_with_me(midi_run, sel);
exit(0);
}
This device
runs in single-threaded mode and never gets the "focus" again,
except when the timed interrupt calls the registered interrupt
routine light_update().
The duration of one timing slice is determined through the itimer
structure (as a global variable, itimer does not conform "Volker's
programming standards").
SHUTDOWN
HOW-TO
The shutdown facilities are known to be not mighty enough yet,
so we are constantly working on them. By now, the device registers
its shutdown function as an interrupt service routine for most
interrupts that are supposed to stop the program. You are advised
to exit the matrix device program with a call of the normal
exit()
function. Threads will be killed and much memory is freed automatically.
Also, some system resources are freed. If an alias is deregistered,
its registered exit function is called. This function should
free its state structure and alias specific system resources.
COMMENTED
SAMPLE DEVICE
CONSOLE
DEVICE (FULL SOURCE)
The following code is the code for a console device (keyboard).
It may do 5 actions and send one event: Actions:
1.
Write a string
2. Write an integer
3. Write a string followed by a new-line
4. Wait for an input string and return this
5. Shut down the console
EVENTS.
1.
Key pressed
only one
alias/matrix!!
INCLUDES
AND DEFINES
First the Includes of this specific device appear and then those
of all devices (config.h is necessary for autoconf).
|
#include
#include
#include "config.h"
#include "devcom.h"
#define ON 1
#define OFF 0
GLOBAL
DECLARATIONS
The struct console_s
represents the state of one console alias. For better handling of
this data it is re-linked to its originating alias. It also contains
a select data type that is linked to the select variable that is
used throughout the device. See the init function for details of
how the state is handled. |
struct console_s {
alias_p alias;
select_p sel;
} console_t;
typedef struct console_s *console_p;
LIBRARY
FUNCTIONS
This is a utilitarian function that handles some terminal functions
of the stdin. This is called at the start of the device file (see
the main function) and when shutting the device down. |
void set_console_attr(int flag)
{
static struct termios save;
static struct termios buf;
static int ok = 0;
if (!ok) {
tcgetattr(STDIN_FILENO, &save);
buf = save;
buf.c_lflag &= ~(ECHO | ICANON | IEXTEN);
buf.c_cc[VMIN] = 0;
buf.c_cc[VTIME] = 0;
ok = 1;
}
switch (flag) {
case ON:
tcsetattr(STDIN_FILENO, TCSANOW, &buf);
break;
case OFF:
default:
tcsetattr(STDIN_FILENO, TCSANOW, &save);
break;
}
}
ACTION FUNCTIONS
The following Actions all follow the same rules: First retrieve
an argument from the parameter string. Then act as expected by the
Matrix. On success, return zero (in this example all functions are
always successful). The shutdown function appears here, because
it is handled as an action. Always exit from a device using exit().
Never use _exit()
- especially when working with threads.
The dev_get_*(n,s)
functions return the argument number n
of the type
* in the string s. Always free strings returned by dev_get_string(). |
long int con_write(char *myparams, alias_p myalias, char *msg, void *data) {
char *c;
c = dev_get_string(1,myparams);
printf(c);
fflush(stdout);
free(c);
return 0;
}
long int con_write_i(char *myparams, alias_p myalias, char *msg, void *data) {
printf(" %ld ",dev_get_int(1,myparams));
fflush(stdout);
return 0;
}
long int con_writeln(char *myparams, alias_p myalias, char *msg, void *data) {
char *c;
c = dev_get_string(1,myparams);
printf(c);
printf("\n");
fflush(stdout);
free(c);
return 0;
}
long int con_shutdown(char *myparams, alias_p myalias, char *msg, void *data) {
set_console_attr(OFF);
printf("shutdown initiated through matrix\n");
exit(1);
return 0;
}
In the con_readln
function, the way of sending back data can be observed: simply copy
the data into the pointer called msg after creating a correct parameter
string from it using dev_create_parastr(). |
long int con_readln(char *myparams, alias_p myalias, char *msg, void *data) {
int len;
char ret[FIELD_SIZE], *paras;
paras = dev_get_string(1,myparams);
printf("\n");
printf(paras);
fflush(stdout);
free(paras);
set_console_attr(OFF);
len = read(STDIN_FILENO,ret,FIELD_SIZE-1);
set_console_attr(ON);
ret[len-1] = '\0'; // kill
paras = dev_create_parastr("s",ret);
sprintf(msg, paras);
free(paras);
return 0;
}
EVENT FUNKTIONS
This event is fully handled by the select function of the libvoc.
If data arrives at the file descriptor of the standard input, this
function is triggered. The state of the corresponding alias is provided
by the libvoc too, so the event sending mechanism may use the alias
re-linked there. |
int con_event(int fd, void *data, void **rdata) {
static unsigned char recv[2] = {'\0','\0'};
char *params;
if (read(fd, &recv, 1) <= 0) return -1;
if ((recv[0] == '\n')||(recv[0] == '\r')) return 0;
params = dev_create_parastr("s",recv);
single_evt(((console_p)data)->alias, "read", params, 0, ((console_p)data)->sel);
free(params);
return 0;
}
INIT FUNCTION
The Initialization function is used for initializing a specific
alias. It is registered in the main()
function and triggered when the Matrix connects to the device. It
registers all functions for its alias and sets the initial state.
In our demo device the state is set to be a console_p
(definition above). We also turn the console on (this could be done
in the main function either). Since we use a single threaded version,
we add a select for the standard input's file descriptor number
(STDIN_FILENO) into our select structure, that was delivered through
the data pointer. We also supply the function con_event()
to the select_addto_rlist()
function call, that will handle incoming data. Then we register
the actions and the events. If the last argument of the dev_register_action()
function call would be the data pointer, we could use the same select
structure to dynamically add and remove other file descriptors.
See the libvoc description for details about this. At last, we print
a message, that includes the device name in the string device. Always
use dev_register_device()
to set the device name since it initializes all device-specific
data. |
long int con_init(char *myparams, alias_p myalias, char *msg, void *data)
{
console_p state;
set_console_attr(ON);
state = (console_p) malloc(sizeof(struct console_s));
state->alias = myalias;
state->sel = (select_p)data;
myalias->state = state;
select_addto_rlist((select_p) data, STDIN_FILENO, con_event, state);
(void)dev_register_action(myalias, "write", con_write, "v", "s", NULL);
(void)dev_register_action(myalias, "write_i", con_write_i, "v", "d", NULL);
(void)dev_register_action(myalias, "writeln", con_writeln, "v", "s", NULL);
(void)dev_register_action(myalias, "shutdown", con_shutdown, "v", "v", NULL);
(void)dev_register_action(myalias, "readln", con_readln, "s", "s", NULL);
(void)dev_register_event(myalias, "read", NULL, "v", "s", NULL);
if (verbose) fprintf(stderr, "console %s ready\n", device);
return 0;
}
EXIT FUNCTIONS
The exit function is called when the device exits. Since our device
only supports one console, it only needs to delete one item of the
read list of the select structure. Then it may print a verbose function
(in case the device was started with the -v option set) and resets
the console. The user-defined exit function is a part of the devcom
exit facilities that kill threads, free memory and so on. If you
call exit()
in it, take an eternity time to wait for the command prompt - it
will only call the same exit function again. The same is applicable
for dev_exit().
|
long int con_exit(char *myparams, alias_p myalias, char *msg, void *data)
{
select_delfrom_rlist((select_p) data, STDIN_FILENO);
if (verbose) printf("console reset!\n");
set_console_attr(OFF);
return 0;
}
FURTHER
NOTES
NAMES
We do not have any strict naming conventions yet, but device
programmers should keep some facts in mind:
-
action/event/device
names are used for scripting
-
registered
functions should be recognized quickly
-
global
variables should be unique (Volker says they are generally
bad style)
-
structures
and pointers should be recognizable by their names
-
one
file = one style · upper case letters are ugly (says Martin)
-
matrix
devices are open source - keep them human readable
COMMAND
LINE OPTIONS
-v.....verbose
output
-d....demon mode (not stable yet)
You can
get usage information by starting a device with any command
line information other that those mentioned above.
The verbose
option sets a global integer variable called verbose. This is
used as in the console device's init function:
if
(verbose) fprintf(stderr, "console %s ready\n", device);
All verbose
messages should be printed on stderr since buffering otherwise
may hide some of them.
FUTURE
ENHANCEMENTS
We are constantly
developing and refining the Matrix. The Main focus lies on:
-
better
and easier scripting
-
bug
fixing (merely memory holes and code cleaning)
-
reworking the shutdown facilities of matrix devices
-
more
transparent function registration
-
better event support
-
stability
-
developing more device
|
DEVCOM
OVERVIEW
#define DEVFUNCP long int (*func)(char *, alias_p, char *, void *)
/* registration */
int dev_register_device(char *device_name, int argc, char *argv[]);
int dev_register_action(alias_p myalias, char *name, DEVFUNCP, char *ret, char *fmt, void *data);
int dev_register_event(alias_p myalias, char *name, DEVFUNCP, char *ret, char *fmt, void *data);
int dev_register_init(char *name, DEVFUNCP, char *ret, char *fmt, void *data);
int dev_register_exit(char *name, DEVFUNCP, char *ret, char *fmt, void *data);
/* deregistration */
void dev_deregister_action(alias_p myalias, char *name);
void dev_deregister_event(alias_p myalias, char *name);
void dev_deregister_init(char *name);
void dev_deregister_exit(char *name);
/* event generation */
int single_evt(alias_p myalias, char *ename, char *params, int ack, select_p sel);
int dev_evt(alias_p myalias, char *ename, char *params, int ack);
/* running */
int dev_run();
int dev_run_with_me(int (*dev_run_once)(void *), void *);
int dev_run_single(select_p sel);
/* exit */
long int dev_exit(alias_p al);
/* util */
alias_p dev_get_alias(void *criteria, int (*comp)(void *, void *));
double dev_get_float(int nr, char *params);
long int dev_get_int(int nr, char *params);
long int *dev_get_ints(int nr, char *params);
char *dev_get_string(int nr, char *params);
char *dev_create_parastr(char *fmt, ...);
void alive();
void dev_clean_up();
|