v5.0 / 19 of 23 / 01 sep 99 / gvg
* Still more interesting VEE examples!
* One of our people in Singapore had a customer who wanted to display a
sequence of waveforms on a single display, adding them one at a time. I
wrote an example program for him that will display up to three waveforms,
but could easily be redesigned to handle more.
The problem is that an XY Trace object cannot be dynamically configured to
display different numbers of traces -- you set it up to display three traces,
it must be given three waveforms. The trick to make it display different
numbers of waveforms -- one, two, or three -- is to provide additional dummy
waveforms that simply show up as a straight line in order to ensure that the
total number of waveforms given to the XY Trace object is always three.
The unused traces show up as a colored line in the middle of the display, but
this is not much of a nuisance (it's not so different from what you would get
on a multi-channel oscilloscope display). The user can specify an offset
to allow the traces to be separated (or not) on the display.
The implementation is a little tricky. I wasn't able to think of a good way
to store the different waveforms in a single data structure, so each gets its
own global variable. Another tricky feature is that the routines don't know
how big the waveforms are going to be until they get the first one.
The program is based on three UserFunctions:
In practice, all the user has to do is specify the OffSet Value and initialize
the number of traces (Ntrace) to 0, and then call ShowTrace to display the
traces:
This example program is available as xmtrace.vee.
* A user wanted to know how he could build an alarm system in HP VEE that
would allow him to specify a time at which VEE would take a specific action.
The alarm might also occur only once; or every day; or only on the weekdays;
and so on.
So I wrote a small sample program to show the basic principles. Being an
example, the program avoids such issues as how the user sets the alarm in the
first place (which is another programming exercise in itself). It requires
setting the alarm time in military format. If the alarm time for the day has
already been passed, then the alarm is set for the following day.
The program is built around two Global variables:
The main part of the program appears as follows:
The If/Then/Else object in turn checks to see if the current time exceeds
the alarm time, and if so fires its output. This drives a second
If/Then/Else object that checks to see what sort of scheduling was specified
and then executes the proper code to do the job.
Note that the current alarm time is displayed whenever the On Cycle object
is fired. This is a useful debugging tool to ensure that the alarm is
properly updated.
Handling a single alarm is easy: the program displays a message box to
indicate an alarm (in a practical program this would be something a little
more useful, but this is an example) and then does a Break to get out of the
On Cycle loop.
Handling a weekday alarm is similar, but adds an If/Then/Else object to check
to see what day of the week it is, using the wday() function:
Other scheduling schemes could be added simply by increasing the number of
values in ASched, adding extra branches to the If/Then/Else object, and
adding the approriate logic to each branch.
* The main program is straightforward, but the TimeInit() function is a
little tricky. It has the layout:
The Formula in the middle adds the time returned by the Formula at top (which
gives the number of seconds into the day that the alarm will occur) and adds
the value of now() at the previous midnight to give an absolute time for
comparisons.
Finally, the Formula at the bottom checks to see if the alarm time has
already passed, and if so adds another day's time, on the assumption that if
somebody entered an alarm time of 10:00 when it was already 13:33, the user
probably meant for the alarm to occur the next day.
* The TimeAdjust() function is something of a kluge:
The Formula then computes the appropriate correction factor in seconds for
the computer's time zone. Note that a value of 0 is a special case and the
Formula checks for it to prevent it from adding a 24 hour offset on the
Greenwich meridian.
* The HP VEE games are full of interesting features. One of them is that in
some programs, like Solitaire, you can "drag and drop" panels displaying
playing card icons and they will "snap" into a grid position.
The lab engineer who designed the games, Doug K, was nice enough to come up
with an example program, xdrag.vee, that shows how this is done. This
particular example program only uses a single icon, but the principles apply
to implementations with more icons.
* The scheme is not entirely intuitive. It is based on three elements:
Using these three elements, the scheme can be implemented as follows:
The main program of this example has the layout:
The "panel()" UserFunction, by the way, just consists of the icon for a
playing card on a panel view, and will not be discussed further.
In detail, the "Initialize()" UserFunction looks like this:
The "ShowGrid()" UserFunction sets up display cosmetics. Although this is
important, it is a distraction from the discussion of how the program handles
the movement of the panel icon, and so we'll discuss the details of
"ShowGrid()" later.
The "CheckPanelPosition()" UserFunction looks like this:
The If/Then/Else box compares them to the coordinates stored in "panelX" and
"panelY". If either has changed, the If/Then/Else box fires a pair of Gate
objects to route the new values out of the output pins of the UserFunctions.
If the position hasn't changed, "CheckPanelPosition()" just checks again.
The new position is then stored in "panelX" and "panelY", and used by
"showPanel()" to put the icon panel in its snap grid position.
* We bypassed the "ShowGrid()" UserFunction earlier. It has the layout:
All these UserFunctions consist of is a black panel view. The "ShowGrid()"
UserFunction lays them down with the appropriate length and a width of 1
pixel in the proper locations. Since there can only be one visible panel
view per UserFunction, there has to be a separate UserFunction for each
line.
A bitmap panel could be used as well, but the advantage of this approach is
that the grid spacing can be modified programmatically. A bitmap would
require rebuilding the bitmap every time the grid spacing changes.
* To implement this scheme for multiple icons, you need to maintain
global variables that stores a list of X and Y positions, and also poll
through a list of panel positions to see if any have moved. Implementing
such a more complicated example is left as an exercise for the reader.
Consulting the "Solitaire" example program would be a good place to start.
[19.1] DISPLAYING SEQUENTIAL TRACES
[19.2] AN ALARM SYSTEM
[19.3] DRAGGING & DROPPING PANELS
[19.1] DISPLAYING SEQUENTIAL TRACES
+-----------------------------------------------+
| Main |
+-----------------------------------------------+
| +--------+ |
| | OffSet | |
| +--------+ |
| | 2 | [ New Trace ] |
| +--------+ |
| +-------------------------------------------+ |
| | XY Trace | |
| +-------------------------------------------+ |
| | | |
| | x | |
| | x x x x x x x x | |
| | x x x x x x x x x x | |
| | x x x x | |
| | | |
| | | |
| | + + + + + | |
| | + + + + + + + + + + + | |
| | + + + + + | |
| | | |
| | | |
| | * * * * * * | |
| | * * * * * * * * | |
| | * * * * * * | |
| | * | |
| +-------------------------------------------+ |
| [ Exit ] |
+-----------------------------------------------+
There are a few restrictions in this example, however. First, you can only
add waveforms; you cannot subtract them. Second, all the waveforms must have
the same number of points (though this is likely to be the case in practice
anyway).
+---------+ +---------------+
| OffSet | | Set Variable |
+---------+ +---------------+
| 0 +--->| OffSet |
+---------+ +-------+-------+
|
+---------+ +-------+-------+
| Integer | | Set Variable |
+---------+ +---------------+
| 0 +--->| Ntrace |
+---------+ +-------+-------+
|
+---+---+
| Until |
| Break +---+
+---+---+ |
| |
+---+---+ |
OK | Exit | |
+-------+ |
|
+-----+-----+
OK | New Trace +--+
+-----------+ |
|
+-----+-----+
| Noise |
| Generator +--+
+-----------+ |
|
+-----------------------------------------------+
|
| +-------------------------
| | XY Trace
| +--------+----------------
| | |
| +----------------+-------------->| Trace1 |
+-->| Call Showtrace +--------+ | |
+----------------+---+ | | |
| | | |
| +----->| Trace2 |
| | |
| | |
| | |
+---------->| Trace3 |
| |
| |
+--->| Auto |
| | Scale |
| | |
| +--------+--------------+--
| |
+----------------------------+
ShowTrace in turn will determine if no traces have been displayed -- and
call InitTrace -- or if there have been other traces displayed -- and call
AddTrace. It then returns the values of all three of the waveform global
variables:
+---------------------------------------------+
| Formula |
+---------------------------------------------+
A -->|( Ntrace == 0 ? InitTrace(A) : AddTrace(A) ) |
+---------------------+-----------------------+
|
+-------+-------+
| Get Variable | USERFUNCTION SHOWTRACE
+---------------+
| Trace1 +----------------------> Trace1
+-------+-------+
|
+-------+-------+
| Get Variable |
+---------------+
| Trace2 +----------------------> Trace2
+-------+-------+
|
+-------+-------+
| Get Variable |
+---------------+
| Trace3 +----------------------> Trace3
+---------------+
InitTrace loads the initial waveform into its global variable, increments
Ntrace, then walks through a count to initialize the other two global
variables to dummy waveforms:
+--------------+
USERFUNCTION INIT | Set Variable |
+--------------+
+------------------------------------>| Trace1 |
| +-------+------+
| |
| +-------+------+
| | Formula |
wave --+----------+ +--------------+
| | | Ntrace = 1 |
| +-------+-------+ +--------------+
| | For Count: 2 +---+
| +---------------+ | +----------------------+
| | | Formula |
| +---------+---------+ +----------------------+ build
| | | +-->| ramp(totsize(A),0,0) +--+ dummy
| | | | +----------------------+ | data
| | | | |
| | | | +--------------------------+
| | | | |
| | +-----+----+ array | | +----------+
+--|-->| UnBuild +-------+ +-->| Build +--+ dummy
| | Waveform +------------->| Waveform | | waveform
| +----------+ time +----------+ |
| |
| +---------------------------------------+
| |
| | +-----------------+ +--------------+
| | | Demultiplexer | | Set Variable |
| | +------+-+--------+ +--------------+
| +->| Data | | Addr 0 +----->| Trace2 |
+----->| Addr | | Addr 1 +--+ +--------------+
+------+-+--------+ |
| +--------------+
| | Set Variable |
| +--------------+
+-->| Trace3 |
+--------------+
AddTrace routes the waveform (with the OffSet added) to its appropriate
global variable and increments Ntrace:
+---------------------+ USERFUNCTION ADDTRACE
| Formula |
+---------------------+
+-->| A + OffSet * Ntrace +--+ input waveform plus offset
| +---------------------+ |
| |
| +--------+
| |
| | +---------------+ +--------------+
| +------------+ | | Demultiplexer | | Set Variable |
| | Formula | | +------+--------+ +--------------+
| +------------+ +->| Data | Addr 0 +----->| Trace2 |
wave ---+-->| Ntrace - 1 +---->| Addr | Addr 1 +--+ +--------------+
+------------+ +------+--------+ |
specify trace to set | | +--------------+
| | | Set Variable |
| | +--------------+
| +-->| Trace3 |
| +--------------+
+----------+----------+
| Formula |
+---------------------+
| Ntrace = Ntrace + 1 | increment trace count
+---------------------+
Note that this example doesn't have provisions for clearing the traces or
preventing the user from adding one trace too many. Such features would have
made the program harder to understand but can be easily added by the user.
[19.2] AN ALARM SYSTEM
+---------+
| Hours | hours in military time
+---------+
| 14 +--+ +------------+
+---------+ | | Set Global |
+-->+---------------+ +------------+
+---------+ | Call TimeInit +-->| ATime | set alarm time
| Minutes | +-->+-------+-------+ +------------+
+---------+ | |
| 11 +--+ +------+-----+
+---------+ | Set Global |
+------------+
+---------+ +-->| ASched | set scheduling
| Integer | | +------+-----+
+---------+ | |
| 0 +---+ |
+---------+ +-------+------+
| On Cycle: 60 +--+-------+
+--------------+ | |
| +----+----+ +----------------+
+----------------------+ | Formula | | AlphaNumeric |
| +---------+ +----------------+
+-----------+-----------+ | Atime +-->| |
| If/Then/Else | +---------+ +----------------+
+----------------+------+ display current alarm time
| now() >= ATime | Then +--+ fire
| | Else | | alarm
+----------------+------+ |
|
+--------------+
|
+-----------+-----------+
| If/Then/Else | handle scheduling
+-------------+---------+
| Asched==0 | Then +--------> handle single alarm
| Asched==1 | Else If +-----> handle daily alarm
| | Else +--> handle weekday alarm
+-------------+---------+
The program begins by setting the alarm time (using the TimeInit() function,
more on this later) and the type of alarm scheduling, then goes into a loop
run by an On Cycle object that fires every 60 seconds and drives an
If/Then/Else object.
handle single alarm --------+
|
+------+------+
| Message Box |
+------+------+
|
+---+---+
| Break |
+-------+
Handling a daily alarm is a little trickier. Before popping up the message
box, the program first adds a day's worth of seconds to the alarm time to set
the alarm for the next day:
handle daily alarm ------------+
|
+------------------+ +------+------+
| Formula | | Set Global |
+------------------+ +-------------+
| Atime + 24*60*60 +-->| ATime | update alarm 24 hours
+------------------+ +------+------+
|
+------+------+
| Message Box |
+------+------+
|
+---+---+
| Next |
+-------+
Any other time increment could be added as well. Note that a Next is used
to start the cycle all over again for the next day.
handle weekday alarm ----------+
|
+------------------+ +------+------+
| Formula | | Set Global |
+------------------+ +-------------+
| Atime + 24*60*60 +-->| ATime | update alarm 24 hours
+------------------+ +------+------+
|
+-----------+-----------+
| If/Then/Else | fire if not day 5 or 6
+----------------+------+
| wday(now())<5 | Then +---------+
| | Else | |
+-----------+----+------+ +------+------+
| | Message Box |
+---+---+ +-------------+
| Next |
+-------+
Note that the Next object is connected to the If/Then/Else to ensure the
cycle is repeated whether the Message Box is executed or not.
+-------------------------------------------+
| Formula |
+---+---------------------------------------+
h ----->| H | hmsToSec(H MOD 24, M, 0) + TimeAdjust +--+
+-->| M | | |
| +---+------------------+--------------------+ |
| | |
| +------------------+ |
| | +--------------------------------+
| | |
| | | +------------------------------+
m --+ | | | Formula |
| | +---+--------------------------+
+---+---+ +-->| T | (N-(N MOD (24*60*60) + T +--+
| now() +--+----->| N | | |
+-------+ | +---+--------------------------+ |
| |
| +-------------------------------------+
| |
| | +------------------------------+
| | | Formula |
| | +---+--------------------------+
| +-->| T | (T>N ? T : (T+24*60*60) +-------> X
+----->| N | |
+---+--------------------------+
This UserFunction accepts the hour and minute values specified and returns
the absolute machine time when the alarm will occur. The Formula at top
converts the hours and minutes specified by the user into a value in seconds;
there are two little tricks added:
+------------------------------+
| To String |
+------------------------------+
| WRITE TEXT 0 TIME:HM:H24 EOL +--+
| | |
+------------------------------+ |
|
+------------------------+
|
| +--------------------------------------------+
| | Formula |
| +--------------------------------------------+
+-->| (asInt32(A)==0 ? 0 : (24-asInt32(A))*3600) +--> X
+--------------------------------------------+
The To String object generates a string giving the time it thinks corresponds
to a value of 0. On the Greenwich meridian, this will be midnight, but it
will decrement by an hour for each timezone going west.
[19.3] DRAGGING & DROPPING PANELS
+-----------------+
| Call Initialize |
+--------+--------+
|
+--------+--------+
| Call ShowGrid |
+--------+--------+
|
+---+---+
| Until |
| Break +---+
+-------+ |
|
+------------+------------+-->+-------------------------+
| Call CheckPanelPosition | | Call RespondToPanelMove |
+-------------------------+-->+-------------------------+
This reflects the scheme outlined above:
+----------------------------------------------------------+
| Initialize |
+----------------------------------------------------------+
| |
| +------------------------+ +------------------------+ |
| | Declare PanelX | | Declare PanelY | |
| +------------------------+ +------------------------+ |
| | Name: [ panelX ] | | Name: [ panelY ] | |
| | Scope: [ Global ] | | Scope: [ Global ] | |
| | Type: [ Int32 ] | | Type: [ Int32 ] | |
| | Num Dims: [ 0 ] | | Num Dims: [ 0 ] | |
| +------------------------+ +------------------------+ |
| |
| +----------------------------------+ |
| | Formula | |
| +----------------------------------+ |
| | panelX = 150 | |
| | panelY = 150 | |
| | showPanel("Panel",panelX,panelY) | |
| +----------------------------------+ |
| |
+----------------------------------------------------------+
Not much to it, it just declares the global variables "panelX" and panelY",
initializes them, and then uses "ShowPanel()" to display the panel at the
initial position.
+-----------------------------------------------------------------------+
| CheckPanelPosition |
+------------------------------------------------------------------+----+
| | |
| +------+ | |
| +------------------------+ | | | |
| | Formula | +---------+ +--+---+ | | |
| +------------------------+ | Unbuild +--+----->| Gate +--|--->|newX|
| | panelPosition("panel") +-->| Coord +--|-+ +------+ | | |
| +------------------------+ +---------+ | | | | |
| | | +------+ | |
| | | | | | |
| +----------------------------------------+ | +--+---+ | | |
| | +----------------------------------------+--->| Gate +--|--->|newY|
| | | +------+ | | |
| | | | | |
| | | +-------------------------------------+ | | |
| | | | If/Then/Else | | | |
| | | +---+--------------------------+------+ | | |
| +-|-->| X |[ x!=panelX OR y!=panelY ]| then +-------------+ | |
| +-->| Y | | else | | |
| +---+--------------------------+------+ | |
| | |
+------------------------------------------------------------------+----+
Again, it's straightforward. It uses "panelPosition()" to poll "panel()" for
its position, then takes the coordinate positions returned and sends then
to an If/Then/Else box.
+---------------------------------------------------------------+
| RespondToPanelMove |
+----+----------------------------------------------------------+
| | |
| | +---------------------------+ |
| | | Formula | |
| | +---+-----------------------+ |
|newX+-->| X | floor((x+37.5)/75)*75 +--+ |
| | +---+-----------------------+ | |
| | | |
| | +----------------------------+ |
| | | |
| | | +---------------------------------------------+ |
| | | | Formula | |
| | | +----------+----------------------------------+ |
| | +-->| snappedy | panelX=snappedX; | |
| | | | panelY=snappedY; | |
| | +-->| snappedy | showPanel("panel",panelX,panelY) | |
| | | +----------+----------------------------------+ |
| | | |
| | +----------------------------+ |
| | | |
| | +---------------------------+ | |
| | | Formula | | |
| | +---+-----------------------+ | |
|newY+-->| Y | floor((x+37.5)/75)*75 +--+ |
| | +---+-----------------------+ |
| | |
+----+----------------------------------------------------------+
This UserFunction assumes that the snap grid spacing is 75 pixels. It
determines which cell in the snap grid the center of the icon panel is
located, using "floor()" function to round down, and multiplies the cell
position by 75 to get the position to snap the icon panel to.
+----------------------------------------------------------------------+
| ShowGrid |
+----------------------------------------------------------------------+
| |
| +-----------------------------------------+ |
| | Formula | |
| +--------------+ +-+---------------------------------------+ |
| | For Count: 7 +-->|i| showPanel("line0"+i, 0, i*75, 800, 1) | |
| +--------------+ +-+---------------------------------------+ |
| |
| +---------------------------------------------+ |
| | Formula | |
| +--------------+ +-+-------------------------------------------+ |
| | For Count: 9 +-->|i| showPanel("line1"+i, (i+1)*75, 0, 1, 500) | |
| +--------------+ +-+-------------------------------------------+ |
| |
+----------------------------------------------------------------------+
The grid in the program's panel view consists of 9 lines across and 7 lines
from top to bottom. Since you can't draw lines on the panel view as such,
the program uses a set of UserFunctions named "line00()" through "line06()"
and "line10()" though "line18" to create the lines.