Re: [Plib-users] (cory vs. pui) The attack of setValuator
Brought to you by:
sjbaker
From: cory b. <co...@gm...> - 2008-06-12 20:53:18
|
Here is the code: //#include <memory.h> #include <stdio.h> #include <sstream> #include <simgear/compiler.h> #include <GL/glut.h> //#include SG_GLUT_H #include <plib/fnt.h> #include <plib/pu.h> #include <string> #include <simgear/io/sg_socket.hxx> #include <simgear/io/sg_serial.hxx> #include <simgear/io/sg_file.hxx> #include "SSAircraft.hxx" #include "SSPid.hxx" using namespace std; //defines #define DECIMALS 6 #define KEY_ESCAPE 27 #define GAIN_MIN -1 //gain values for sliders MIN should be 0 or less #define GAIN_MAX 1 #define DEG_MIN -180 //target value limiters #define DEG_MAX 180 //pui gui constants #define WINDOW_X 360 //window width #define WINDOW_Y 300 //window height #define PUI_X_OFFSET 0 #define PUI_Y_OFFSET 0 #define PUI_CTRL_X_OFFSET PUI_BUTTON_WIDTH + PUI_X_GAP * 2 #define PUI_CTRL_WIDTH PUI_ARROW_WIDTH * 2 + PUI_TEXT_WIDTH + PUI_X_GAP * 2 #define PUI_LABEL_X_OFFSET #define PUI_X_GAP 10 #define PUI_Y_GAP 20 #define PUI_TEXT_WIDTH 80 #define PUI_TEXT_HEIGHT 30 #define PUI_ARROW_WIDTH 30 #define PUI_ARROW_HEIGHT 30 #define PUI_BUTTON_WIDTH 120 #define PUI_BUTTON_HEIGHT 30 #define PUI_RADIO_WIDTH 100 #define PUI_RADIO_HEIGHT 60 //global items //objects SGIOChannel *in_channel, *out_channel, *log_file; SSAircraft *sim_craft = new SSAircraft(); SSPid *aileron_pid = new SSPid(); SSPid *elevator_pid = new SSPid(); //vars bool logging = false; bool pauseSoftSim = true; char inport[ 256 ] = "5500"; char outport[ 256 ] = "5501"; int sock; float update = 1.0f; char out_file[ 1024 ] = "sLog.csv"; char save_buf[ 2 * 2048 ]; int save_len = 0; //pui widgets from PLIB library puFrame *gui_frame; //radio buttons char *controller_selector_labels[] = { "Roll", "Pitch", NULL }; puButtonBox *controller_selector; //plain old buttons puButton *pause_button, *log_button; //arrow buttons for adjusting control values puArrowButton *kp_arrow_l, *kp_arrow_r, *ki_arrow_l, *ki_arrow_r, *kd_arrow_l, *kd_arrow_r, *target_arrow_l, *target_arrow_r; //text input boxes puInput *kp_text, *ki_text, *kd_text, *target_text, *hertz_text; char *valid_input = "-.0123456789"; //valid input mask for text boxes char kp_str[ PUSTRING_MAX ], ki_str[ PUSTRING_MAX ], kd_str[ PUSTRING_MAX ], target_str[ PUSTRING_MAX ]; //labels puText *roll_err_label, *pitch_err_label, *udp_label; char roll_text[ PUSTRING_MAX ], pitch_text[ PUSTRING_MAX ]; //function prototypes void timer( int value ); bool parse_fgear_data( char *buf ); void redrawWindow( void ); void reshapeWindow( int w, int h ); string command_output( void ); void processNormalKeys( unsigned char key, int x, int y ); void init_pui_widgets( void ); void pui_callback( puObject *pob ); void gui_update( SSPid *pid ); void processMousefn ( int button, int updown, int x, int y ); void processMotionfn ( int x, int y ); int main( int argc, char **argv ) { //glut setup glutInit( &argc, argv ); glutInitDisplayMode( GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGB ); glutInitWindowPosition( 100, 100 ); glutInitWindowSize( WINDOW_X, WINDOW_Y ); glutCreateWindow( "SoftSim" ); glutDisplayFunc( redrawWindow ); glutReshapeFunc( reshapeWindow ); glutIdleFunc( redrawWindow ); glutMouseFunc ( processMousefn ); // process mouse clicks glutMotionFunc ( processMotionfn ); // process mouse moves glutPassiveMotionFunc ( processMotionfn ); glutKeyboardFunc( processNormalKeys ); glEnable( GL_DEPTH_TEST ); // enable depth testing //this takes care of timing for the program glutTimerFunc( ( int )( 1000.0f/update ), timer, 0 ); //set up networking in_channel = new SGSocket( "", inport, "udp" ); out_channel = new SGSocket( "", outport, "udp" ); in_channel->open( SG_IO_IN ); out_channel->open( SG_IO_OUT ); //set up logging string f_name = out_file; log_file = new SGFile( f_name ); log_file->open( SG_IO_OUT ); //initialize PID controllers //the gains are some that work ok with the default plane aileron_pid->set_gains( 0.06, 0.004, -0.04 ); aileron_pid->set_reference( 0 ); aileron_pid->set_limits( -1, 1 ); elevator_pid->set_gains( -0.05, -0.001, 0 ); elevator_pid->set_reference( 0 ); elevator_pid->set_limits( -1, 1 ); //setup all the buttons and gui stuff init_pui_widgets(); //start glut glutMainLoop(); //clean up in_channel->close(); out_channel->close(); log_file->close(); return 0; } void timer( int value ) { char buffer[ 512 ]; size_t found; string log_str; //string to log to file int length; //do if not paused if ( !pauseSoftSim ){ //read incoming data while ( ( length = in_channel->readline( buffer, 512 ) ) > 0 ) { parse_fgear_data( buffer ); if ( logging ) { //prepare log string log_str = buffer; //find \n char and replace with comma found=log_str.find( '\n' ); if ( found!=string::npos ) { log_str.replace( found, 1, "," ); } } } string command_str = command_output(); //send outgoing data out_channel->write( command_str.c_str(), command_str.size() ); //log to file if ( logging ) { //don't log if no data from udp if ( log_str.length () > 0 ) { log_str.append( command_str ); log_file->writestring( log_str.c_str() ); //cout << command_str.c_str() << endl; //cout << log_str << endl; } } //make some updates system( "clear" ); //this is not portable. I should probably //be using the ncurses library cout.precision( DECIMALS ); cout << "Roll error: " << aileron_pid->get_error() << endl; cout << "Pitch error: " << elevator_pid->get_error() << endl; } glutTimerFunc( ( int )( 1000.0f/update ), timer, value ); } //parse incoming data from flight gear bool parse_fgear_data( char *buf ) { //variables //control double aileron, elevator, rudder, throttle; //position double latitudeDegree, longitudeDegree, altitude; //attitude double headingDegree, rollDegree, pitchDegree, sideSlip; //speed / accel double airspeed, verticalSpd, xAccel, yAccel, zAccel; //printf("%s\n", buf); string msg = buf; string::size_type begin, end; begin = 0; end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } //aileron aileron = atof( msg.substr( begin, end ).c_str() ); begin = end + 1; //elevator end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } elevator = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //rudder end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } rudder = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //throttle end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } throttle = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //latitude end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } latitudeDegree = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //longitude end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } longitudeDegree = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //altitude end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } altitude = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //roll end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } rollDegree = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //pitch end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } pitchDegree = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //heading end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } headingDegree = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //side slip end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } sideSlip = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //airspeed end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } airspeed = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //vertical speed end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } verticalSpd = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //x accel end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } xAccel = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //y accel end = msg.find( ",", begin ); if ( end == string::npos ) { return false; } yAccel = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //z accel end = msg.find( "\n", begin ); if ( end == string::npos ) { return false; } zAccel = atof( msg.substr( begin, end - begin ).c_str() ); begin = end + 1; //set aircraft values sim_craft->set_aileron( aileron ); sim_craft->set_elevator( elevator ); sim_craft->set_rudder( rudder ); sim_craft->set_throttle( throttle ); sim_craft->set_latitude( latitudeDegree ); sim_craft->set_longitude( longitudeDegree ); sim_craft->set_altitude( altitude ); sim_craft->set_roll( rollDegree ); sim_craft->set_pitch( pitchDegree ); sim_craft->set_heading( headingDegree ); sim_craft->set_sideslip( sideSlip ); sim_craft->set_airspeed( airspeed ); sim_craft->set_vert_speed( verticalSpd ); sim_craft->set_xaccel( xAccel ); sim_craft->set_yaccel( yAccel ); sim_craft->set_zaccel( zAccel ); return true; } string command_output( void ) { stringstream s; s.precision( DECIMALS ); s << aileron_pid->update( sim_craft->get_roll() ) << "," << elevator_pid->update( sim_craft->get_pitch() ) << endl; /* s << sim_craft->get_aileron() << "," << sim_craft->get_elevator() << "," << sim_craft->get_rudder() << "," << sim_craft->get_throttle() << endl; */ //cout << s.str() << endl; return s.str(); } void redrawWindow( void ) { puDisplay(); glutSwapBuffers(); glutPostRedisplay(); } void reshapeWindow( int w, int h ) { //I am trying to preven window resizing, but this doesn't seem to work glutReshapeWindow( WINDOW_X, WINDOW_Y ); //set window back to initial size } void processMousefn ( int button, int updown, int x, int y ) { // Invoke the PUI mouse function puMouse ( button, updown, x, y ) ; glutPostRedisplay () ; } void processMotionfn ( int x, int y ){ // Invoke the PUI mouse motion function puMouse ( x, y ) ; glutPostRedisplay () ; } void processNormalKeys(unsigned char key, int x, int y) { // Invoke the PUI keyboard function puKeyboard ( key, PU_DOWN ) ; glutPostRedisplay() ; } /********************************************************************** * Initialize pui controls *********************************************************************/ void init_pui_widgets( void ) { int curx = PUI_X_GAP * 4 + PUI_ARROW_WIDTH * 2 + PUI_TEXT_WIDTH; int cury = PUI_RADIO_HEIGHT + PUI_Y_GAP * 2; puInit(); gui_frame = new puFrame( PUI_X_OFFSET, PUI_Y_OFFSET, PUI_X_OFFSET + WINDOW_X, PUI_Y_OFFSET + WINDOW_Y ); //log button log_button = new puButton( curx, cury, "Log: sLog.csv"); log_button->setSize( PUI_BUTTON_WIDTH, PUI_BUTTON_HEIGHT ); log_button->setValue( 0 ); //logging off by default log_button->setCallback( pui_callback ); cury += PUI_Y_GAP + PUI_BUTTON_HEIGHT; //pause button pause_button = new puButton( curx, cury, "Pause" ); pause_button->setSize( PUI_BUTTON_WIDTH, PUI_BUTTON_HEIGHT ); pause_button->setValue( 1 ); //pause by default pause_button->setCallback( pui_callback ); curx = PUI_X_GAP; cury = PUI_Y_GAP; //radio buttons controller_selector = new puButtonBox ( curx, cury, curx + PUI_RADIO_WIDTH, cury + PUI_RADIO_HEIGHT, controller_selector_labels, 1 ); //value corresponds to index of item in labels array controller_selector->setCallback( pui_callback ); cury += PUI_RADIO_HEIGHT + PUI_Y_GAP; //kd items kd_arrow_l = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_LEFT ); kd_arrow_l->setCallback( pui_callback ); curx += PUI_ARROW_WIDTH + PUI_X_GAP; //kd_text kd_text = new puInput ( curx, cury, curx + PUI_TEXT_WIDTH, cury + PUI_TEXT_HEIGHT ) ; kd_text->setValidData( valid_input ); sprintf( kd_str, "%.3f", aileron_pid->get_kd() ); //format label kd_text->setValue( kd_str ); //set value to aileron gains since roll is selected by default kd_text->setLabel( "Kd" ); kd_text->setLabelPlace( PUPLACE_TOP_CENTERED ); kd_text->setCallback( pui_callback ); curx += PUI_TEXT_WIDTH + PUI_X_GAP; kd_arrow_r = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_RIGHT ); kd_arrow_r->setCallback( pui_callback ); curx = PUI_X_GAP; cury += PUI_ARROW_HEIGHT + PUI_Y_GAP; //ki items ki_arrow_l = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_LEFT ); ki_arrow_l->setCallback( pui_callback ); curx += PUI_ARROW_WIDTH + PUI_X_GAP; //ki_text ki_text = new puInput ( curx, cury, curx + PUI_TEXT_WIDTH, cury + PUI_TEXT_HEIGHT ) ; ki_text->setValidData( valid_input ); sprintf( ki_str, "%.3f", aileron_pid->get_ki() ); //format label ki_text->setValue( ki_str ); //set value to aileron gains since roll is selected by default ki_text->setLabel( "Ki" ); ki_text->setLabelPlace( PUPLACE_TOP_CENTERED ); ki_text->setCallback( pui_callback ); curx += PUI_TEXT_WIDTH + PUI_X_GAP; ki_arrow_r = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_RIGHT ); ki_arrow_r->setCallback( pui_callback ); curx = PUI_X_GAP; cury += PUI_ARROW_HEIGHT + PUI_Y_GAP; //kp items kp_arrow_l = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_LEFT ); kp_arrow_l->setCallback( pui_callback ); curx += PUI_ARROW_WIDTH + PUI_X_GAP; //kp_text kp_text = new puInput ( curx, cury, curx + PUI_TEXT_WIDTH, cury + PUI_TEXT_HEIGHT ) ; kp_text->setValidData( valid_input ); sprintf( kp_str, "%.3f", aileron_pid->get_kp() ); //format label kp_text->setValue( kp_str ); //set value to aileron gains since roll is selected by default kp_text->setLabel( "Kp" ); kp_text->setLabelPlace( PUPLACE_TOP_CENTERED ); kp_text->setCallback( pui_callback ); curx += PUI_TEXT_WIDTH + PUI_X_GAP; kp_arrow_r = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_RIGHT ); kp_arrow_r->setCallback( pui_callback ); curx = PUI_X_GAP; cury += PUI_ARROW_HEIGHT + PUI_Y_GAP; //target items target_arrow_l = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_LEFT ); target_arrow_l->setCallback( pui_callback ); curx += PUI_ARROW_WIDTH + PUI_X_GAP; //target_text target_text = new puInput ( curx, cury, curx + PUI_TEXT_WIDTH, cury + PUI_TEXT_HEIGHT ) ; target_text->setValidData( valid_input ); sprintf( target_str, "%.3f", aileron_pid->get_reference() ); //format label target_text->setValue( target_str ); //set value to aileron gains since roll is selected by default target_text->setLabel( "Target" ); target_text->setLabelPlace( PUPLACE_TOP_CENTERED ); target_text->setCallback( pui_callback ); curx += PUI_TEXT_WIDTH + PUI_X_GAP; target_arrow_r = new puArrowButton ( curx, cury, curx + PUI_ARROW_WIDTH, cury + PUI_ARROW_HEIGHT, PUARROW_RIGHT ); target_arrow_r->setCallback( pui_callback ); curx += PUI_ARROW_WIDTH + PUI_X_GAP; cury -= PUI_ARROW_HEIGHT - PUI_Y_GAP; //labels roll_err_label = new puText ( curx, cury ) ; roll_err_label->setLabel( "Roll error: 0.000" ); cury += PUI_ARROW_HEIGHT; pitch_err_label = new puText ( curx, cury ) ; pitch_err_label->setLabel( "Pitch error: 0.000" ); curx = PUI_RADIO_WIDTH + PUI_X_GAP * 2; cury = PUI_Y_GAP; udp_label = new puText ( curx, cury ) ; udp_label->setLabel( "UDP in: 5500 | UDP out: 5501" ); //hertz text input hertz_text = new puInput ( curx, cury, curx + PUI_TEXT_WIDTH, cury + PUI_TEXT_HEIGHT ) ; //hertz_text->setValidData( valid_input ); hertz_text->setValuator( &update ); hertz_text->setLabel( "Hertz" ); hertz_text->setLabelPlace( PUPLACE_TOP_CENTERED ); //hertz_text->setCallback( pui_callback ); } /********************************************************************** * pui callback *********************************************************************/ void pui_callback( puObject *pob ){ const float interval = 0.001; //increment or decrement interval //this will be 0 for roll and 1 for pitch int val = controller_selector->getValue(); bool aileron; if ( val == 0 ) { aileron = true; } else { aileron = false; } if ( pob == log_button ) { //toggle logging logging = log_button->getValue(); //cout << "log_button" << endl; } else if ( pob == pause_button ) { //toggle pause pauseSoftSim = pause_button->getValue(); //cout << "pause_button" << endl; } else if ( pob == controller_selector ) { //update gui if ( aileron ) { //update for ailerons gui_update( aileron_pid ); } else { //update for elevator gui_update( elevator_pid ); } } else if ( pob == kp_arrow_l ) { //decrement kp if ( aileron ) { aileron_pid->set_kp( aileron_pid->get_kp() - interval ); gui_update( aileron_pid ); } else { elevator_pid->set_kp( elevator_pid->get_kp() - interval ); gui_update( elevator_pid ); } } else if ( pob == kp_arrow_r ) { //increment kp if ( aileron ) { aileron_pid->set_kp( aileron_pid->get_kp() + interval ); gui_update( aileron_pid ); } else { elevator_pid->set_kp( elevator_pid->get_kp() + interval ); gui_update( elevator_pid ); } } else if ( pob == ki_arrow_l ) { //decrement ki if ( aileron ) { aileron_pid->set_ki( aileron_pid->get_ki() - interval ); gui_update( aileron_pid ); } else { elevator_pid->set_ki( elevator_pid->get_ki() - interval ); gui_update( elevator_pid ); } } else if ( pob == ki_arrow_r ) { //increment ki if ( aileron ) { aileron_pid->set_ki( aileron_pid->get_ki() + interval ); gui_update( aileron_pid ); } else { elevator_pid->set_ki( elevator_pid->get_ki() + interval ); gui_update( elevator_pid ); } } else if ( pob == kd_arrow_l ) { //decrement kd if ( aileron ) { aileron_pid->set_kd( aileron_pid->get_kd() - interval ); gui_update( aileron_pid ); } else { elevator_pid->set_kd( elevator_pid->get_kd() - interval ); gui_update( elevator_pid ); } } else if ( pob == kd_arrow_r ) { //increment kd if ( aileron ) { aileron_pid->set_kd( aileron_pid->get_kd() + interval ); gui_update( aileron_pid ); } else { elevator_pid->set_kd( elevator_pid->get_kd() + interval ); gui_update( elevator_pid ); } } else if ( pob == target_arrow_l ) { //decrement target if ( aileron ) { aileron_pid->set_reference( aileron_pid->get_reference() - 1 ); gui_update( aileron_pid ); } else { elevator_pid->set_reference( elevator_pid->get_reference() - 1 ); gui_update( elevator_pid ); } } else if ( pob == target_arrow_r ) { //increment target if ( aileron ) { aileron_pid->set_reference( aileron_pid->get_reference() + 1 ); gui_update( aileron_pid ); } else { elevator_pid->set_reference( elevator_pid->get_reference() + 1 ); gui_update( elevator_pid ); } } else { cout << "Unknown callback to pui_callback" << endl; } } void gui_update( SSPid *pid ) { sprintf( kp_str, "%.3f", pid->get_kp() ); //format label kp_text->setValue( kp_str ); sprintf( ki_str, "%.3f", pid->get_ki() ); //format label ki_text->setValue( ki_str ); sprintf( kd_str, "%.3f", pid->get_kd() ); //format label kd_text->setValue( kd_str ); sprintf( target_str, "%.3f", pid->get_reference() ); //format label target_text->setValue( target_str ); } On Thu, Jun 12, 2008 at 4:42 PM, Fay John F Dr CTR USAF 46 SK <joh...@eg...> wrote: > Hmmm ... unless somebody else steps in, please forward me a copy of the > offending code. I've tried putting in your code snippet and the program > works just fine for me, so evidently something else is going on. > > John F. Fay > Technical Fellow > Jacobs Technology TEAS Group > 850-883-1294 > > -----Original Message----- > From: pli...@li... > [mailto:pli...@li...] On Behalf Of cory > barton > Sent: Thursday, June 12, 2008 3:19 PM > To: PLIB Users > Subject: [Plib-users] (cory vs. pui) The attack of setValuator > > I found the following in the PUI documentation today: > There are many occasions when you'd really like to have the PUI widget > directly drive and/or reflect the value of some memory location in the > application code. These calls let you do that: > > void puObject::setValuator ( int *i ) ; > void puObject::setValuator ( float *f ) ; > void puObject::setValuator ( char *s ) ; > > > Once you make one of these calls, PUI will automatically update the > memory location indicated with the current value of the widget > whenever it changes - and also update the appearance of the widget to > reflect the value stored in that memory location whenever the widget > is redrawn. This is often a lot more convenient than using a callback > function to register changes in the widget's value. > > So I tried it out: > //global variable > float update = 1.0f; > > //initialize my puInput inside a function > //hertz text input > hertz_text = new puInput ( curx, cury, > curx + > PUI_TEXT_WIDTH, > cury + > PUI_TEXT_HEIGHT ) ; > hertz_text->setValuator( &update ); > hertz_text->setLabel( "Hertz" ); > hertz_text->setLabelPlace( PUPLACE_TOP_CENTERED ); > > > This compiles, and runs. > > It seems to work correctly as long as I do not remove all of the > characters from the puInput box. However if I am trying to change the > value, when I am typing in the new value, if the new value ever > becomes a non valid float value, then the program crashes. For example > if I want to change 1.0 to 2.0 and I delete 1.0 so I can type 2.0, > then instant crash. I can change from 1.0 to 2.0 if I type the 2 after > the 1, and then erase the 1. > > Searching the mail archives and googling didn't help, so I am posting > here. > > I suspect either my weak understanding of pointers means that I am > passing an incorrect argument to the setValuator function, or maybe > the puInput object is writing invalid data to the update variable. > > Please let me know how to fix this. > > thanks, > > Cory > > ------------------------------------------------------------------------ > - > Check out the new SourceForge.net Marketplace. > It's the best place to buy or sell services for > just about anything Open Source. > http://sourceforge.net/services/buy/index.php > _______________________________________________ > plib-users mailing list > pli...@li... > https://lists.sourceforge.net/lists/listinfo/plib-users > > ------------------------------------------------------------------------- > Check out the new SourceForge.net Marketplace. > It's the best place to buy or sell services for > just about anything Open Source. > http://sourceforge.net/services/buy/index.php > _______________________________________________ > plib-users mailing list > pli...@li... > https://lists.sourceforge.net/lists/listinfo/plib-users > |