v5.0 / 16 of 23 / 01 sep 99 / gvg
* More fun little gimmicks.
* For some odd reason (that nobody seems to recollect) you can use
transaction objects in VEE to convert a number (say, 233) to an octal string
("351") or hex string ("0E9"), but you can't convert one to binary string
format ("011101001" and so on).
So I devised a function of my own, called "BinFmt", to do this (it is part of
the function library xgenfunc.vee). It has the overall layout:
In more detail:
Since "A" is the octal string and "B" is the For Count counter value, the
"StrFromLen" function extracts each digit from the octal string in turn;
this digit is then used as an index to the array "C", which represents the
array of binary-triplet strings.
If this makes you dizzy, all it does is take each digit out of the octal
string sequentially and map it to the appropriate binary equivalent for
that digit.
This works efficiently, but some caution must be taken in handling its
output. VEE tends to convert back and forth between strings and numbers in
an invisible fashion, and your string "1011100" will most likely evaluate to
the decimal value 1,011,100. Be careful.
* Similarly, one user wanted to print numbers with leading zeroes in VEE. I
decided to implement an example program that allowed the user to output a
number with a specified fractional-part field width, and a specified
integral-part field width using zero-padding. For example, if the user
wanted a fractional-part field width of 3 and an integral-part field-width of
6, and printed:
The "Formula" box is the tricky part; it has three inputs:
The formula it contains is as follows:
This works cleanly. This particular formula does not blow up if the
integral part is longer than the field width. I originally tried to do the
fractional formatting in the "Formula" box, but this lead to some very
peculiar fractional roundoff problems. I decided to use the right tool for
the job and let the "To String" object do that for me.
Note that some care must be taken with the output, however. VEE handles data
types interchangeably and some operations could convert this string back to a
value, meaning the leading zeroes would be lost.
* In Europe, it is common to format numeric data in style that is reversed
from that used in the US, with a comma between the integer and fractional
part. Unfortunately there's no way to set this as a default in VEE, so I
devised a UserFunction to do the job, with the syntax:
The actual UserFunction (to be found in the xgenfunc.vee library) simply
consists of a single To String object:
The second transaction is based on a CASE statement because it has to ensure
that the proper number of digits of precision are printed in any case. This
might seem easy to do, just using a component of the third line above:
First, if the number of digits of precision is specified as "0", this still
gives a trailing zero. So you have to test for that and just return a
null string if that is the case:
* While VEE has the capability to display engineering format numbers in
Alphanumeric objects and the like, transaction objects cannot provide output
in an engineering format. The only way around this is to create a
UserFunction that provides such a format.
Such a UserFunction can be created using two objects:
It is useful to specify this designation as a parameter to the function so
that it can be returned as part of the result string -- otherwise, the result
would simply be intepreted as a floating-point number, and VEE would reformat
it as it pleased. You can specify a null string ("") as the parameter if you
don't like this feature (or make it a "_" or some other harmless character if
you don't want to mess with reformatting hassles).
The To String object in the UserFunction does most of the formatting. It
uses the CASE transaction to select the proper metric suffix for the output.
However, the CASE transaction requires a nice 0-1-2-3- kind of input to act
as an index; the Formula box creates this index.
The mapping can be given by the following table:
This done, the core calculation takes the base-ten log of the absolute value
of the input (taking the absolute value ensures that it doesn't try to take a
log of a negative number), which gives the decimal exponent needed in the
range of -18 to 14. A bias value of 18 is then added to shift the values to
a range of 0 to 32, and is then integer-divided by 3 to give the index from 0
to 10, as required.
Now this index value is fed to the To String object, along with the other
parameters passed to the UserFunction:
The second transaction simply provides the proper suffix as designated by the
index given by the Formula box using the CASE transaction. Again, the EOL is
suppressed.
The third transaction tacks the "Meas" value on the end, and completes the
string output by issuing an EOL.
* You could then incorporate the UserFunction in a Formula box as follows:
* An HP SE needed to get a Julian-date function for VEE -- that is, a
function that returned the current day of the year, with the value from 1 to
365. I figured that would be easy to implement. In itself it was, but
figuring out some of the calendar issues was tricky.
The biggest problem was calculating leap years. Every year that is evenly
divisible by 4 is a leap year, with the exception of turn-of-the-century
years like 1700, 1800, or 1900, but with the exception to the exceptions that
any turn-of-the-century year that is divisible by 400 (such as the upcoming
year 2000) is a leap year.
Well, anyway, the function turns out to be dead simple (see the
xgenfunc.vee library for the source):
This UserFunction accepts the current value of "now()" and returns an integer
date:
* VEE transaction I/O has greater capabilities than most users give it credit
for. For example, A user had a VEE application where he wanted to be able to
select the following strings under program control:
Note that the enum values are displayed as the label but the To String
object (through the magic of VEE data transformations it sees them as
index numbers).
Note that if you wanted to directly send this string to an instrument you
could do exactly the same thing in a Direct I/O object. If you wanted to do
it to a file you could do it in a To File object. All transaction-I/O
objects support roughly the same transactions (within the limits of the
medium they are performing the transactions through).
* VEE's flexible data-handling tools can greatly assist instrument-control
tasks. For a useful example, a new user was trying to use VEE to talk with a
Tektronix instrument and having a lot of trouble. I looked over his task and
was able to sort it out quickly. It made an excellent example of what VEE
does well.
The Tek instrument was of the sort that apparently thought it was talking to
a printer or a terminal. It returned output of the form:
I put this data in a String constant in order to dummy up the user
application, then pulled the data out using the program (see xextract.vee
for the source):
* A user had a serial instrument, and like many such devices, it had a
bizarre programming model. The instrument returned 57-character strings of
ASCII text that represented binary status bytes.
Now this sounds OK, but the details were strange:
The user wanted to obtain a particular field out of the data and convert
the bytes in it to a numeric value.
The details of the instrument made life unusually difficult. I gave it some
thought and came up with some conclusions:
Since I didn't have the instrument, I decided to use From File to simulate
the user's system. I wrote the following file (using a sample provided by
the user) to provide data strings:
I came up with the following program to read the file:
* The program operates in a loop, with an Until Break object driving a From
File object. The From File object has the transaction:
If an EOF occurs, the Break object is activated and the program exits the
loop. If a byte is retrieved, it is sent to the If/Then/Else object, where
it is compared to the value 10 (the ASCII value for a line-feed):
The data frame is fetched by another From File object with the transaction:
The Formula box takes the needed ASCII characters and concatenates them in
the proper order:
The Formula box fires a Message Box that simply asks if you want to continue
or not. If you click on Cancel, a Break object is executed to bail out of
the loop, if you click on OK, a Next object kicks off another loop to start
looking for line-feeds again. Note that these objects simply simulate any
actions you might care to perform on the string.
When the Until Break bails out of the loop, it fires another Message Box to
announce the program is over. This simply simulates whatever actions you
might care to take in the program after obtaining the frames.
Note that since Windows buffers serial data, you should have no worries about
losing frames as long as you check for data every now and then.
* I have to conclude, however, that this particular device confirms my
impression that many of the outfits building serial instruments have no
concept of how to actually build a programmable instrument. Certainly much
more effective means of transferring a status string are available -- for
example, they could have used a 488.2-definite-length block to read the data
as binary:
The other alternative would have been to have simply sent the entire thing as
a set of decimal numbers, coded in ASCII and separated by commas (so-called
"comma-separated variable" format):
[16.1] CONVERTING A NUMBER TO A BINARY STRING
[16.2] PRINTING NUMBERS WITH LEADING ZEROES
[16.3] EURO NUMBER FORMATS
[16.4] DISPLAYING ENGINEERING FORMAT NUMBERS
[16.5] A JULIAN-DATE FUNCTION
[16.6] ADVANCED TRANSACTION I/O
[16.7] I/O DATA FORMATTING EXAMPLE
[16.8] MORE I/O DATA FORMATTING
[16.1] CONVERTING A NUMBER TO A BINARY STRING
+-----------------------------------------------------------------------+
| BinFmt |
+---+---------------------------------------------------------------+---+
| | +------------------------------+ | |
| | | To String | | |
| | +---+----------------------+---+ +--------+ | |
| | +-->| A | WRITE TEXT a OCT EOL | R +--+-->| strLen +--+ | |
| | | +---+----------------------+---+ | +--------+ | | |
| | | | | | |
| | | +---------------------------------|---------------+ | |
| | | | | | |
| | | | +--------------+ | |
| | | | | | |
| | | | +-----------+ +-->+---------+ +-------------+ | |
| A +---+ +-->| For Count +----->| Formula +-->| Accumulator +--->| R |
| | +-----------+ +-->+---------+ +-------------+ | |
| | | | |
| | +---------------+ | | |
| | | Text | | | |
| | +---------------+ | | |
| | | 0000: 000 | | | |
| | | 0000: 001 | | | |
| | | 0000: 010 | | | |
| | | 0000: 011 +---+ | |
| | | 0000: 100 | | |
| | | 0000: 101 | | |
| | | 0000: 110 | | |
| | | 0000: 111 | | |
| | +---------------+ | |
+---+---------------------------------------------------------------+---+
The Formula box is central to this function:
+-------------------------------------+
| Formula |
+---+------------------------+--------+
| A | | |
| B | C[StrFromLen(A, B, 1)] | Result |
| C | | |
+---+------------------------+--------+
The basic philosophy here is that the input value is converted to an octal
string, and then each digit ("0" through "7") is converted to its binary
string equivalent ("000" through "111"). These triplets are then
concatenated together and shot out the output pin.
[16.2] PRINTING NUMBERS WITH LEADING ZEROES
51.4
-- he'd get:
000051.400
If the number is negative, the "-" is regarded as part of the field width,
meaning that:
-51.4
-- is printed as:
-00051.400
The following UserFunction (it's in the xgenfunc.vee file) did the job:
+-------------------------------------------------------------------+
| UserFunction: ZeroFmt |
+------+--------------------------------------------------------+---+
| | | |
| Nval +--+ | |
| | | | |
| | | +------------------------------------------+ | |
| | | | To String | | |
| | | +---+----------------------------------+---+ | |
| | +-->| A | WRITE TEXT abs(A) REAL FIX:B EOL | r +--+ | |
| Fdig +--|-->| B | | | | | |
| | | +---+----------------------------------+---+ | | |
| | | | | |
| | | +-------------------------------+ | |
| | | | | |
| | +-----------------|-->+---------+ | |
| Idig +--------------------|-->| Formula +-------------------->| R |
| | +-->+---------+ | |
| | | |
+------+--------------------------------------------------------+---+
All the "To String" box does is format the number to the specified number of
fractional digits (it specifies a fixed output format with the number of
fractional digits given by the "B" input pin. Note that the absolute value
is taken of the input value since an output minus sign would screw things
up). This formatted output string is then passed to the "Formula" box, which
prefixes the string with the proper number of leading zeroes (and restores
the minus sign if need be).
(A<0 ? "-" : "") + strFromLen((B*"0"), 0, (B - strLen(intPart(A)))) + C
The following concepts are required to explain how it works:
[16.3] EURO NUMBER FORMATS
ef(<nval>,<ndigits>)
-- where "nval" is a numeric value to be formatted and "ndigits" is 0 or more
digits of precision to be displayed (the number is right-padded with zeroes
if necessary).
+------------------------------------------------------+
| UserFunction: ef |
+---------+-----------------------------------+--------+
| | | |
| | +------------------------+ | |
| nval +--+ | To String | | |
| | | +---+--------------------+ | |
| | +-->| A | +-->| result |
| ndigits +----->| B | | | |
| | +---+--------------------+ | |
| | | |
+---------+-----------------------------------+--------+
The two transactions in the To String are a little devious, though:
WRITE TEXT intpart(A), "," INT
WRITE CASE sgn(abs(fracpart(A))) OF
B*"0",
(B==0 ? "" : intpart(fracpart(A)*10^B)) EOL
The first transaction:
WRITE TEXT intpart(A), "," INT
-- just writes the integer part of the number (without an EOL). It's the
second transaction (shown above as three lines for clarity, though it's
actually just one) that's tricky.
intpart(fracpart(A)*10^B))
This takes the fractional part of the number, shifts it the required number
of spaces by multiplying it by ten to the power of the required number of
digits of precision, and then trims off the fractional part of that.
However, this breaks down in two ways.
(B==0 ? "" : intpart(fracpart(A)*10^B))
Second, if there is no fractional part to the numeric value, this will
always return a null string no matter what the number of digits of precision
is, and we want to pad with zeroes. So we have to implement this as a CASE
statement to perform two different actions, depending on whether the result
is zero or not. This is done with:
WRITE CASE sgn(abs(fracpart(A))) OF
This takes the absolute value of the fractional part of the input number and
then runs it through the signum function, which returns "0" for a zero value
and "1" for a positive value. (It will return "-1" for a negative number,
but we eliminated negative values by using the "abs()" function.) If the
result is "0", the first case is evaluated:
B*"0"
This multiplies the string "0" times the number of digits of precision to
create a string of zeros as long as the number of digits of precision (this
is a string multiplication, not a numeric multiplication). If the result
is 1, the fractional part is given by:
(B==0 ? "" : intpart(fracpart(A)*10^B))
This not only provides a useful function, but also insights in how to use the
CASE transaction in conjuction with math functions to perform complicated
control operations. However, as you can see, this leads to extremely tricky
code and some documentation is essential if even you want to remember later
how the thing works.
[16.4] DISPLAYING ENGINEERING FORMAT NUMBERS
+-------------------------------------------------------+
| EngFmt |
+------+--------------------------------------------+---+
| | | |
| NVal +---+-----------------+ | |
| | | | +----------+ | |
| | | +---------+ +----->| | | |
| | +-->| Formula +--------->| To +--->| R |
| | +---------+ +----->| String | | |
| | | +-->| | | |
| Ndig +---------------------+ | +----------+ | |
| | | | |
| Meas +------------------------+ | |
| | | |
+------+--------------------------------------------+---+
This UserFunction is passed the numeric value to be formatted ("Nval"), the
number of digits after the decimal point to be displayed ("Ndig"), and the
designation for the quantity being specified ("Meas"), such as "F" for
"farads", "Hz" for hertz, and so on.
__________________________________________________________
magnitude metric_prefix suffix_character index_value
__________________________________________________________
e12 tera "T" 10
e9 giga "G" 9
e6 mega "M" 8
e3 kilo "k" 7
e0 - "" 6
e-3 milli "m" 5
e-6 micro "u" 4
e-9 nano "n" 3
e-12 pico "p" 2
e-15 femto "f" 1
e-18 atto "a" 0
__________________________________________________________
The formula box creates this mapping with a formula as follows (displayed on
multiple lines for clarity, though in practice it's all on the same line in
the formula box):
( A==0 ? 6 :
( A>=1e15 ? 10 :
( A<1e-18 ? 0 :
intpart((log10(abs(A))+18)/3)
)
)
)
The three conditional operators test for the zero value and values beyond the
range of normal metrix prefixes, and assign the appropriate indexes for the
suffix character. You have to nail the zero value in this way, because the
core calculation takes a log value, and there are no logs for values of zero
or less.
+-------------------------------------------------------------------------+
| To String |
+------+--------------------------------------------------------------+---+
| Nval | WRITE TEXT Nval*10^(18-3*Idx) REAL FIX:Ndig | |
| Idx | WRITE CASE Idx OF "a","f","p","n","u","m","","k","M","G","T" | R |
| Ndig | WRITE TEXT Meas EOL | |
| Meas | | |
+------+--------------------------------------------------------------+---+
The first transaction scales the numeric value so that the proper suffix can
be added and still keep the output value in the proper range. It
reverse-engineers the index provided by the Formula box to create a
multiplier to shift the input value by the required scale. It also outputs
the value with the requested number of digits after the decimal point. Note
that the EOL is suppressed.
+--------------------------------------------+
| Formula |
+---+----------------------------------------+
| A | "The frequency is " + EngFmt(A,0,"Hz") +-->
+---+----------------------------------------+
-- or, more relevant to our original purpose, in a To String box:
+---------------------------------------------------------+
| To String |
+-------+-------------------------------------------------+
| Ntest | WRITE TEXT "Test number: ",Ntest |
| Nval | WRITE TEXT " / Frequency: ",EngFmt(Nval,0,"Hz") |
| C0 | WRITE TEXT " / Channels: ",C0,";",C1 EOL |
| C1 | |
+-------+-------------------------------------------------+
The source for this UserFunction is available in the User Library
xgenfunc.vee.
[16.5] A JULIAN-DATE FUNCTION
+------------------------------------------------------+
| Formula |
+---+--------------------------------------------------+
| A | 1 + IntPart(( A - dmytodate(1,1,year(A))/86400)) |
+---+--------------------------------------------------+
This subtracts the time value (in seconds) at the beginning of the first day
of the year from the current time value, then divides that by the number of
seconds in the day (60 * 60 * 24 = 86,400), takes the integer part of the
result, and adds 1 to get the day of the year.
Julian(now())
This is simple. Learning about calendars was a little sticky, but did you
know that there are actually proposals to make the year 13 months of 28 days
each (with various adjustments thrown in once or twice a year)? Interesting
idea, but I won't hold my breath waiting to see if it will happen.
[16.6] ADVANCED TRANSACTION I/O
PAT PR,
PAT PR,PG,
PAT PR,PB,
PAT PR,PG,PB,
I suggested that TO STRING might be able to do this effectively, and so I
ended up writing the following example to show how (see xstrslc.vee for
the source):
+-----------+ +-----------------------------------------------------+
| Enum | | To String |
+-----------+ +---+---------------------------------------------+---+
| <*> Val1 | | | WRITE TEXT "PAT PR," | |
| < > Val2 | | | WRITE CASE A OF "","PG,","PB,","PG,PB," EOL | |
| < > Val3 +--+->| A | | R +--+
| < > Val4 | | | | | | |
+-----------+ | | | | | |
| +---+---------------------------------------------+---+ |
| |
| +-------------------------------+
| |
| +-------------+ | +-------------+
+--->| Val1 | +-->| PAT PR, |
+-------------+ +-------------+
The Enum object is used to drive a WRITE CASE statement in the To String
object. The two AlphaNumeric objects at the bottom are just handy window
dressing and not an essential part of the program. Some important comments:
[16.7] I/O DATA FORMATTING EXAMPLE
#1 Measurement Results
12:00:00
Chrom/Lum Gain Delay
Composite
Field = 2 Line = 14
Average 32
_____________________________________________________________________
Chroma Gain 104.8 %
Chroma Delay -7.3 n sec
_____________________________________________________________________
What the user wanted to get was the two data values at the bottom.
+-----------------+
| String Constant +--+
+-----------------+ |
|
+---------------------+
|
| +----------------------+ +-----------------------------+
| | Formula | | From String |
| +---+---------+--------+ +------+------------------+---+ +-------+
+-->| A |[ A[7] ]| Result +-->| AStr | READ TEXT X REAL | X +-->| 104.8 |
| +---+---------+--------+ +------+------------------+---+ +-------+
|
| +----------------------+ +-----------------------------+
| | Formula | | From String |
| +---+---------+--------+ +------+------------------+---+ +-------+
+-->| A |[ A[8] ]| Result +-->| AStr | READ TEXT X REAL | X +-->| -7.3 |
+---+---------+--------+ +------+------------------+---+ +-------+
The program simply read the data into an array (note that blank lines are
skipped normally in file or direct-I/O), extracted the proper lines from the
array, then grabbed the real number out of each (the From String objects
simply discarded anything that didn't look like a number). In practice the
user could use Direct I/O to replace From File and not notice the
difference.
[16.8] MORE I/O DATA FORMATTING
420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
420e081042664b00e33a912024c6910d245ad086e32a50e724a2edb
^
Note that this was a 5-line file, the first line was blank. This ensured
that a CR-LF preceded each string (since a CR-LF is the default line
terminator on a PC). The specific byte I was after is marked with the "^" in
this illustration.
+-------+ loop ...
| Until +---+
| Break | |
+---+---+ | get
| | byte byte == 10? yes
| +---+---+ +--------------+----->---+
| | From +---->| If/Then/Else | |
| | File +--+ +--------------+--+ |
| +-------+ | | |
| | | |
| +---+---+ +---+---+ |
| | Break | | Next | |
| +-------+ +-------+ |
| bail out no, get |
| if EOF next byte |
| |
| +------------<---------------------+
| |
| | get swap convert +--------------+
| | string characters from hex | AlphaNumeric |
| +---+---+ +---------+ +-------------+ +--------------+
| | From +---->| Formula +-->| From String +-->| 66 |
| | File +--+ +----+----+ +-------------+ +--------------+
| +-------+ | |
| | |
| +---+---+ |
| | Break | |
| +-------+ |
| bail out |
| if EOF | the message box here announces that you have
| | obtained a frame and asks if you want to get
| | another one
| +----+----+
| | Message +-------------+
| | Box +--+ |
| +---------+ | |
| | |
| +---+---+ +---+---+
+----+----+ no, bail out | Break | | Next | yes, loop again
| Message | +-------+ +-------+
| Box |
+---------+ display "Done!"
The details are as follows:
READ BINARY X BYTE
-- and also has an EOF pin set up as an output to detect end-of-file
condition. In practice, the user would use a Direct I/O object and have
a timeout pin brought out, but the program works very much the same in either
case.
A == 10
If there is no match, the "Else" pin of the If/Then/Else object fires a Next
object to put the program through the next iteration of the loop and fetch a
new byte. If there is a match, the "Then" pin is fired to get the rest of
the data frame.
READ TEXT X STR MAXFW:1 ARRAY 55
This reads all 55 characters in the data frame individually into elements of
a string array where each element is a 1-character string. It makes it
easier to manipulate the characters this way. As with the first From File
object, the EOF pin has been set up and drives a Break object to exit the
loop if an EOF occurs.
A[33] + A[32]
The two-character hex string is then shoved through a From String object
containing the transaction:
READ TEXT X HEX
-- which converts it to the proper numeric value.
#255<byte1><byte2> ... <byte55>
In this scheme the block starts with a "#", followed by a "2" to specify that
two digits follow -- "55" -- that specify the number of bytes to be sent.
34,25,88,128, ... ,99<CR-LF>
This is the simplest means of reading array data and can be implemented in
almost any appropriate I/O control language.