Chapter 12. Sensors

Chapter Objectives

After reading this chapter, you'll be able to do the following:

This chapter describes how to add sensors to the scene graph. A sensor is an Inventor object that watches for various types of events and invokes a user-supplied callback function when these events occur. Sensors fall into two general categories: data sensors, which respond to changes in the data contained in a node's fields, in a node's children, or in a path; and timer sensors, which respond to certain scheduling conditions.

Introduction to Sensors

Sensors are a special class of objects that can be attached to the database. They respond to database changes or to certain timer events by invoking a user-supplied callback function. Data sensors (derived from SoDataSensor) monitor part of the database and inform the application when that part changes. Timer sensors (such as SoAlarmSensor and SoTimerSensor) notify the application when certain types of timer events occur. Note that timer “events” occur within Inventor and are not part of the event model described in Chapter 10. See Figure 12-1 for a diagram of the portion of the class tree that includes sensors.

Figure 12-1. Sensor Classes


Sensor Queues

As the class tree in Figure 12-1 suggests, sensors are placed in one of two queues:

  • Timer queue, which is called when an alarm or timer sensor is scheduled to go off

  • Delay queue, which is called whenever the CPU is idle (that is, there are no events or timer sensors to handle) or after a user-specifiable time-out

When processing of either queue begins, all sensors in that queue are processed once, in order (see “Using a Field Sensor”).

Key Terms

The following discussion of data and timer sensors uses a few new terms.

  • Triggering a sensor means calling its user-defined callback function and removing it from the queue.

  • Scheduling a sensor means adding it to a queue so that it can be triggered at some future time. If a sensor is already scheduled, scheduling it again has no effect. Unscheduling a sensor means removing it from the queue without processing it.

  • Notifying a data sensor means letting it know that the node (or field or path) to which it is attached has changed. A data sensor automatically schedules itself when it is notified of a change.

Data Sensors

There are three types of data sensors:

  • SoFieldSensor, which is attached to a field

  • SoNodeSensor, which is attached to a node

  • SoPathSensor, which is attached to a path

An SoFieldSensor is notified whenever data in a particular field changes. An SoNodeSensor is notified when data changes within a certain node, when data changes within any of the child nodes underneath that node, or when the graph topology changes under the node. An SoPathSensor is notified whenever data within any of the nodes in a certain path changes, or when nodes are added to or deleted from that path. A node is considered to be in the path if traversing the path would cause the node to be traversed.


Tip: Setting the value of a field to the same value it had before (for example,

field.setValue(field.getValue()))

is considered a change. Calling the touch() method of a field or node is also considered a change.

A render area attaches a node sensor to the root of the scene graph so that it can detect when you make any changes to the scene. It then automatically renders the scene again.

Data sensors are also useful if you want to monitor changes in part of a scene and communicate them to another element in the scene. For example, suppose you have a material in the scene graph with an editor attached to it. If the material changes, the editor needs to change the values of its sliders to reflect the new material. An SoNodeSensor supplies this feedback to the material editor.


Tip: Field-to-field connections are another way of keeping different parts of the scene graph in sync. See Chapter 13.


General Sequence for Data Sensors

The following sequence describes the necessary steps for setting up a data sensor:

  1. Construct the sensor.

  2. Set the callback function (see the next section).

  3. Set the priority of the sensor (see “Priorities”).

  4. Attach the sensor to a field, node, or path.

  5. When you are finished with the sensor, delete it.

Callback Function

Callback functions, as their name suggests, allow Inventor to call back to the application when some predefined event occurs. A callback function usually takes a single argument of type void* that can be used to pass extra user-defined data to the function. Callback functions used by sensors also have a second argument of type SoSensor*. This argument is useful if the same callback function is used by more than one sensor. The argument is filled with a pointer to the sensor that caused the callback.

In C++, a sensor callback function can be declared as a static member function of a class. In this case, because static functions have no concept of this, you need to explicitly pass an instance of the class you want to modify as user data:

colorSensor->setData(this);

Nonstatic C++ member functions are not suitable for use as callback functions.

Priorities

Classes derived from SoDelayQueueSensor use priorities to maintain sorting in the delay queue. The following methods are used to set and obtain the priority of a given sensor:

setPriority(priority)  


assigns a priority to the sensor. All delay queue sensors have a default priority of 100. Sensors are sorted in the queue in order of their priority, with lower numbers first.

getPriority() 

obtains the priority of a given sensor.

getDefaultPriority()  


obtains the default priority (100) for a sensor.

A sensor with a priority of 0 has the highest priority. It triggers as soon as the change to the scene graph is complete. If two sensors have the same priority, there is no guarantee about which sensor will trigger first.

The SoXtRenderArea has a redraw data sensor with a default priority of 10000. You can schedule other sensors before or after the redraw by choosing appropriate priorities.

For example, to set the priority of a sensor so that it is triggered right before redraw:

SoNodeSensor   *s;
SoRenderArea   *renderArea;
s->setPriority(renderArea->getRedrawPriority() - 1);

Triggering a Data Sensor

When data in the sensor's field, node, or path changes, the following things happen:

  1. The sensor is notified that the data changed.

  2. The sensor is scheduled—that is, it is added to the delay queue, according to its priority.

  3. At some future time, the queue is processed and all sensors in it are triggered.

  4. When triggered, the sensor is removed from the queue, and it invokes its callback function.

  5. The callback function executes. This function can access the trigger field, trigger node, or trigger path responsible for the original notification (see “Using the Trigger Node and Field”).

Using a Field Sensor

Example 12-1 shows attaching a field sensor to the position field of a viewer's camera. A callback function reports each new camera position.

Example 12-1. Attaching a Field Sensor


#include <Inventor/SoDB.h>
#include <Inventor/Xt/SoXt.h>
#include <Inventor/Xt/viewers/SoXtExaminerViewer.h>
#include <Inventor/nodes/SoCamera.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/sensors/SoFieldSensor.h>

// Callback that reports whenever the viewer's position changes.
static void
cameraChangedCB(void *data, SoSensor *)
{
   SoCamera *viewerCamera = (SoCamera *)data;

   SbVec3f cameraPosition = viewerCamera->position.getValue();
   printf("Camera position: (%g,%g,%g)\n",
            cameraPosition[0], cameraPosition[1],
            cameraPosition[2]); 
}

main(int argc, char **argv)
{
   if (argc != 2) {
      fprintf(stderr, "Usage: %s filename.iv\n", argv[0]);
      exit(1);
   }

   Widget myWindow = SoXt::init(argv[0]);
   if (myWindow == NULL) exit(1);

   SoInput inputFile;
   if (inputFile.openFile(argv[1]) == FALSE) {
      fprintf(stderr, "Could not open file %s\n", argv[1]);
      exit(1);
   }
   
   SoSeparator *root = SoDB::readAll(&inputFile);
   root->ref();

   SoXtExaminerViewer *myViewer =
            new SoXtExaminerViewer(myWindow);
   myViewer->setSceneGraph(root);
   myViewer->setTitle("Camera Sensor");
   myViewer->show();

   // Get the camera from the viewer, and attach a 
   // field sensor to its position field:
   SoCamera *camera = myViewer->getCamera();
   SoFieldSensor *mySensor = 
            new SoFieldSensor(cameraChangedCB, camera);
   mySensor->attach(&camera->position);

   SoXt::show(myWindow);
   SoXt::mainLoop();
}

Using the Trigger Node and Field (Advanced)

You can use one of the following methods to obtain the field, node, or path that initiated the notification of any data sensor:

  • getTriggerField()

  • getTriggerNode()

  • getTriggerPath()

These methods work only for immediate (priority 0) sensors.

The trigger path is the chain of nodes from the last node notified down to the node that initiated notification. To obtain the trigger path, you must first use setTriggerPathFlag() to set the trigger-path flag to TRUE since it's expensive to save the path information. You must make this call before the sensor is notified. Otherwise, information on the trigger path is not saved and getTriggerPath() always returns NULL. (By default, this flag is set to FALSE.) The trigger field and trigger node are always available. Note that getTriggerField() returns NULL if the change was not to a field (for example, addChild() or touch() was called).

Example 12-2 shows using getTriggerNode() and getTriggerField() in a sensor callback function that prints a message whenever changes are made to the scene graph.

Example 12-2. Using the Trigger Node and Field


#include <Inventor/SoDB.h>
#include <Inventor/nodes/SoCube.h>
#include <Inventor/nodes/SoSeparator.h>
#include <Inventor/nodes/SoSphere.h>
#include <Inventor/sensors/SoNodeSensor.h>

// Sensor callback function:
static void
rootChangedCB(void *, SoSensor *s)
{
   // We know the sensor is really a data sensor:
   SoDataSensor *mySensor = (SoDataSensor *)s;
    
   SoNode *changedNode = mySensor->getTriggerNode();
   SoField *changedField = mySensor->getTriggerField();
    
   printf("The node named '%s' changed\n",
            changedNode->getName().getString());

   if (changedField != NULL) {
      SbName fieldName;
      changedNode->getFieldName(changedField, fieldName);
      printf(" (field %s)\n", fieldName.getString());
   } 
   else 
      printf(" (no fields changed)\n");
}

main(int, char **)
{
   SoDB::init();

   SoSeparator *root = new SoSeparator;
   root->ref();
   root->setName("Root");

   SoCube *myCube = new SoCube;
   root->addChild(myCube);
   myCube->setName("MyCube");

   SoSphere *mySphere = new SoSphere;
   root->addChild(mySphere);
   mySphere->setName("MySphere");

   SoNodeSensor *mySensor = new SoNodeSensor;

   mySensor->setPriority(0);
   mySensor->setFunction(rootChangedCB);
   mySensor->attach(root);

   // Now, make a few changes to the scene graph; the sensor's
   // callback function will be called immediately after each
   // change.
   myCube->width = 1.0;
   myCube->height = 2.0;
   mySphere->radius = 3.0;
   root->removeChild(mySphere);
}

Other Delay-Queue Sensors

In addition to data sensors, two other types of sensors are added to the delay queue: the SoOneShotSensor and the SoIdleSensor.

General Sequence for One-Shot and Idle Sensors

The following sequence describes the necessary steps for setting up one-shot and idle sensors:

  1. Construct the sensor.

  2. Set the callback function (see “Callback Function”).

  3. Set the priority of the sensor (see “Priorities”).

  4. Schedule the sensor using the schedule() method.

  5. When you are finished with the sensor, delete it.

Note that these sensors must be scheduled explicitly. Use the unschedule() method to remove a sensor from the queue.

SoOneShotSensor

An SoOneShotSensor invokes its callback once whenever the delayed sensor queue is processed. This sensor is useful for a task that does not need to be performed immediately or for tasks that should not be performed immediately (possibly because they are time-consuming). For example, when handling events for a device that generates events quickly (such as the mouse), you want to be able to process each event quickly so that events don't clog up the event queue. If you know that a certain type of event is time-consuming, you can schedule it with a one-shot sensor. For example:

handleEvent(SoHandleEventAction *action)
{ 
   //Check for correct event type ...
   .
   .
   .
   // Remember information from event for later processing
   currentMousePosition = event->getPosition();

   // Schedule a one-shot sensor to do hard work later
   SoOneShotSensor oneShot = new SoOneShotSensor(
         OneShotTriggerCallback, NULL);
   oneShot->schedule();
}
void OneShotTriggerCallback(void *userData, SoSensor *) 
{
   // Do lengthy operation based on current mouse position;
}

Note that sensors that invoke their callback one time only, such as SoOneShotSensor, SoIdleSensor, and SoAlarmSensor, continue to exist after their callback has been executed, but they do not trigger again unless they are rescheduled. Use the unschedule() method to stop any sensor from invoking its callback when it is scheduled.

The following example uses an SoOneShotSensor to delay rendering until the CPU is idle.

SoOneShotSensor *renderTask;

main() {
   ...
   renderTask = new SoOneShotSensor(doRenderCallback, NULL);
   // ... set up events, UI, which will call changeScene()
   // routine.
}

void
changeScene()
{
   // ... change scene graph ...
   renderTask->schedule();
}

void
doRenderCallback(void *userData, SoSensor *)
{
   // ... does rendering ...
}

SoIdleSensor

An SoIdleSensor invokes its callback once whenever the application is idle (there are no events or timers waiting to be processed). Use an idle sensor for low-priority tasks that should be done only when there is nothing else to do. Call the sensor's schedule() method in its callback function if you want it to go off repeatedly (but beware, since this keeps the CPU constantly busy). Note that idle sensors may never be processed if events or timers happen so often that there is no idle time; see “Processing the Sensor Queues” for details.

Timer-Queue Sensors

Timer-queue sensors, like data sensors, can be used to invoke user-specified callbacks. Instead of attaching a timer-queue sensor to a node or path in the scene graph, however, you simply schedule it, so that its callback is invoked at a specific time. (Timer-queue sensors are sorted within the timer queue by time rather than by priority.) Inventor includes two types of timer-queue sensors: SoAlarmSensor and SoTimerSensor.

General Sequence for Timer-Queue Sensors

The following sequence describes the necessary steps for setting up timer-
queue sensors:

  1. Construct the sensor.

  2. Set the callback function (see “Callback Function”).

  3. Set the timing parameters for the sensor.

  4. Schedule the sensor using the schedule() method.

  5. When you are finished with the sensor, delete it.

Timing parameters (when and how often the sensor is triggered) should not be changed while a sensor is scheduled. Use the unschedule() method to remove a sensor from the queue, change the parameter(s), and then schedule the sensor again.

SoAlarmSensor

An SoAlarmSensor, like an alarm clock, is set to go off at a specified time. When that time is reached or passed, the sensor's callback function is invoked. A calendar program might use an SoAlarmSensor, for example, to put a flag on the screen to indicate that it's time for your scheduled 2 o'clock meeting.

Use one of the following methods to set the time for this sensor:

setTime(time) 

schedules a sensor to occur at time

setTimeFromNow(time) 


schedules a sensor to occur at a certain amount of time from now

The time is specified using the SbTime class, which provides several different formats for time. Use the getTime() method of SoAlarmSensor to obtain the scheduled time for an alarm sensor.

Example 12-3 shows using an SoAlarmSensor to raise a flag on the screen when one minute has passed.

Example 12-3. Using an Alarm Sensor


static void
raiseFlagCallback(void *data, SoSensor *)
{
   // We know data is really a SoTransform node:
   SoTransform *flagAngleXform = (SoTransform *)data;
   
   // Rotate flag by 90 degrees about the z axis:
   flagAngleXform->rotation.setValue(SbVec3f(0,0,1), M_PI/2);
}

{
   ...

   SoTransform *flagXform = new SoTransform;

   // Create an alarm that will call the flag-raising callback:
   SoAlarmSensor *myAlarm =
       new SoAlarmSensor(raiseFlagCallback, flagXform);
   myAlarm->setTimeFromNow(60.0);
   myAlarm->schedule();
}

SoTimerSensor

An SoTimerSensor is similar to an SoAlarmSensor, except that it is set to go off at regular intervals—like the snooze button on your alarm clock. You might use an SoTimerSensor for certain types of animation, for example, to move the second hand of an animated clock on the screen. You can set the interval and the base time for an SoTimerSensor using these methods:

setInterval(interval) 


schedules a sensor to occur at a given interval, for example, every minute. The default interval is 1/30 of a second.

setBaseTime(time) 


schedules a sensor to occur starting at a given time. The default base time is right now—that is, when the sensor is first scheduled.

Before changing either the interval or the base time, you must first unschedule the sensor, as shown in Example 12-4.

Example 12-4 creates two timer sensors. The first sensor rotates an object. The second sensor goes off every 5 seconds and changes the interval of the rotating sensor. The rotating sensor alternates between once per second and ten times per second. (This example is provided mainly for illustration purposes. It would be better (and easier) to use two engines to do the same thing (see Chapter 13).

Example 12-4. Using a Timer Sensor


// This function is called either 10 times/second or once every
// second; the scheduling changes every 5 seconds (see below):
static void
rotatingSensorCallback(void *data, SoSensor *)
{
   // Rotate an object...
   SoRotation *myRotation = (SoRotation *)data;
   SbRotation currentRotation = myRotation->rotation.getValue();
   currentRotation = SbRotation(SbVec3f(0,0,1), M_PI/90.0) *
            currentRotation;
   myRotation->rotation.setValue(currentRotation);
}

// This function is called once every 5 seconds, and
// reschedules the other sensor.
static void
schedulingSensorCallback(void *data, SoSensor *)
{
   SoTimerSensor *rotatingSensor = (SoTimerSensor *)data;
   rotatingSensor->unschedule();
   if (rotatingSensor->getInterval() == 1.0)
      rotatingSensor->setInterval(1.0/10.0);
   else 
      rotatingSensor->setInterval(1.0);
      rotatingSensor->schedule();
}

{
   ...

   SoRotation *myRotation = new SoRotation;
   root->addChild(myRotation);

   SoTimerSensor *rotatingSensor =
      new SoTimerSensor(rotatingSensorCallback, myRotation);
   rotatingSensor->setInterval(1.0); //scheduled once per second
   rotatingSensor->schedule();

   SoTimerSensor *schedulingSensor =
   new SoTimerSensor(schedulingSensorCallback, rotatingSensor);
   schedulingSensor->setInterval(5.0); // once per 5 seconds
   schedulingSensor->schedule();
}

Processing the Sensor Queues (Advanced)

The following descriptions apply only to applications using the Inventor Component Library with the Xt toolkit. Other window system toolkits may have a different relationship between processing of the different queues. If you aren't interested in the details of how timers are scheduled, you can skip this section.

The general order of processing is event queue, timer queue, delay queue. A slight deviation from this order arises because the delay queue is also processed at regular intervals, whether or not there are timers or events pending. The sequence can be described as follows:

SoXt main loop calls XtAppMainLoop:

BEGIN:
If there's an event waiting:
Process all pending timers.
Process the delay queue if the delay queue time-out is
         reached.
Process the event.
Go back to BEGIN.
else (no event waiting)
if there are timers,
   Process timers.
   Go back to BEGIN.
else (no timers or events pending)
Process delay queue.
Go back to BEGIN.

When the timer queue is processed, the following conditions are guaranteed:

  • All timer or alarm sensors that are scheduled to go off before or at the time processing of the queue ends are triggered, in order.

  • When timer sensors are rescheduled, they are all rescheduled at the same time, after they have all been triggered.

For example, in Figure 12-2, at time A after the redraw, the timer queue is processed. Three timers have been scheduled in the queue (timers 0, 1, and 2). Timers 0 and 1 are ready to go off (their trigger time has already passed). Timer 2 is set to go off sometime in the future. The sequence is as follows:

  1. Timer 0 is triggered.

  2. Timer 1 is triggered.

  3. The scheduler checks the timer queue (the time is now B) and notices that timer 2's time has passed as well , so it triggers timer 2.

  4. Timers 0, 1, and 2 are rescheduled at time C.

  5. The scheduler returns to the main event loop to check for pending events.

    Figure 12-2. Triggering and Rescheduling Timers


The delay queue is processed when there are no events or timer sensors pending or when the delay queue's time-out interval elapses. By default, the delay queue times out 30 times a second. You can change this interval with the SoDB::setDelaySensorTimeout() method. Idle sensors are ignored when the delay sensor causes processing of the delay queue (because the CPU is not really idle under this condition).

When the delay queue is processed, the following conditions are guaranteed:

  • All sensors in the delay queue are triggered, in order of priority.

  • Each sensor is triggered once and only once, regardless of whether the sensor reschedules itself.