/**
 **
 ** CamerasInterface.java - Hardware interface for Elphel camera array control
 **
 ** Copyright (C) 2019-2024 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  CamerasInterface.java is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  This program is distributed in the hope that it will be useful,
 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 **  GNU General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */
package com.elphel.imagej.calibration.hardware;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
//import org.xml.sax.SAXParseException;

import com.elphel.imagej.calibration.MatchSimulatedPattern;
import com.elphel.imagej.calibration.UVLEDandLasers;
import com.elphel.imagej.common.WindowTools;
import com.elphel.imagej.jp4.JP46_Reader_camera;

import ij.IJ;
import ij.ImagePlus;
import ij.Prefs;
import ij.gui.GenericDialog;
import ij.io.FileSaver;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;

public class CamerasInterface{
//		JP46_Reader_camera JP4_INSTANCE= new JP46_Reader_camera(false);
		public LaserPointersHardware laserPointers=null;
		private int masterSubCamera=0; // "master" camera index of IP in the list
		private int masterPort=     0; // "master" camera port (0..3) to apply trigger to
		private JP46_Reader_camera [] jp4_Instances=null;
		private String [] resetURLs=null;
		private String [] imageURLs=null;
		private String [] metaURLs=null;
		private String    triggerURL="";
		private boolean [] flipImages=null;
		private ImagePlus [] images=   null; // to reuse same instances
		private ImagePlus [] imagesIP= null;
// TODO: when saving/restoring save cameraSubnet, iBaseIP, cameraIPs, so any IPs are OK through config, generate - sequential
		private String cameraSubnet="192.168.0.";
		private int iBaseIP=236;
        private String [] cameraIPs = null; // since nc393 port is a part of cameraIPs[]
        private int [] channelIPPort =    null; // index in camareIPs (each IP/port combination) for each individual sensor
        private int imgsrvPort=8081;
        private String resetURLcmd="towp/save/pointers"; // advance buffer, next time will wait for the next frame acquired
// will return XML, just "trig" - 1x1 GIF
        private String triggerURLcmd="trig/pointers"; // TRIG=4 should be set in advance, this command will set in single-shot trigger mode
        private String imageURLcmd="torp/wait/bimg"; // will wait if needed. If repeated (as when reading Exif)- won't wait
        private String metaURLcmd="torp/wait/meta";  // will get XML, including timestamp
        private String lastTimestamp="";
		public int debugLevel=2;
		private double lastTemperature=Double.NaN;
		private int     colorMode=5; // JP4
		private boolean noWait=     true; // when false, IRQ_SMART=3 and the frame is available only 1 frame later, when true IRQ_SMART=6, frame is available after compression end
		private boolean nc393 =     false;
		private int     debugSensorNumber=-1; // increase debug level for this particular sensor
		private int     JPEGquality=99;  // JPEG quality
		private boolean cameraAutoExposure =    false;
		private boolean cameraAutoWhiteBalance =false;
		private String  cameraExtraURLCommon =  ""; // (should start with "&")
		private boolean setupTriggerMode=    false;
		private boolean externalTriggerCabling=false;
		private boolean noCabling=           false; // for single camera
		private double  cameraExposure=         5.0;
		private double  scaleExposureForLasers= 0.4;
		private double  scaleExposureForHeadLasers= 0.05;
		private double  cameraAutoExposureMax=  30.0; //Maximal autoexposure value
		private double  cameraGain =  2.0;
		private double  cameraRScale =1.25;
		private double  cameraBScale =1.5;
		private double  cameraGScale =1.0;
		private double [] cameraExposureCorr=null; // per-camera exposure correction1
		private double [] cameraGainCorr   = null;     // per-camera gain correction;
		private double [] cameraRScaleCorr = null;   // per-camera R/G scale correction;
		private double [] cameraBScaleCorr = null;   // per-camera B/G scale correction;
		private double [] cameraGScaleCorr = null;   // per-camera GB/G scale correction;
		private String [] cameraExtraURL =   null;   // per-camera extra URL (should start with "&")
		// these are initialized after being null, when the cameras are probed
		private int    []  cameraFrameNumber=null;
		private boolean [] triggeredMode=    null;   // true - triggered, false - free running
		private boolean [][] sensorPresent=  null;   // probe which sensors (of 3) are detected per system board (NC393 - per board/port)
		// TODO - try if skipping setting TRIG_PERIOD=0, IRQ_SMART=6 (when they are already set) will fix hanging
		private int [] triggerPeriod=        null;
		private int [] cameraMasterPort=     null;
		private int [] irqSmart=             null;
		private int [] motorsPosition=      null; // motors steps when the images were acquired (for null)
		private long startTime=System.nanoTime();
		private long lastTime=startTime;
		private long thisTime=startTime;
		public boolean reportTiming=false;
		public int cameraBootTimeSeconds=100;
		public int connectionTimeoutMilliseconds=3000;
		public boolean is_10398 = false;  

    	private void printTiming(String title){
    		if (this.reportTiming) {
    			this.thisTime=System.nanoTime();
    			System.out.println(title+ " done at "+IJ.d2s(0.000000001*(this.thisTime-this.startTime),3)+
    					" (+"+IJ.d2s(0.000000001*(this.thisTime-this.lastTime),3)+") sec");
    			this.lastTime=this.thisTime;
    		}
    	}
    	private void printTimingInit(){
    		this.startTime=System.nanoTime();
    		this.lastTime=this.startTime;
    		this.thisTime=this.startTime;
    	}

		private int [][] channelMap_23_393={ // ip index, channel number, port
				{0,0,3},{0,0,2},{0,0,0},{0,0,1},{1,0,3},{1,0,2},{1,0,0},{1,0,1},
				{0,1,3},{0,1,2},{0,1,0},{0,1,1},{1,1,3},{1,1,2},{1,1,0},{1,1,1},
				{0,2,3},{0,2,2},{0,2,0},{0,2,1},{1,2,3},{1,2,2},{1,2,0},{1,2,1},
				{2,0,2},{2,0,3}};


		private int [][] channelMap21={ // ip index, channel number
				{0,1,0},{0,0,0},{0,2,0},
				{1,1,0},{1,0,0},{1,2,0},
				{2,1,0},{2,0,0},{2,2,0},
				{3,1,0},{3,0,0},{3,2,0},
				{4,1,0},{4,0,0},{4,2,0},
				{5,1,0},{5,0,0},{5,2,0},
				{6,1,0},{6,0,0},{6,2,0}};
		private int [][] channelMap1={ // ip index, channel number
//				{0,-1}}; // negative channel - single camera
		{0,0,0}}; // Try with 0
		private int [][] channelMap2={ // ip index, channel number
		{0,0,0},{0,1,0}};
		private int [][] channelMap3={ // ip index, channel number
//				{0,-1}}; // negative channel - single camera
		{0,0,0},{1,0,0},{2,0,0}};
		private int [][] channelMap=null;
		public int maxNumberOfThreads=100;
		/**
		 * Initialize JP46_Reader_camera instances, one per sub-camera
		 */

        public CamerasInterface(int size, LaserPointersHardware laserPointers){
        	this.laserPointers=laserPointers;
        	initDefaultMap(size);
        	initIPs();
        	initJP4();
        	initCamParsDefaultArrays(this.cameraIPs.length);
        }
        public CamerasInterface(int size){
        	initDefaultMap(size);
        	initIPs();
        	initJP4();
        	initCamParsDefaultArrays(this.cameraIPs.length);
        }
        public void setNumberOfThreads(int n){
        	this.maxNumberOfThreads=n;
        }
        public int getSubCamera (int channelNumber){
        	return ((channelNumber>=0)&& (channelNumber<this.channelMap.length))?this.channelMap[channelNumber][0]:-1;
        }
        public int getSubChannel (int channelNumber){
        	return ((channelNumber>=0)&& (channelNumber<this.channelMap.length))?this.channelMap[channelNumber][1]:-1;
        }

        public int getSensorPort (int channelNumber){
        	return ((channelNumber>=0)&& (channelNumber<this.channelMap.length))?this.channelMap[channelNumber][2]:-1;
        }
        
        public boolean is14Mpix() {
        	return this.is_10398; 
        }
        
        // not used anywhere
        public int getChannel (int subCam, int subChn){
        	return getChannel (subCam, subChn, 0); // for compatibility with 353
        }

        public int getChannel (int subCam, int subChn, int port){
        	for (int channelNumber=0;channelNumber<this.channelMap.length;channelNumber++)
        		if ((this.channelMap[channelNumber][0]==subCam) &&
        				(this.channelMap[channelNumber][1]==subChn) &&
        				(this.channelMap[channelNumber][2]==port)) return  channelNumber;
        	return -1;
        }


		private void initJP4(){
			this.jp4_Instances=new JP46_Reader_camera[this.cameraIPs.length];
			this.resetURLs=new String [this.cameraIPs.length];
			this.imageURLs=new String [this.cameraIPs.length];
			this.metaURLs= new String [this.cameraIPs.length];
			// this.triggerURL is already defined
//			this.triggerURL="http://"+this.cameraIPs[this.masterSubCamera]+":"+(this.imgsrvPort+this.masterPort)+"/"+triggerURLcmd;

			this.images=   new ImagePlus[this.channelMap.length];
			this.imagesIP= new ImagePlus[this.cameraIPs.length];

			for (int i=0; i<this.cameraIPs.length;i++){
				this.jp4_Instances[i]=new JP46_Reader_camera(false);// invisible
//				this.jp4_Instances[i].camera_url="http://"+this.cameraIPs[i]+":"+this.imgsrvPort+"/";
				this.jp4_Instances[i].camera_url="http://"+this.cameraIPs[i]+"/";
				this.jp4_Instances[i].camera_img=    this.imageURLcmd; // not currently used
				this.jp4_Instances[i].camera_img_new=this.imageURLcmd; //"torp/wait/" will survive, only "towp/wait/" is removed for Exif re-read
				this.jp4_Instances[i].ABSOLUTELY_SILENT=true;
//				this.resetURLs[i]="http://"+this.cameraIPs[i]+":"+this.imgsrvPort+"/"+resetURLcmd;
//				this.imageURLs[i]="http://"+this.cameraIPs[i]+":"+this.imgsrvPort+"/"+this.imageURLcmd;
//				this.metaURLs[i]= "http://"+this.cameraIPs[i]+":"+this.imgsrvPort+"/"+metaURLcmd;
				this.resetURLs[i]="http://"+this.cameraIPs[i]+"/"+resetURLcmd;
				this.imageURLs[i]="http://"+this.cameraIPs[i]+"/"+this.imageURLcmd;
				this.metaURLs[i]= "http://"+this.cameraIPs[i]+"/"+metaURLcmd;
				this.imagesIP[i]= null;
			}
			for (int i=0; i<this.images.length;i++) this.images[i]= null;
		}

		private void initCamParsDefaultArrays(int num){
			this.cameraExposureCorr=new double[num];
			this.cameraGainCorr=    new double[num];
			this.cameraRScaleCorr=  new double[num];
			this.cameraBScaleCorr=  new double[num];
			this.cameraGScaleCorr=  new double[num];
			this.cameraExtraURL=    new String[num];
			for (int i=0; i<num;i++){
				this.cameraExposureCorr[i]=1.0;
				this.cameraGainCorr[i]=    1.0;
				this.cameraRScaleCorr[i]=  1.0;
				this.cameraBScaleCorr[i]=  1.0;
				this.cameraGScaleCorr[i]=  1.0;
				this.cameraExtraURL[i]=    "";
			}
		}


		/**
		 * Initialize cameraIPs from subNet and baseIP (sequentially)
		 */
		private void initIPs(){
			ArrayList<Integer> ip_ports_list= new ArrayList<Integer>();
			for (int i=0;i<this.channelMap.length;i++) {
				Integer ip_port=(this.channelMap[i][0]<<2) + this.channelMap[i][2];
				if (!ip_ports_list.contains(ip_port)) ip_ports_list.add(ip_port);
			}
			Collections.sort(ip_ports_list);
			this.cameraIPs =   new String [ip_ports_list.size()];
			this.channelIPPort = new int [this.channelMap.length];
			for (int i = 0; i<this.cameraIPs.length; i++){
				int ip_index= ip_ports_list.get(i)>>2;
				int sensor_port = ip_ports_list.get(i) & 3;
				this.cameraIPs[i] = this.cameraSubnet+(this.iBaseIP + ip_index) + ":"+ (this.imgsrvPort+sensor_port);
				for (int j = 0; j<this.channelMap.length; j++){
					if ((this.channelMap[j][0] == ip_index) && (this.channelMap[j][2] == sensor_port)) {
						this.channelIPPort[j] = i;
					}
				}
			}

			this.triggerURL="http://"+this.cameraSubnet+(this.iBaseIP+this.masterSubCamera)+":"+
				(this.imgsrvPort+ (this.masterPort & 3))+"/"+triggerURLcmd;
			if (this.debugLevel>2) System.out.println("DEBUG393: initIPs(): this.triggerURL ="+this.triggerURL);
		}
/*
//pre nc393
		private void initIPs(){
			if (this.debugLevel>2) System.out.println("initIPs(): this.iBaseIP=" + this.iBaseIP );
			int size=0;
			for (int i=0;i<this.channelMap.length;i++) if (this.channelMap[i][0]>size) size=this.channelMap[i][0];
			size++;
			this.cameraIPs=new String [size];
			for (int i=0;i<size;i++) this.cameraIPs[i]=this.cameraSubnet+(this.iBaseIP+i);
//			this.masterSubCamera=0;
		}
 */
		/**
		 * Initialize default subcamera map
		 * @param size number of subcameras
		 */
		private void initDefaultMap(int size){
			this.channelMap=new int [size][];
			this.flipImages=new boolean[size];
			int []port_seq={1,0,2,3};
			this.masterSubCamera=0;
			this.masterPort=0;
			if (this.nc393){
				this.imgsrvPort=2323;
				this.resetURLcmd="towp/save/pointers"; // advance buffer, next time will wait for the next frame acquired
				this.imageURLcmd="torp/wait/timestamp_name/bimg"; // will wait if needed. If repeated (as when reading Exif)- won't wait
				this.metaURLcmd="torp/wait/meta";  // will get XML, including timestamp
				if (size == 26) {
					for (int i=0;i<size;i++){
						this.channelMap[i]=channelMap_23_393[i].clone();
						this.flipImages[i]=false;
					}
					this.masterSubCamera = 2;
					this.masterPort=2;
				} else for (int i=0;i<size;i++){
					this.flipImages[i]=false;
					this.channelMap[i]=new int[3];
					this.channelMap[i][0]= i >> 2;
					this.channelMap[i][1]= 0;
					if (size <4) {
						this.channelMap[i][2]= i;
					} else {
						this.channelMap[i][2]= port_seq[i & 3];
					}
				}
			} else {
				if (size==1) { // single camera - old lens focusing
					this.channelMap[0]=channelMap1[0].clone();
					this.flipImages[0]=true;
				} else if (size==2){ // New lens focusing machine
					this.channelMap[0]=channelMap2[0].clone();
					this.flipImages[0]=true;  // main sensor under test
					this.channelMap[1]=channelMap2[1].clone();
					this.flipImages[1]=false; // extra sensor for location

				} else if (size==3){
					for (int i=0;i<size;i++){
						this.flipImages[i]=false;
						int i0=((i>=this.channelMap3.length)?(this.channelMap3.length-1):i);
						//					 this.channelMap[i]=this.channelMap21[i0].clone();
						this.channelMap[i]=this.channelMap3[i0].clone();
					}
				} else for (int i=0;i<size;i++){
					this.flipImages[i]=false;
					int i0=((i>=this.channelMap21.length)?(this.channelMap21.length-1):i);
					this.channelMap[i]=this.channelMap21[i0].clone();
				}
			}
		}

		public void setProperties(String prefix,Properties properties){
    		properties.setProperty(prefix+"cameraSubnet",this.cameraSubnet);
    		properties.setProperty(prefix+"iBaseIP",this.iBaseIP+"");
    		properties.setProperty(prefix+"masterSubCamera",this.masterSubCamera+"");
    		properties.setProperty(prefix+"masterPort",this.masterPort+"");
    		properties.setProperty(prefix+"cameraBootTimeSeconds",this.cameraBootTimeSeconds+"");
    		properties.setProperty(prefix+"connectionTimeoutMilliseconds",this.connectionTimeoutMilliseconds+"");
    		properties.setProperty(prefix+"imgsrvPort",this.imgsrvPort+"");
    		properties.setProperty(prefix+"resetURLcmd",this.resetURLcmd);
    		properties.setProperty(prefix+"triggerURLcmd",this.triggerURLcmd);
    		properties.setProperty(prefix+"imageURLcmd",this.imageURLcmd);
    		properties.setProperty(prefix+"metaURLcmd",this.metaURLcmd);
    		properties.setProperty(prefix+"channelMap.length",this.channelMap.length+"");
    		for (int i=0;i<this.channelMap.length;i++) {
            		properties.setProperty(prefix+"channelMap_"+i+"_IPindex",   this.channelMap[i][0]+"");
            		properties.setProperty(prefix+"channelMap_"+i+"_subchannel",this.channelMap[i][1]+"");
            		properties.setProperty(prefix+"channelMap_"+i+"_port",      this.channelMap[i][2]+"");
            		properties.setProperty(prefix+"flipImages_"+i              ,this.flipImages[i]?"1":"0");
    		}

    		properties.setProperty(prefix+"cameraIPs.length",this.cameraIPs.length+"");
    		properties.setProperty(prefix+"colorMode",this.colorMode+"");
    		properties.setProperty(prefix+"noWait",this.noWait+"");
    		properties.setProperty(prefix+"nc393",this.nc393+"");
    		properties.setProperty(prefix+"debugSensorNumber",this.debugSensorNumber+"");
    		properties.setProperty(prefix+"JPEGquality",this.JPEGquality+"");
    		properties.setProperty(prefix+"cameraAutoExposure",this.cameraAutoExposure+"");
    		properties.setProperty(prefix+"cameraAutoWhiteBalance",this.cameraAutoWhiteBalance+"");
    		properties.setProperty(prefix+"cameraExtraURLCommon","<![CDATA["+this.cameraExtraURLCommon+"]]>");
    		properties.setProperty(prefix+"setupTriggerMode",this.setupTriggerMode+"");
    		properties.setProperty(prefix+"externalTriggerCabling",this.externalTriggerCabling+"");
    		properties.setProperty(prefix+"noCabling",this.noCabling+"");
    		properties.setProperty(prefix+"cameraExposure",this.cameraExposure+"");
    		properties.setProperty(prefix+"scaleExposureForLasers",this.scaleExposureForLasers+"");
    		properties.setProperty(prefix+"scaleExposureForHeadLasers",this.scaleExposureForHeadLasers+"");
    		properties.setProperty(prefix+"cameraAutoExposureMax",cameraAutoExposureMax+"");
    		properties.setProperty(prefix+"cameraGain",this.cameraGain+"");
    		properties.setProperty(prefix+"cameraRScale",this.cameraRScale+"");
    		properties.setProperty(prefix+"cameraBScale",this.cameraBScale+"");
    		properties.setProperty(prefix+"cameraGScale",this.cameraGScale+"");
			for (int i=0;i<this.cameraIPs.length;i++){
        		properties.setProperty(prefix+"cameraIPs_"+i,this.cameraIPs[i]+"");
        		properties.setProperty(prefix+"cameraExposureCorr_"+i,this.cameraExposureCorr[i]+"");
        		properties.setProperty(prefix+"cameraGainCorr_"+i,   this.cameraGainCorr[i]+"");
        		properties.setProperty(prefix+"cameraRScaleCorr_"+i,   this.cameraRScaleCorr[i]+"");
        		properties.setProperty(prefix+"cameraBScaleCorr_"+i,   this.cameraBScaleCorr[i]+"");
        		properties.setProperty(prefix+"cameraGScaleCorr_"+i,   this.cameraGScaleCorr[i]+"");
        		properties.setProperty(prefix+"cameraExtraURL_"+i,   "<![CDATA["+this.cameraExtraURL[i]+"]]>");

			}
    	}
     	public void getProperties(String prefix,Properties properties){
    		if (properties.getProperty(prefix+"channelMap.length")!=null) {
    			// next initializes default values, so it should be before reading them from saved properties
    			initDefaultMap (Integer.parseInt(properties.getProperty(prefix+"channelMap.length")));
    			this.flipImages=new boolean[this.channelMap.length];
        		for (int i=0;i<this.channelMap.length;i++) {
            		if (properties.getProperty(prefix+"channelMap_"+i+"_IPindex")!=null)
            			this.channelMap[i][0]=Integer.parseInt(properties.getProperty(prefix+"channelMap_"+i+"_IPindex"));
            		if (properties.getProperty(prefix+"channelMap_"+i+"_subchannel")!=null)
            			this.channelMap[i][1]=Integer.parseInt(properties.getProperty(prefix+"channelMap_"+i+"_subchannel"));
            		if (properties.getProperty(prefix+"channelMap_"+i+"_port")!=null)
            			this.channelMap[i][2]=Integer.parseInt(properties.getProperty(prefix+"channelMap_"+i+"_port"));
            		if (properties.getProperty(prefix+"flipImages_"+i)!=null)
            			this.flipImages[i]=(Integer.parseInt(properties.getProperty(prefix+"flipImages_"+i))>0);
        		}
    		}

    		int numCams=0;
    		if (properties.getProperty(prefix+"cameraIPs.length")!=null) {
    			numCams=Integer.parseInt(properties.getProperty(prefix+"cameraIPs.length"));
    			this.cameraIPs=new String[numCams];
        		for (int i=0;i<numCams;i++) {
            		if (properties.getProperty(prefix+"cameraIPs_"+i)!=null)
            			this.cameraIPs[i]=properties.getProperty(prefix+"cameraIPs_"+i);

        		}
    		}
    		initCamParsDefaultArrays(numCams);

    		if (properties.getProperty(prefix+"cameraSubnet")!=null)
    			this.cameraSubnet=properties.getProperty(prefix+"cameraSubnet");
    		if (properties.getProperty(prefix+"iBaseIP")!=null)
    			this.iBaseIP=Integer.parseInt(properties.getProperty(prefix+"iBaseIP"));
    		if (properties.getProperty(prefix+"masterSubCamera")!=null)
    			this.masterSubCamera=Integer.parseInt(properties.getProperty(prefix+"masterSubCamera"));
    		if (properties.getProperty(prefix+"masterPort")!=null)
    			this.masterPort=Integer.parseInt(properties.getProperty(prefix+"masterPort"));
    		if (properties.getProperty(prefix+"cameraBootTimeSeconds")!=null)
    			this.cameraBootTimeSeconds=Integer.parseInt(properties.getProperty(prefix+"cameraBootTimeSeconds"));
    		if (properties.getProperty(prefix+"connectionTimeoutMilliseconds")!=null)
    			this.connectionTimeoutMilliseconds=Integer.parseInt(properties.getProperty(prefix+"connectionTimeoutMilliseconds"));
    		if (properties.getProperty(prefix+"imgsrvPort")!=null)
    			this.imgsrvPort=Integer.parseInt(properties.getProperty(prefix+"imgsrvPort"));

    		if (properties.getProperty(prefix+"resetURLcmd")!=null)
    			this.resetURLcmd=properties.getProperty(prefix+"resetURLcmd");
    		if (properties.getProperty(prefix+"triggerURLcmd")!=null)
    			this.triggerURLcmd=properties.getProperty(prefix+"triggerURLcmd");
    		if (properties.getProperty(prefix+"imageURLcmd")!=null)
    			this.imageURLcmd=properties.getProperty(prefix+"imageURLcmd");
    		if (properties.getProperty(prefix+"metaURLcmd")!=null)
    			this.metaURLcmd=properties.getProperty(prefix+"metaURLcmd");


// both defaults were here



			if (properties.getProperty(prefix+"colorMode")!=null)
				this.colorMode=Integer.parseInt(properties.getProperty(prefix+"colorMode"));
			if (properties.getProperty(prefix+"debugSensorNumber")!=null)
				this.debugSensorNumber=Integer.parseInt(properties.getProperty(prefix+"debugSensorNumber"));
			if (properties.getProperty(prefix+"JPEGquality")!=null)
				this.JPEGquality=Integer.parseInt(properties.getProperty(prefix+"JPEGquality"));
			if (properties.getProperty(prefix+"noWait")!=null)
				this.noWait=Boolean.parseBoolean(properties.getProperty(prefix+"noWait"));
			if (properties.getProperty(prefix+"nc393")!=null)
				this.nc393=Boolean.parseBoolean(properties.getProperty(prefix+"nc393"));
			if (properties.getProperty(prefix+"cameraAutoExposure")!=null)
				this.cameraAutoExposure=Boolean.parseBoolean(properties.getProperty(prefix+"cameraAutoExposure"));
			if (properties.getProperty(prefix+"cameraAutoWhiteBalance")!=null)
				this.cameraAutoWhiteBalance=Boolean.parseBoolean(properties.getProperty(prefix+"cameraAutoWhiteBalance"));
    		if (properties.getProperty(prefix+"cameraExtraURLCommon")!=null) {
    			this.cameraExtraURLCommon=properties.getProperty(prefix+"cameraExtraURLCommon");
				if ((this.cameraExtraURLCommon.length()>10) && this.cameraExtraURLCommon.substring(0,9).equals("<![CDATA["))
					this.cameraExtraURLCommon=this.cameraExtraURLCommon.substring(9,this.cameraExtraURLCommon.length()-3);
    		}
			if (properties.getProperty(prefix+"setupTriggerMode")!=null)
				this.setupTriggerMode=Boolean.parseBoolean(properties.getProperty(prefix+"setupTriggerMode"));
			if (properties.getProperty(prefix+"externalTriggerCabling")!=null)
				this.externalTriggerCabling=Boolean.parseBoolean(properties.getProperty(prefix+"externalTriggerCabling"));
			if (properties.getProperty(prefix+"noCabling")!=null)
				this.noCabling=Boolean.parseBoolean(properties.getProperty(prefix+"noCabling"));
			if (properties.getProperty(prefix+"cameraExposure")!=null)
				this.cameraExposure=Double.parseDouble(properties.getProperty(prefix+"cameraExposure"));
			if (properties.getProperty(prefix+"scaleExposureForLasers")!=null)
				this.scaleExposureForLasers=Double.parseDouble(properties.getProperty(prefix+"scaleExposureForLasers"));
			if (properties.getProperty(prefix+"scaleExposureForHeadLasers")!=null)
				this.scaleExposureForHeadLasers=Double.parseDouble(properties.getProperty(prefix+"scaleExposureForHeadLasers"));

			if (properties.getProperty(prefix+"cameraAutoExposureMax")!=null)
				this.cameraAutoExposureMax=Double.parseDouble(properties.getProperty(prefix+"cameraAutoExposureMax"));
			if (properties.getProperty(prefix+"cameraGain")!=null)
				this.cameraGain=Double.parseDouble(properties.getProperty(prefix+"cameraGain"));
			if (properties.getProperty(prefix+"cameraRScale")!=null)
				this.cameraRScale=Double.parseDouble(properties.getProperty(prefix+"cameraRScale"));
			if (properties.getProperty(prefix+"cameraBScale")!=null)
				this.cameraBScale=Double.parseDouble(properties.getProperty(prefix+"cameraBScale"));
			if (properties.getProperty(prefix+"cameraGScale")!=null)
				this.cameraGScale=Double.parseDouble(properties.getProperty(prefix+"cameraGScale"));

    		for (int i=0;i<numCams;i++) {
        		if (properties.getProperty(prefix+"cameraExposureCorr_"+i)!=null)
        			this.cameraExposureCorr[i]=Double.parseDouble(properties.getProperty(prefix+"cameraExposureCorr_"+i));
        		if (properties.getProperty(prefix+"cameraGainCorr_"+i)!=null)
        			this.cameraGainCorr[i]=Double.parseDouble(properties.getProperty(prefix+"cameraGainCorr_"+i));
        		if (properties.getProperty(prefix+"cameraRScaleCorr_"+i)!=null)
        			this.cameraRScaleCorr[i]=Double.parseDouble(properties.getProperty(prefix+"cameraRScaleCorr_"+i));
        		if (properties.getProperty(prefix+"cameraBScaleCorr_"+i)!=null)
        			this.cameraBScaleCorr[i]=Double.parseDouble(properties.getProperty(prefix+"cameraBScaleCorr_"+i));
        		if (properties.getProperty(prefix+"cameraGScaleCorr_"+i)!=null)
        			this.cameraGScaleCorr[i]=Double.parseDouble(properties.getProperty(prefix+"cameraGScaleCorr_"+i));
        		if (properties.getProperty(prefix+"cameraExtraURL_"+i)!=null) {
        			this.cameraExtraURL[i]=properties.getProperty(prefix+"cameraExtraURL_"+i);
    				if ((this.cameraExtraURL[i].length()>10) && this.cameraExtraURL[i].substring(0,9).equals("<![CDATA["))
    					this.cameraExtraURL[i]=this.cameraExtraURL[i].substring(9,this.cameraExtraURL[i].length()-3);
        		}

    		}
    		initIPs(); // was missing here?
    		initJP4();
     	}

	   	public boolean showDialog(String title, int numCams, boolean askRegenerate) { // numCams<=0 -> do not initialize number
	   		if (numCams>0){
//				 initDefaultMap(1);
				 initDefaultMap(numCams);
				 initIPs();
		         initCamParsDefaultArrays(this.cameraIPs.length);
		    	 initJP4();
	   		}
//http://192.168.0.223/parsedit.php?title=PHASES&SENSOR_PHASE&MULTI_PHASE1&MULTI_PHASE2&MULTI_PHASE3&DEBUG&CABLE_TIM&FPGA_TIM0&FPGA_TIM1&DLY359_P1&DLY359_P2&DLY359_P3&DLY359_C1&DLY359_C2&DLY359_C3&SENSOR&FRAME_SIZE=@&refresh
	   		//http://192.168.0.228/parsedit.php?immediate&EXPOS=25000*0&AUTOEXP_ON=0*0&WB_EN=0*0
	   		//http://192.168.0.221/parsedit.php?immediate&EXPOS=25000*0&AUTOEXP_ON=0*0&WB_EN=0*0&COLOR=1*0&GAINR=0x20000*0&GAING=0x20000*0&GAINB=0x36666*0&GAINGB=0x20000*0
	   		//http://192.168.0.221/parsedit.php?title=PHASES&SENSOR_PHASE&MULTI_PHASE1&MULTI_PHASE2&MULTI_PHASE3&FRAME_SIZE=@
//http://192.168.0.222/parsedit.php?title=day&immediate&EXPOS=20000*0&AUTOEXP_ON=0*0&WB_EN=0*0&COLOR=1*0&GAINR=0x270a4*0&GAING=0x20000*0&GAINB=0x2999a*0&GAINGB=0x20000*0
//http://192.168.0.236/parsedit.php?title=night&immediate&EXPOS=13000*0&AUTOEXP_ON=0*0&WB_EN=0*0&COLOR=1*0&GAINR=0x1fb35*0&GAING=0x20000*0&GAINB=0x3961c*0&GAINGB=0x20000*0
//http://192.168.0.221/parsedit.php?title=bright-day&immediate&EXPOS=10000*0&AUTOEXP_ON=0*0&WB_EN=0*0&COLOR=1*0&GAINR=0x270a4*0&GAING=0x20000*0&GAINB=0x2999a*0&GAINGB=0x20000*0
//http://192.168.0.221/parsedit.php?immediate&TRIG&TRIG_PERIOD
    		GenericDialog gd = new GenericDialog(title);
    		gd.addCheckbox    ("NC393 (unchecked - nc353)", this.nc393);
    		gd.addStringField ("Subnet of the cameras (3 first of the four IPv4 address ending with '.')",this.cameraSubnet,12);
    		gd.addNumericField("Last byte of the first sub-camera IP address)",this.iBaseIP,0);
    		gd.addNumericField("Camera boot time",this.cameraBootTimeSeconds,0,3,"sec");
    		gd.addNumericField("Network connection timeout",this.connectionTimeoutMilliseconds,0,3,"ms");

    		gd.addNumericField("Index (in IP table) of the master camera (used for triggering)",this.masterSubCamera,0);
    		gd.addNumericField("Master (used for triggering), normally lowest connected port (0..3)",this.masterPort,0);
    		gd.addNumericField("Image server port number)",this.imgsrvPort,0);
    		gd.addStringField ("Image server command to reset image buffer",this.resetURLcmd,25);
    		gd.addStringField ("Image server command to trigger acquisition",this.triggerURLcmd,25);
    		gd.addStringField ("Image server command to acquire image (waits for the new one after reset)",this.imageURLcmd,25);
    		gd.addStringField ("Image server command to receive XML metadata (with timestamp)",this.metaURLcmd,25);
    		gd.addMessage("Configure each sub-camera - which IP index and channel does it use.");
    		if (askRegenerate) gd.addMessage("You may change number of subcameras and press REGENERATE below");
    		for (int i=0; i< this.channelMap.length;i++){
    			gd.addMessage("---------------------------------------------------------");
        		gd.addNumericField("Subcamera "+(i+1)+" IP index (starting from 0)", this.channelMap[i][0],0);
        		gd.addNumericField("Subcamera "+(i+1)+" port (0..3)",                this.channelMap[i][2],0);
        		gd.addNumericField("Subcamera "+(i+1)+" channel (0,1 or 2)",         this.channelMap[i][1],0);
        		gd.addCheckbox("Subcamera "+(i+1)+" - used mirror",                  this.flipImages[i]);
    		}
			gd.addMessage("---------------------------------------------------------");
    		if (askRegenerate) {
        		gd.addCheckbox ("Individually overwrite IP addresses",false);
    			gd.addNumericField("Number of subcameras (need to press REGENERATE button to change)", this.channelMap.length, 0);
    			gd.enableYesNoCancel("OK", "REGENERATE");
    		}
    	    WindowTools.addScrollBars(gd);
    	    gd.showDialog();
    	    if (gd.wasCanceled()) return false;
    	    this.nc393=                     gd.getNextBoolean();
    	    this.cameraSubnet=              gd.getNextString();
    	    if (!this.cameraSubnet.endsWith(".")) this.cameraSubnet += ".";
    	    int bip=                  (int) gd.getNextNumber();
    	    if ((bip>0) && (bip<255) && (bip!=this.iBaseIP)){
    	    	this.iBaseIP=bip;
    	    	initIPs();
    	    }
    		this.cameraBootTimeSeconds=(int) gd.getNextNumber();
    		this.connectionTimeoutMilliseconds=(int) gd.getNextNumber();
    	    this.masterSubCamera=     (int) gd.getNextNumber(); // should be after initIPs()!
    	    this.masterPort=          (int) gd.getNextNumber(); // should be after initIPs()!

    	    this.imgsrvPort=          (int) gd.getNextNumber();
    	    this.resetURLcmd=            gd.getNextString();
    	    this.triggerURLcmd=          gd.getNextString();
    	    this.imageURLcmd=            gd.getNextString();
    	    this.metaURLcmd=             gd.getNextString();
    		for (int i=0; i< this.channelMap.length;i++){
        		this.channelMap[i][0]=(int) gd.getNextNumber();
        		this.channelMap[i][2]=(int) gd.getNextNumber();
        		this.channelMap[i][1]=(int) gd.getNextNumber();
        		this.flipImages[i]=         gd.getNextBoolean();
    		}
    		if (askRegenerate) {
    			boolean overwriteIPs=gd.getNextBoolean();
    			int newSubCams=(int) gd.getNextNumber();
//    			if (!gd.wasOKed() &&(newSubCams!=this.channelMap.length)) {
       			if (!gd.wasOKed()) { // regenerate always if pressed, even if number is the same
//    				 initDefaultMap(newSubCams);
//    				 initIPs();
    				 int [][] backupMap=this.channelMap;
    				 if (!showDialog(title, newSubCams, false)) {
    					 this.channelMap=backupMap;
    					 return false; // channelMap is restored, but some other fields may be broken
    				 }
    			}
    			if (overwriteIPs && !editSubCamerasIPs()) return false;
    		}
	   		return true;
	   	}
	   	/**
	   	 * Wait for camera to boot
	   	 * @param chn - camera channel number (0)
	   	 * @return  -1 if camera is not yet set to trigger mode, else - last frame number acquired
	   	 * throws on timeout
	   	 */
	   	public int getCurrentFrameNumberWithTimeout(int chn, boolean showStatus, AtomicInteger stopRequested){
			long startTime=System.nanoTime();
			double dTimeOut=1E9* this.cameraBootTimeSeconds;
			long endTime=startTime+(long) dTimeOut;
			while (true){
				if (probeCameraState(chn, showStatus, true,this.connectionTimeoutMilliseconds)){
					if (showStatus) IJ.showProgress(0.0);
					if (!this.triggeredMode[chn]) return -1;
					if (this.triggerPeriod[chn]>1) return -1; // trigger period is not set to 1 - not yet programmed
					return this.cameraFrameNumber[chn];
				}
				long time=System.nanoTime();
				if (time>endTime){
					if (showStatus) IJ.showProgress(0.0);
					throw new IllegalArgumentException ("Timeout while waiting for the camera #"+chn+" to respond");
				}
				if (stopRequested.get()>0) {
					System.out.println("User requested stop");
					if (showStatus) IJ.showProgress(0.0);
					throw new IllegalArgumentException ("Waiting for camera #"+chn+" aborted by user request");
				}
				if (showStatus) IJ.showProgress((time-startTime)/dTimeOut);
			}
	   	}

	   	public String getSerialNumber(int chn, int EEPROM_chn){
	   		String [] ser_mod = getSerialNumberModel(chn, EEPROM_chn);
	   		if ((ser_mod==null) || (ser_mod.length < 1)) return null;
	   		return ser_mod[0]; 
	   	}
	   	public String getModelNumber(int chn, int EEPROM_chn){
	   		String [] ser_mod = getSerialNumberModel(chn, EEPROM_chn);
	   		if ((ser_mod==null) || (ser_mod.length < 2)) return null;
	   		return ser_mod[1]; 
	   	}
	   	public String []  getSerialNumberModel(int chn, int EEPROM_chn){
	   		int colon_index = this.cameraIPs[chn].indexOf(":");
	   		int sensor_port = Integer.parseInt(this.cameraIPs[chn].substring(colon_index+1)) - this.imgsrvPort;
	   		String ip=this.cameraIPs[chn].substring(0, colon_index);
	   		//	   		String url="http://"+this.cameraIPs[chn]+"/i2c.php?cmd=fromEEPROM0&EEPROM_chn="+EEPROM_chn;
	   		//	   		ip = "192.168.0.236";
	   		String url="http://"+ip+"/i2c.php?cmd=fromEEPROM" + sensor_port+ "&EEPROM_chn="+EEPROM_chn;
	   		Document dom=null;
	   		String serial=null;
	   		String model=null;
	   		try {
	   			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	   			DocumentBuilder db = dbf.newDocumentBuilder();

	   			InputStream in = new URL( url ).openStream();

	   			byte[] bytes = new byte[0];
	   			try {
	   				bytes = IOUtils.toByteArray(in);
	   			} finally {
	   				IOUtils.closeQuietly(in); // always gets here
	   			}
	   			for (byte b:bytes) {
	   				if (b < 0) {
	   					String msg="Sensor EEPROM is not initialized. Use bootblock393 to initialize it. Will throw now";
	   					IJ.showMessage("Error",msg);
	   					System.out.println(msg);
	   					throw new IllegalArgumentException ("SAX error");
	   					//	    					return null;
	   				}
	   			}
	   			dom = db.parse(url);
	   			if (!dom.getDocumentElement().getNodeName().equals("board")) {
	   				String msg="Root element: expected 'board', got \"" + dom.getDocumentElement().getNodeName()+"\"";
	   				IJ.showMessage("Error",msg);
	   				throw new IllegalArgumentException (msg);
	   			}

	   			serial=(dom.getDocumentElement().getElementsByTagName("serial").item(0).getChildNodes().item(0)).getNodeValue();
	   			// remove opening and closing "
	   			if (serial==null){
	   				String msg="Could not read tag <serial>";
	   				IJ.showMessage("Error",msg);
	   				System.out.println(msg);
	   				return null;
	   			}
	   			if (serial.startsWith("\"")){
	   				serial=serial.substring(1, serial.length()-1);
	   			}
	   			model=(dom.getDocumentElement().getElementsByTagName("model").item(0).getChildNodes().item(0)).getNodeValue();
	   			if (model !=null){
	   				if (model.startsWith("\"")){
	   					model=model.substring(1, model.length()-1);
	   				}
	   			}

	   		} catch(MalformedURLException e){
	   			String msg="Please check the URL:" + e.toString();
	   			IJ.showMessage("Error",msg);
	   			throw new IllegalArgumentException (msg);
	   		} catch(IOException  e1){
	   			String msg = e1.getMessage();
	   			if (msg==null || msg.equals(""))  msg = ""+e1;
	   			IJ.showMessage("Error",msg);
	   			throw new IllegalArgumentException (msg);
	   		}catch(ParserConfigurationException pce) {
	   			pce.printStackTrace();
	   			throw new IllegalArgumentException ("PCE error");
	   		}catch(SAXException se) {
	   			se.printStackTrace();
	   			throw new IllegalArgumentException ("SAX error");
	   			//    			}catch (SAXParseException spe) {

	   		}
	   		this.is_10398 = "10398".equals(model);
	   		return new String[] {serial, model};
	   	}

	   	public double getSensorTemperature(int chn, int EEPROM_chn){
	   		// Need to configure camera to be able to read temperature
	   		if (sensorPresent==null) { // cameras were not probed/configured
				probeCameraState(); // testing detection
		   		printTiming("=== probeCameraState()");
				setupCameraAcquisition();
		   		printTiming("=== setupCameraAcquisition()");
	   		}
	   		if (!this.sensorPresent[chn][0] && !this.sensorPresent[chn][1] && !this.sensorPresent[chn][2]) EEPROM_chn=0; // no 10359 - null pointer while "lens center" if first

	   		int colon_index = this.cameraIPs[chn].indexOf(":");
	   		int sensor_port = Integer.parseInt(this.cameraIPs[chn].substring(colon_index+1)) - this.imgsrvPort;
	   		String ip=this.cameraIPs[chn].substring(0, colon_index);
//	   		String url="http://"+this.cameraIPs[chn]+"/i2c.php?cmd=fromEEPROM0&EEPROM_chn="+EEPROM_chn;
	   		String url="http://"+ip+"/i2c.php?cmd=fromEEPROM"+ sensor_port +"&EEPROM_chn="+EEPROM_chn;
	   		this.lastTemperature=Double.NaN;
	   		Document dom=null;
	   		try {
	   			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	   			DocumentBuilder db = dbf.newDocumentBuilder();
	   			dom = db.parse(url);
	   			if (!dom.getDocumentElement().getNodeName().equals("board")) {
	   				String msg="Root element: expected 'board', got \"" + dom.getDocumentElement().getNodeName()+"\"";
	   				IJ.showMessage("Error",msg);
	   				throw new IllegalArgumentException (msg);
	   			}

	   			String sTemperature=null;

	   			if ((dom.getDocumentElement().getElementsByTagName("sensorTemperature").item(0))!=null){
	   				sTemperature=(dom.getDocumentElement().getElementsByTagName("sensorTemperature").item(0).getChildNodes().item(0)).getNodeValue();
	   			}else{
	   				sTemperature= "0.0";
	   			}

	   			// remove opening and closing "
	   			if (sTemperature==null){
	   				String msg="Could not read sensor temperature";
	   				//    					IJ.showMessage("Error",msg);
	   				System.out.println("Warning: "+msg);
	   				return Double.parseDouble(sTemperature);
	   			}
	   			this.lastTemperature= Double.parseDouble(sTemperature);
	   		} catch(MalformedURLException e){
	   			String msg="Please check the URL:" + e.toString();
	   			IJ.showMessage("Error",msg);
	   			throw new IllegalArgumentException (msg);
	   		} catch(IOException  e1){
	   			String msg = e1.getMessage();
	   			if (msg==null || msg.equals(""))  msg = ""+e1;
	   			IJ.showMessage("Error",msg);
	   			throw new IllegalArgumentException (msg);
	   		}catch(ParserConfigurationException pce) {
	   			pce.printStackTrace();
	   			throw new IllegalArgumentException ("PCE error");
	   		}catch(SAXException se) {
	   			se.printStackTrace();
	   			throw new IllegalArgumentException ("SAX error");
	   		}
	   		return this.lastTemperature;
	   	}


//		private int [] motors=               null; // motors steps when the images were acquired (for null)
	   	public int [] getMotorsPosition() {return this.motorsPosition;} // may be null
	   	public void setMotorsPosition (int [] position) {this.motorsPosition=position;}
	   	public void resetInitialization(){
	   		System.out.println("resetInitialization()");
	   		this.sensorPresent=null;
	   	}
	   	public int probeCameraState(){
	   		int numOnline=0;
	   		int numSensors=0;
	   		int numChn=this.cameraIPs.length;
	   		initCameraArrays(numChn);
	   		/*
	   		this.cameraFrameNumber=new int[numChn];
	   		this.triggeredMode=new boolean[numChn];
	   		this.sensorPresent=new boolean[numChn][];
			// TODO - try if skipping setting TRIG_PERIOD=0, IRQ_SMART=6 (when they are already set) will fix hanging
	   		this.triggerPeriod=new int[numChn];
	   		this.irqSmart=     new int[numChn];
	   		*/


	   		for (int chn=0;chn<numChn;chn++){
		   		this.cameraFrameNumber[chn]=-1;
		   		this.triggeredMode[chn]=false;
		   		this.sensorPresent[chn]=null; // camera did not respond, {false,false,false} - single camera (no 10359)
		   		if (this.debugLevel>1) System.out.println("Probing camera "+chn+" ("+this.cameraIPs[chn]+")...");
		   		IJ.showStatus("Probing camera "+chn+" ("+this.cameraIPs[chn]+")...");
	   			if (probeCameraState(chn)) {
	   				numOnline++;
		   			printTiming("===== probing channel "+chn);
		   			boolean single_no_mux= (!this.sensorPresent[chn][0] && !this.sensorPresent[chn][1] && !this.sensorPresent[chn][2]);
			   		if (this.debugLevel>1) System.out.println("Frame number: "+this.cameraFrameNumber[chn]+
			   				", Trigger mode:"+(this.triggeredMode[chn]?"ex":"in")+"ternal, "+
			   				" sensors attached:"+(this.sensorPresent[chn][0]?"1 ":"")+(this.sensorPresent[chn][1]?"2 ":"")+(this.sensorPresent[chn][2]?"3 ":"")+
			   				(single_no_mux?"single-sensor, no multiplexer":"") +
			   				(" Master port "+this.cameraMasterPort[chn])
			   				);
			   		for (int i=0;i<this.sensorPresent[chn].length;i++) if (this.sensorPresent[chn][i]) numSensors++;
			   		if (single_no_mux) numSensors++;
	   			} else {
	   				if (this.debugLevel>1) System.out.println("Camera did not respond");
	   			}
	   		}
	   		IJ.showStatus("Found "+numOnline+" camera IP, "+numSensors+" sensors");
			if (this.debugLevel>1) System.out.println("Found "+numOnline+" camera IP, "+numSensors+" sensors");
	   		return numOnline;
	   	}
	   	public void initCameraArrays(int numChn){
	   		if (this.debugLevel>1) System.out.println("initCameraArrays("+numChn+")");
	   		this.cameraFrameNumber=new int[numChn];
	   		this.triggeredMode=    new boolean[numChn];
	   		this.sensorPresent=    new boolean[numChn][];
			// TODO - try if skipping setting TRIG_PERIOD=0, IRQ_SMART=6 (when they are already set) will fix hanging
	   		this.triggerPeriod=    new int[numChn];
	   		this.irqSmart=         new int[numChn];
	   		this.cameraMasterPort= new int[numChn]; // not sure if will use it - master port for each camera:port
	   	}
	   	public boolean probeCameraState(
	               int chn){
	                 return probeCameraState(chn, false, false,0);
	               }

		   	public boolean probeCameraState(
	               int chn,
	               boolean showStatus,
	               boolean doNotThrow,
	               int timeout // ms
	               ){
	   		//http://192.168.0.221/parsedit.php?immediate&TRIG&TRIG_PERIOD&FRAME
	   		String url;
	   		int colon_index = this.cameraIPs[chn].indexOf(":");
	   		int sensor_port = Integer.parseInt(this.cameraIPs[chn].substring(colon_index+1)) - this.imgsrvPort;
	   		String ip=this.cameraIPs[chn].substring(0, colon_index);

	   		if (this.nc393){
//	   			url="http://"+this.cameraIPs[chn]+"/parsedit.php?immediate&TRIG&TRIG_PERIOD&SENS_AVAIL&FRAME";
	   			url="http://"+ip+"/parsedit.php?sensor_port="+sensor_port+"&immediate&TRIG&TRIG_PERIOD&SENS_AVAIL&FRAME&TRIG_MASTER";
	   		} else {
//	   			url="http://"+this.cameraIPs[chn]+"/parsedit.php?immediate&TRIG&TRIG_PERIOD&IRQ_SMART&SENS_AVAIL&FRAME";
	   			url="http://"+ip+"/parsedit.php?immediate&TRIG&TRIG_PERIOD&IRQ_SMART&SENS_AVAIL&FRAME";
	   		}
	   		if (this.debugLevel>1) System.out.println("url="+url);
	   		Document dom=null;
	   		if ((this.cameraFrameNumber==null) ||    ((this.cameraFrameNumber.length<(chn+1)))) initCameraArrays(chn+1);
	   		// did not find yet - why cameraFrameNumber is not null, but sensorPresent is? Added next lines until find
	   		if ((this.triggeredMode==null) ||        ((this.triggeredMode.length<(chn+1)))) initCameraArrays(chn+1);
	   		if ((this.sensorPresent==null) ||        ((this.sensorPresent.length<(chn+1)))) initCameraArrays(chn+1);
	   		if ((this.triggerPeriod==null) ||        ((this.triggerPeriod.length<(chn+1)))) initCameraArrays(chn+1);
	   		if (!this.nc393) {
	   			if ((this.irqSmart==null) ||         ((this.irqSmart.length<(chn+1))))         initCameraArrays(chn+1);
	   		} else {
	   			if ((this.cameraMasterPort==null) || ((this.cameraMasterPort.length<(chn+1)))) initCameraArrays(chn+1);
	   		}
	   		try {
	   			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	   			DocumentBuilder db = dbf.newDocumentBuilder();
	   			if (timeout<=0){
	   			  dom = db.parse(url);
	   			} else {
	   	            URL uUrl = new URL(url);
	   	            URLConnection con = uUrl.openConnection();
	   	            con.setConnectTimeout(timeout);//The timeout in mills
	   	            dom = db.parse(con.getInputStream());
	   			}
	   			if (!dom.getDocumentElement().getNodeName().equals("parameters")) {
	   				String msg="Root element: expected 'parameters', got \"" + dom.getDocumentElement().getNodeName()+"\"";
	   				if (showStatus) IJ.showStatus(msg);
	   				if (doNotThrow) return false;
	   				IJ.showMessage("Error",msg);
	   				throw new IllegalArgumentException (msg);
	   			}
	   			//	   	    		String sNode;
	   			if (this.triggeredMode==null) System.out.println("this.triggeredMode==null");
	   			if (this.triggeredMode.length<(chn+1))  System.out.println("this.triggeredMode.length="+this.triggeredMode.length+" chn="+chn);

	   			this.triggeredMode[chn]= (Integer.parseInt((dom.getDocumentElement().getElementsByTagName("TRIG").item(0).getChildNodes().item(0)).getNodeValue())==4);
	   			int sensAvail= Integer.parseInt((dom.getDocumentElement().getElementsByTagName("SENS_AVAIL").item(0).getChildNodes().item(0)).getNodeValue());

	   			if (this.sensorPresent==null) System.out.println("sensorPresent==null");
	   			if (this.sensorPresent.length<(chn+1))  System.out.println("sensorPresent="+this.sensorPresent.length+" chn="+chn);

	   			this.sensorPresent[chn]=new boolean[3];
	   			for (int i=0;i<this.sensorPresent[chn].length;i++) this.sensorPresent[chn][i]=(sensAvail & (1<<i))!=0;
	   			this.triggerPeriod[chn]=     Integer.parseInt((dom.getDocumentElement().getElementsByTagName("TRIG_PERIOD").item(0).getChildNodes().item(0)).getNodeValue());
	   			if (!this.nc393) {
	   				this.irqSmart[chn]=          Integer.parseInt((dom.getDocumentElement().getElementsByTagName("IRQ_SMART").item(0).getChildNodes().item(0)).getNodeValue());
	   				this.cameraMasterPort[chn]=  sensor_port;
	   			} else {
	   				this.cameraMasterPort[chn]=  Integer.parseInt((dom.getDocumentElement().getElementsByTagName("TRIG_MASTER").item(0).getChildNodes().item(0)).getNodeValue());
	   			}
	   			this.cameraFrameNumber[chn]= Integer.parseInt((dom.getDocumentElement().getElementsByTagName("FRAME").item(0).getChildNodes().item(0)).getNodeValue());
	   		} catch(MalformedURLException e){
	   			String msg="Please check the URL:" + e.toString();
	   			IJ.showMessage("Error",msg);
   				if (showStatus) IJ.showStatus(msg);
   				if (doNotThrow) return false;
	   			throw new IllegalArgumentException (msg);
	   		} catch(IOException  e1){
	   			String msg = e1.getMessage();
	   			if (msg==null || msg.equals(""))  msg = ""+e1;
	   			msg="Camera "+chn+" ("+this.cameraIPs[chn]+") did not respond: "+msg; // "Connection refused"
   				if (showStatus) IJ.showStatus(msg);
   				if (doNotThrow) return false;
	   			IJ.showMessage("Error",msg);
	   			System.out.println(msg);
	   			return false;
	   		}catch(ParserConfigurationException pce) {
	   			pce.printStackTrace();
	   			String msg="PCE error "+pce.getMessage();
   				if (showStatus) IJ.showStatus(msg);
   				if (doNotThrow) return false;
	   			throw new IllegalArgumentException (msg);
	   		}catch(SAXException se) {
	   			se.printStackTrace();
	   			String msg="SAX error "+se.getMessage();
   				if (showStatus) IJ.showStatus(msg);
   				if (doNotThrow) return false;
	   			throw new IllegalArgumentException (msg);
	   		}
	   		return true;
	   	}
	   	public boolean setupCameraAcquisition(){
	   		return setupCameraAcquisition(Double.NaN);
	   	}
	   	public boolean setupCameraAcquisition(final double exposureScale){
//	   		final boolean mp14 = is14Mpix();
	   		final int ipLength=this.resetURLs.length;
	   		final boolean [] results=new boolean[ipLength];
	   		for (int chn=0;chn<ipLength;chn++) results[chn]=(this.sensorPresent[chn]!=null);
	   		final Thread[] threads = newThreadArray(this.maxNumberOfThreads);
	   		final AtomicInteger chnAtomic = new AtomicInteger(0);
	   		for (int ithread = 0; ithread < threads.length; ithread++) {
	   			threads[ithread] = new Thread() {
	   				@Override
					public void run() {
	   					for (int chn=chnAtomic.getAndIncrement(); chn<ipLength;chn=chnAtomic.getAndIncrement()) if (results[chn]){
	   						results[chn]=setupCameraAcquisition(chn,exposureScale);
	   					}
	   				}

	   			};
	   		}
	   		startAndJoin(threads);
	   		if (Double.isNaN(exposureScale)){ // full init

	   			int nRepeat=this.setupTriggerMode?4:1;
	   			int sleep_ms = 1000;
	   			if (is14Mpix()) {
	   				nRepeat=this.setupTriggerMode?4:2; // FIXME: Modified for 14Mpix
	   				sleep_ms = 2000;
	   			}
	   			if (this.nc393) nRepeat++; // is it needed?
	   			for (int i=0;i<nRepeat;i++){
	   				if (this.debugLevel>0) System.out.println((i+1)+" of "+nRepeat+": Triggering cameras to give parameters a chance to propagate");
	   				trigger();
	   				try
	   				{
	   					Thread.sleep( sleep_ms ); // ms // FIXME: Modified for 14Mpix
	   				}
	   				catch ( InterruptedException e )
	   				{
	   					System.out.println( "awakened prematurely" );
	   				}
	   			}
	   		} else if (is14Mpix()) {
   				try
   				{
   					Thread.sleep( 2000 ); // ms // FIXME: Modified for 14Mpix
   				}
   				catch ( InterruptedException e )
   				{
   					System.out.println( "awakened prematurely" );
   				}
   				trigger();
	   		}
	   		boolean result=true;
	   		for (int chn=0;chn<ipLength;chn++) if (this.sensorPresent[chn]!=null) result &=results[chn];
	   		return result;
	   	}
	   	public boolean setupCameraAcquisitionNoTHreads(double exposureScale){
	   		boolean result=true;
	   		for (int chn=0;chn<this.cameraIPs.length;chn++) if (this.sensorPresent[chn]!=null){
	   			result&=setupCameraAcquisition(chn,exposureScale);
	   		}
	   		// trigger ?
	   		if (Double.isNaN(exposureScale)){ // just a hack - if exposureScale is non-NaN only update exposure
			   trigger();
			//sleep (1000);
			  for ( long  endTime=System.nanoTime()+(1000000000);endTime<System.nanoTime(););
	   		}
	   		return result;
	   	}
	   	// TODO: issue several TRIG pulses after setting parameters?
	   	public boolean setupCameraAcquisition(int chn, double exposureScale){
	   		int colon_index = this.cameraIPs[chn].indexOf(":");
	   		int sensor_port = Integer.parseInt(this.cameraIPs[chn].substring(colon_index+1)) - this.imgsrvPort;
	   		String ip=this.cameraIPs[chn].substring(0, colon_index);
	   		boolean isMasterSubcamera = ip.equals(this.cameraSubnet+(this.iBaseIP+this.masterSubCamera));
	   		boolean isMasterPort =  (this.cameraMasterPort[chn] == sensor_port); // for 353 should always be master port
	   		if (isMasterSubcamera && (this.cameraMasterPort[chn] != this.masterPort)){
	   			System.out.println("Master port mismatch for camera "+ip+" (master) - parameters master port = "+
	   					this.masterPort+", camera responded with "+this.cameraMasterPort[chn] );
	   		}
	   		if (this.debugLevel>1) System.out.println("DEBUG393: chn="+chn+" sensor_port="+sensor_port+" ip= "+ip+" isMasterSubcamera="+isMasterSubcamera+ " isMasterPort="+isMasterPort);
	   		boolean exposureOnly=!Double.isNaN(exposureScale);
	   		int minExposure=10; // usec
	   		int maxExposure=1000000; //usec
	   		int minGain=(int) (0x10000*1.0);
	   		int maxGain=(int) (0x10000*15.75);
	   		int minScale=0;
	   		int maxScale=(int) (0x10000*4.0);
	   		int exposure= (int) (Math.round(1000*this.cameraExposure*this.cameraExposureCorr[chn])*(exposureOnly?exposureScale:1.0));
	   		int autoExposureMax= (int) (Math.round(1000*this.cameraAutoExposureMax*this.cameraExposureCorr[chn]));
	   		int gain=     (int) (Math.round(0x10000*this.cameraGain*this.cameraGainCorr[chn]));
	   		int rScale=   (int) (Math.round(0x10000*this.cameraRScale*this.cameraRScaleCorr[chn]));
	   		int bScale=   (int) (Math.round(0x10000*this.cameraBScale*this.cameraBScaleCorr[chn]));
	   		int gScale=   (int) (Math.round(0x10000*this.cameraGScale*this.cameraGScaleCorr[chn]));
	   		int autoExp=  this.cameraAutoExposure?1:0;
	   		int autoWB=   this.cameraAutoWhiteBalance?1:0;

	   		String extraURL="";
	   		if (this.cameraExtraURLCommon.length()>0){
	   			extraURL+=(this.cameraExtraURLCommon.substring(0,1).equals("&"))?"":"&"+this.cameraExtraURLCommon;
	   		}
	   		if (this.cameraExtraURL[chn].length()>0){
	   			extraURL+=(this.cameraExtraURL[chn].substring(0,1).equals("&"))?"":"&"+this.cameraExtraURL[chn];
	   		}
	   		if (exposure<minExposure) exposure=minExposure; else if (exposure>maxExposure) exposure=maxExposure;
	   		if (autoExposureMax<minExposure) autoExposureMax=minExposure; else if (autoExposureMax>maxExposure) autoExposureMax=maxExposure;
	   		if (gain<minGain) gain= minGain ; else if (gain> maxGain) gain= maxGain;
	   		if (rScale<minScale) rScale= minScale ; else if (rScale> maxScale) rScale= maxScale;
	   		if (bScale<minScale) bScale= minScale ; else if (bScale> maxScale) bScale= maxScale;
	   		if (gScale<minScale) gScale= minScale ; else if (gScale> maxScale) gScale= maxScale;

	   		String triggerMode="";


	   		if (this.nc393) {
	   			if (isMasterPort) {
	   				/*
	   				 * P_TRIG_OUT (outputs):
	   				 * off:      0x00000
	   				 * external: 0x02000
	   				 * internal: 0x20000
	   				 * both:     0x22000
	   				 * P_TRIG_IN (inputs):
	   				 * off:      0x00000
	   				 * external: 0x80000
	   				 * internal: 0x08000
	   				 * both:     0x88000
	   				 */
	   				if (this.setupTriggerMode){
	   					triggerMode+="&TRIG_CONDITION="+(this.noCabling?(0):(this.externalTriggerCabling?"0x80000":"0x08000"))+"*0"+
	   							"&TRIG_OUT="+(this.noCabling?(0):(this.externalTriggerCabling?"0x02000":"0x20000"))+"*0"+
	   							"&TRIG=4*3";
	   				}

	   				if ((this.triggerPeriod[chn]>1) || this.setupTriggerMode) {
	   					triggerMode+="&TRIG_PERIOD=0*1"; // just stop it if it wasn't already, imgsrv /trig does not set it, only FPGA register
	   				}
	   			}
	   		}else {
	   			if (this.setupTriggerMode){
	   				triggerMode+="&TRIG_CONDITION="+(this.noCabling?(0):(this.externalTriggerCabling?"0x200000":"0x20000"))+"*0"+
	   						"&TRIG_OUT="+(this.noCabling?(0):(this.externalTriggerCabling?"0x800000":"0x80000"))+"*0"+
	   						"&TRIG=4*3";
	   			}
	   			if ((this.triggerPeriod[chn]>1) || this.setupTriggerMode){
	   				triggerMode+="&TRIG_PERIOD=1*0"; // just imgsrv /trig does not set it, only FPGA register
	   			}
	   		}


	   		String url="http://"+ip+"/parsedit.php?immediate";

	   		if (this.nc393) {
	   			url += "&sensor_port="+sensor_port;

	   		}



	   		url+="&EXPOS="+exposure+"*0"; // always
	   		if (!exposureOnly){
	   			if (!this.nc393) {
	   				if (this.irqSmart[chn]!=(this.noWait?6:3)){
	   					url+="&IRQ_SMART="+(this.noWait?6:3)+"*0";
	   				}
	   			}
	   			url+="&COLOR="+this.colorMode+"*"+(this.nc393?"1":"0")+
	   			"&QUALITY="+this.JPEGquality+"*0"+
	   			"&EXPOS="+exposure+"*0"+
	   			"&AUTOEXP_EXP_MAX="+autoExposureMax+"*0"+
	   			"&AUTOEXP_ON="+autoExp+"*0"+
	   			"&GAING="+gain+"*0"+
	   			"&RSCALE="+rScale+"*0"+
	   			"&BSCALE="+bScale+"*0"+
	   			"&GSCALE="+gScale+"*0"+ // GB/G ratio
	   			"&WB_EN="+autoWB+"*0"+
	   			"&DAEMON_EN_TEMPERATURE=1"+"*0"+
	   			 triggerMode+ // needed here?
	   			"&FRAME"+
	   			extraURL;
	   		}
//	   		if (this.debugLevel>2) {
	   		if (this.debugLevel>1) { // temporary - debugging
	   			System.out.println("Setting camera "+chn+", URL="+url);
	   		}
	   		Document dom=null;
	   		try {
	   			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	   			DocumentBuilder db = dbf.newDocumentBuilder();
	   			dom = db.parse(url);
	   			if (!dom.getDocumentElement().getNodeName().equals("parameters")) {
	   				String msg="Root element: expected 'parameters', got \"" + dom.getDocumentElement().getNodeName()+"\"";
	   				IJ.showMessage("Error",msg);
	   				throw new IllegalArgumentException (msg);
	   			}
	   			//	   	    		String sNode;
	   			if (!exposureOnly) this.cameraFrameNumber[chn]= Integer.parseInt((dom.getDocumentElement().getElementsByTagName("FRAME").item(0).getChildNodes().item(0)).getNodeValue());
	   		} catch(MalformedURLException e){
	   			String msg="Please check the URL:" + e.toString();
	   			IJ.showMessage("Error",msg);
	   			throw new IllegalArgumentException (msg);
	   		} catch(IOException  e1){
	   			String msg = e1.getMessage();
	   			if (msg==null || msg.equals(""))  msg = ""+e1;
	   			msg="Camera "+chn+" ("+this.cameraIPs[chn]+") did not respond\n"+msg;
	   			IJ.showMessage("Error",msg);
	   			System.out.println(msg);
	   			return false;
	   		}catch(ParserConfigurationException pce) {
	   			pce.printStackTrace();
	   			throw new IllegalArgumentException ("PCE error");
	   		}catch(SAXException se) {
	   			se.printStackTrace();
	   			throw new IllegalArgumentException ("SAX error");
	   		}
	   		return true;
	   	}



	   	private boolean editSubCamerasIPs() {
			GenericDialog gd = new GenericDialog("Edit IPs/hosts and ports of the sub cameras");
			for (int i=0;i<this.cameraIPs.length;i++){
	    		gd.addStringField(i+": IP address/host:sensor port of the subcamera",this.cameraIPs[i],25);
			}
    	    WindowTools.addScrollBars(gd);
    	    gd.showDialog();
    	    if (gd.wasCanceled()) return false;
			for (int i=0;i<this.cameraIPs.length;i++){
	    		this.cameraIPs[i]=gd.getNextString();
			}
			initJP4(); // initialize JP46_Reader_camera class instances
///        	initCamParsDefaultArrays(this.cameraIPs.length); - probably not needed as number of cameras is not changed
			return true;

	   	}

	   	public boolean editCameraSettings(String title){
			GenericDialog gd = new GenericDialog("title");
			gd.addCheckbox    ("NC393 (unchecked - nc353)", this.nc393);
    		gd.addNumericField("Camera exposure",this.cameraExposure,2,8,"ms");
    		gd.addNumericField("Scale camera exposure for target laser detection (4 lasers)",100*this.scaleExposureForLasers,1,5,"%");
    		gd.addNumericField("Scale camera exposure for optical head laser detection (2 lasers)",100*this.scaleExposureForHeadLasers,1,5,"%");
			gd.addCheckbox    ("Enable autoexposure", this.cameraAutoExposure);
    		gd.addNumericField("Maximal automatic exposure",this.cameraAutoExposureMax,2,8,"ms");
    		gd.addNumericField("Analog gain (2.0 recommended)",this.cameraGain,3,6,"x");
    		gd.addNumericField("Red-to-green color balance",this.cameraRScale,3,6,"x");
    		gd.addNumericField("Blue-to-green color balance",this.cameraBScale,3,6,"x");
    		gd.addNumericField("Green(blue row) to green (red row) color balance",this.cameraGScale,3,6,"x");
// TODO: Make one-time WB by averaging scales from all channels
			gd.addCheckbox    ("Enable auto white balance", this.cameraAutoWhiteBalance);


    		gd.addNumericField("Color mode (JP4=5)",this.colorMode,0,6,"");
    		gd.addNumericField("Compression quality",this.JPEGquality,0,6,"%");
			gd.addCheckbox    ("No wait for the next frame sync (IRQ_SMART=6, unchecked - IRQ_SMART=3)", this.noWait);

			gd.addCheckbox    ("Setup trigger mode", this.setupTriggerMode);
			gd.addCheckbox    ("Use external trigger wiring", this.externalTriggerCabling);
			gd.addCheckbox    ("Use internal FPGA trigger (no cables, for single camera)", this.noCabling);

//			this.setupTriggerMode
//			this.externalTriggerCabling



    		gd.addStringField ("Extra URL for all sub-cameras",this.cameraExtraURLCommon,40);
			for (int i=0;i<this.cameraIPs.length;i++){
				gd.addMessage("\n"+i+": camera "+this.cameraIPs[i]+" :");
	    		gd.addNumericField("     Exposure correction ",this.cameraExposureCorr[i],2,8,"x");
	    		gd.addNumericField("     Gain correction     ",this.cameraGainCorr[i],2,8,"x");
	    		gd.addNumericField("     R/G correction      ",this.cameraRScaleCorr[i],2,8,"x");
	    		gd.addNumericField("     B/G correction      ",this.cameraBScaleCorr[i],2,8,"x");
	    		gd.addNumericField("     G(B)/G(R) correction",this.cameraGScaleCorr[i],2,8,"x");
	    		gd.addStringField ("     Extra URL string    ",this.cameraExtraURL[i],40);
			}
    		gd.addNumericField("Debug sensor number (<0 - none)",this.debugSensorNumber,0,6,"");

    	    WindowTools.addScrollBars(gd);
    	    gd.showDialog();
    	    if (gd.wasCanceled()) return false;
    	    this.nc393=                          gd.getNextBoolean();
    	    this.cameraExposure=                 gd.getNextNumber();
    	    this.scaleExposureForLasers=    0.01*gd.getNextNumber();
    	    this.scaleExposureForHeadLasers=0.01*gd.getNextNumber();
    	    this.cameraAutoExposure=             gd.getNextBoolean();
    	    this.cameraAutoExposureMax=          gd.getNextNumber();
    	    this.cameraGain=                     gd.getNextNumber();
    	    this.cameraRScale=                   gd.getNextNumber();
    	    this.cameraBScale=                   gd.getNextNumber();
    	    this.cameraGScale=                   gd.getNextNumber();
    	    this.cameraAutoWhiteBalance=         gd.getNextBoolean();
    		this.colorMode=                (int) gd.getNextNumber();
    		this.JPEGquality=              (int) gd.getNextNumber();
    		this.noWait=                         gd.getNextBoolean();
			this.setupTriggerMode=               gd.getNextBoolean();
			this.externalTriggerCabling=         gd.getNextBoolean();
			this.noCabling=                      gd.getNextBoolean();

    	    this.cameraExtraURLCommon=           gd.getNextString();
			for (int i=0;i<this.cameraIPs.length;i++){
	    	    this.cameraExposureCorr[i]=      gd.getNextNumber();
	    	    this.cameraGainCorr[i]=          gd.getNextNumber();
	    	    this.cameraRScaleCorr[i]=        gd.getNextNumber();
	    	    this.cameraBScaleCorr[i]=        gd.getNextNumber();
	    	    this.cameraGScaleCorr[i]=        gd.getNextNumber();
	    	    this.cameraExtraURL[i]=          gd.getNextString();
			}
			this.debugSensorNumber=        (int) gd.getNextNumber();
	   		return true;
	   	}
	   	public void resetCameras(){
	   		resetCameras(selectAllSubcameras());
	   	}
	   	public double[] timestampCameras(){
	   		return timestampCameras(selectAllSubcameras());
	   	}
	   	public void resetIPs(){
	   		resetIPs(selectAllIPs());
	   	}
	   	public double[] timestampIPs(){
	   		return timestampIPs(selectAllIPs());
	   	}
	   	public boolean [] selectAllSubcameras(){
	   		boolean [] all= new boolean[this.channelMap.length];
	   		for (int i=0;i<all.length;i++) all[i]=true;
	   		return all;
	   	}
	   	public boolean [] selectNoneSubcameras(){
	   		boolean [] all= new boolean[this.channelMap.length];
	   		for (int i=0;i<all.length;i++) all[i]=false;
	   		return all;
	   	}
	   	public boolean [] selectAllIPs(){
	   		boolean [] all= new boolean[this.cameraIPs.length];
	   		for (int i=0;i<all.length;i++) all[i]=true;
	   		return all;
	   	}

	   	public boolean [] selectIPs(boolean [] selectCameras){
	   		boolean [] IPs= new boolean[this.cameraIPs.length];
	   		for (int i=0;i<IPs.length;i++) IPs[i]=false;
	   		for (int i=0;i<selectCameras.length;i++) if (i<this.channelMap.length) {
//	   			IPs[this.channelMap[i][0]]=true;
	   			IPs[this.channelIPPort[i]] = true;  // since NC393
	   		}
	   		return IPs;
	   	}


	   	public void trigger(){
			try {
				DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
				DocumentBuilder db = dbf.newDocumentBuilder();
				String url=this.triggerURL;
				if (this.debugLevel>2) System.out.println(">>> trigger:" + url );
				db.parse(url); // should be some XML (currently discarded)
			} catch(MalformedURLException e){
				System.out.println("Please check the URL:" + e.toString() );
				return;
			} catch(IOException  e1){
				IJ.showStatus("");
				String error = e1.getMessage();
				if (error==null || error.equals(""))  error = ""+e1;
				IJ.showMessage("trigger() ERROR", ""+error);
				return;
			}catch(ParserConfigurationException pce) {
				pce.printStackTrace();
				return;
			}catch(SAXException se) {
				se.printStackTrace();
				return;
			}
		}

	   	public double[] timestampCameras(boolean [] selection){
	   		double [] ts_ip=timestampIPs(selectIPs(selection));
	   		double [] ts=new double [selection.length];
	   		for (int i=0;i<ts.length;i++){
	   			ts[i]=0.0;
	   			if (i<this.channelMap.length) ts[i]=ts_ip[this.channelMap[i][0]];
	   		}
	   		return ts;
	   	}

	   	public double[] timestampIPs(boolean [] ipSelection){

	   		final Thread[] threads = newThreadArray(this.maxNumberOfThreads);
	   		final AtomicInteger ipIndexAtomic = new AtomicInteger(0);
	   		final int ipLength=this.resetURLs.length;
	   		final int debugLevel=this.debugLevel;
			final String []	metaURLs=this.metaURLs;
	   		final double [] ts= new double[this.cameraIPs.length];
	   		for (int i=0;i<ts.length;i++) ts[i]=0.0;


	   	//TODO: Multithread the next cycle (per-sensor)
	   		for (int ithread = 0; ithread < threads.length; ithread++) {
	   			threads[ithread] = new Thread() {
	   				@Override
					public void run() {
	   					for (int ipIndex=ipIndexAtomic.getAndIncrement(); ipIndex<ipLength;ipIndex=ipIndexAtomic.getAndIncrement()){

	   						//			for (int ipIndex=0;ipIndex<this.resetURLs.length;ipIndex++) if ((ipIndex<ipSelection.length) && ipSelection[ipIndex]){
	   						try {
	   							Document dom=null;
	   							DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	   							DocumentBuilder db = dbf.newDocumentBuilder();
	   							String url=metaURLs[ipIndex];
//	   							if (debugLevel>2) System.out.println("timestampIPs:" + url );
	   							if (debugLevel>0) System.out.println("timestampIPs:" + url );
	   							dom = db.parse(url);
	   							if (!dom.getDocumentElement().getNodeName().equals("meta")) {
	   								System.out.println("Root element: expected 'meta', got'" + dom.getDocumentElement().getNodeName()+"'");
	   								IJ.showMessage("Error","Root element: expected 'meta', got'" + dom.getDocumentElement().getNodeName()+"'");
	   								continue;
	   							}
	   							if (dom.getDocumentElement().getElementsByTagName("frame").getLength()==0) {
	   								String sError="reading timestamp failed. Do you have camera firmware version >=8.1.1.1?";
	   								System.out.println("ERROR: "+sError );
	   								IJ.showMessage("Error",sError);
	   								continue;
	   							}
	   							ts[ipIndex]=Double.parseDouble(
	   									(dom.getDocumentElement().getElementsByTagName("timestamp").item(0).getChildNodes().item(0)).getNodeValue());

	   						} catch(MalformedURLException e){
	   							System.out.println("Please check the URL:" + e.toString() );
	   							continue;
	   						} catch(IOException  e1){
	   							IJ.showStatus("");
	   							String error = e1.getMessage();
	   							if (error==null || error.equals(""))  error = ""+e1;
	   							IJ.showMessage("timestampIPs() ERROR", ""+error);
	   							continue;
	   						}catch(ParserConfigurationException pce) {
	   							pce.printStackTrace();
	   							continue;
	   						}catch(SAXException se) {
	   							se.printStackTrace();
	   							continue;
	   						}
	   					}
	   				}
	   			};
	   		}
	   		startAndJoin(threads);
	   		return ts;
	   	}

	   	public void resetCameras(boolean [] selection){
	   		if (this.debugLevel>2) {
	   			System.out.println("resetCameras(...)");
	   		    for (int ii=0;ii<this.cameraIPs.length;ii++)System.out.println(ii+":  "+this.cameraIPs[ii]);
	   		}

	   		resetIPs(selectIPs(selection));

	   	}

	   	public void resetIPs(boolean [] ipSelection){
	   		final Thread[] threads = newThreadArray(this.maxNumberOfThreads);
	   		final AtomicInteger ipIndexAtomic = new AtomicInteger(0);
	   		final int ipLength=this.resetURLs.length;
	   		final int debugLevel=this.debugLevel;
			final String []	resetURLs=this.resetURLs;
	   		for (int ithread = 0; ithread < threads.length; ithread++) {
	   			threads[ithread] = new Thread() {
	   				@Override
					public void run() {
	   					for (int ipIndex=ipIndexAtomic.getAndIncrement(); ipIndex<ipLength;ipIndex=ipIndexAtomic.getAndIncrement()){
	   						String url="";
	   						try {
	   							DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	   							DocumentBuilder db = dbf.newDocumentBuilder();
	   							url=resetURLs[ipIndex];
	   							if (debugLevel>2) System.out.println("--- resetURLs:" + url );
//	   							if (debugLevel>1) System.out.println("--- resetURLs:" + url );
	   							db.parse(url); // should be some XML (currently discarded)
	   						} catch(MalformedURLException e){
	   							System.out.println("Please check the URL:" + e.toString() );
	   							continue;
	   						} catch(IOException  e1){
	   							IJ.showStatus("");
	   							String error = e1.getMessage();
	   							if (error==null || error.equals(""))  error = ""+e1;
	   							IJ.showMessage("resetIPs() ERROR", url+"\n"+error);
	   							continue;
	   						}catch(ParserConfigurationException pce) {
	   							pce.printStackTrace();
	   							continue;
	   						}catch(SAXException se) {
	   							se.printStackTrace();
	   							continue;
	   						}
	   					}
	   				}

	   			};
	   		}
	   		startAndJoin(threads);
	   	}

//	   	public boolean preTrigger()	{ // mt9f002 fails if more than 1-3 sec between triggers 
//	   		return true;
//	   	}
	   	
	   	
	   	public ImagePlus [] getImages(final boolean [] acquire, boolean resetAndTrigger, final boolean show){
		   	final boolean [] acquireIPs=selectIPs(acquire);
			if (this.debugLevel>2) {
				System.out.println("getImages(...) 2");
			    for (int ii=0;ii<this.cameraIPs.length;ii++)System.out.println(ii+":  "+this.cameraIPs[ii]);
			}

			if (resetAndTrigger) {
				if (is14Mpix()) {
					try { // FIXME: Make conditional for 14MPix
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
				resetCameras();
				trigger();
			}
		   	final double [] timestamps= timestampIPs(acquireIPs);
			if (this.debugLevel>2) System.out.println("getImages(): this.imagesIP.length=" + this.imagesIP.length);
	   		final Thread[] threads = newThreadArray(this.maxNumberOfThreads);
	   		final AtomicInteger ipIndexAtomic = new AtomicInteger(0);
	   		final int ipLength=this.resetURLs.length;
	   		final int debugLevel=this.debugLevel;
			final String []	imageURLs=this.imageURLs;
			final ImagePlus []	imagesIP=this.imagesIP;
			final JP46_Reader_camera [] jp4_Instances=this.jp4_Instances;
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
						for (int ipIndex=ipIndexAtomic.getAndIncrement(); ipIndex<ipLength;ipIndex=ipIndexAtomic.getAndIncrement()){
							//							for (int i=0;i<this.imagesIP.length;i++){
							if (debugLevel>1/*2*/) System.out.println("getImages()3: ipIndex="+ipIndex+" acquireIPs.length=" +acquireIPs.length+
									((ipIndex<acquireIPs.length)? (" acquireIPs[ipIndex]="+acquireIPs[ipIndex]):""));
							if ((ipIndex<acquireIPs.length) && acquireIPs[ipIndex]) {
								if (debugLevel>2) System.out.println("getImages:" + imageURLs[ipIndex] );
								if ((debugLevel>3) &&(imagesIP[ipIndex]!=null) && show){
									System.out.println("=============== old image ==========");
									jp4_Instances[ipIndex].listImageProperties(imagesIP[ipIndex]);

								}
								imagesIP[ipIndex]=jp4_Instances[ipIndex].openURL( // gains inside were OK
										imageURLs[ipIndex],
										"",
										true, //scale
										imagesIP[ipIndex],
										false); //show); // show image
								imagesIP[ipIndex].setProperty("timestamp", IJ.d2s(timestamps[ipIndex],6));
								imagesIP[ipIndex].setProperty("MIRRORED","NO");
								if (debugLevel>2) {
									jp4_Instances[ipIndex].listImageProperties(imagesIP[ipIndex],true); // to console - properties old - fixed
								}


								if (show){
									imagesIP[ipIndex].updateAndDraw(); /// Redisplays final image
									if (debugLevel>2){
										System.out.println("=============== new image ==========");
										jp4_Instances[ipIndex].listImageProperties(imagesIP[ipIndex]);
									}
								}
							}
						}
					}
				};
			}
			startAndJoin(threads);
			//TODO: Multithread the next cycle (per-sensor)
			final ImagePlus []	images=this.images;
			final int [][] channelMap = this.channelMap;
			final int [] channelIPPort = this.channelIPPort;
	   		final AtomicInteger imageIndexAtomic = new AtomicInteger(0);
	   		final int [] motorsPosition=this.motorsPosition;

	   		for (int ithread = 0; ithread < threads.length; ithread++) {
	   			threads[ithread] = new Thread() {
	   				@Override
					public void run() {
	   					// Next calls do not require image acquisition, so default settings for the new instance are OK
						JP46_Reader_camera jp4_Instance= new JP46_Reader_camera(false);
	   					for (int imageIndex=imageIndexAtomic.getAndIncrement(); imageIndex<images.length;imageIndex=imageIndexAtomic.getAndIncrement())
	   						if ((imageIndex<acquire.length) && acquire[imageIndex]){
	   							//		   	for (int i=0;i<this.images.length;i++) if ((i<acquire.length) && acquire[i]) {
//	   							int iIP=channelMap[imageIndex][0];
	   							int iIP=channelIPPort[imageIndex]; // index in composite images (per ip/port)
	   							if (sensorPresent[iIP]==null) { // system board for this channel did not respond null pointer - check cameras were detected
	   								images[imageIndex]=null;
	   								continue;
	   							}
	   							boolean singleSensor=true;
	   							for (int s=0;s<sensorPresent[iIP].length;s++) singleSensor&= !sensorPresent[iIP][s];
	   							if (singleSensor){ // no 10359 multiplexor
	   								if (debugLevel>1) System.out.println("DEBUG393: demuxing single sensor imageIndex="+imageIndex+" iIP="+iIP);
	   								images[imageIndex]=jp4_Instance.demuxClone(imagesIP[iIP]);
	   							} else {
	   								int subCam=channelMap[imageIndex][1];
	   								if (!sensorPresent[iIP][subCam]){ // requested sensor does not exist
	   									images[imageIndex]=null;
	   									continue;
	   								}
	   								// skip missing channels, then demux
	   								for (int s=0;s<channelMap[imageIndex][1];s++) if (!sensorPresent[iIP][s]) subCam--;
	   								if (debugLevel>1) System.out.println("DEBUG393: demuxing imageIndex="+imageIndex+" iIP="+iIP+" subCam="+subCam);
	   								images[imageIndex]=jp4_Instance.demuxImage(imagesIP[iIP],subCam);
	   							}
	   							//		   		this.images[i]=this.jp4_Instances[j].demuxImageOrClone(this.imagesIP[j],this.channelMap[i][1]);
	   							if (images[imageIndex]==null) continue;
	   							if (debugLevel>2) jp4_Instance.listImageProperties(images[imageIndex]);
	   							if (debugLevel>2) jp4_Instance.encodeProperiesToInfo(images[imageIndex]);
	   							if (debugLevel>2) {
	   								jp4_Instance.listImageProperties(images[imageIndex]);
	   								JP46_Reader_camera.decodeProperiesFromInfo(images[imageIndex]);
	   								jp4_Instance.listImageProperties(images[imageIndex]);
	   							}
	   							String title=IJ.d2s(timestamps[iIP],6).replace('.','_')+String.format("-%02d.tiff", imageIndex); // sensor number
	   							images[imageIndex].setTitle(title);

	   							if (flipImages[imageIndex]) { // this is only for physical mirror
	   								flipKeepBayer(images[imageIndex]);
	   								if (debugLevel>2){
	   									System.out.println("=============== flipKeepBayer ==========");
	   									jp4_Instance.listImageProperties(images[imageIndex]);
	   								}
	   								if (show) images[imageIndex].updateAndDraw();
	   							}
	   							images[imageIndex].setProperty("channel", String.format("%02d", imageIndex));
	   							images[imageIndex].setProperty("subcamera",""+getSubCamera(imageIndex));
	   							images[imageIndex].setProperty("sensor_port",""+getSensorPort(imageIndex));
	   							images[imageIndex].setProperty("subchannel", ""+getSubChannel(imageIndex));
	   							//		private int [] motorsPosition=      null; // motors steps when the images were acquired (for null)
	   							if (motorsPosition!=null) for (int m=0;m<motorsPosition.length;m++ ) {
	   								images[imageIndex].setProperty("MOTOR"+(m+1), ""+motorsPosition[m]);
	   							}
	   							jp4_Instance.encodeProperiesToInfo(images[imageIndex]);
	   						}
	   				}
	   			};
	   		}
	   		startAndJoin(threads);
	   		return this.imagesIP;
	   	}


	// should be already triggered!

	   	public ImagePlus [] getImages(
	   			UVLEDandLasers uvLEDandLasers, // or null - not null only for adjustment machine lasers
	   			boolean [] pre_acquire,
	   			boolean [] pre_lasers,
	   			boolean resetAndTrigger,
	   			final boolean show){
//	   		this.reportTiming=this.debugLevel>1; // Need to be set by caller
	   		printTimingInit();
//			long startTime = System.nanoTime();
	   		final int debugLevel=this.debugLevel;
	   		final boolean opticalHeadLasersMode=(uvLEDandLasers!=null);
//	   		boolean [] lasers2={true,true};
	   		final boolean [] lasers=opticalHeadLasersMode?pre_acquire:pre_lasers;
	   		if (this.sensorPresent==null) { // cameras were not probed/configured
				probeCameraState(); // testing detection
		   		printTiming("=== probeCameraState()");
				setupCameraAcquisition();
		   		printTiming("=== setupCameraAcquisition()");
	   		}
	   		boolean [] acquire= new boolean[this.channelMap.length];
	   		for (int i=0;i<acquire.length;i++) {
	   			acquire[i]= ((i<pre_acquire.length) && pre_acquire[i]) || ((lasers!=null) && (i<lasers.length) && lasers[i]);
	   		}
	   		boolean [] opticalHeadLasersInitial=null;
	   		int[] opticalHeadSequence={0,1,2};
	   		final boolean [][] headLaserWasOn={{false,true,false},{false,false,true}};
	   		if (opticalHeadLasersMode){
	   			opticalHeadLasersInitial=uvLEDandLasers.getLasers();
	   		} else if ((lasers!=null) && (this.laserPointers!=null)) {
	   			if (this.debugLevel>2) System.out.println("************ turning lasers off *************");
	   			this.laserPointers.setLasers(0);// turn off all lasers
	   			//lasersSequence
	   			if (this.debugLevel>2) {
	   				System.out.println("getImages(...) 1");
	   				for (int ii=0;ii<this.cameraIPs.length;ii++)System.out.println(ii+":  "+this.cameraIPs[ii]);
	   			}
	   		}

	   		if 	((lasers==null) && !opticalHeadLasersMode) {
		   		getImages(acquire, resetAndTrigger, show); // get images w/o lasers, flip if needed
	   			printTiming("=== Image acquisition");
	   			return this.imagesIP;
	   		}
//scaleExposureForLasers
	   		// reduce exposure for lasers (both 4 and 2)
	   		final double scaleExposureForLasers=opticalHeadLasersMode?
	   				(((this.scaleExposureForHeadLasers>0.0) && (this.scaleExposureForHeadLasers<1.0))?this.scaleExposureForHeadLasers:0.0):
	   				(((this.scaleExposureForLasers>0.0) && (this.scaleExposureForLasers<1.0))?this.scaleExposureForLasers:0.0);
	   		if (scaleExposureForLasers>0) setupCameraAcquisition(scaleExposureForLasers);
	   		final boolean [] lasersIPs= selectIPs(opticalHeadLasersMode?acquire:lasers);
	   		final int [] sequence=opticalHeadLasersMode?opticalHeadSequence:this.laserPointers.getSequence();
	   		if (debugLevel>2)	for (int i=0;i<sequence.length;i++) System.out.println(String.format("Laser sequence[%d]=0x%x", i,sequence[i]));
	   		final ImagePlus [][] laserImagesIP=new ImagePlus[sequence.length-1][this.imagesIP.length];
	   		if (this.debugLevel>2) System.out.println("this.imagesIP.length="+this.imagesIP.length+" sequence.length="+sequence.length);
	   		final String [] imageURLs=this.imageURLs;
			final JP46_Reader_camera [] jp4_Instances=this.jp4_Instances;
	   		final Thread[] threads = newThreadArray(this.maxNumberOfThreads);
	   		final AtomicInteger ipIndexAtomic = new AtomicInteger(0);
	   		for (int nSeqNum=1; nSeqNum<sequence.length;nSeqNum++){
	   			if (opticalHeadLasersMode) {
	   				boolean [] bLasers={(sequence[nSeqNum]&1)!=0,(sequence[nSeqNum]&2)!=0};
	   				uvLEDandLasers.setLasersAndUV(bLasers,null,null);
			   		if (debugLevel>2)	System.out.println("uvLEDandLasers.setLasersAndUV("+bLasers[0]+","+bLasers[1]+")");
	   			} else {
	   				this.laserPointers.setLasers (sequence[nSeqNum]);// turn on selected laser
			   		if (debugLevel>2)	System.out.println(String.format("this.laserPointers.setLasers (0x%x)", sequence[nSeqNum]));
	   			}
	   			resetIPs(selectIPs(lasersIPs)); // flush buffer
	   			if (is14Mpix()) {
	   				try { // FIXME: Make conditional for 14MPix
	   					Thread.sleep(2000);
	   				} catch (InterruptedException e) {
	   					// TODO Auto-generated catch block
	   					e.printStackTrace();
	   				}
	   			}
	   			trigger(); // trigger cameras
	   			ipIndexAtomic.set(0);
	   			final int fnSeqNum=nSeqNum;
	   			final boolean reportTiming=this.reportTiming;
	   			for (int ithread = 0; ithread < threads.length; ithread++) {
	   				threads[ithread] = new Thread() {
	   					@Override
						public void run() {
	   						for (int ipIndex=ipIndexAtomic.getAndIncrement(); ipIndex<lasersIPs.length;ipIndex=ipIndexAtomic.getAndIncrement()){
	   							long st=System.nanoTime();
	   							if (debugLevel>1) {
	   								System.out.println("image url["+ipIndex+"]="+imageURLs[ipIndex]);
	   							}
	   							laserImagesIP[fnSeqNum-1][ipIndex]=jp4_Instances[ipIndex].openURL(
	   									imageURLs[ipIndex],
	   									"",
	   									true, //scale
	   									null, // new image, nothing to reuse
	   									false); // show image no timestamps here
	   							if (reportTiming){
	   								System.out.println("==== "+imageURLs[ipIndex]+ " opened in "+IJ.d2s(0.000000001*(System.nanoTime()-st),3)+" sec");
	   							}
	   							laserImagesIP[fnSeqNum-1][ipIndex].setProperty("MIRRORED","NO");
//	   							laserImagesIP[fnSeqNum-1][ipIndex].setTitle("raw"+fnSeqNum);  //FIXME:Debugging 
//	   							laserImagesIP[fnSeqNum-1][ipIndex].show(); //FIXME:Debugging
	   						}
	   					}
	   				};
	   			}
	   			startAndJoin(threads);
	   			printTiming("=== Image # "+nSeqNum+" acquisition");

	   		}
// acquire no-laser image after all others are acquired, so the camera is completely still at that time
   			if (opticalHeadLasersMode) {
   				boolean [] bLasers={(sequence[0]&1)!=0,(sequence[0]&2)!=0};
   				uvLEDandLasers.setLasersAndUV(bLasers,null,null);
		   		if (debugLevel>2)	System.out.println("uvLEDandLasers.setLasersAndUV("+bLasers[0]+","+bLasers[1]+")");
   			} else {
   				this.laserPointers.setLasers (sequence[0]);// turn on selected laser
		   		if (debugLevel>2)	System.out.println(String.format("this.laserPointers.setLasers (0x%x)", sequence[0]));
   			}
//	   		this.laserPointers.setLasers (0);// turn off all lasers
	   		if (scaleExposureForLasers>0.0) setupCameraAcquisition(1.0);

	   		getImages(acquire, resetAndTrigger, show); // get images w/o lasers, flip if needed
   			printTiming("=== Final (no-laser) image # 0 acquired");

   			if (opticalHeadLasersMode) {
   				uvLEDandLasers.setLasersAndUV(opticalHeadLasersInitial,null,null);
		   		if (debugLevel>2)	System.out.println("Restoring lasers: uvLEDandLasers.setLasersAndUV("+opticalHeadLasersInitial[0]+","+opticalHeadLasersInitial[1]+")");
   			}


	   		// now demux images (if composite) and process laser spots
	   		//		   	for (int sensorNum=0;sensorNum<this.images.length;sensorNum++) if (this.images[sensorNum]!=null){
//	   		final MatchSimulatedPattern matchSimulatedPattern = new MatchSimulatedPattern();
//	   		final ImagePlus imp_pointed=null;
	   		final int bayerG1=0;
	   		final int bayerG2=3;
	   		final int bayerR=1;
	   		final int bayerB=2;
	   		final boolean useOther=laserPointers.laserPointer.useOther;
	   		final boolean otherGreen=laserPointers.laserPointer.otherGreen;

	   		final int [][] channelMap=this.channelMap;
			final int []   channelIPPort = this.channelIPPort;
	   		final ImagePlus [] images=this.images;
			final boolean [] flipImages=this.flipImages;
			final LaserPointersHardware laserPointers=this.laserPointers;
			final boolean [][] sensorPresent=this.sensorPresent;
			final int debugSensorNumber=this.debugSensorNumber;
   			printTiming("=== Acquisition done, starting multi-threaded laser pointer location processing");

//TODO: Multithread the next cycle (per-sensor)
			final AtomicInteger sensorNumAtomic = new AtomicInteger(0);
			for (int ithread = 0; ithread < threads.length; ithread++) {
				threads[ithread] = new Thread() {
					@Override
					public void run() {
	   					// Next calls do not require image acquisition, so default settings for the new instance are OK
						JP46_Reader_camera jp4_Instance= new JP46_Reader_camera(false);
						MatchSimulatedPattern matchSimulatedPattern = new MatchSimulatedPattern();
						for (int sensorNum=sensorNumAtomic.getAndIncrement(); sensorNum<lasers.length;sensorNum=sensorNumAtomic.getAndIncrement()) // null pointer
							if (lasers[sensorNum] && (images[sensorNum]!=null)){
								//	   		for (int sensorNum=0;sensorNum<lasers.length;sensorNum++)  if (lasers[sensorNum] && (this.images[sensorNum]!=null)){ // lasers - here sensors to use lasers for
//								int iIP=channelMap[sensorNum][0];
	   							int iIP=channelIPPort[sensorNum]; // index in composite images (per ip/port)
								double saturationRed=255.0;
								if (images[sensorNum].getProperty("saturation_0")!=null) saturationRed=Double.parseDouble((String)images[sensorNum].getProperty("saturation_0"));
								if (scaleExposureForLasers>0) saturationRed*=scaleExposureForLasers; // scaled to reduced exposure time
								double[][] backgroundBayer =matchSimulatedPattern.splitBayer (images[sensorNum],  null, true); // full window
								double[][] pointedBayer;
								double [][] pontersXY=null;
								//		   			int iIP=this.channelMap[sensorNum][0];
								int len=backgroundBayer[bayerG1].length;
								double [][] greens=useOther?(new double [sequence.length][]):null;
								double [][] reds=  new double [sequence.length][];
								if (useOther){
									greens[0]=otherGreen?(new double[len]):backgroundBayer[bayerB];
									if (otherGreen) for (int j=0; j<len;j++) greens[0][j]=0.5*(backgroundBayer[bayerG1][j]+backgroundBayer[bayerG2][j]);
								}
								reds[0]=backgroundBayer[bayerR]; // no need to clone?
								if (debugLevel>2)  System.out.println("getImages (): sensorNum="+sensorNum+", iIP="+iIP+" sequence.length="+sequence.length);
//								if ((debugLevel>1) && (sensorNum==13))  System.out.println("getImages (): sensorNum="+sensorNum+", iIP="+iIP+" sequence.length="+sequence.length);
								boolean singleSensor=true;
								for (int s=0;s<sensorPresent[iIP].length;s++) singleSensor&= !sensorPresent[iIP][s];
								ImagePlus imp_pointed=null;
								for (int nSeqNum=1; nSeqNum<sequence.length; nSeqNum++){
									if (debugLevel>2) System.out.println("getImages(): sensorNum="+sensorNum+", iIP="+iIP+" nSeqNum="+nSeqNum+" this.channelMap["+sensorNum+"][1]="+channelMap[sensorNum][1]);
									// here we are processing only same sensors that produced no-laser images, no need to check if they exist again
									if (singleSensor){ // no 10359 multiplexor
										//imp_pointed=jp4_Instances[iIP].demuxClone(laserImagesIP[nSeqNum-1][iIP]);
										imp_pointed=jp4_Instance.demuxClone(laserImagesIP[nSeqNum-1][iIP]);
									} else {
										int subCam=channelMap[sensorNum][1];
										// skip missing channels, then demux
										for (int s=0;s<channelMap[sensorNum][1];s++) if (!sensorPresent[iIP][s]) subCam--;
										//imp_pointed=jp4_Instances[iIP].demuxImage(laserImagesIP[nSeqNum-1][iIP],subCam);
										if (laserImagesIP[nSeqNum-1][iIP]==null){
											System.out.println("getImages(): laserImagesIP["+(nSeqNum-1)+"]["+iIP+"]==null");
										}
										imp_pointed=jp4_Instance.demuxImage(laserImagesIP[nSeqNum-1][iIP],subCam);
									}
									imp_pointed.setTitle(imp_pointed.getTitle()+"_"+sensorNum);
									if (flipImages[sensorNum]) {
										flipKeepBayer(imp_pointed);
										if (show) imp_pointed.updateAndDraw();
									}
									pointedBayer=matchSimulatedPattern.splitBayer (imp_pointed, null, true);
									if (useOther){
										greens[nSeqNum]=otherGreen?(new double[len]):pointedBayer[bayerB];
										if (otherGreen) for (int j=0; j<len;j++) greens[nSeqNum][j]=0.5*(pointedBayer[bayerG1][j]+pointedBayer[bayerG2][j]);
									}
									reds[nSeqNum]=pointedBayer[bayerR].clone();
								}

//minimalIntensity		scaleExposureForLasers>0	saturationRed
								pontersXY=laserPointers.laserPointer.getPointerXY( // returns x,y pair or null if pointer not detected
										opticalHeadLasersMode,
										saturationRed,
										scaleExposureForLasers, //>0.0,
										greens,        // combined Bayer greens for each image, starting with no-laser
										reds,          // red Bayer component for each image, starting with no-laser
										opticalHeadLasersMode?headLaserWasOn:laserPointers.laserWasOn(), // array specifying which image should have pointer on, for each laser
										imp_pointed.getWidth(),// image width in pixels
										imp_pointed.getTitle(),             // String title,
										debugLevel+  ((sensorNum==debugSensorNumber)?1:0)           // debug level (normal == 1)
//										debugLevel
								);
								int pointersDetected=0;
								if (!opticalHeadLasersMode){
									for (int nPointer=0; nPointer<laserPointers.getNumberOfLasers();nPointer++){
										if (pontersXY[nPointer]!=null){
											if (debugLevel>1) System.out.println("image:"+sensorNum+" pointer #"+(nPointer+1)+
													", X="+pontersXY[nPointer][0]+", Y="+pontersXY[nPointer][1]);
											images[sensorNum].setProperty("POINTER_X_"+nPointer, IJ.d2s(pontersXY[nPointer][0],1));
											images[sensorNum].setProperty("POINTER_Y_"+nPointer, IJ.d2s(pontersXY[nPointer][1],1));
											
											if ((laserPointers.laserPointer.laserUVMap!=null) && (laserPointers.laserPointer.laserUVMap[nPointer]!=null)) {
												images[sensorNum].setProperty("POINTER_U_"+nPointer, IJ.d2s(laserPointers.laserPointer.laserUVMap[nPointer][0],1));
												images[sensorNum].setProperty("POINTER_V_"+nPointer, IJ.d2s(laserPointers.laserPointer.laserUVMap[nPointer][1],1));
												
											}
											pointersDetected++;
										}

									}
									if ((pointersDetected>0) && (debugLevel>0)) {
										System.out.println("image:"+sensorNum+" - "+pointersDetected+" pointer"+((pointersDetected>1)?"s":"")+" detected");
									}
								} else {
									for (int nPointer=0; nPointer<2;nPointer++){
										if (pontersXY[nPointer]!=null){
											if (debugLevel>1) System.out.println("image:"+sensorNum+" pointer #"+(nPointer+1)+
													", X="+pontersXY[nPointer][0]+", Y="+pontersXY[nPointer][1]);
											images[sensorNum].setProperty("HEAD_POINTER_X_"+nPointer, IJ.d2s(pontersXY[nPointer][0],1));
											images[sensorNum].setProperty("HEAD_POINTER_Y_"+nPointer, IJ.d2s(pontersXY[nPointer][1],1));
											pointersDetected++;
										}
									}
								}
								images[sensorNum].setProperty("HEAD_POINTERS", pointersDetected+"");
								//jp4_Instances[iIP].encodeProperiesToInfo(images[sensorNum]);
								jp4_Instance.encodeProperiesToInfo(images[sensorNum]);
							}
					}
				};
			}
			startAndJoin(threads);

//	   		this.laserPointers.setLasers (0);// turn off all lasers
   			printTiming("=== Image acquisition/laser pointers location");
	   		return this.imagesIP;
	   	}

	   	public double [][] getHeadPointers(ImagePlus imp){
	   		if (imp.getProperty("HEAD_POINTERS")==null) return null;
	   		int numHeadPointers=Integer.parseInt((String) imp.getProperty("HEAD_POINTERS"));
	   		double [][] headPointers=new double[numHeadPointers][];
	   		for (int n=0;n<headPointers.length;n++){
	   			if ((imp.getProperty("HEAD_POINTER_X_"+n)!=null) && (imp.getProperty("HEAD_POINTER_Y_"+n)!=null)){
	   				headPointers[n]=new double[2];
	   				headPointers[n][0]=Double.parseDouble((String)imp.getProperty("HEAD_POINTER_X_"+n));
	   				headPointers[n][1]=Double.parseDouble((String)imp.getProperty("HEAD_POINTER_Y_"+n));
	   			} else headPointers[n]=null;
	   		}
	   		return headPointers;
	   	}




	   	//	   	return array of last acquired images
	   	public ImagePlus [] getImages(){
	   		return this.images;
	   	}
	   	public ImagePlus [] getImages(int numberOfPointers){ // return images with at least this number of pointers detected
	   		ImagePlus [] filteredImages=this.images.clone();
	   		for (int i=0;i<filteredImages.length;i++) if (getNumberOfPointers(i)<numberOfPointers) filteredImages[i]=null;
	   		return filteredImages;
	   	}

	   	public int getNumberOfPointers (int sensorNum){
	   		return getNumberOfPointers (this.images[sensorNum]);
	   	}

	   	public int getNumberOfPointers (ImagePlus imp){
	   		if (imp==null) return 0;
	   		if (imp.getProperty("POINTERS")!=null) return Integer.parseInt((String) imp.getProperty("POINTERS"));
	   		else return 0;
	   	}

	   	public int saveImages(boolean [] selection, String directory, boolean updateStatus){
	   		int numImg=0;
	   		boolean notYetPrinted=true;
	   		for (int i=0;(i<this.channelMap.length) && ((selection==null) || (i<selection.length));i++)
	   			if ((this.images[i]!=null) && ((selection==null) || selection[i])){
	   				FileSaver fs=new FileSaver(this.images[i]);
	   				String path=directory+Prefs.getFileSeparator()+this.images[i].getTitle();
	   				if (updateStatus) IJ.showStatus("Saving "+path);
	   				if (this.debugLevel>0){
	   					if (this.debugLevel>1) System.out.println("Saving "+path); // was >0
	   					else if (notYetPrinted) {
	   						System.out.println("Saving "+path+ " (and other with the same timestamp)" ); // was >0
	   						notYetPrinted=false;
	   					}
	   				}
	   				fs.saveAsTiff(path);
	   				numImg++;
	   			}
	   		return numImg;
	   	}
/*
								imp_psf = new ImagePlus(filePaths[dirNum][fileNum][1], stack);
								//							if (DEBUG_LEVEL>1) imp_psf.show();
								if (DEBUG_LEVEL>1) System.out.println("Saving result to"+filePaths[dirNum][fileNum][1]+ " at "+ IJ.d2s(0.000000001*(System.nanoTime()-startTime),3));
								FileSaver fs=new FileSaver(imp_psf);
								fs.saveAsTiffStack(filePaths[dirNum][fileNum][1]);

 */

	   	/**
	   	 * FLips image vertically and crops by one pixel at the top and bottom (to preserve Bayer pattern)
	   	 * @param imp
	   	 * @return
	   	 */
	   	public ImagePlus flipAndCrop(ImagePlus imp){
	   		String title=imp.getTitle();
	   		imp.setTitle(title+"-original");
	   		ImageProcessor ip=imp.getProcessor();
	   		ip.setRoi(0,1,ip.getWidth(),ip.getHeight()-2);
	   		ImageProcessor ip2=ip.crop();
	   		ip2.flipVertical();
	   		ImagePlus imp_new=new ImagePlus(title,ip2);
// copy all properties and add "MIRRORED=Y"
			Set<Object> imp_set;
			Properties imp_prop;
			Iterator<Object> itr;
			String str;
			imp_prop=imp.getProperties();
			if (imp_prop!=null) {
				imp_set=imp_prop.keySet();
				itr=imp_set.iterator();
				while(itr.hasNext()) {
					str = (String) itr.next();
					imp_new.setProperty(str,imp_prop.getProperty(str));
				}
			}
			imp_new.setProperty("MIRRORED","YES");
	   		return imp_new;
	   	}
	   	public ImagePlus flipKeepBayer(ImagePlus imp){
	   		ImageProcessor ip=imp.getProcessor();
	   		float [] pixels=(float[]) ip.getPixels();
	   		float [] new_pixels=new float[pixels.length];
	   		int width=ip.getWidth();
	   		int height=ip.getHeight();
	   		for (int i=1;i<height-1;i++) for (int j=0;j<width;j++){
	   			new_pixels[i*width+j]=pixels[(height-i)*width+j];
	   		}
	   	    for (int j=0;j<width;j++) new_pixels[j]=new_pixels[2*width+j]; // copy second line to 0
	   	    ip.setPixels(new_pixels);
			imp.setProperty("MIRRORED","YES");
	   		return imp;
	   	}
	   	public ImagePlus flipKeepBayerNew(ImagePlus imp){
	   		float [] pixels=(float []) imp.getProcessor().getPixels();
	   		float [] new_pixels=new float[pixels.length];
	   		int width=imp.getWidth();
	   		int height=imp.getHeight();
	   		for (int i=1;i<height-1;i++) for (int j=0;j<width;j++){
	   			new_pixels[i*width+j]=pixels[(height-i)*width+j];
	   		}
	   		for (int j=0;j<width;j++) new_pixels[j]=new_pixels[2*width+j]; // copy second line to 0
	   		ImageProcessor ip=new FloatProcessor(imp.getWidth(), imp.getHeight());
	   		ip.setPixels(new_pixels);
	   		ip.resetMinAndMax();
	   		ImagePlus imp_flipped=  new ImagePlus(imp.getTitle()+"-flipped", ip);
	   		imp_flipped.setProperty("MIRRORED","YES");
	   		return imp_flipped;
	   	}

// in detection mode - reduce exp

	   	public void test1(boolean useLasers) {
	   		getImages(
	   				null, // UVLEDLasers
	   				selectAllSubcameras(),
	   				(useLasers?selectAllSubcameras():null),
	   				true,
	   				this.debugLevel>1); // reset and trigger
			if (this.debugLevel>1) {
				if (this.debugLevel>2) System.out.println("++++++++++++++++++ Image Properies ++++++++++++++++++++++++++++");
				for (int i=0;i<this.images.length;i++) if (this.images[i]!=null) {
//					int j=this.channelMap[i][0];
					int j=channelIPPort[i]; // sincer NC393 adder port
					if (this.debugLevel>2) {
						System.out.println("Image #"+i);
						this.jp4_Instances[j].listImageProperties(this.images[i]);
					}
					if ((this.debugLevel>2) || (getNumberOfPointers (i)>0)) {
						this.images[i].show();
						this.images[i].updateAndDraw();
					}
				}
			}
	   	}

	   	public void acquire(String directory, boolean useLasers, boolean updateStatus) {
			getImages(
	   				null, // UVLEDLasers
					selectAllSubcameras(),
					(useLasers?selectAllSubcameras():null),
					true,
					this.debugLevel>1); // reset and trigger
		   	int numImg=saveImages(selectAllSubcameras(), directory, updateStatus);
		   	if (this.debugLevel>1) System.out.println("Saved "+numImg+" images.");
	   	}
	    public ImagePlus acquireSingleImage (boolean useLasers, boolean updateStatus){
			getImages(
	   				null, // UVLEDLasers
					selectAllSubcameras(),
					(useLasers?selectAllSubcameras():null),
					true,
					this.debugLevel>1); // reset and trigger
	    	this.lastTimestamp=(String) this.images[0].getProperty("timestamp");
	    	return this.images[0];
	    }
	    public ImagePlus [] acquireSeveralImages (boolean useLasers, boolean updateStatus){
			getImages(
	   				null, // UVLEDLasers
					selectAllSubcameras(),
					(useLasers?selectAllSubcameras():null),
					true,
					this.debugLevel>1); // reset and trigger
	    	this.lastTimestamp=(String) this.images[0].getProperty("timestamp");
	    	return this.images;
	    }

	    public ImagePlus acquireSingleImage (UVLEDandLasers uvLEDLasers, boolean updateStatus){
			getImages(
					uvLEDLasers, // UVLEDLasers
					selectAllSubcameras(),
					null,
					true,
					this.debugLevel>1); // reset and trigger
	    	this.lastTimestamp=(String) this.images[0].getProperty("timestamp");
	    	return this.images[0];
	    }


	    public String getLastTimestampUnderscored(){
	    	return this.lastTimestamp.replace('.','_');
	    }


		/* Create a Thread[] array as large as the number of processors available.
		 * From Stephan Preibisch's Multithreading.java class. See:
		 * http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
		 */
		private Thread[] newThreadArray(int maxCPUs) {
			int n_cpus = Runtime.getRuntime().availableProcessors();
			if (n_cpus>maxCPUs)n_cpus=maxCPUs;
			return new Thread[n_cpus];
		}
	/* Start all given threads and wait on each of them until all are done.
		 * From Stephan Preibisch's Multithreading.java class. See:
		 * http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
		 */
		private static void startAndJoin(Thread[] threads)
		{
			for (int ithread = 0; ithread < threads.length; ++ithread)
			{
				threads[ithread].setPriority(Thread.NORM_PRIORITY);
				threads[ithread].start();
			}

			try
			{
				for (int ithread = 0; ithread < threads.length; ++ithread)
					threads[ithread].join();
			} catch (InterruptedException ie)
			{
				throw new RuntimeException(ie);
			}
		}
	}