[Skip top navbar]

Andrew Gregory's Web Pages

Guilderton Lighthouse 30°20'24"S 115°29'31"E

-

Development of the QVRemote Application


Introduction

This page describes the development of the SIBO version of the QVRemote application, using the Programmer's Toolbox library.

The QVRemote application is a remote control for Casio QV-series digital cameras, allowing the camera to be operated via the serial data cable. It can also be set to control time lapse photography, taking photos at pre-determined intervals.


The Application Framework

Application Manager Selection

To start with, the first decision to make is which application manager to use. The library supports two: Asynchronous and Synchronous. I decided that I would like to use the built-in dialogs to handle the time-lapse status display, and that requires the use of the Asynchronous version. I placed a SIBO PIC version of the QVRemote icon into my PIC directory and started on the framework.

The Startup Procedure

I'm developing the application using my preferred tool: OPP and #include'ing the library modules. Since I'm intending for this to be both a comprehensive example, and of professional quality, I'll be using my Resource IDE application and storing all the text and online help in a resource file.

APP QVRemote TYPE $9000 ICON "\pic\qvremote.pic" ENDA PROC main: ESCAPE OFF CACHE 6000, 6000 mpStart%:( "init", "done" ) ENDP #include "qvremote.inc" #include "\opl\lib\amasync.opl" #include "\opl\lib\asyncdlg.opl" #include "\opl\lib\findfil.opl" #include "\opl\lib\ini.opl" #include "\opl\lib\ini.oph" #include "\opl\lib\iomngr.opl" #include "\opl\lib\misc.opl" #include "\opl\lib\rsmsg.opl" #include "\opl\lib\rsutil.opl" #include "\opl\lib\ui.opl"

You'll note that I've #include'd many library modules, plus an extra file called qvremote.inc. That file is created by my Resource IDE application from the resource source file. I'll set that up next:

#rscfile \app\qvremote\qvremote.~20 #incfile \opp\qvremote.inc #target wa #string QVREMOTE QVRemote

This defines the destination resource file, the generated include file, and the target machine. For the resource file, I'm using the extension ~20, where 20 is the language code for Australian English. My findRsc$: procedure performs a smart search for resource files and will find my resource file. See the findfil.opl source for full information. If you like, you can use your language code instead of 20, or just use RSC.

The include file is used to give the resource id numbers meaningful names for you to use in your source code. More on this later. The target machine is used to automatically word-wrap the online help topics. If you're not sure, use the wa (Workabout) or sna (Siena) codes as these machines have the narrowest screens. If you select a machine with a wide screen, and someone tries to use your help on a machine with a narrow screen, they won't be able to see all your help! When in doubt, use the Workabout code.

Important Note: Every time you change your resource data and translate it, the include file is updated and most likely changed. You must remember to rebuild your application so that it is using the correct resource ID numbers.

I've also included a single string that I'm sure I'll need at some point! This string has the ID "QVREMOTE" and the text "QVRemote". When the include file is generated, it will have the following entry in it:

#define STR_QVREMOTE 1

You'll notice that the ID has "STR_" prepended to it to indicate that it is a string resource. The number 1 is simply its ID. In your code you'll use a library procedure to access the string:

foo$ = rsMsg$:( STR_QVREMOTE )

For the most part, you can ignore the contents of the generated include file - just remember to rebuild your application when you make changes to your resources.

Initialisation and Cleanup

After setting up the resource file and the mainline of the application, the next step is writing the initialisation and cleanup procedures.

REM initialise PROC init%: DEFAULTWIN 1 STATUSWIN ON FONT -$3FFF, 0 amLdRsc%:( findRsc$:( 0 ) ) :REM load resource file EvtFunc$( 1 ) = "hdlkey" EvtFunc$( 17 ) = "hdlcmd" ldprefs: ENDP REM cleanup PROC done%: ENDP

For now, the initialisation procedure simply loads the resources, sets up some event handlers, then loads the preferences. The cleanup procedure doesn't even do anything, yet - I expect it to eventually free memory used to manage the remote control buttons.

The event handlers are important - they allow the application to respond to outside events, such as keys being pressed. The ID numbers of the most common event handlers are listed below:

Event Handler Identifiers
IDDescription
1Keyboard
5Application has gone to background
6Application has come to foreground
17System command
20Machine has been switched on
22Date has changed

You can find a complete list in the application manager source code.

Event Handlers

Now that the initialisation procedure has created some references to event handlers, they should be written:

REM handle keyboard events PROC hdlkey%:( key%, kmod%, krep% ) REM ignore keys while menus/dialogs are visible IF amTstUI%:( 3 ) <> 0 :RETURN 0 :ENDIF REM a temporary escape key IF key% = 27 :RETURN 2 :ENDIF ENDP REM handle system command events PROC hdlcmd%:( c$, filenam$ ) IF c$ = "X" :RETURN 2 :ENDIF ENDP

The keyboard handler does two things. First, it ensures keys are ignored when menus and dialog boxes are visible. Since we are using the asynchronous version of the Application Manager, our application will receive all key events, even when menus and dialogs are visible! Normally you will want to ignore such keystrokes, however, later on we will add Help key handling that will support help even from inside dialog boxes. Secondly, it specifies that pressing the Escape key will exit the application.

The system command handler just handles the eXit command. There is no need for it to handle Open or Close commands as QVRemote does not operate on files. Handling this command means that QVRemote will respond to the "Exit application" command from the System screen.

You may have noticed the different return values. A value of zero means that the event was handled. A value of one means that the event should be ignored. A value of two means that the current Application Manager event loop should be exited.

Preferences

Next, the procedures to load and save the application preferences.

REM load preferences PROC ldprefs: LOCAL inihdl% inihdl% = iniOpen%:( findIni$:( "" ), 1 ) GET_INI_NUM( inihdl%, model%, "CameraModel", 3000 ) iniClose:( inihdl% ) ENDP REM save preferences PROC svprefs: LOCAL inihdl% inihdl% = iniOpen%:( findIni$:( "" ), 2 ) SET_INI_NUM( inihdl%, model%, "CameraModel" ) iniClose:( inihdl% ) ENDP

These store the user's preferred camera model in a setting named "CameraModel". GET_INI_NUM and SET_INI_NUM are macros defined in \opl\lib\ini.oph and require OPP to translate them. They greatly simplify INI file access. There are corresponding macros to handle strings:

GET_INI_STR( inihdl%, name$, "UserName", "John Smith" ) SET_INI_STR( inihdl%, name$, "UserName" )

The important things to note are the open mode: 1 for reading and 2 for writing, and that the reading includes a default value saving you from needing to specially initialise the INI file or the relevant application variables.

The findIni$: procedure automatically searches your local drives for the INI file. The default directory is \OPD and the default name is that of the application, in this case QVREMOTE.INI. If it doesn't exist it will be created automatically in the \OPD directory on your internal drive (if the directory itself doesn't exist, it will be created automatically too).

We now need to add the global variable to store the camera model to the main: procedure:

GLOBAL model%

Summary To Now

So far, the application will start and read its preferences, then wait for you to press Escape or "Exit" the application from the System screen. All these functions will most likely be similar for every application you write. From now on, there will be mostly application-specific code.

Remote Control Buttons

Now that the major parts of the application framework are present, we can add some application-specific code. For this part of the example, we'll add the remote control buttons.

REM Add a button to the linked list PROC addBtn:( title$, x&, y&, w&, h&, code%, keyc% ) LOCAL p& REM add to linked list p& = ALLOC( 24 + 1 + LEN( title$ ) ) IF p& <> 0 POKEL UADD( p&, 0 ), btnlist& POKEL UADD( p&, 4 ), x& POKEL UADD( p&, 8 ), y& POKEL UADD( p&, 12 ), w& POKEL UADD( p&, 16 ), h& POKEW UADD( p&, 20 ), code% POKEW UADD( p&, 22 ), keyc% POKE$ UADD( p&, 24 ), title$ btnlist& = p& ENDIF ENDP PROC freeBtns: LOCAL p& WHILE btnlist& <> 0 p& = PEEKL( btnlist& ) FREEALLOC btnlist& btnlist& = p& ENDWH ENDP REM Draw a single button PROC drawBtn:( btn&, style% ) LOCAL x&, y&, w&, h&, keyc%, title$( 255 ) LOCAL i%( 32 ) x& = PEEKL( UADD( btn&, 4 ) ) y& = PEEKL( UADD( btn&, 8 ) ) w& = PEEKL( UADD( btn&, 12 ) ) h& = PEEKL( UADD( btn&, 16 ) ) keyc% = PEEKW( UADD( btn&, 22 ) ) IF btnstyl% = 1 title$ = PEEK$( UADD( btn&, 24 ) ) ELSE title$ = keyName$:( keyc% ) ENDIF gINFO i%() gFONT 9 gSTYLE 1 gAT x&, y& gBUTTON title$, 1, w&, h&, style% gSTYLE i%( 20 ) gAT i%( 26 ), i%( 27 ) ENDP REM Return the name of the key PROC keyName$:( keyc% ) IF keyc% = 27 :RETURN "Esc" :ENDIF IF keyc% = 32 :RETURN "Space" :ENDIF IF keyc% = 8 :RETURN "Del" :ENDIF IF keyc% = 9 :RETURN "Tab" :ENDIF IF keyc% = 13 :RETURN "Enter" :ENDIF IF keyc% = 290 :RETURN "Menu" :ENDIF IF keyc% = 256 :RETURN "Up" :ENDIF IF keyc% = 257 :RETURN "Down" :ENDIF IF keyc% = 259 :RETURN "Left" :ENDIF IF keyc% = 258 :RETURN "Right" :ENDIF IF keyc% = 260 :RETURN "PgUp" :ENDIF IF keyc% = 261 :RETURN "PgDn" :ENDIF IF keyc% = 262 :RETURN "Home" :ENDIF IF keyc% = 263 :RETURN "End" :ENDIF RETURN UPPER$( CHR$( keyc% ) ) ENDP REM Draw all the buttons PROC drawBtns: LOCAL p& p& = btnlist& WHILE p& <> 0 drawBtn:( p&, 0 ) p& = PEEKL( p& ) ENDWH ENDP REM Given a button, return the command character PROC btnCmd$:( btn& ) RETURN CHR$( PEEKW( UADD( btn&, 20 ) ) ) ENDP REM Given a button, return the keycode PROC btnKey%:( btn& ) RETURN PEEKW( UADD( btn&, 22 ) ) ENDP REM Given a keycode, return the button PROC findBtn&:( keyc% ) LOCAL p&, k$( 1 ) IF keyc% = 0 :RETURN 0 :ENDIF k$ = UPPER$( CHR$( keyc% ) ) p& = btnlist& WHILE p& <> 0 IF UPPER$( CHR$( btnKey%:( p& ) ) ) = k$ BREAK ENDIF p& = PEEKL( p& ) ENDWH RETURN p& ENDP

This code requires some new global variables:

GLOBAL btnlist&, btnstyl%

and an addition to the init%: procedure:

... btnstyl% = 1 ...

Now I'll add the code that defines the button layouts for the various cameras (plus a few utility procedures):

PROC makeBtns:( model% ) @( "mkQV" + GEN$( model%, 9 ) ): ENDP REM Define QV-3000EX key codes PROC mkQV3000: addBtn:( "Menu" , 35, 0, 70, 28, %e, %m ) addBtn:( "Up" , 35, 30, 70, 28, %f, 256 ) addBtn:( "Left" , 0, 60, 70, 28, %g, 259 ) addBtn:( "Right" , 75, 60, 70, 28, %h, 258 ) addBtn:( "Down" , 35, 90, 70, 28, %j, 257 ) addBtn:( "Set" , 35, 120, 70, 28, %i, %n ) addBtn:( "Focus Lock", 155, 0, 70, 28, %a, %g ) addBtn:( "Shutter" , 235, 0, 70, 28, %b, 13 ) addBtn:( "Flash" , 155, 30, 70, 28, %k, %l ) addBtn:( "Mode" , 235, 30, 70, 28, %n, %p ) addBtn:( "Focus" , 155, 60, 70, 28, %l, %f ) addBtn:( "Self Timer", 235, 60, 70, 28, %p, %t ) addBtn:( "Preview" , 155, 90, 70, 28, %q, %v ) addBtn:( "Tele" , 235, 90, 70, 28, %d, %a ) addBtn:( "Disp" , 155, 120, 70, 28, %o, %d ) addBtn:( "Wide" , 235, 120, 70, 28, %c, %z ) ENDP REM Define QV-2000UX key codes PROC mkQV2000: addBtn:( "Menu" , 35, 0, 70, 28, %e, %m ) addBtn:( "Up" , 35, 30, 70, 28, %f, 256 ) addBtn:( "Left" , 0, 60, 70, 28, %g, 259 ) addBtn:( "Right" , 75, 60, 70, 28, %h, 258 ) addBtn:( "Down" , 35, 90, 70, 28, %j, 257 ) addBtn:( "Set" , 35, 120, 70, 28, %i, %n ) addBtn:( "Focus Lock", 155, 0, 70, 28, %a, %g ) addBtn:( "Shutter" , 235, 0, 70, 28, %b, 13 ) addBtn:( "Flash" , 155, 30, 70, 28, %k, %l ) addBtn:( "Self Timer", 235, 30, 70, 28, %p, %t ) addBtn:( "Focus" , 155, 60, 70, 28, %l, %f ) addBtn:( "Tele" , 235, 90, 70, 28, %d, %a ) addBtn:( "Wide" , 235, 120, 70, 28, %c, %z ) ENDP REM Define QV-2300UX key codes (untested) PROC mkQV2300: addBtn:( "Focus Lock", 0, 0, 70, 28, %a, %g ) addBtn:( "Shutter" , 75, 0, 70, 28, %b, 13 ) addBtn:( "-" , 0, 40, 70, 28, %g, 259 ) addBtn:( "+" , 75, 40, 70, 28, %h, 258 ) addBtn:( "Menu" , 35, 80, 70, 28, %e, %m ) addBtn:( "Shift" , 0, 120, 70, 28, %k, %s ) addBtn:( "Flash Mode", 75, 120, 70, 28, %j, %l ) addBtn:( "Wide" , 155, 0, 70, 28, %c, %z ) addBtn:( "Tele" , 235, 0, 70, 28, %d, %a ) addBtn:( "Off" , 155, 80, 70, 28, %p, %o ) addBtn:( "Focus Mode", 155, 120, 70, 28, %f, %f ) addBtn:( "Timer" , 235, 120, 70, 28, %l, %t ) ENDP REM Define QV-2800UX key codes (untested) PROC mkQV2800: mkQV2300: REM 2300, 2800 use the same buttons ENDP REM Define QV-8000UX key codes (untested) PROC mkQV8000: addBtn:( "Menu" , 35, 0, 70, 28, %e, %m ) addBtn:( "-" , 0, 40, 70, 28, %g, 259 ) addBtn:( "+" , 75, 40, 70, 28, %h, 258 ) addBtn:( "Set" , 35, 80, 70, 28, %i, %n ) addBtn:( "Flash" , 0, 120, 70, 28, %j, %l ) addBtn:( "Focus" , 75, 120, 70, 28, %f, %f ) addBtn:( "Focus Lock", 155, 0, 70, 28, %a, %g ) addBtn:( "Shutter" , 235, 0, 70, 28, %b, 13 ) addBtn:( "Wide" , 155, 40, 70, 28, %c, %z ) addBtn:( "Tele" , 235, 40, 70, 28, %d, %a ) addBtn:( "Self Timer", 155, 120, 70, 28, %l, %t ) addBtn:( "Disp" , 235, 120, 70, 28, %k, %d ) ENDP

Now to update the init%: procedure:

... ldprefs: makeBtns:( model% ) drawBtns: ...

and the done%: procedure:

... freeBtns: ...

If the program is run, the following screen will appear:

Screen shot
Screen shot

It still doesn't do anything yet, so press Escape to quit.


Keyboard Control

Now that the buttons are appearing on the screen, the next step is to start handling keyboard events. Here are the functions to be handled:

The EPOC operating offers keyboard press and release events, unfortunately, SIBO does not. This makes it impossible to support things like zooming in and out nicely. This SIBO version will simply have to make do with key repeats.

Camera Keys

The first thing is to check if the key "belongs" to a button. This is easy - findBtn&: does it:

PROC handkey%:( key%, kmod%, krep% ) LOCAL btn& ... IF kmod% = 0 btn& = findBtn&:( key% ) IF btn& <> 0 PRINT btnCmd$:( btn& ) :REM temporary - for testing ENDIF ENDIF ENDP

If you run the application now and press some keys, the corresponding camera command character will appear on the screen. For example, press Enter to do a shutter release, and the letter "b" will appear. We'll come back a little later to add serial port support and actual camera control.

The Menu key is easy, but first we'll create some resources. Use the Resource IDE to edit the qvremote.rsc file and add the following to the end:

#menutitle CAMERA Camera #menuitems CAMERA Shutter,s,Time lapse,l #menutitle SPECIAL Special #menuitems SPECIAL Preferences,q,About,-a,Exit,x #string PREFS Preferences #string MODEL Camera model #choice MODEL QV-3000,QV-2000,QV-2300,QV-2800,QV-8000 #string ABT About #string ABT_1 QVRemote v1.0.0 for SIBO 16-Apr-2001 #string ABT_2 by Andrew Gregory #string ABT_3 andrew at scss.com.au #string ABT_4 http://www.scss.com.au/ #buttons ABT Close,-27

Save and translate the resources. Return to the application source. The following code added to the end of the keyboard handler looks after the Menu key:

IF key% = 290 IF kmod% = 4 REM +Control : Change status window (+Shift:bigger, otherwise:smaller) sz% = -1 :IF kmod% AND 2 :sz% = 1 :ENDIF sz% = sz% + STATUSWININFO( -1, xy%() ) maxsz% = 2 :REM default for 3a,3c,3mx,Workabout IF CALL( $2A8B ) / $1000 = $4 :maxsz% = 1 :ENDIF :REM Siena IF sz% < 0 :sz% = maxsz% :ENDIF IF sz% > maxsz% :sz% = 0 :ENDIF STATUSWIN ON, sz% FONT -$3FFF, 0 drawBtns: ELSE mINIT rsAddMCX:( MNU_CAMERA , CRD_CAMERA ) rsAddMCX:( MNU_SPECIAL, CRD_SPECIAL ) ret% = dXitKey%:( wsMenu%:( 0 ) ) IF ret% <> 0 IF ret% = 291 RETURN @%( EvtFunc$( 1 ) ):( 291, 0, 1 ) :REM Help key ELSEIF ret% >= %A AND ret% <= %Z RETURN @%( EvtFunc$( 1 ) ):( ret% + 512 + 32, 10, 1 ) ELSE RETURN @%( EvtFunc$( 1 ) ):( ret% + 512 , 8, 1 ) ENDIF ENDIF ENDIF ENDIF

The following variables needed to be added to the keyboard handler:

... LOCAL ret%, sz%, maxsz%, xy%( 4 ) ...

Now for some explanations: The status window code should be fairly self-explanatory. It first figures out which "direction" the status window is "growing" and modifies the current size of the status window accordingly. Then it makes sure the new size is valid for the machine being used. Finally the status window size is updated and the screen redrawn.

The Menu key is handled by building the menu cards from the resources we created. Then, the menu is displayed and the exit key obtained. The dXitKey%: procedure is there to detect if the Help key was pressed to exit the Menu (otherwise, the Help key would be indistinguishable from the Escape key). The selected menu item is handled by calling the keyboard handler directly with the return value. The tests make sure the menu keys are handled exactly as if their shortcut keys had been pressed directly.

Shortcut Keys

Now we need to add the code to handle the menu functions (this goes at the end of the keyboard handler):

... IF ket% AND 512 IF key% = %s + 512 :REM Shutter ENDIF IF key% = %l + 512 :REM Time lapse ENDIF IF key% = %q + 512 :REM Preferences sz% = mdl2id%:( model% ) IF sz% = 0 :sz% = 1 :ENDIF dINIT rsMsg$:( STR_PREFS ) dCHOICE sz%, rsMsg$:( STR_MODEL ), rsMsgDC$:( CHC_MODEL, 32 ) ret% = wsDial%: IF ret% <> 0 model% = id2mdl%:( sz% ) svprefs: freeBtns: makeBtns:( model% ) FONT -$3FFF, 0 drawBtns: ENDIF ENDIF IF key% = %a + 512 :REM About dINIT rsMsg$:( STR_ABT ) dTEXT "", rsMsg$:( STR_ABT_1 ) dTEXT "", rsMsg$:( STR_ABT_2 ) dTEXT "", rsMsg$:( STR_ABT_3 ) dTEXT "", rsMsg$:( STR_ABT_4 ) dBUTTONS rsMsgDB$:( BTN_ABT, 1, ADDR( ret% ) ), ret% wsDial%: ENDIF IF key% = %x + 512 :REM Exit RETURN 2 ENDIF ENDIF ...

You can now delete the "Escape" key handling:

REM a temporary escape key IF key% = 27 :RETURN 2 :ENDIF

The Preferences handling needs these new utility procedures:

PROC mdl2id%:( model% ) IF model% = 3000 :RETURN 1 :ENDIF IF model% = 2000 :RETURN 2 :ENDIF IF model% = 2300 :RETURN 3 :ENDIF IF model% = 2800 :RETURN 4 :ENDIF IF model% = 8000 :RETURN 5 :ENDIF ENDP PROC id2mdl%:( id% ) IF id% = 1 :RETURN 3000 :ENDIF IF id% = 2 :RETURN 2000 :ENDIF IF id% = 3 :RETURN 2300 :ENDIF IF id% = 4 :RETURN 2800 :ENDIF IF id% = 5 :RETURN 8000 :ENDIF ENDP

The Shutter and Time Lapse functions aren't handled yet. The Preferences is handled via a choice list. When a selection is made, the new preferences are saved and the screen redrawn with the new buttons. The About and Exit functions should be easy to figure out.

If you try the application now, you'll find the menus now work and you can select different camera models. The model you select will be remembered for the next time you run the application. To exit the application, press Psion-X.

Help Key

To display some help requires some new resources and new code. The resources first. The help topics are quite lengthy, so I won't reproduce them all:

#topic MAIN MAIN QVRemote #endt #index MAIN INTRO LIMITS PLANS PREFS REMCTRL SUPPORTED TIMELAPSE WEB #endi #topic INTRO 0 Introduction QVRemote is a free program that lets you operate your Casio QV-series digital camera via a serial cable. It also supports automated time-lapse photography, where photographs are taken at fixed intervals. #endt ...

The first topic, MAIN, is empty - it exists solely to display the help index, which is defined next. After "#topic" the first MAIN is the resource ID, the second MAIN is the ID of the index to be displayed at the bottom of the help topic after the help text. The rest of the line is the help topic heading.

The help index lists all the help topics, of which the first is INTRO.

Help topics are automatically word-wrapped. Exceptions are lines starting with a space. They are not wrapped with previous lines but start a fresh new line. If you want to start a new paragraph in a help topic, start its first line with a space.

Save and translate the resources.

Next, the following code must be added to the start of the keyboard handler, before the amTstUI%: call:

REM Handle help IF key% = 291 IF amTstUI%:( 2 ) = 0 REM only show help if menus are not visible REM if they *are* the Help key is detected REM after the menus close and the keyboard REM handler is then specially called (see below) amHelp%:( HLP_MAIN, HLP_MAIN ) RETURN 1 ENDIF ENDIF

That's it! Help is now available at all times, even in dialogs!

Tab Key

The Tab key will toggle the camera control button labels between showing the function and the character that activates that function. For example, between "Menu" and "m".

... IF kmod% = 0 IF key% = 9 btnstyl% = 3 - btnstyl% :REM switch between 1 and 2 drawBtns: ELSE btn& = findBtn&:( key% ) IF btn& <> 0 PRINT btnCmd$:( btn& ) :REM temporary - for testing ENDIF ENDIF ENDIF ...


Serial Port Interface

Now we can start accessing the serial port. First the following procedures to handle the communications:

PROC sendChar:( ch$ ) sendSrl%:( ch$ ) PAUSE 1 ENDP REM Send a character to the serial port PROC sendSrl%:( ch$ ) LOCAL b% b% = ASC( ch$ ) RETURN IOWRITE( serialh%, ADDR( b% ), 1 ) ENDP REM Open the serial port PROC openSrl%: LOCAL ret% ret% = IOOPEN( serialh%, "TTY:A", -1 ) IF ret% < 0 RAISE ret% ELSE srlstat% = -46 rsset%:( serialh%, 15, 0, 8, 1, 4, &0 ) ENDIF RETURN ret% ENDP REM Close the serial port PROC clsSrl%: LOCAL ret% ret% = 0 IF serialh% <> 0 ret% = IOCLOSE( serialh% ) IF ret% >= 0 serialh% = 0 ENDIF ENDIF RETURN ret% ENDP

Going back to the keyboard handler, we can now implement the shutter release function:

IF key% = %s + 512 :REM Shutter sendChar:( "B" ) sendChar:( "b" ) ENDIF

and the general button commands (replacing the test PRINT statement):

IF kmod% = 0 IF key% = 9 btnstyl% = 3 - btnstyl% :REM switch between 1 and 2 drawBtns: ELSE btn& = findBtn&:( key% ) IF btn& <> 0 drawBtn:( btn&, 2 ) sendChar:( UPPER$( btnCmd$:( btn& ) ) ) PAUSE 5 sendChar:( LOWER$( btnCmd$:( btn& ) ) ) drawBtn:( btn&, 0 ) ENDIF ENDIF ENDIF

Add the new required global variables:

GLOBAL serialh%, srlstat%

And new initialisation and cleanup code:

PROC init%: ... serialh% = 0 openSrl%: ENDP PROC done%: ... clsSrl%: ENDP

Now the application can control the camera. Only one thing left to do...

Time Lapse Control

The time lapse function will use the TIM: device to set absolute "alarms" that will wake up QVRemote to perform the various time lapse actions.

It will use the asynchronous dialog box feature of the Asynchronous Application Manager to display the current state of the time lapse session. A background process will run to update the status.

First, some new global variables are required:

... GLOBAL timerh% :REM timer handle GLOBAL timers% :REM timer slot GLOBAL backgs% :REM background process slot GLOBAL pixDlg% :REM time lapse dialog handle GLOBAL pixCnt& :REM number of pictures to take GLOBAL curCnt& :REM number of pictures left to take GLOBAL pixInt& :REM interval in seconds between pictures GLOBAL pixPreF& :REM pre-focus time in seconds GLOBAL pixBDur& :REM bulb duration in seconds (0=normal use/no bulb) GLOBAL pixBDly& :REM bulb delay after picture GLOBAL pixNext& :REM time of the next event GLOBAL pixStat& :REM time delay state: 1=prefocus,2=shutter/start bulb,3=end bulb/start bulb delay ...

Initialisation for these needs to be added to the init%: procedure:

... timerh% = 0 pixDlg% = 0 pixCnt& = 1 curCnt& = 0 pixInt& = 600 pixPreF& = 0 pixBDur& = 0 pixBDly& = 20 ...

Now some resources need to be added:

... #string TIMELAPSE Time Lapse #string TL_COUNT Count #string TL_INT Interval #string TL_PREF Pre-focus #string TL_BULB Bulb #string TL_BDLY Bulb delay #buttons TIMELAPSE Cancel,-27,Start,s #string TL_TOOSHORT Interval too short - extending #string TLSTAT_STPPD Stopped #string TLSTAT_PREF Pre Focus #string TLSTAT_SHTTR Shutter #string TLSTAT_BDLY Bulb Delay #string TLSTAT_WAIT Wait #buttons TLSTAT Stop,-27 ...

Next, some procedures to do the processing related to the time lapse function:

PROC pixTtl&: LOCAL total& total& = pixBDur& + pixPreF& IF pixBDur& > 0 total& = total& + pixBDly& ENDIF RETURN total& ENDP REM Handle timer events PROC hdltmr%:( hdl% ) IF pixStat& = 1 REM end of delay to prefocus/start of delay to shutter pixStat& = 2 pixNext& = pixNext& + pixPreF& IF pixPreF& > 0 REM send focus lock command sendChar:( "A" ) sendChar:( "a" ) GOTO next:: ENDIF ENDIF IF pixStat& = 2 REM end of delay to shutter/start of bulb pixStat& = 3 pixNext& = pixNext& + pixBDur& REM send shutter command sendChar:( "B" ) IF pixBDur& > 0 GOTO next:: ELSE sendChar:( "b" ) ENDIF ENDIF IF pixStat& = 3 REM end of bulb/start of bulb delay pixStat& = 4 curCnt& = curCnt& + 1 IF pixBDur& > 0 pixNext& = pixNext& + pixBDly& REM send shutter command sendChar:( "b" ) GOTO next:: ENDIF ENDIF IF pixStat& = 4 REM end of bulb delay/start of delay to prefocus IF pixPreF& = 0 pixStat& = 2 ELSE pixStat& = 1 ENDIF pixNext& = pixNext& + ( pixInt& - pixTtl&: ) ENDIF next:: IF curCnt& >= pixCnt& stopTmr: ELSE IOC( timerh%, 2, #ioGstat%:( hdl% ), pixNext& ) :REM absolute timer ENDIF updStat: ENDP PROC updStat: LOCAL desc$( 255 ), nxt& LOCAL yr%, mo%, dy%, hr%, mn%, sc%, yrday% nxt& = pixNext& - DATETOSECS( YEAR, MONTH, DAY, HOUR, MINUTE, SECOND ) SECSTODATE nxt&, yr%, mo%, dy%, hr%, mn%, sc%, yrday% IF timerh% = 0 desc$ = rsMsg$:( STR_TLSTAT_STPPD ) ELSE IF pixStat& = 1 :desc$ = rsMsg$:( STR_TLSTAT_PREF ) :ENDIF IF pixStat& = 2 :desc$ = rsMsg$:( STR_TLSTAT_SHTTR ) :ENDIF IF pixStat& = 3 :desc$ = rsMsg$:( STR_TLSTAT_BDLY ) :ENDIF IF pixStat& = 4 :desc$ = rsMsg$:( STR_TLSTAT_WAIT ) :ENDIF ENDIF sTEXT%:( pixDlg%, 2, desc$ ) desc$ = "" IF timerh% <> 0 desc$ = desc$ + RIGHT$( "0" + GEN$( hr%, 2 ), 2 ) desc$ = desc$ + ":" desc$ = desc$ + RIGHT$( "0" + GEN$( mn%, 2 ), 2 ) desc$ = desc$ + ":" desc$ = desc$ + RIGHT$( "0" + GEN$( sc%, 2 ), 2 ) ENDIF sTEXT%:( pixDlg%, 3, desc$ ) desc$ = "" IF timerh% <> 0 desc$ = GEN$( pixCnt& - curCnt&, 9 ) ENDIF sTEXT%:( pixDlg%, 4, desc$ ) ENDP PROC stopTmr: IF timerh% <> 0 IOWAIT :REM 'soak up' background process IF PEEKW( ioGstat%:( timers% ) ) = -46 IOCANCEL( timerh% ) IOWAITSTAT #ioGstat%:( timers% ) ENDIF clsTmr%: ioRem%:( timers% ) ioRem%:( backgs% ) ENDIF ENDP REM Open timer device PROC openTmr%: LOCAL ret% ret% = 0 IF timerh% = 0 ret% = IOOPEN( timerh%, "TIM:", -1 ) IF ret% < 0 RAISE ret% ENDIF ENDIF RETURN ret% ENDP REM Close timer device PROC clsTmr%: LOCAL ret% ret% = 0 IF timerh% <> 0 ret% = IOCLOSE( timerh% ) IF ret% >= 0 timerh% = 0 ENDIF ENDIF RETURN ret% ENDP REM Background process - update time-lapse status PROC hdlbg%:( hdl% ) IF timerh% <> 0 updStat: IOSIGNAL ELSE ioRem%:( hdl% ) ENDIF ENDP

Now the time lapse dialog can be done:

... IF key% = %l + 512 :REM Time lapse dINIT rsMsg$:( STR_TIMELAPSE ) dLONG pixCnt&, rsMsg$:( STR_TL_COUNT ), 1, 999 dTIME pixInt&, rsMsg$:( STR_TL_INT ), 3, 0, DATETOSECS( 1970, 1, 1, 23, 59, 59 ) dTIME pixPreF&, rsMsg$:( STR_TL_PREF ), 3, 0, 60 dTIME pixBDur&, rsMsg$:( STR_TL_BULB ), 3, 0, 600 dTIME pixBDly&, rsMsg$:( STR_TL_BDLY ), 3, 0, 60 dBUTTONS rsMsgDB$:( BTN_TIMELAPSE, 1, ADDR( xy%( 1 ) ) ), xy%( 1 ), rsMsgDB$:( BTN_TIMELAPSE, 2, ADDR( xy%( 2 ) ) ), xy%( 2 ) ret% = wsDial%: IF pixTtl&: > pixInt& REM interval isn't long enough GIPRINT rsMsg$:( STR_TL_TOOSHORT ) pixInt& = pixTtl&: ENDIF IF ret% <> 0 IF openTmr%: >= 0 REM start the time-lapse sequence pixStat& = 1 pixNext& = DATETOSECS( YEAR, MONTH, DAY, HOUR, MINUTE, SECOND ) curCnt& = 0 dINIT rsMsg$:( STR_TIMELAPSE ) dTEXT "", rsMsg$:( STR_TLSTAT_WAIT ), 2 dTEXT "", "00:00:00", 2 dTEXT rsMsg$:( STR_TL_COUNT ), "0" dBUTTONS rsMsgDB$:( BTN_TLSTAT, 1, ADDR( xy%( 1 ) ) ), xy%( 1 ) backgs% = ioAdd%:( "hdlbg", 1 ) IOSIGNAL timers% = ioAdd%:( "hdltmr", 127 ) hdltmr%:( timers% ) wsDial%: stopTmr: ENDIF ENDIF ENDIF ...

QVRemote is now complete:

Screen shot
Screen shot

Finishing Up

All that's left now is getting QVRemote ready for distribution. Firstly, I'll run it through OPACloak. Before that I need to create an exclusion file for those procedures that are referred to indirectly. It must be called QVREMOTE.EXC (to correspond to QVREMOTE.OPA) and be in the same directory as QVREMOTE.OPP:

PROC init%: PROC done%: PROC hdlkey%: PROC hdlcmd%: PROC hdltmr%: PROC hdlbg%: PROC mkQV3000: PROC mkQV2000: PROC mkQV2300: PROC mkQV2800: PROC mkQV8000: #include "\opl\lib\opllib.exc"

Before using OPACloak the OPA file was 27975 bytes long. Afterwards it became 15889 bytes:

Cloaking statistics
Cloaking statistics

Next is the creation of the PsiSetup control file, QVREMOTE.CTL:

[\APP\]QVREMOTE.OPA [\APP\QVREMOTE\]QVREMOTE.~20

and a README.TXT file:

QVRemote ======== http://www.scss.com.au/family/andrew/ Automatic Installation ---------------------- Unzip all the files to the same directory and double-click on QVREMOTE.CTL. Manual Installation ------------------- 1. Copy QVREMOTE.OPA to a \APP directory on any drive. 2. Copy all other files (except README.TXT and QVREMOTE.OPA) to a \APP\QVREMOTE directory on any drive.

Finally, QVREMOTE.OPA, QVREMOTE.~20, QVREMOTE.CTL, and README.TXT are ZIPped up into QVREMOTE.ZIP.

QVRemote web page.


-