Index: /trunk/ppImage/src/ppFocus.c
===================================================================
--- /trunk/ppImage/src/ppFocus.c	(revision 7706)
+++ /trunk/ppImage/src/ppFocus.c	(revision 7706)
@@ -0,0 +1,46 @@
+#include "ppImage.h"
+
+int main(int argc, char **argv) {
+
+    psLibInit(NULL);
+
+    psTimerStart(TIMERNAME);
+
+    // Parse the configuration and arguments
+    // Open the input image(s)
+    // Determine camera, format from header if not already defined
+    // Construct camera in preparation for reading
+    pmConfig *config = ppFocusArguments(argc, argv);
+    if (config == NULL) {
+        psErrorStackPrint(stderr, "");
+        exit(1);
+    }
+
+    // define recipe options
+    // define the active I/O files
+    ppImageOptions *options = ppFocusParseCamera(config);
+    if (options == NULL) {
+        psErrorStackPrint(stderr, "");
+        exit(1);
+    }
+    
+    
+    
+
+    // Image Arithmetic Loop
+    if (!ppImageLoop(config, options)) {
+        psErrorStackPrint(stderr, "");
+        exit(1);
+    }
+
+    psLogMsg ("ppImage", 3, "complete ppImage run: %f sec\n", psTimerMark (TIMERNAME));
+
+    // Cleaning up
+    ppImageCleanup(config, options);
+    return EXIT_SUCCESS;
+}
+
+// ppFocus is a lot like ppImage, but with a few important differences:
+// - the input list is a set of independent images (not multiple files for a single image)
+// - each pass to ppImageLoop performs the analysis on a different pmFPAfile
+// - 
Index: /trunk/ppImage/src/ppFocusArguments.c
===================================================================
--- /trunk/ppImage/src/ppFocusArguments.c	(revision 7706)
+++ /trunk/ppImage/src/ppFocusArguments.c	(revision 7706)
@@ -0,0 +1,70 @@
+#include "ppImage.h"
+
+static void usage (void) {
+    fprintf (stderr, "USAGE: ppFocus [-file INPUT.fits] [-list INPUT.txt] OUTPUT\n");
+    exit (2);
+}
+
+pmConfig *ppFocusArguments(int argc, char **argv) {
+
+    int N;
+    bool status;
+
+    if (argc == 1) usage ();
+
+    // load the site-wide configuration information
+    pmConfig *config = pmConfigRead(argc, argv);
+    if (config == NULL) {
+        psErrorStackPrint(stderr, "Can't find site configuration!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    // Storage for other command-line arguments
+    config->arguments = psMetadataAlloc(); // The arguments, with default values
+
+    // save these recipe options until we have loaded the options
+    psMetadata *options = psMetadataAlloc ();
+    psMetadataAddPtr (config->arguments, PS_LIST_TAIL, "PPIMAGE.OPTIONS",  PS_DATA_METADATA, "", options);
+
+    // the following options override the PPIMAGE recipe options
+
+    // recipe option: -usemask : override MASK setting in phase2.recipe
+    if ((N = psArgumentGet(config->argc, config->argv, "-usemask"))) {
+        psArgumentRemove (N, &config->argc, config->argv);
+        psMetadataAddBool (options, PS_LIST_TAIL, "MASK", PS_META_REPLACE, "", true);
+        psArgumentRemove (N, &config->argc, config->argv);
+    }
+
+    // XXX add other PPIMAGE recipe options here
+
+    // drop the local view on the options (saved on config->arguments)
+    psFree (options);
+
+    // the input file is a required argument; if not found, we will exit
+    status = pmConfigFileSetsMD (config->arguments, config, "INPUT", "-file", "-list");
+    if (!status) { usage ();}
+
+    // if these command-line options are supplied, load the file name lists into config->arguments
+    // override any configuration-specified source for these files
+    pmConfigFileSetsMD (config->arguments, config, "BIAS", "-bias", "-biaslist");
+    pmConfigFileSetsMD (config->arguments, config, "DARK", "-dark", "-darklist");
+    pmConfigFileSetsMD (config->arguments, config, "FLAT", "-flat", "-flatlist");
+    pmConfigFileSetsMD (config->arguments, config, "MASK", "-mask", "-masklist");
+    pmConfigFileSetsMD (config->arguments, config, "FRINGE", "-fringe", "-fringelist");
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (config->argc, config->argv, "-chip"))) {
+        psArgumentRemove (N, &config->argc, config->argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_DATA_STRING, "",
+                          config->argv[N]);
+        psArgumentRemove (N, &config->argc, config->argv);
+    }
+
+    if (config->argc != 2) usage ();
+
+    // Add the input and output images (which remain on the command-line) to the arguments list
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "Name of the output image",
+                     config->argv[1]);
+
+    return config;
+}
Index: /trunk/ppImage/src/ppFocusParseCamera.c
===================================================================
--- /trunk/ppImage/src/ppFocusParseCamera.c	(revision 7706)
+++ /trunk/ppImage/src/ppFocusParseCamera.c	(revision 7706)
@@ -0,0 +1,104 @@
+# include "ppImage.h"
+
+// XXX clean up error checks: return NULL, not psAbort
+ppImageOptions *ppFocusParseCamera (pmConfig *config) {
+
+    bool status = false;
+
+    // the first input image defines the camera, and all recipes and options that follow
+    // select only the first file from the INPUT array
+    pmFPAfile *input = pmFPAfileDefineFromArgs (&status, config, "PPIMAGE.INPUT", "INPUT");
+    if (!status) {
+	psError(PS_ERR_IO, false, "Failed to build FPA from PPIMAGE.INPUT");
+	return NULL;
+    }
+
+    // add recipe options supplied on command line
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, "PHASE2");
+    psMetadata *arglist = psMetadataLookupPtr (&status, config->arguments, "PPIMAGE.OPTIONS");
+    psMetadataCopy (recipe, arglist);
+
+    // parse the options from the metadata format to the ppImageOptions structure
+    ppImageOptions *options = ppImageOptionsParse (config);
+
+    // the following are defined from the argument list, if given, 
+    // otherwise they revert to the config information or detrend database if specified
+    // not all input or output images are used in a given recipe
+    if (options->doBias) {
+	bool status = false;
+	pmFPAfileDefineFromArgs  (&status, config, "PPIMAGE.BIAS", "BIAS");
+	pmFPAfileDefineFromConf  (&status, config, "PPIMAGE.BIAS");
+	pmFPAfileDefineFromDetDB (&status, config, "PPIMAGE.BIAS", input->fpa, PM_DETREND_TYPE_BIAS);
+	if (!status) psAbort ("ppImageParseDetrend", "can't find a bias image source");
+    }
+    if (options->doDark) {
+	bool status = false;
+	pmFPAfileDefineFromArgs  (&status, config, "PPIMAGE.DARK", "DARK");
+	pmFPAfileDefineFromConf  (&status, config, "PPIMAGE.DARK");
+	pmFPAfileDefineFromDetDB (&status, config, "PPIMAGE.DARK", input->fpa, PM_DETREND_TYPE_DARK);
+	if (!status) psAbort ("ppImageParseDetrend", "can't find a dark image source");
+    }
+    if (options->doMask) {
+	bool status = false;
+	pmFPAfileDefineFromArgs  (&status, config, "PPIMAGE.MASK", "MASK");
+	pmFPAfileDefineFromConf  (&status, config, "PPIMAGE.MASK");
+	pmFPAfileDefineFromDetDB (&status, config, "PPIMAGE.MASK", input->fpa, PM_DETREND_TYPE_MASK);
+	if (!status) psAbort ("ppImageParseDetrend", "can't find a mask image source");
+    }
+    if (options->doFlat) {
+	bool status = false;
+	pmFPAfileDefineFromArgs  (&status, config, "PPIMAGE.FLAT", "FLAT");
+	pmFPAfileDefineFromConf  (&status, config, "PPIMAGE.FLAT");
+	pmFPAfileDefineFromDetDB (&status, config, "PPIMAGE.FLAT", input->fpa, PM_DETREND_TYPE_FLAT);
+	if (!status) psAbort ("ppImageParseDetrend", "can't find a flat image source");
+    }
+
+    // the following files are output targets
+    pmFPAfile *output = pmFPAfileDefineOutput (config, input->fpa, "PPIMAGE.OUTPUT");
+    pmFPAfile *byChip = pmFPAfileDefineNewCamera (config, "PPIMAGE.OUTPUT.CHIP");
+    pmFPAfile *byFPA1 = pmFPAfileDefineNewCamera (config, "PPIMAGE.OUTPUT.FPA1");
+    pmFPAfile *byFPA2 = pmFPAfileDefineNewCamera (config, "PPIMAGE.OUTPUT.FPA2");
+
+    // save any of these files?
+    output->save = options->BaseFITS;
+    byChip->save = options->ChipFITS;
+    byFPA1->save = options->FPA1FITS;
+    byFPA2->save = options->FPA2FITS;
+
+    // output is used as a carrier: input to byChip
+    output->freeLevel = PM_FPA_LEVEL_CHIP; 
+
+    // define the binned target files (which may just be carriers for some camera configurations)
+    pmFPAfile *bin1 = pmFPAfileDefineFromFPA (config, byChip->fpa, options->xBin1, options->yBin1, "PPIMAGE.BIN1");
+    pmFPAfile *bin2 = pmFPAfileDefineFromFPA (config, byChip->fpa, options->xBin2, options->yBin2, "PPIMAGE.BIN2");
+
+    // bin1 and bin2 are used as carriers: input for byFPA1, byFPA2
+    bin1->freeLevel = PM_FPA_LEVEL_FPA;
+    bin2->freeLevel = PM_FPA_LEVEL_FPA;
+
+    pmFPAfile *jpg1 = pmFPAfileDefineOutput (config, byFPA1->fpa, "PPIMAGE.JPEG1");
+    pmFPAfile *jpg2 = pmFPAfileDefineOutput (config, byFPA2->fpa, "PPIMAGE.JPEG2");
+
+    // XXX we could potentially not define these pmFPAfiles if no output is requested...
+    bin1->save = options->Bin1FITS;
+    bin2->save = options->Bin2FITS;
+    jpg1->save = options->Bin1JPEG;
+    jpg2->save = options->Bin2JPEG;
+
+    // Chip selection: turn on only the chips specified (pass status to suppress missing-key log msg)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS");
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	pmFPASelectChip (input->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(input->fpa, chipNum, false)) {
+		psError(PS_ERR_IO, false, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+
+    return (options);
+}
