/**   AnalogApplet.java
 *
 *	Z-World, 2001
 *
 *      An applet framework for control of the analog output on a BL2000.
 *      Uses a TCP connection back to BL2000 for control messages.
 *	Code tries to use java 1.1 classes. It uses the version 1.1 input
 *	event model.
 *
 *	@Version 0.05
 *	@date	May 2001
 *	@Author	Z-World, Inc (software)
 *
 */


import java.awt.*;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ItemListener;
import java.awt.event.ItemEvent;
import java.applet.*;
import java.net.*;
import java.io.*;
import java.util.Vector;


/**
 *	Applet class.  Creates visual components and network connection objects
 *	as well.  User drags the scrollbars to control the DAC outputs.
 *	<br> The applet appearance looks like this:
 *<pre> 	Message Line..
 *	{ label(0) + slider(0).. }
 *	{ label(1) + slider(1).. }
 *	Status Text..
 *	Value Text.. </pre>
 *	The &quot;Status Text&quot; is separate from the browser status line.
 *
 *	<P> Will not connect with session ID 0.  That means server already
 *	has a client it is serving.  Ensures Java 1.1 classes we need
 *	(e.g. AWT Listener) are present.
 */
public class AnalogApplet extends Applet
{

    /**
     *	Create layout with text labels and value scrollers.
     *	Connects back to host establishing a command socket.
     *	Uses <code>this.getCodeBase().getHost()</code> to find host system.
     *  Will not connect with session ID 0.  That means server already
     *  has a client it is serving.
     *
     *	<p> Ensures Java 1.1 classes we need (AWT Listener) are present.
     */
    public void init()
    {
	int    server_tcp_port = 0;

	/*  Proof of geekiness: We use the GridBagLayout manager.  Rows
	 *  with text labels stretch to full width with left-side alignment.
	 *  Rows with scrollbars and value bars have a fixed width label
	 *  on the leftside with a full-stretch scroll "cell".
	 */
	myGridBag = new GridBagLayout();
	presetConstraints();

	/*  Now pile on the "cells" */
        messageLine = new Label();
	myGridBag.setConstraints( messageLine, _fullWidthConstraint );
        add( messageLine );

	myAnalogController = new ValueListenerBoardUpdater();
	myAnalogController.init();

	/*  Make sure classes exist before trying to use them. */
	if( !checkRuntime() ) {
	    outMessage( "ERROR: Required Java 1.1 classes are missing!");
	    outStatus( "ERROR: Applet cannot continue." );
	    return ;
	}

	/** We've check that all our Java 1.1 classes exist, proceed. **/

        makeControl(0);
        makeControl(1);

	if( SW_A_TO_D_WORKING ) {
	makeValueBar(-1);		// "-1" means no A/D selected.
	}

        statusLine = new Label();
	myGridBag.setConstraints( statusLine, _fullWidthConstraint );
        add( statusLine );

	valueText = new Label();
	myGridBag.setConstraints( valueText, _fullWidthConstraint );
	add( valueText );
	valueText.setText( "    " );

	String param = "";
        try {
	    param = this.getParameter( "SESSION_ID" );
            if( (sessID = Integer.parseInt(param)) < 0 ) {
		outMessage( "ERROR: Session ID isn't valid." );
            }
	    param = this.getParameter( "CMD_PORT" );
	    if( (server_tcp_port = Integer.parseInt(param)) <= 0 ) {
		outMessage( "ERROR: Illegal server TCP port." );
	    }
	    param = this.getParameter( "MAX_VOLTAGE" );
	    if( (myMaxVolts = (new Float(param).floatValue())) <= 0.0 ) {
		outMessage( "ERROR: Illegal server TCP port." );
	    }
        } catch( NumberFormatException e ) {
	    /*  Either SESSION_ID wasn't found or a number conversion 
	     *  occurred.  Oh well, display a diagnostic.
	     */
	    outStatus( "ERROR: Bad params ("+param+"), you need to restart." +
		       sessID + "/" + server_tcp_port + "/" + myMaxVolts );
	    return ;
	}

	if( sessID == 0 ) {
	    /*  Could make a frame here, effectively creating a dialog box.
	     *  That exercise is left to the reader... :)
	     */
	    outMessage( "NOTICE: Analog server is busy." );
	    outStatus(  "NOTICE: Not connecting to remote server." );
	    return ;
	}

	myAnalogController.setOutput( statusLine );
	myAnalogController.setVoltageLabel( valueText );

	/*  Connect to Rabbit "server" */
	showStatus( "Connecting..." );
	myConn = new RabbitNetworkClient();
	myConn.hookupTCP( this.getCodeBase().getHost(), server_tcp_port, this ); 
	myAnalogController.setConnection( myConn );

	/*  Display `ready' message in browser's status line. */
	showStatus( "applet ready" );

    }  /* end init() method */

    /**  Make sure network object can close and cleanup.  "destory"
     *   called when applet no longer needed (not just hidden).
     */
    public void destroy()	/* override */
    {
	myConn.destroy();
    }   /* end destroy() method */

    /* ---------------------------------------------------------- */

    /**
     *	Ensure our 1.1 classes exist in the browser's Java runtime.
     *  Returns TRUE when life is good.  Otherwise FALSE.  <b>The
     *	application must not continue since required classes are
     *	missing!</b>
     *
     *	@return	<code>FALSE</code> if a Java 1.1 class is missing.
     */
    private boolean checkRuntime()
    {
	Class	gotit;

	try {
	    gotit = Class.forName( "java.awt.AWTEvent" );
	    gotit = Class.forName( "java.util.EventListener" );
	    gotit = Class.forName( "java.awt.event.AdjustmentListener" );
	}
	catch( ClassNotFoundException ex ) {
	    return false;
	}
	return true;
    }   /* end checkRuntime() */

    /* ---------------------------------------------------------- */

    /**
     *	Returns identification string for this application.
     *	Includes a psuedo-version number.  String viewable through browser's
     *	&quot;About..&quot; dialog, but this author can't seem to find the
     *	right dialog...
     *	@return	String with program name and version number.
     */
    public String getAppletInfo() /* override */
    {
	return "BL2000 DAC and A/D controller / Z-World / Vers 0.05" ;
    }

    /**
     *	Returns list of <tt>PARAM</tt> keys this applet understands.
     */
    public String[][] getParameterInfo()	/* override */
    {
	String[][]	result = new String[3 /* =count */ ][3];

	result[0][0] = "SESSION_ID";
	result[0][1] = "int";
	result[0][2] = "Session ID cookie to pass to remote target.";

	result[1][0] = "CMD_PORT";
	result[1][1] = "int";
	result[1][2] = "TCP port where command interpreter is listening on host.";

	result[2][0] = "MAX_VOLTAGE";
	result[2][1] = "float";
	result[2][2] = "Maximum voltage can set on DAC channel.";

	return( result );
    }

    /* ---------------------------------------------------------- */

    /**
     *	Display messages for human.  outMessage() display above sliders.
     *  Usually this is system status.
     */
    public void outMessage( String text )
    {
	messageLine.setText( text );
    }
    /**
     *	Display messages for human.  outStatus() display below sliders.
     *	Usually this is value updates and transient information.
     *	@see	java.applet.showStatus
     */
    public void outStatus( String text )
    {
	statusLine.setText( text );
    }

    /* ---------------------------------------------------------- */

    /**  Create a scrollbar with a label to the leftside.  Label is based
     *   on the DAC channel we're to control.  Uses proper constraints.
     *   The slider is an input component, sending commands to remote.
     */
    private void makeControl( int channel )
    {
        /*  Scrollbar as slider with label... */
	Scrollbar    slider = new Scrollbar( Scrollbar.HORIZONTAL );

        slider.setValues( 0, 0, MIN_DAC_STEP_VALUE, MAX_DAC_STEP_VALUE+1 );
        slider.setUnitIncrement((MAX_DAC_STEP_VALUE-MIN_DAC_STEP_VALUE+1) / 8);
	slider.setBlockIncrement( slider.getUnitIncrement()/2 );

	Label lab = new Label( "DAC "+channel, Label.LEFT );
	myGridBag.setConstraints( lab, _labelConstraint );
	lab.setAlignment( Label.CENTER );
	add( lab );

	myGridBag.setConstraints( slider, _valueBarConstraint );
        add( slider );

	myAnalogController.addInputComponent( slider, channel );

    }   /* end makeControl() */


    /**  Create an output-only "value" bar with a choice component with a Choice
     *   component to the left-hand side.  
     */
    private void makeValueBar(int which)
    {
	Choice	ch = new Choice();
	int 	j;

	ch.addItem( "none" );
	for( j=0 ; j < MAX_ADC_CHANNELS ; ++j ) {
	    ch.addItem( "ADC "+ j );
	}

	myGridBag.setConstraints( ch, _labelConstraint );
	add( ch );
	// XXX- ch.addItemListener( myConn );

	ValueDisplay	vb = new ValueDisplay(this);
	// XXX- myConn.setAnaOutDisplay( vb );
	myGridBag.setConstraints( vb, _valueBarConstraint );
	add( vb );


    }   /* end makeValueBar() */

    /* ---------------------------------------------------------- */

    /*  Build three containts: _fullWidth stretches a component to fill
     *	a full row.  _label wraps tightly around and lives in first column.
     *  _valueBar Lives in the second column, and stretches to fill space.
     */
    private void presetConstraints()
    {
	setLayout( myGridBag );

	_fullWidthConstraint = new GridBagConstraints();
	_fullWidthConstraint.gridx = 1;
	_fullWidthConstraint.gridy = GridBagConstraints.RELATIVE;
	_fullWidthConstraint.gridwidth = GridBagConstraints.REMAINDER;
	_fullWidthConstraint.gridheight = 1;
	_fullWidthConstraint.weightx = 10.0;
	_fullWidthConstraint.weighty = 1.0;
	_fullWidthConstraint.fill = GridBagConstraints.BOTH;
	_fullWidthConstraint.insets = new Insets( 4, 1, 4, 1 );

	_labelConstraint = new GridBagConstraints();
	_labelConstraint.gridx = 1;
	_labelConstraint.gridy = GridBagConstraints.RELATIVE;
	_labelConstraint.gridwidth = 1;
	_labelConstraint.gridheight = 1;
	_labelConstraint.weightx = 0.0;
	_labelConstraint.weighty = 1.0;
	_labelConstraint.fill = GridBagConstraints.BOTH;
	_labelConstraint.insets = new Insets( 2,2,2,2 );

	_valueBarConstraint = new GridBagConstraints();
	_valueBarConstraint.gridx = 2;
	_valueBarConstraint.gridy = GridBagConstraints.RELATIVE;
	_valueBarConstraint.gridwidth = GridBagConstraints.REMAINDER;
	_valueBarConstraint.gridheight = 1;
	_valueBarConstraint.weightx = 10.0;
	_valueBarConstraint.weighty = 1.0;
	_valueBarConstraint.fill = GridBagConstraints.BOTH;
	_valueBarConstraint.insets = new Insets( 4, 4, 4, 4 );

    }   /* end presetConstraints() */

    /* ---------------------------------------------------------- */

    /*  If A/D code working, change this to "true" */
    final static boolean	SW_A_TO_D_WORKING = false;

    final static int    MIN_DAC_STEP_VALUE = 0;
    final static int    MAX_DAC_STEP_VALUE = 4095;
    static float	myMaxVolts;

    final static int 	MAX_ADC_CHANNELS = 8;

    /**  _fullWidth stretches a component to fill a full row. */
    static GridBagConstraints	_fullWidthConstraint;

    /**  _label wraps tightly around and lives in first column.  */
    static GridBagConstraints	_labelConstraint;

    /**  _valueBar Lives in the second column, and stretches to fill space. */
    static GridBagConstraints	_valueBarConstraint;

    GridBagLayout		myGridBag;

    Label		valueText;		/* Current voltage */
    Label               messageLine;            /* Above slider */
    Label               statusLine;             /* Below slider */

    RabbitNetworkClient  myConn;
    ValueListenerBoardUpdater  myAnalogController;

    /**  Session key sent as PARAM value to this applet. */
    int                 sessID = 0;

}   /* end class AnalogApplet */


/* ============================================================= */
/* ============================================================= */

/**	Some other class listens. 
 *	Keep the <code>ValueDisplay->myValue</code> and
 *	<code>ValueDisplay->myMax</code> consistent.
 *	Call <code>ValueDisplay()->repaint()</code> when something changes.
 */
class ValueDisplay extends Label
{
    /**
     *	Build a displaying element with some default value.
     *  Since we extends Label, must give it default value.
     *	Label text alignment is <tt>LEFT</tt>.
     */
    public ValueDisplay(AnalogApplet owner)
    {
	super( "---", Label.LEFT );
	myOwner = owner;

	myValue = 2.0f;
	myMax = 4.0f;
    }   /* end constructor */

    /* ---------------------------------------------------------- */

    public void paint( Graphics g )	/* override */
    {
	final int 	INSET = 1;
	final int 	BORDER = 1;
	Dimension d = getSize();
	Color	bgclr = getBackground();
	Color	fgclr = getForeground();
	Color	temp;
	float	ratio = 0.0f;
	int 	xoff;

	try {
	    ratio = myValue / myMax;
	    if( ratio > 1.0f ) { ratio = 1.0f; }
	    xoff = (int) (ratio * (d.width - 2 * INSET));

	    /*  Hue:	cool=160(blue), hot=0(red)
	     *  Saturation:  cool=40, hot=255.
	     *  Value:	cool=64, hot=200
	     */
	    temp = new Color(
			Color.HSBtoRGB(
				       (1.0f - ratio) * 0.63f,
				       0.156f + (ratio * 0.84F),
				       0.25f + (ratio * 0.75f) ) );

	} catch ( RuntimeException e ) {
	    g.drawString( " oppss..("+ ratio +")",
			  (int) (d.width * 0.40), (int)(d.height * 0.75) );
	    return ;
	}
	/*  Always clear our background.  It would be better to
	 *  have an offscreen area and then just blit...
	 */
	g.setColor( bgclr );
	g.fillRect( 0, 0, d.width, d.height );

	/*  Form a triangle that has one edge along the left-side
	 *  edge (top to bottom).  The point of the triangle lies
	 *  along the bottom of the "value area".  The 90 deg corner
	 *  is in the lower-left corner.
	 */
	{
	    int[]    xp = new int[4];
	    int[]    yp = new int[4];

	    xp[0] = INSET;		yp[0] = INSET;
	    xp[1] = INSET;		yp[1] = d.height-INSET;
	    xp[2] = xoff + INSET;	yp[2] = d.height-INSET;
	    xp[3] = INSET;		yp[3] = INSET;

	    g.setColor( temp );
	    g.fillPolygon( xp, yp, 4 );
	}

	/*  Once the interior is done, draw a border.  Makes the value
	 *  bar look neater if container drawn later.
	 */
	g.setColor( Color.black );
	g.drawRect( INSET, INSET, d.width-2*INSET, d.height-2*INSET );

	if(true) {
	    g.drawString( " ("+ xoff +", "+ temp +") ",
		      (int) (d.width * 0.3), (int)(d.height * 0.75) );
	}

    }   /* end paint() */

    /* ---------------------------------------------------------- */    

    AnalogApplet		myOwner;

    /**	Current value in display.  Should be <tt>&lt;=</tt> <code>myMax</code>.
     */
    float			myValue;

    /**	Maximum value we can display. Helps code with scaling.
     *	Code will clip if <code>myValue</code> is too high.
     */
    float			myMax;

}   /* end class ValueDisplay */

/* ============================================================= */
/* ============================================================= */


/**
 *	Object to communicate with a remote Rabbit target.
 *	This implementation uses TCP, so after init'ing, the application
 *	calls #hookupTCP() to get us there.
 *	<br> If an error occurs, there is no &quot;status&quot; a client
 *	can query to... (sigh)
 */
class RabbitNetworkClient
{
    /** (empty) */
    public void init()
    {
    }

    /**
     *	Perform cleanup, including closing network connection.
     *	First closes the data reader/writer buffered streams, then
     *	closes the socket.
     */
    public void destroy()
    {
	try {
	    myFromServer.close();
	} catch( java.io.IOException e ) { }

	myToServer.close();

	try {
	    mySock.close();
	} catch( java.io.IOException e ) { }


    }   /* end destroy() */

    /**
     *	Connect to remote system using TCP port.  Catches most exceptions.
     *	@param	serverHost	Passed to "new Socket()" as host parameter.
     *	@param	port 		Passed to "new Socket()" as IP port number.
     *	@param	owner		Object with outMessage() and outStatus().
     *	@return	<code>TRUE</code> if TCP allows the connection, ie there was
     *			a listen waiting for us.
     *		<code>FALSE</code> if can't connect, ie host unknown, no
     *			server waiting for us, or can't write to socket.
     */
    public synchronized boolean
	hookupTCP( String serverHost, int port, AnalogApplet owner )
    {
	myOwner = owner;
	if( serverHost.equals("") ) {
	    serverHost = "localhost";
	}
	if( null == serverHost ) {
	    serverHost = "NULL";
	}
	myOwner.outMessage( " on " + serverHost + " @ " +
			    Integer.toString( port ) );

	/*  Open up a socket to BL2000 server: */
	try {
	    mySock = new Socket( serverHost, port );
	    myFromServer = new BufferedReader( new InputStreamReader(mySock.getInputStream() ));
	    myToServer = new PrintWriter( new OutputStreamWriter(mySock.getOutputStream() ));
	} catch( java.net.UnknownHostException e ) {
	    owner.showStatus( "ERROR: Can't resolve hostname" );
	    return false;
	} catch( java.io.IOException e ) {
	    owner.showStatus( "ERROR: can't read/write server." );
	    return false;
	}

	/*  Oddly, these two methods don't throw exceptions.. */
	myToServer.println( "PING" );
	myToServer.flush();

	/*  We're connected and ready. */
	return true;
    }   /* end hookupTCP() */

    /* ---------------------------------------------------------- */

    /**
     *	Worker method to send a &quot;DAC update command&quot; to remote target.
     *	Assumes both parameters have already been range checked.
     *	Method formats them and passes string to remote.
     *	Does <b>not</b> wait for response.
     *	@param	channel	DAC channel to affect.
     *	@param	value	Voltage to set on DAC channel.
     *	@see	ValueListenerBoardUpdater#updateBoard()
     */
    public synchronized void
	updateServer( int channel, float value )
    {
	myToServer.println( "SETDAC "+ channel +" "+ value );
	myToServer.flush();

	String   mesg = null;
	try {
	    if( myFromServer.ready() ) {
		mesg = myFromServer.readLine();
		if( mesg.startsWith( "ERR" ) ) {
		    myOwner.outStatus( mesg );
		} else {
		    myOwner.showStatus( "["+ mesg +"]" );
		}
	    }
	} catch( java.io.IOException e ) {
	    myOwner.showStatus( "update err: " + e );
	}

    }   /* end updateServer() */

    /* ---------------------------------------------------------- */

    AnalogApplet	myOwner;

    /**  Basic socket to remote target. */
    protected Socket		mySock;

    /**  Data from target arrives here. */
    protected BufferedReader	myFromServer;

    /**  Send commands to target here. */
    protected PrintWriter 	myToServer;

}   /* end class RabbitNetworkClient */


/* ============================================================= */
/* ============================================================= */

/**
 *	A floating-point version of Java 1.1 class AdjustmentEvent.
 *	That version holds just an integer.  We require floating-point
 *	to track voltage.
 *	@see	java.awt.event.AdjustmentEvent
 */
class  FloatingPointAdjustmentEvent extends java.awt.AWTEvent
{
    public FloatingPointAdjustmentEvent( Adjustable aSource, int aId,
					 float aValue )
    {
	super( aSource, aId );
	value = aValue;
	source = aSource;
    }

    /**
     *	Sets value in event.
     *	@param	aValue	Floating-point value.
     */
    public void setValue( float aValue )
    {
	value = aValue;
    }

    /**
     *	Get floating-point value in event.
     *	@return	float-point value stored in event.
     */
    public float getValue()
    {
	return( value );
    }

    /**
     *	Get adjusting source object.
     *	@return	Source object that created the event.
     */
    public Object getSource()
    {
	return( source );
    }

    Adjustable	source;
    float	value;

}   /* end class FloatingPointAdjustmentEvent */


abstract interface FloatingPointAdjustmentListener
    extends java.util.EventListener
{
    public abstract void valueUpdated( FloatingPointAdjustmentEvent e );
}   /* end class FloatingPointAdjustmentListener */


/**
 *	Listens to notifications from the target board and calls
 *	methods to update the GUI (which are mainly output-only 
 *	components).  Object is the only listener on the network
 *	connection.
 *	The dual of <code>ValueListenerBoardUpdater</code>.
 *	<br><b> THIS CLASS NOT FINISHED </b>
 */
class BoardListenerValueUpdater
		implements Runnable
{
    public void init()
    {
	ls = new FloatingPointAdjustmentListener[ AnalogApplet.MAX_ADC_CHANNELS ];
    }   /* end init() */

    synchronized public void addValueListener( int channel, FloatingPointAdjustmentListener l )
    {
	ls[ channel ] = l;
    }   /* end addValueListener() */

    /* ---------------------------------------------------------- */

    public void run()		/* Runnable override */
    {
    }	/* end run() */

    /* ---------------------------------------------------------- */

    FloatingPointAdjustmentListener	ls[];


}   /* end class BoardListenerValueUpdater */


/* ============================================================= */
/* ============================================================= */

/**
 *	Listens to GUI components.  When the user changes their value,
 *	sends a command via  <code>RabbitNetworkClient</code>
 *	object to have the target board change its outputs.
 *	The dual of BoardListenerValueUpdater.
 */
class ValueListenerBoardUpdater
		implements AdjustmentListener, ItemListener
{
    public void init()
    {
	out_sink = null;
	bars = new Component[10];
    }   /* end init() */

    /**
     *	Adds new input scrollbar component.  Enroll ourselves as listener
     *	when scrollbar gets frobbed by the user.
     *	Can have at most one scrollbar per channel.  Any previous
     *	scrollbar is disconnected.
     *	@param	guiScrollbar	Scrollbar element of DAC control.
     *	@param	index		Which channel scrollbar affects.
     */
    public void addInputComponent( Scrollbar guiScrollbar, int index )
    {
	if( index < bars.length ) {
	    /* XXX - if( bars[index] != NULL ) { bars[index].removeListener(this); } */
	    bars[ index ] = guiScrollbar;
	    guiScrollbar.addAdjustmentListener( this );
	} else {
	    System.out.println( "bars.add["+index+"] out of range." );
	}
    }

    /**
     *	Some client has already opened the network connection to the remote
     *	Rabbit target board.  Cache a copy.
     *	@param	netConn	Opened connection to remote target.
     */
    public void setConnection( RabbitNetworkClient netConn )
    {
	myConnection = netConn;
    }

    /**
     *	Some client informs us of place to output DEBUG commands to.
     *	@param	outs	Data output label (for debug mostly).
     */
    public void setOutput( Label outs )
    {
	out_sink = outs;
    }

    /**
     *	Some client informs us of label to output DAC voltage change.
     *	@label	volts	label to format new DAC voltage to.
     */
    public void setVoltageLabel( Label volts )
    {
	out_volts = volts;
    }

    /* ---------------------------------------------------------- */

    /**
     *	Set a listener for when a A/D channel changes value.
     *	@param var Output value displayer for remote A/D channel.
     */
    public void setAnalogOutListener( ValueDisplay bar )
    {
	out_analog = bar;
    }   /* end setAnalogOutListener() */

    /**
     *	user can choose which A/D channel to view.  When a selection occurs,
     *	Java GUI informs use with a notify.
     *	<BR><B>CODE NOT IMPLEMENTED</b>
     *	@param	e	Item even from Choice component.
     */
    public void itemStateChanged( ItemEvent e )
    {
	;
    }   /* end ItemListener::itemStateChanged() */

    /* ---------------------------------------------------------- */

    /**
     *	When a slider changes the Java GUI system notifies us.
     *	All sliders <b>must</b> be pre-registered.
     *  The scrollbar value is extract and <var>#updateBoard()</var> is called.
     *	@param e Adjustment event from a registered DAC scrollbar.
     */
    public void adjustmentValueChanged(AdjustmentEvent e)
    {
	Object   src = e.getSource();
	int     j;

	for( j=bars.length ; --j >= 0 ; ) {
	    if( bars[j] == src ) {
		break;
	    }
	}

	if( j >= 0 ) {
	    Scrollbar   s = (Scrollbar) bars[j];
	    int  where = s.getValue();
	    updateBoard( j, where );
	}
    }   /* end adjustmentValueChanged() */

    /* ---------------------------------------------------------- */

    /**
     *	When a DAC slider changes, send a command to the remote to change
     *  to a new value.
     *	@param channel Which DAC channel to affect.
     *	@param value Integer in range 0 to 4096. Here it is scaled to volts and sent.
     *	@see	AnalogApplet#myMaxVolts
     *	@see	RabbitNetworkClient#updateServer
     */
    public void updateBoard( int channel, int value )
    {
	if( VLBU_DEBUG ) {
	    /*  DEBUG - Send command to server. */
	    String cmd = " set DAC " + Integer.toString(channel) + " " +
                            Integer.toString(value);
	    out_sink.setText( cmd );
	}

	/*  Since there is no direct feedback from BL2000 when DAC
	 *  changes value.  Therefore, we just assume it will track...
	 */
	float	vf = (float)(value * AnalogApplet.myMaxVolts) / 4096.0f ;
	out_volts.setText( Float.toString( vf ) );

	myConnection.updateServer( channel, vf );

    }   /* end updateBoard() */

    /* ---------------------------------------------------------- */

    /**  Scrollbars that control DAC channels on remote. Indexed by DAC channel. */
    Component	 	bars[];

    Label		out_sink;
    Label		out_volts;
    ValueDisplay	out_analog;

    /**  Active network connection to remote target. */
    RabbitNetworkClient	myConnection;

    static final boolean VLBU_DEBUG = false;

}   /* end class ValueListenerBoardUpdater */

/* eof */
