Update of /cvsroot/playerstage/code/player/server/drivers/limb
In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv19057

Added Files:
        Makefile.am eeDHcontroller.cc 
Log Message:
New driver for computing the position of the End-Effector of an arm using
the ROBOOP library's Inverse Kinematics routines. 


--- NEW FILE: Makefile.am ---
noinst_LTLIBRARIES =

if INCLUDE_EEDHCONTROLLER
noinst_LTLIBRARIES += libeeDHcontroller.la
endif

AM_CPPFLAGS = -Wall -I$(top_srcdir) @EEDHCONTROLLER_EXTRA_CPPFLAGS@

libeeDHcontroller_la_SOURCES = eeDHcontroller.cc
libeeDHcontroller_la_LIBADD = @EEDHCONTROLLER_EXTRA_CPPFLAGS@

--- NEW FILE: eeDHcontroller.cc ---
/*
 *  Player - One Hell of a Robot Server
 *  Copyright (C) 2007
*      Radu Bogdan Rusu ([EMAIL PROTECTED])
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
/*
 Desc: Calculates the joint commands for a given End Effector pose using 
       ROBOOP's Inverse Kinematics algorithms.
 Author: Radu Bogdan Rusu and Dan Pojar
 Date: 10 May 2007
 CVS: $Id: eeDHcontroller.cc,v 1.1 2007/05/22 22:11:03 veedee Exp $
*/

/** @ingroup drivers Drivers */
/** @{ */
/** @defgroup driver_eeDHcontroller eeDHcontroller
 * @brief Calculates the joint commands for a given End Effector pose using 
ROBOOP's Inverse Kinematics algorithms.

The roboopIK driver performs inverse kinematics calculations using the
ROBOOP library for a given robot arm's end effector, and sends the resulting
joint commands to the appropriate actarray interface. The arm model is
specified in the Player configuration file using the Denavit-Hartenberg
convention parameters (see
http://prt.fernuni-hagen.de/lehre/KURSE/PRT001/course_main/node15.html for
more details).

When a positioning command of the limb is received via
PLAYER_LIMB_SETPOSITION_CMD or PLAYER_LIMB_SETPOSE_CMD, the driver computes
the joint commands and sends them in ascending order (base to end effector)
to the actarray interface using PLAYER_ACTARRAY_POS_CMD.

When a homing command of the limb is received via PLAYER_LIMB_HOME_CMD, the
driver will send a PLAYER_ACTARRAY_HOME_CMD to every joint provided by the
actarray interface in descending order (end effector to base).

The driver also computes the current pose of the end effector using forward
kinematics (given the current joint positions taken from the actarray
interface) and returns it as a data packet.

@par Compile-time dependencies

- the ROBOOP library (http://www.cours.polymtl.ca/roboop/)

@par Provides

- @ref interface_limb

@par Requires

- @ref interface_actarray

@par Configuration requests

- PLAYER_LIMB_SPEED_REQ
  
@par Configuration file options

- nr_joints (integer)
  - The number of joints that we provide DH parameters for (should be the
    same as the number of actuators the actarray interface provides).
  
- jointX_DH (integer tuple)
  - [ R/P theta d a alfa th_min th_max ] - DH parameters for joint X

- error_pos (float)
  - Default: 0
  - User allowed error value in position in degrees. This is needed for joints 
    who do not change their state (eg. they remain idle) when a command is 
given 
    and the joint is already in that position.

- debug (int)
  - Default: 0
  - Enable debugging mode (detailed information messages are printed on screen)
    Valid values: 0 (disabled), 1 (enabled).

@par Example 

@verbatim
driver
(
  name "eeDHcontroller"
  provides ["limb:0"]
  requires ["actarray:0"]

  nr_joints 6

  # [ R/P theta d a alfa th_min th_max ]
  joint1_DH [ 0 0 0.180  0  1.57 -1.57 2.0  ]
  joint2_DH [ 0 0 0.215  0 -1.57 -1.57 1.57 ]
  joint3_DH [ 0 0 0.0    0  1.57 -1.57 1.57 ]
  joint4_DH [ 0 0 0.308  0 -1.57 -1.57 1.57 ]
  joint5_DH [ 0 0 0.0    0  1.57 -2.09 2.09 ]
  joint6_DH [ 0 0 0.2656 0  0     0    0 ]

  # Allowed positioning error in degrees
  error_pos 0.01
  
  # Enable debug mode
  debug 1
)
@endverbatim

@author Radu Bogdan Rusu and Dan Pojar

*/
/** @} */


#include <robot.h>
#include <libplayercore/playercore.h>

// Structure for the command thread
typedef struct
{
  void *driver;
  ColumnVector q;
} CmdLoopStruc;

////////////////////////////////////////////////////////////////////////////////
// The EEDHController device class.
class EEDHController : public Driver
{
  public:
    // Constructor
    EEDHController (ConfigFile* cf, int section);

    // Destructor
    ~EEDHController ();

    // Implementations of virtual functions
    virtual int Setup ();
    virtual int Shutdown ();

    // This method will be invoked on each incoming message
    virtual int ProcessMessage (MessageQueue* resp_queue, 
                                player_msghdr * hdr,
                                void * data);

  private:

    player_actarray_data_t actarray_data;
    player_limb_data_t     limb_data;
    bool actarray_data_received;

    // device bookkeeping
    player_devaddr_t actarray_addr;
    Device* actarray_device;

    // Main function for device thread.
    virtual void Main ();
    virtual void RefreshData  ();

    // Joints count and their DH parameters
    Matrix DHMatrixModel;
    unsigned int nr_joints;
    
    // Create two Robot objects (for IK and for FK)
    Robot robot_IK, robot_FK;
    ColumnVector q_cmd;

    // FK and IK methods
    int ComputeQ (unsigned int dof, double x, double y, double z,
                  double roll, double pitch, double yaw, ColumnVector &q);
    ColumnVector ComputeEEPose (ColumnVector q_cmd);
    float m_atan2 (float a, float b);

    // Send commands to the actarray
    void CommandJoints (ColumnVector q_cmd);
    
    // Keeping track of whether joints are still moving or not
    int actarray_state[PLAYER_ACTARRAY_NUM_ACTUATORS];

    // Threads used for homing or moving the actarray
    pthread_t a_th_home, a_th_cmd;
    bool command_thread, homing_thread;

    // Functions defining the threads
    static void* DummyAHomeLoop (void *driver);
    static void* DummyACmdLoop  (void *structure);
    void AHomeLoop ();
    void ACmdLoop  (ColumnVector q);

    int debug;
    
    // Allowed positioning error in degrees
    float error_pos;
};

////////////////////////////////////////////////////////////////////////////////
// Factory creation function. This functions is given as an argument when
// the driver is added to the driver table
Driver* EEDHController_Init (ConfigFile* cf, int section)
{
  // Create and return a new instance of this driver
  return ((Driver*)(new EEDHController (cf, section)));
}

////////////////////////////////////////////////////////////////////////////////
// Registers the driver in the driver table. Called from the 
// player_driver_init function that the loader looks for
void
  EEDHController_Register (DriverTable* table)
{
  table->AddDriver ("eeDHcontroller", EEDHController_Init);
}

////////////////////////////////////////////////////////////////////////////////
// Constructor.  Retrieve options from the configuration file and do any
// pre-Setup() setup.
EEDHController::EEDHController (ConfigFile* cf, int section)
  : Driver (cf, section, false, PLAYER_MSGQUEUE_DEFAULT_MAXLEN,
            PLAYER_LIMB_CODE)
{
  unsigned int i, j;

  // Must have an input actarray
  if (cf->ReadDeviceAddr(&this->actarray_addr, section, "requires",
                         PLAYER_ACTARRAY_CODE, -1, NULL) != 0)
  {
    PLAYER_ERROR ("must have an input actarray");
    this->SetError(-1);
    return;
  }
  this->actarray_device = NULL;
  this->actarray_data_received = false;

  nr_joints = cf->ReadInt (section, "nr_joints", 0);

  // Create initial DH parameters model
  DHMatrixModel = Matrix (6, 23);
  DHMatrixModel = 0;
    
  for (i = 0; i < nr_joints; i++)
  {
    char joint_nr[10];
    sprintf (joint_nr, "joint%d_DH", (i+1));

    for (j = 0; j < 7; j++)
    {
      float bla = cf->ReadTupleFloat (section, joint_nr, j, 0);
      DHMatrixModel (i+1, j+1) = bla;
    }
    
    // Set the initial status of joints (IDLE)
    actarray_state[i] = PLAYER_ACTARRAY_ACTSTATE_IDLE;
  }
    
  // Instantiate the robot(s) with the DH parameter matrix
  robot_IK = Robot (DHMatrixModel);
  robot_FK = Robot (DHMatrixModel);
  q_cmd = ColumnVector (nr_joints);
  
  debug = cf->ReadInt (section, "debug", 0);
  
  // Allowed positioning error in degrees
  error_pos = cf->ReadFloat (section, "error_pos", 1.0);
}


////////////////////////////////////////////////////////////////////////////////
// Destructor.
EEDHController::~EEDHController()
{
}

////////////////////////////////////////////////////////////////////////////////
// Set up the device.  Return 0 if things go well, and -1 otherwise.
int
  EEDHController::Setup ()
{
  PLAYER_MSG0 (1, "> EEDHController starting up... [done]");

  // Subscribe to the actarray
  if (!(this->actarray_device = deviceTable->GetDevice (this->actarray_addr)))
  {
      PLAYER_ERROR ("unable to locate a suitable actarray device");
      return (-1);
  }
  if (this->actarray_device->Subscribe (this->InQueue) != 0)
  {
      PLAYER_ERROR ("unable to subscribe to the actarray device");
      return (-1);
  }

  // Start the device thread
  StartThread ();
  
  return (0);
}

////////////////////////////////////////////////////////////////////////////////
// Shutdown the device
int
  EEDHController::Shutdown ()
{
  // Stop the driver thread
  StopThread ();

  pthread_cancel (a_th_cmd);
  pthread_cancel (a_th_home);
  
  this->actarray_device->Unsubscribe (this->InQueue);

  PLAYER_MSG0 (1, "> EEDHController driver shutting down... [done]");
  return (0);
}


////////////////////////////////////////////////////////////////////////////////
// Main function for device thread
void
  EEDHController::Main () 
{
  timespec sleepTime = {0, 1000};

  command_thread = homing_thread = false;
  
  // The main loop; interact with the device here
  while (true)
  {
    // test if we are supposed to cancel
    pthread_testcancel ();

    // Process any pending messages
    this->ProcessMessages ();

    // Refresh data
    this->RefreshData ();

    nanosleep (&sleepTime, NULL);
  }
}

////////////////////////////////////////////////////////////////////////////////
// Compute the joint commands using IK
int 
  EEDHController::ComputeQ (unsigned int dof, 
                            double x, double y, double z, 
                            double roll, double pitch, double yaw, 
                            ColumnVector &q)
{
  // Position/Orientation of the end effector
  Matrix result (4, 4);

  // Set the orientation
  ColumnVector orient (3);
  orient (3) = roll;
  orient (2) = pitch;
  orient (1) = yaw;

  result = rpy (orient);

  result (1, 4) = x;
  result (2, 4) = y;
  result (3, 4) = z;

  // Call RoboOp's inv_kin
  bool converged;
  
  q = robot_IK.inv_kin (result, 1, dof, converged);
  if (q.is_zero () && !converged)
    return (1);

  return (0);
}

////////////////////////////////////////////////////////////////////////////////
// Complete definition range atan2
float
  EEDHController::m_atan2 (float a, float b)
{
  float rad = 0;
        
  if (a == 0)
    rad = b < 0 ? -M_PI/2 : M_PI/2;
  else
  {
    rad = atan (b / a);
    if (a < 0)
      rad += M_PI;
  }
  if (rad < 0)
    rad += 2*M_PI;
  return rad;
}

////////////////////////////////////////////////////////////////////////////////
// Compute the End-Effector pose from the joint's position using FK
ColumnVector 
  EEDHController::ComputeEEPose (ColumnVector q_cmd)
{
  robot_FK.set_q (q_cmd);
  Matrix position (4, 4);

  ColumnVector ret (6);

  // use robot_FK for forward kinematics
  position = robot_FK.kine ();
  // Roll/X, Pitch/Y, Yaw/Z
  ret(1) = m_atan2 (position (3, 3), position (3, 2));
  ret(2) = m_atan2 (sqrt ((position (3, 2) * position (3, 2)) + 
                          (position (3, 3) * position (3, 3))), 
                    -position (3, 1));
  ret(3) = m_atan2 (position(1, 1), position (2, 1));
  // X, Y, Z
  ret(4) = position (1, 4);
  ret(5) = position (2, 4);
  ret(6) = position (3, 4);

  return ret;
}

////////////////////////////////////////////////////////////////////////////////
// RefreshData function
void
  EEDHController::RefreshData ()
{
  unsigned int i;
  if (actarray_data_received)
  {
    assert (nr_joints == actarray_data.actuators_count);
    
    // Set the vector state of the actuators
    limb_data.state = PLAYER_LIMB_STATE_BRAKED;
    for (i = 0; i < nr_joints; i++)
    {
      actarray_state[i] = actarray_data.actuators[i].state;
      if (actarray_state[i] == PLAYER_ACTARRAY_ACTSTATE_MOVING)
        limb_data.state = PLAYER_LIMB_STATE_MOVING;
    }
    // Assure that the moving/homing threads are not started
    if ((homing_thread) || (command_thread))
      limb_data.state = PLAYER_LIMB_STATE_MOVING;

    ColumnVector q_cmd (nr_joints);

    // Get the joints positions
    for (i = 0; i < nr_joints; i++)
      q_cmd (i + 1) = actarray_data.actuators[i].position;

    ColumnVector pose = ComputeEEPose (q_cmd);

    // Fill the limb data structure with values and publish it
    limb_data.position.px = pose (4); 
    limb_data.position.py = pose (5); 
    limb_data.position.pz = pose (6); 
  
    limb_data.approach.px = -1; limb_data.approach.py = -1; 
limb_data.approach.pz = -1; 
  
    limb_data.orientation.px = pose (1); 
    limb_data.orientation.py = pose (2); 
    limb_data.orientation.pz = pose (3); 
  
    Publish (device_addr, NULL, PLAYER_MSGTYPE_DATA, PLAYER_LIMB_DATA,
             &limb_data, sizeof (limb_data), NULL);

    actarray_data_received = false;
  }
}

////////////////////////////////////////////////////////////////////////////////
// Dummy start for the AHomeLoop
void*
  EEDHController::DummyAHomeLoop (void *driver)
{
  ((EEDHController*)driver)->AHomeLoop ();
  return (0);
}

////////////////////////////////////////////////////////////////////////////////
// Main AHomeLoop, checks if joints are moving
void
  EEDHController::AHomeLoop ()
{
  timespec sleepTime = {0, 1000};
  player_actarray_home_cmd_t cmd;
  int i;
  
  // Home one joint at a time
  for (i = nr_joints-1; i > -1; i--)
  { 
    cmd.joint = i;

    this->actarray_device->PutMsg (this->InQueue, 
                                   PLAYER_MSGTYPE_CMD,
                                   PLAYER_ACTARRAY_HOME_CMD,
                                   (void*)&cmd,
                                   sizeof (cmd), NULL);
    if (debug)
      printf (">> Homing joint %d...", i);
    int p_state = actarray_state[i];
    int c_state = actarray_state[i];

    while (! (
      (p_state != c_state) 
      && 
      (c_state != PLAYER_ACTARRAY_ACTSTATE_MOVING)
      ))
    {
      p_state = c_state;
      c_state = actarray_state[i];
      nanosleep (&sleepTime, NULL);
    }
    if (debug)
      printf ("[done]\n");
  }
  if (debug)
    printf (">> Homing complete.\n");
  homing_thread = false;
}

////////////////////////////////////////////////////////////////////////////////
// Send joint command values to the underlying actarray device
void
  EEDHController::CommandJoints (ColumnVector q_cmd)
{
  command_thread = true;
  CmdLoopStruc *s = new CmdLoopStruc;
  s->driver = this;
  s->q      = q_cmd;
  // Start the actarray homing thread
  pthread_create (&a_th_cmd, NULL, &DummyACmdLoop, (void*)s);
}

////////////////////////////////////////////////////////////////////////////////
// Dummy start for the ACmdLoop
void*
  EEDHController::DummyACmdLoop (void *structure)
{
  CmdLoopStruc *s = (CmdLoopStruc*)structure;
  ((EEDHController*)s->driver)->ACmdLoop (s->q);
  return (0);
}

////////////////////////////////////////////////////////////////////////////////
// Main ACmdLoop, checks if joints are moving
void
  EEDHController::ACmdLoop (ColumnVector q_cmd)
{
  timespec sleepTime = {0, 1000};
  player_actarray_position_cmd_t cmd;
  int i;
  
  // Write the commands on screen if debug enabled
  if (debug)
  {
    printf (">> Sending the following joint commands: ");
    for (i = 0; i < q_cmd.nrows (); i++)
      printf ("%f ", q_cmd (i + 1));
    printf ("\n");
  }

  // Position one joint at a time
  for (i = 0; i < q_cmd.nrows (); i++)
  { 
    cmd.joint    = i;
    cmd.position = q_cmd (i + 1);

    if (debug)
      printf (">>> Sending command %f to joint %d... ", cmd.position, 
cmd.joint);

    // If the current joint is already there +/- some user preferred 
positioning error
    if (abs(cmd.position - actarray_data.actuators[i].position) < DTOR 
(error_pos))
      break;
    
    this->actarray_device->PutMsg (this->InQueue, 
                                   PLAYER_MSGTYPE_CMD,
                                   PLAYER_ACTARRAY_POS_CMD,
                                   (void*)&cmd,
                                   sizeof (cmd), NULL);
    int p_state = actarray_state[i];
    int c_state = actarray_state[i];

    while (! (
      (p_state != c_state) 
      && 
      (c_state != PLAYER_ACTARRAY_ACTSTATE_MOVING)
      ))
    {
      p_state = c_state;
      c_state = actarray_state[i];
      nanosleep (&sleepTime, NULL);
    }
    
    if (debug)
      printf ("[done]\n");
  }
  if (debug)
    printf (">> Commands sent.\n");
  command_thread = false;
}

////////////////////////////////////////////////////////////////////////////////
// ProcessMessage function
int
  EEDHController::ProcessMessage (MessageQueue* resp_queue, 
                          player_msghdr * hdr,
                          void * data)
{
  // Check for capabilities requests first
  HANDLE_CAPABILITY_REQUEST (device_addr, resp_queue, hdr, data, 
PLAYER_MSGTYPE_REQ, PLAYER_CAPABILTIES_REQ);
  HANDLE_CAPABILITY_REQUEST (device_addr, resp_queue, hdr, data, 
PLAYER_MSGTYPE_REQ, PLAYER_LIMB_SPEED_REQ);
  HANDLE_CAPABILITY_REQUEST (device_addr, resp_queue, hdr, data, 
PLAYER_MSGTYPE_CMD, PLAYER_LIMB_SETPOSE_CMD);
  HANDLE_CAPABILITY_REQUEST (device_addr, resp_queue, hdr, data, 
PLAYER_MSGTYPE_CMD, PLAYER_LIMB_SETPOSITION_CMD);
  HANDLE_CAPABILITY_REQUEST (device_addr, resp_queue, hdr, data, 
PLAYER_MSGTYPE_CMD, PLAYER_LIMB_HOME_CMD);
  
  // First look whether we have incoming data from the actarray interface
  if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_DATA, 
    PLAYER_ACTARRAY_DATA_STATE, actarray_addr))
  {
    actarray_data = *((player_actarray_data_t*)data);
    actarray_data_received = true;
    return (0);
  }

  // Set the desired position to the actarray driver
  else if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_CMD, 
    PLAYER_LIMB_SETPOSE_CMD, device_addr))
  {
    player_limb_setpose_cmd_t & command = 
      *reinterpret_cast<player_limb_setpose_cmd_t * > (data);

    int converged = ComputeQ (nr_joints, command.position.px, 
                              command.position.py, command.position.pz, 
                              command.orientation.px, command.orientation.py,
                              command.orientation.pz, q_cmd);
    if (converged != 0)
    {
      PLAYER_WARN6 ("Couldn't find solution for %f,%f,%f/%f,%f,%f", 
command.position.px, 
                   command.position.py, command.position.pz, 
command.orientation.px, 
                   command.orientation.py, command.orientation.pz);
      // Do we actually need to send back a NACK ?
      Publish (device_addr, resp_queue, PLAYER_MSGTYPE_RESP_NACK, hdr->subtype);
      return (-1);
    }
    CommandJoints (q_cmd);

    return (0);
  }

  // Set the desired position to the actarray driver
  else if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_CMD, 
    PLAYER_LIMB_SETPOSITION_CMD, device_addr))
  {
    player_limb_setposition_cmd_t & command = 
      *reinterpret_cast<player_limb_setposition_cmd_t * > (data);
    
    int converged = ComputeQ (nr_joints, command.position.px, 
                              command.position.py, command.position.pz, 0, 0, 
0, q_cmd);
    
    if (converged != 0)
    {
      // Do we actually need to send back a NACK ?
      PLAYER_WARN3 ("Couldn't find solution for %f,%f,%f", command.position.px, 
                   command.position.py, command.position.pz);
      Publish (device_addr, resp_queue, PLAYER_MSGTYPE_RESP_NACK, hdr->subtype);
      return (-1);
    }
    CommandJoints (q_cmd);

    return (0);
  }

  // Home the limb (we do this by homing all the joints)
  else if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_CMD, 
    PLAYER_LIMB_HOME_CMD, device_addr))
  {
    homing_thread = true;
    // Start the actarray homing thread
    pthread_create (&a_th_home, NULL, &DummyAHomeLoop, this);
    return (0);
  }


  // POWER_REQ not implemented
  else if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_REQ, 
        PLAYER_LIMB_POWER_REQ, device_addr))
  {
    Publish (device_addr, resp_queue, PLAYER_MSGTYPE_RESP_NACK, hdr->subtype);
    return (-1);
  }

  // BRAKES_REQ not implemented
  else if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_REQ, 
        PLAYER_LIMB_BRAKES_REQ, device_addr))
  {
    Publish (device_addr, resp_queue, PLAYER_MSGTYPE_RESP_NACK, hdr->subtype);
    return (-1);
  }

  // GEOM_REQ not implemented
  else if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_REQ, 
        PLAYER_LIMB_GEOM_REQ, device_addr))
  {
    Publish (device_addr, resp_queue, PLAYER_MSGTYPE_RESP_NACK, hdr->subtype);
    return (-1);
  }

  // SPEED_REQ - Set the speed on all joints equal to the EE
  else if (Message::MatchMessage (hdr, PLAYER_MSGTYPE_REQ, 
        PLAYER_LIMB_SPEED_REQ, device_addr))
  {
    player_limb_speed_req_t & cfg = 
      *reinterpret_cast<player_limb_speed_req_t *> (data);
    
    int i;
    player_actarray_speed_config_t act_cfg;
    for (i = 0; i < q_cmd.nrows (); i++)
    { 
      act_cfg.joint = i;
      act_cfg.speed = cfg.speed;

      Message *msg;
      if (!(msg = this->actarray_device->Request (this->InQueue, 
                                                  PLAYER_MSGTYPE_REQ,
                                                  PLAYER_ACTARRAY_SPEED_REQ,
                                                  (void*)&act_cfg,
                                                  sizeof (act_cfg), NULL, 
false)))
      {
        PLAYER_WARN1 ("failed to send speed command to actuator %d", i);
      }
      else
        delete msg;
    }
    
    Publish (device_addr, resp_queue, PLAYER_MSGTYPE_RESP_ACK, hdr->subtype);
    return (0);
  }

  return (0);
}
//------------------------------------------------------------------------------



-------------------------------------------------------------------------
This SF.net email is sponsored by DB2 Express
Download DB2 Express C - the FREE version of DB2 express and take
control of your XML. No limits. Just data. Click to get it now.
http://sourceforge.net/powerbar/db2/
_______________________________________________
Playerstage-commit mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/playerstage-commit

Reply via email to