Copyright © 1999-2007 by Semaphore Corporation. All rights reserved.
This document is intended for programmers wishing to access a FASTforward "black box" containing the Postal Service™ change-of-address database. The information herein, or derived versions, may not be distributed, sold, or incorporated into a product or service for sale, without prior permission from Semaphore Corporation.
1. First, setup the hardware. You need a 32-bit Windows machine with a 68-pin external SCSI connector, such as the connector found on an Adaptec AHA-2940 Ultra Wide SCSI board. If you're adding a SCSI interface to your computer for the first time, start up your computer after installing the SCSI board but without attaching the black box or any other SCSI devices, to make sure Windows properly recognizes your new SCSI board and adds it to the list of devices in your System control panel without any complaints or problems.
2. Once you have a computer ready with a properly installed SCSI interface, and with both the black box and your computer turned off, connect the black box to your computer using the SCSI cable that came with the black box. Turn on the black box and wait for it to finish booting.
3. Turn on your computer. Your SCSI board will detect and display the black box SCSI ID during startup. Windows will detect the black box and announce it has found new hardware and that it's an unknown device. Windows will display dialogs for selecting a device driver. Don't give Windows the option to automatically search for the best driver. Instead, choose the option to display a list of devices so you can select the driver. In the device list dialog, choose "other devices". In the dialog showing the list of models for other devices, choose "unsupported device". Because of the multi-unit nature of the SCSI interface, Windows will force you to choose "other devices/unsupported device" eight times for eight logical devices. When you're done and Windows has finished booting, the device list in your System control panel will show eight duplicate entries for the FASTforward black box in the "other devices" category.
4. To use the software described below, your machine must have the latest version of ASPI (Advanced SCSI Programming Interface) software installed, which simplifies the interface to SCSI devices. Download the ASPICHK.EXE utility from Adaptec, which will report which version of ASPI is on your system and whether it needs to be upgraded. If you install a new version of ASPI, reboot your computer.
5. Now you're ready to write a program to send an old address to the black box and get back a new forwarding address. First you have to initialize the ASPI interface:
(NOTE: The following code is written in Delphi 2.0, but can easily be translated to any convenient programming environment. Most ASPI code samples and documentation use the C language. Also, production code should use the symbolic constants defined in the ASPI interfaces and featured in our combined code sample, instead of the numeric values used below. You should read and understand the Postal Service FASTforward Interface Developers Guide for Mailing List Correction and read and understand Adaptec's ASPI for Win32 Technical Reference before trying to understand the rest of this document.)
if hibyte(loword(GetASPI32SupportInfo)) <> $01 then showmessage('Can''t initialize ASPI!');GetASPI32SupportInfo is an external function defined in our combined code sample. Once the ASPI interface has been initialized by GetASPI32SupportInfo, you can begin exchanging transactions with the black box. The very first transaction is always the exchange of an "initial" access record, which can be accomplished with the following procedure:
procedure ffinitial; begin accessrecord.id := 'P'; accessrecord.atype := 'I'; accessrecord.password := 'INITIAL.28.CHARACTER.PASSWRD'; // as supplied by the Postal Service accessrecord.mlc := 'C'; executeandack('P', @accessrecord, sizeof(accessrecord)); if ackrecord.error = '000' then savereturnedpassword(ackrecord.password); end;The savereturnedpassword procedure is any convenient routine that lets you save the password that was returned to you in the ackrecord, per the security requirements of your FASTforward license. The global accessrecord and ackrecord are standard FASTforward data structures defined in our combined code sample. The executeandack procedure sends a command to the black box and waits for an acknowledgement:
procedure executeandack(codechar: char; bufptr: pointer; bufsize: word); begin executecommand(codechar, bufptr, bufsize); ackrecord.error := 'xxx'; // to make sure we don't see junk executecommand('K', @ackrecord, sizeof(ackrecord)); // retrieve ack if ackrecord.error <> '000' then showmessage('ACK error ' + ackrecord.error + ' for command ' + codechar); end;The executecommand procedure waits for the black box to be ready, then sends the black box a command and confirms success:
procedure executecommand(codechar: char; bufptr: pointer; bufsize: word); begin waitunitready; executescsicommand(codechar, bufptr, bufsize); if srb.srb_status <> $01 then showmessage('Command ' + codechar + ' error = ' + inttohex(srb.srb_status, 2)); end;The waitunitready procedure waits up to five seconds for the black box to respond to a "test unit ready" command:
procedure waitunitready; var start: longint; begin start := gettickcount; repeat executescsicommand('U', nil, 0); // test unit ready if srb.srb_status = $01 then exit; // unit ready until (gettickcount - start) > 5000; // allow 5 seconds showmessage('waitunitready timed out'); end;The executescsicommand procedure uses the ASPI interface to send various commands to the black box. In this program, each command is identified by a unique code character parameter, such as 'U' for "test unit ready" or 'W' for "inquiry":
procedure executescsicommand(codechar: char; bufptr: pointer; bufsize: word); const ffdrivenum = 6; // known black box drive number cmdlen = 6; // all supported commands use 6 bytes begin // execute a scsi send, receive, testunitready, or inquiry command fillchar(srb, sizeof(srb), char(0)); // sets all defaults and our zero adaptor number too srb.srb_cmd := $02; if (codechar = 'O') or (codechar = 'K') then // read black box output record or ack begin srb.cdbbyte := $08; srb.srb_flags := $08; end else if codechar = 'U' then // test unit ready srb.cdbbyte := $00 else if codechar = 'W' then // inquiry begin srb.cdbbyte := $12; srb.srb_flags := $08; end else // assume an output command begin srb.cdbbyte := $0A; srb.srb_flags := $10; end; srb.cdbbyte := hibyte(bufsize); srb.cdbbyte := lobyte(bufsize); srb.srb_flags := srb.srb_flags or $40; // event notification srb.srb_target := ffdrivenum; srb.srb_buflen := bufsize; srb.srb_bufpointer := bufptr; srb.srb_senselen := 14; // sense length srb.srb_cdblen := cmdlen; srb.srb_postproc := pointer(completionevent); resetevent(completionevent); sendaspi32command(@srb); if srb.srb_status = $00 then waitforsingleobject(completionevent, infinite); // pending end;The executescsicommand procedure passes a standard ASPI srb record to the ASPI interface by calling sendaspi32command, which is an external function defined in our combined code sample. The srb record, described in ASPI documentation, is a global variable also defined in our combined code sample. The executescsicommand procedure waits for sendaspi32command to signal completion via event notification, which uses an event handle called completionevent, which should be created once at the start of program execution before the ASPI interface is used:
completionevent := createevent(nil, true, false, nil); // destroyed by Windows when we quit if completionevent = 0 then showmessage('Event creation failed!');Once access to the black box has been established with the ffinitial procedure described above, the black box will require a "job start" transaction, which can be accomplished by calling the ffjobstart procedure:
procedure ffjobstart; var currentdatetime: tdatetime; begin jobrecord.id := 'S'; copyfixed(@jobrecord.platform, 'AAANNNNN', sizeof(jobrecord.platform)); currentdatetime := now; copyfixed(@jobrecord.date, formatdatetime('yyyymmdd', currentdatetime), sizeof(jobrecord.date)); copyfixed(@jobrecord.time, formatdatetime('hhmmss', currentdatetime) + '00', sizeof(jobrecord.time)); executeandack('S', @jobrecord, sizeof(jobrecord)); // job start runrecord.id := 'R'; runrecord.platform := jobrecord.platform; copyfixed(@runrecord.customer, 'AAANNNNN', sizeof(runrecord.customer)); if [individual option] then runrecord.logic := 'I' // optional individual else runrecord.logic := ' '; // standard family executeandack('R', @runrecord, sizeof(runrecord)); // run header end;Definitions for the jobrecord and runrecord variables are shown in our combined code sample. When processing actual jobs, you'll have to replace 'AAANNNNN' with a FASTforward-approved customer number. The [individual option] is any mechanism you wish (such as an on-screen checkbox) to control whether the black box should use "standard" or "optional" matching logic per FASTforward specifications. The copyfixed procedure copies strings into the fixed fields of FASTforward data structures:
procedure copyfixed(dest: pchar; s: string; size: longint); begin // pad or truncate s and save at dest if length(s) > size then s := copy(s, 1, size) else while length(s) < size do s := s + ' '; strmove(dest, pchar(s), size); end;After using the ffjobstart procedure, you'll want to send one or more address records to the black box and retrieve the black box lookup results. Submitting an address record to the black box is accomplished by filling in the fields of a "detail record" with an old address (including a unique record ID field) then calling the ffsubmitdetail procedure:
procedure ffsubmitdetail(const detailrecord: detailrecordtype); begin // start black box working on a record executecommand('D', @detailrecord, sizeof(detailrecord)); end;The layout of a standard FASTforward detailrecord is shown in our combined code sample. After submitting one or more detail records (the queueing techniques necessary for efficient black box throughput are beyond the scope of this discussion), you can tell the black box you want it to cough up the results for a particular detail record by giving the ffdemandanswer procedure the unique eight-character record ID identifying the detail record you previously submitted to the ffsubmitdetail procedure:
procedure ffdemandanswer(const recidwanted: char8type); begin demandrecord.id := 'A'; copyfixed(@demandrecord.platform, 'AAANNNNN', sizeof(demandrecord.platform)); demandrecord.recid := recidwanted; executecommand('A', @demandrecord, sizeof(demandrecord)); end;The layout of a standard FASTforward demandrecord is shown in our combined code sample. Replace 'AAANNNNN' with a FASTforward-approved customer number. To actually retrieve the black box results, use the ffretrieveanswer function and give it the same parameter you passed to the ffdemandanswer procedure:
function ffretrieveanswer(const recidwanted: char8type): ffoutputrecordtype; var start: longint; stuck: boolean; begin start := gettickcount; repeat executecommand('O', @result, sizeof(result)); // retrieve output if result.response = 'Y' then exit else if result.response = 'N' then exit else // assume 'T' for timeout begin stuck := (gettickcount - start) > 2000; // 2 secs if not stuck then // make black box start over begin ffdemandanswer(recidwanted); sleep(10); end; end; until stuck; // stuck on timeouts (shouldn't happen, but does) fillchar(result, sizeof(result), ' '); result.response := 'N'; end;The layout of a standard FASTforward output record as returned by the ffretrieveanswer function is shown in our combined code sample. Opinions vary about what to do when ffretrieveanswer keeps receiving a timeout response; this example re-demands for two seconds then treats the chronically repeating timeout as an 'N' response.
Once you're done repeatedly submitting detail records and demanding and retrieving output records, call ffjobend to terminate the job. Your call will have to include the total number of records you processed in the job:
procedure ffjobend(count: longint); begin trailerrecord.id := 'E'; copyfixed(@trailerrecord.platform, 'AAANNNNN', sizeof(trailerrecord.platform)); copyfixed(@trailerrecord.total, inttostr(count), sizeof(trailerrecord.total)); executeandack('E', @trailerrecord, sizeof(trailerrecord)); jobendrecord.id := 'F'; jobendrecord.platform := trailerrecord.platform; jobendrecord.total := trailerrecord.total; executeandack('F', @jobendrecord, sizeof(jobendrecord)); end;The layout of the standard FASTforward trailerrecord and jobendrecord are shown in our combined code sample. Replace 'AAANNNNN' with a FASTforward-approved customer number. Once you're done repeatedly submitting jobs, call ffendofday to terminate the day's transactions:
procedure ffendofday; begin enddayrecord.id := 'T'; executeandack('T', @enddayrecord, sizeof(enddayrecord)); end;The layout of the standard FASTforward enddayrecord is shown in our combined code sample. After the first ffinitial call, always call ffnormal instead of ffinitial before starting any FASTforward jobs:
procedure ffnormal; begin accessrecord.id := 'P'; accessrecord.atype := 'N'; copyfixed(@accessrecord.password, retrievesavedpassword, sizeof(accessrecord.password)); accessrecord.mlc := 'C'; executeandack('P', @accessrecord, sizeof(accessrecord)); if ackrecord.error = '000' then savereturnedpassword(ackrecord.password); end;The retrievesavedpassword function is any convenient routine that returns the password saved by a previous call to the savereturnedpassword procedure made by the ffinitial or ffnormal procedures, per the security requirements of your FASTforward license.
That concludes the code necessary for black box access. Complete compilable units can be inspected in our combined code sample.