Creating your own CAJUN devices

CAJUN devices can be one of 4 types: input, output, audio, and utility. Each of these types uses the same basic template, with only minor modifications.

To start, here's what the basic template should look like:

package DEVICEPACKAGENAME;

use strict;
use lib '/usr/share/cajun/lib';
use PARENTDEVICE;
our @ISA=qw(PARENTDEVICE);

# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# CAJUN version 4.0, Copyright (C) 2001.
# CAJUN comes with ABSOLUTELY NO WARRANTY; for details see the LICENSE file.
# This is free software, and you are welcome to redistribute it
# under certain conditions; also see the LICENSE file for details.
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

#!!! Gui parameters

#!!! Gui parameters

# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# onLoad gets called once when the object is created (optional).
# you'll have the following data at hand, once your SUPER's onLoad
# returns:
# 
# $self->{db}: a connection to the sql database
# $self->{config}{devparam}: a hash ref to this device's paramters
# $self->{config}{global}: a hash ref to CAJUN's global params
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
sub onLoad {
  my($self)=@_;

  $self->SUPER::onLoad();    # *** MUST call this first!
	
	# add your code here.
	$self->logmsg("Device initialized and ready to go.");
}
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# onUnLoad gets called if CAJUN ever exits. You don't need it, but if
# you have one, be sure to call the SUPER's onUnLoad first.
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
sub onUnLoad {
  my($self)=@_;

  $self->SUPER::onUnLoad();   # *** MUST call this first!
	$self->logmsg("We're outta here!");
}

You'll want to replace DEVICEPACKAGENAME with the name of the device package you'll place the object into, and PARENTDEVICE as one of InputDevice,OutputDevice,AudioDevice, or UtilityDevice as appropriate. Note that you have a logging method, "logmsg", to log arbitrary strings formatted nicely. Output will be prepended with the device name and number for easy identification.

Now, for the driver type specific changes:


Input Drivers

Input drivers only need one method. It's called when CAJUN needs a keypress from the input device, and is allowed to block until that keypress is received. Simply return the ascii bytes in string form to the caller:

sub readMsgFromInputDev {
  my ($self)=@_;
  my $buf=somehowGetThisDevicesChars();
  return $buf;
};

Output Drivers

Somewhere in the onLoad code, the output driver is required to call one of two methods:

Full Updates

Full updates are requested by calling $self->requestFullDisplayUpdates in the onLoad method. It declares that the physical device is incapable of cursor positioning, and the screen must be completely redrawn every time.

Partial Updates

Partial updates are requested by calling $self->requestPartialDisplayUpdates in the onLoad method. This indicates that the device is capable of cursor positioning, and that only the areas of the display that need updating can be delivered.

The Output driver needs to declare two methods:

sub display {
  my($self,@work)=@_;
  # do stuff with @work
}

sub sendRaw {
  my($self,$str)=@_;
  my $portfh=$self->{portfh};
  print $portfh $str;
}

In the case of full updates, each entry in @work will be a full line, exactly the width of the display. The number of entries in @work will correspond with the display height. If partial updates are requested, each entry in @work is a hash reference with the following keys:

  • X (denotes the X coordinate, starting at 0 for left side)
  • Y (denotes the Y coordinate, starting at 0 for top)
  • STRING (denotes the output to be displayed)

    In this case, you must iterate through the @work array and place output strings as appropriate.

    The 'sendRaw' routine is used when a user wants to send a byte stream to the display during initalization.

    Optionally, you can declare a few other methods:

    sub backlight {
      my($self,$state)=@_;
      if ($state eq "on") {
        # turn light on
      } else {
        # turn light off
      }
    }
    
    

    to control the backlight. CAJUN will turn the light on and off as required.

    Also, for misc. string display (such as startup/shutdown messages), implement the optional 'displayString' method similar to:

    sub displayString {
      my($self,$str)=@_;
      # position cursor, check length, center string
    	$str=substr($str,0,$self->{width}-1) if length($str)>=$self->{width};
    	my $x=int($self->{width}/2)-(length($str)/2);
      my $y=int($self->{height}/2)-1;
      print $lcdport $self->{clear} . sprintf("%c%c%c",27,$x,$y,$str);
    }
    

    If you have some background work to do, simply implement the following method, and it will be called periodically for you. You cannot control the rate at which you'll be called, but it will usually be at least once a second. Implement a timer check if you can't be called too often:

    sub backgroundThread {
      my($self)=@_;
      # do stuff
    }
    

    Audio Drivers

    Audio drivers have a little more work to do. First, they must provide activate() and deactivate() methods, which are called by the parent when an audio device is activated/deactivated by the CAJUN core. Upon deactivation, the device should immediately silence all output, and cease communication with the core (more in a moment) until activated again. It should also be noted that while activated, all audio methods should return as quickly as possible. Any blocking will cause poor input device response, and could potentially overflow socket queues.

    Next, the driver can have a polling method if it would like. Wherever desired, call $self->setBackgroundThreadTimer(1), where 1 is the number of seconds between polls (partial seconds are acceptable). Set it to 0 to turn off polling. The callback occurs in a method "backgroundThread". Remember to minimize time spent in this method. Also, try not to ask for too short a polling interval; anything less than .10 seconds may be too little for slower machines to handle. If you don't ask for polling, the callback method is not needed.

    Also, CAJUN can watch sockets for you, and alert you when input is available on them. Simply call addMonitorFds/removeMonitorFds to add or remove file descriptors (not file handles) to the watch list, and define a 'monitorFd' method to be called when something happens:

    sub onLoad {
      ...
      $self->addMonitorFd(fileno($mysocket));
    }
    sub montorFd {
      my($self)=@_;
      sysread($mysocket,$buf,1024);
      ...
    }
    

    Last, a "processAction" method needs to be declared, taking one parameter, an action name. This action will have been delivered from the input device through the core.

    An audio driver needs to track which state it is currently in. A default state method is already present in the audio device; it takes either 0 or one arguments. If a state name is passed in, the module changes to that state, alerts the core, and logs a message. Either way, the (possibly now current) state name is returned. If you want to implement your own state method, make sure you call your SUPER's state() method first, and let it maintain your audio object's {state} variable for you.

    It's common to also provide the following methods in your driver (for your use only; they're not expected by the parent driver):

  • state(), to change driver state and return the current state
  • updateDisplay(), to deliver display data back to the core.

    Here's what the missing pieces from a sample audio driver would look like:

    sub processAction {
      my($self,$action)=@_;
    
      # decide what to do with this action
      $self->updateDisplay;   # since most actions result in display changes
    }
    
    sub activate {
      my($self)=@_;
    
      # initialize and get ready for action
      $self->logmsg("activating");
    }
    sub deactivate {
      my($self)=@_;
    
      # shut us down and be quiet
      $self->logmsg("deactivating");
    }
    sub backgroundThread {
      my($self)=@_;
    
      # do something here? update a clock? whatever?
      $self->updateDisplay;   
    }
    sub state {
      my($self,$state)=@_;
    
      $self->SUPER::state($state);
    
    	# any additional processing here.
    	if ($self->{state} eq "stopped") {
    		# for example, silence everybody...
    	}
      return $state;
    }
    
    sub updateDisplay {
      my($self)=@_;
      return unless $self->{activated};
      
      $self->sendMsg(
        _msgtype => "display",
        _state => $self->state,
        artist => $self->{current}{artist},
        disc => $self->{current}{disc},
        song => $self->{current}{song},
      );
    } 
    

    Now, a little discussion about sending back display data. (See the CAJUN documentation on "Configuration: Output Drivers" for more help). The output driver is going to expect you to send the information about screen data in a single call to $self->sendMsg, which formats a message and sends it to the core (who will translate it and forward it to the output driver). The only two required fields are "_msgtype", which is always "display", and "_state", which is the current state of your application. The output driver will use the "_state" field to determine which screen to display. It can be helpful to send fields that don't exist in the default display - for example, in a MP3 player, you can send the "bitrate" and "genre" fields, but they don't necessarily have to be used by the display filter and sent to the display. Users with larger displays will want to customize their display filters to display the additional data you provide.

    For additional clarity, go to your CAJUN's internal web site configuration and examine the displayFilters for the output devices you have configured.


    Utility Drivers

    Please be careful when implementing utility devices; they run in the CAJUN core, and all methods must execute quickly to avoid blocking the main event loop. Note that utility object instantiation may be initiated from the web GUI; be careful when implementing onLoad and onUnLoad... CAJUN might not be running! If the PAULB XXX flag has been set, then you're likely running in the web environment. In utility functions, the list of actions may be static (such as in the volume control, which has volUp, volDown, etc.), or it may be variable (a volume manager may create an action for each volume). Utilities should always ensure that the list of actions reflects the configuration of the object - i.e., when a new volume is added to the volume manager, actions should be immediately created to reflect this. The parent object does not handle this for you (it can't, really.) PAULB NEED TO CHANGE THE ACTIONMAP STRUCTURE BAD NAME The object will need to implement an 'actionVerify' method, which is called when the device is first added, when it's configuration has been modified, or when information about the current actions is needed. The object needs to return a hash as follows: ( action1 => { DESC => "full text description",STATES => [state1,state2] }, action2 => { DESC => "full text description",STATES => [state1,state2] }, ), Each action has information describing what it does, and which states it is valid in. Utilities need to declare a processAction, just like audio devices, to handle incoming keypresses. They will also need to call sendMsg when it's time to update the screen. Utilities need to call the 'state' method whenever they change state. The first call should be in onLoad, where initial state is declared. For most utilities, that will be the only state call they need, but you may change state at any time if you've implemented more than one state. Utility devices are always "running"; but may not be activated. In the context of utilities, "activated" means that their output is sent to the display, and overrides the running audio application (though the audio application continues to run, unaware that its output is not being displayed). They may be activated by executing any action that belongs to the device. PAULB TODO MORE DETAIL: Also, a utility that's processing data in the background (see setPollRate below) may call TODO to request that it be activated. After a utility has been activated, it must deactivate itself. If it expects to manually deactivate itself (for example, because the user pressed an 'exit' type of action), it may call deactivate with no parameters. If it expects the core to deactivate it via timeout, it should call deactivate(float), representing the amount of time (seconds) it would like to be deactivated in if no further keypresses are received for this device. Of course, the user may deactivate it by pressing an action that does not belong to this device. During processing, a utility may find out if it's currently active by calling 'isActive'. (though, if you're currently in a processAction call, you are indeed active). You may have the core poll you if you'd like; simply call setPollRate(float), and implement the devicePoll method. Set it to 0 to stop polling. You'll be polled at no more than the rate you ask for, though you may not get called as frequently as you ask (depending the machine's load). It's likely not a good idea to request, say, below .25 seconds or so, if you can avoid it. (CAJUN will not call you below its own polling frequency, anyway). Note that polling continues, even if you're not activated (good for background tasks).