001/*----------------------------------------------------------------------------*/
002/* Copyright (c) FIRST 2014. All Rights Reserved.                             */
003/* Open Source Software - may be modified and shared by FRC teams. The code   */
004/* must be accompanied by the FIRST BSD license file in the root directory of */
005/* the project.                                                               */
006/*----------------------------------------------------------------------------*/
007
008package edu.wpi.first.wpilibj.vision;
009
010import edu.wpi.first.wpilibj.image.ColorImage;
011import edu.wpi.first.wpilibj.image.HSLImage;
012import edu.wpi.first.wpilibj.image.NIVisionException;
013
014import java.io.DataInputStream;
015import java.io.IOException;
016import java.io.OutputStream;
017import java.net.InetSocketAddress;
018import java.net.Socket;
019import java.nio.ByteBuffer;
020
021import static com.ni.vision.NIVision.Image;
022import static com.ni.vision.NIVision.Priv_ReadJPEGString_C;
023import static edu.wpi.first.wpilibj.Timer.delay;
024
025/**
026 * Axis M1011 network camera
027 */
028public class AxisCamera {
029    public enum WhiteBalance {
030        kAutomatic,
031        kHold,
032        kFixedOutdoor1,
033        kFixedOutdoor2,
034        kFixedIndoor,
035        kFixedFluorescent1,
036        kFixedFluorescent2,
037    }
038
039    public enum ExposureControl {
040        kAutomatic,
041        kHold,
042        kFlickerFree50Hz,
043        kFlickerFree60Hz,
044    }
045
046    public enum Resolution {
047        k640x480,
048        k480x360,
049        k320x240,
050        k240x180,
051        k176x144,
052        k160x120,
053    }
054
055    public enum Rotation {
056        k0, k180
057    }
058
059    private static final String[] kWhiteBalanceStrings =
060            {"auto", "hold", "fixed_outdoor1", "fixed_outdoor2", "fixed_indoor",
061                    "fixed_fluor1", "fixed_fluor2",};
062
063    private static final String[] kExposureControlStrings =
064            {"auto", "hold", "flickerfree50", "flickerfree60",};
065
066    private static final String[] kResolutionStrings =
067            {"640x480", "480x360", "320x240", "240x180", "176x144", "160x120",};
068
069    private static final String[] kRotationStrings =
070            {"0", "180",};
071
072    private final static int kImageBufferAllocationIncrement = 1000;
073
074    private String m_cameraHost;
075    private Socket m_cameraSocket;
076
077    private ByteBuffer m_imageData = ByteBuffer.allocate(5000);
078    private final Object m_imageDataLock = new Object();
079    private boolean m_freshImage = false;
080
081    private int m_brightness = 50;
082    private WhiteBalance m_whiteBalance = WhiteBalance.kAutomatic;
083    private int m_colorLevel = 50;
084    private ExposureControl m_exposureControl = ExposureControl.kAutomatic;
085    private int m_exposurePriority = 50;
086    private int m_maxFPS = 0;
087    private Resolution m_resolution = Resolution.k640x480;
088    private int m_compression = 50;
089    private Rotation m_rotation = Rotation.k0;
090    private final Object m_parametersLock = new Object();
091    private boolean m_parametersDirty = true;
092    private boolean m_streamDirty = true;
093
094    private boolean m_done = false;
095
096    /**
097     * AxisCamera constructor
098     *
099     * @param cameraHost The host to find the camera at, typically an IP address
100     */
101    public AxisCamera(String cameraHost) {
102        m_cameraHost = cameraHost;
103        m_captureThread.start();
104    }
105
106    /**
107     * Return true if the latest image from the camera has not been retrieved by calling GetImage() yet.
108     * @return true if the image has not been retrieved yet.
109     */
110    public boolean isFreshImage() {
111        return m_freshImage;
112    }
113
114    /**
115     * Get an image from the camera and store it in the provided image.
116     *
117     * @param image The imaq image to store the result in. This must be an HSL or RGB image.
118     * @return <code>true</code> upon success, <code>false</code> on a failure
119     */
120    public boolean getImage(Image image) {
121        if (m_imageData.limit() == 0) {
122            return false;
123        }
124
125        synchronized (m_imageDataLock) {
126            Priv_ReadJPEGString_C(image, m_imageData.array());
127        }
128
129        m_freshImage = false;
130
131        return true;
132    }
133
134    /**
135     * Get an image from the camera and store it in the provided image.
136     *
137     * @param image The image to store the result in. This must be an HSL or RGB image
138     * @return true upon success, false on a failure
139     */
140    public boolean getImage(ColorImage image) {
141        return this.getImage(image.image);
142    }
143
144    /**
145     * Instantiate a new image object and fill it with the latest image from the camera.
146     *
147     * @return a pointer to an HSLImage object
148     */
149    public HSLImage getImage() throws NIVisionException {
150        HSLImage image = new HSLImage();
151        this.getImage(image);
152        return image;
153    }
154
155    /**
156     * Request a change in the brightness of the camera images.
157     *
158     * @param brightness valid values 0 .. 100
159     */
160    public void writeBrightness(int brightness) {
161        if (brightness < 0 || brightness > 100) {
162            throw new IllegalArgumentException("Brightness must be from 0 to 100");
163        }
164
165        synchronized (m_parametersLock) {
166            if (m_brightness != brightness) {
167                m_brightness = brightness;
168                m_parametersDirty = true;
169            }
170        }
171    }
172
173    /**
174     * @return The configured brightness of the camera images
175     */
176    public int getBrightness() {
177        synchronized (m_parametersLock) {
178            return m_brightness;
179        }
180    }
181
182    /**
183     * Request a change in the white balance on the camera.
184     *
185     * @param whiteBalance Valid values from the <code>WhiteBalance</code> enum.
186     */
187    public void writeWhiteBalance(WhiteBalance whiteBalance) {
188        synchronized (m_parametersLock) {
189            if (m_whiteBalance != whiteBalance) {
190                m_whiteBalance = whiteBalance;
191                m_parametersDirty = true;
192            }
193        }
194    }
195
196    /**
197     * @return The configured white balances of the camera images
198     */
199    public WhiteBalance getWhiteBalance() {
200        synchronized (m_parametersLock) {
201            return m_whiteBalance;
202        }
203    }
204
205    /**
206     * Request a change in the color level of the camera images.
207     *
208     * @param colorLevel valid values are 0 .. 100
209     */
210    public void writeColorLevel(int colorLevel) {
211        if (colorLevel < 0 || colorLevel > 100) {
212            throw new IllegalArgumentException("Color level must be from 0 to 100");
213        }
214
215        synchronized (m_parametersLock) {
216            if (m_colorLevel != colorLevel) {
217                m_colorLevel = colorLevel;
218                m_parametersDirty = true;
219            }
220        }
221    }
222
223    /**
224     * @return The configured color level of the camera images
225     */
226    public int getColorLevel() {
227        synchronized (m_parametersLock) {
228            return m_colorLevel;
229        }
230    }
231
232    /**
233     * Request a change in the camera's exposure mode.
234     *
235     * @param exposureControl A mode to write in the <code>Exposure</code> enum.
236     */
237    public void writeExposureControl(ExposureControl exposureControl) {
238        synchronized (m_parametersLock) {
239            if (m_exposureControl != exposureControl) {
240                m_exposureControl = exposureControl;
241                m_parametersDirty = true;
242            }
243        }
244    }
245
246    /**
247     * @return The configured exposure control mode of the camera
248     */
249    public ExposureControl getExposureControl() {
250        synchronized (m_parametersLock) {
251            return m_exposureControl;
252        }
253    }
254
255    /**
256     * Request a change in the exposure priority of the camera.
257     *
258     * @param exposurePriority Valid values are 0, 50, 100.
259     *                         0 = Prioritize image quality
260     *                         50 = None
261     *                         100 = Prioritize frame rate
262     */
263    public void writeExposurePriority(int exposurePriority) {
264        if (exposurePriority != 0 && exposurePriority != 50 && exposurePriority != 100) {
265            throw new IllegalArgumentException("Exposure priority must be 0, 50, or 100");
266        }
267
268        synchronized (m_parametersLock) {
269            if (m_exposurePriority != exposurePriority) {
270                m_exposurePriority = exposurePriority;
271                m_parametersDirty = true;
272            }
273        }
274    }
275
276    /**
277     * @return The configured exposure priority of the camera
278     */
279    public int getExposurePriority() {
280        synchronized (m_parametersLock) {
281            return m_exposurePriority;
282        }
283    }
284
285    /**
286     * Write the maximum frames per second that the camera should send
287     * Write 0 to send as many as possible.
288     *
289     * @param maxFPS The number of frames the camera should send in a second, exposure permitting.
290     */
291    public void writeMaxFPS(int maxFPS) {
292        synchronized (m_parametersLock) {
293            if (m_maxFPS != maxFPS) {
294                m_maxFPS = maxFPS;
295                m_parametersDirty = true;
296                m_streamDirty = true;
297            }
298        }
299    }
300
301    /**
302     * @return The configured maximum FPS of the camera
303     */
304    public int getMaxFPS() {
305        synchronized (m_parametersLock) {
306            return m_maxFPS;
307        }
308    }
309
310    /**
311     * Write resolution value to camera.
312     *
313     * @param resolution The camera resolution value to write to the camera.
314     */
315    public void writeResolution(Resolution resolution) {
316        synchronized (m_parametersLock) {
317            if (m_resolution != resolution) {
318                m_resolution = resolution;
319                m_parametersDirty = true;
320                m_streamDirty = true;
321            }
322        }
323    }
324
325    /**
326     * @return The configured resolution of the camera (not necessarily the same
327     * resolution as the most recent image, if it was changed recently.)
328     */
329    public Resolution getResolution() {
330        synchronized (m_parametersLock) {
331            return m_resolution;
332        }
333    }
334
335    /**
336     * Write the compression value to the camera.
337     *
338     * @param compression Values between 0 and 100.
339     */
340    public void writeCompression(int compression) {
341        if (compression < 0 || compression > 100) {
342            throw new IllegalArgumentException("Compression must be from 0 to 100");
343        }
344
345        synchronized (m_parametersLock) {
346            if (m_compression != compression) {
347                m_compression = compression;
348                m_parametersDirty = true;
349                m_streamDirty = true;
350            }
351        }
352    }
353
354    /**
355     * @return The configured compression level of the camera images
356     */
357    public int getCompression() {
358        synchronized (m_parametersLock) {
359            return m_compression;
360        }
361    }
362
363    /**
364     * Write the rotation value to the camera.
365     * If you mount your camera upside down, use this to adjust the image for you.
366     *
367     * @param rotation A value from the {@link Rotation} enum
368     */
369    public void writeRotation(Rotation rotation) {
370        synchronized (m_parametersLock) {
371            if (m_rotation != rotation) {
372                m_rotation = rotation;
373                m_parametersDirty = true;
374                m_streamDirty = true;
375            }
376        }
377    }
378
379    /**
380     * @return The configured rotation mode of the camera
381     */
382    public Rotation getRotation() {
383        synchronized (m_parametersLock) {
384            return m_rotation;
385        }
386    }
387
388    /**
389     * Thread spawned by AxisCamera constructor to receive images from cam
390     */
391    private Thread m_captureThread = new Thread(new Runnable() {
392        @Override
393        public void run() {
394            int consecutiveErrors = 0;
395
396            // Loop on trying to setup the camera connection. This happens in a background
397            // thread so it shouldn't effect the operation of user programs.
398            while (!m_done) {
399                String requestString = "GET /mjpg/video.mjpg HTTP/1.1\n" +
400                        "User-Agent: HTTPStreamClient\n" +
401                        "Connection: Keep-Alive\n" +
402                        "Cache-Control: no-cache\n" +
403                        "Authorization: Basic RlJDOkZSQw==\n\n";
404
405                try {
406                    m_cameraSocket = AxisCamera.this.createCameraSocket(requestString);
407                    AxisCamera.this.readImagesFromCamera();
408                    consecutiveErrors = 0;
409                } catch (IOException e) {
410                    consecutiveErrors++;
411
412                    if (consecutiveErrors > 5) {
413                        e.printStackTrace();
414                    }
415                }
416
417                delay(0.5);
418            }
419        }
420    });
421
422    /**
423     * This function actually reads the images from the camera.
424     */
425    private void readImagesFromCamera() throws IOException {
426        DataInputStream cameraInputStream = new DataInputStream(m_cameraSocket.getInputStream());
427
428        while (!m_done) {
429            String line = cameraInputStream.readLine();
430
431            if (line.startsWith("Content-Length: ")) {
432                int contentLength = Integer.valueOf(line.substring(16));
433
434                /* Skip the next blank line */
435                cameraInputStream.readLine();
436                contentLength -= 4;
437
438                /* The next four bytes are the JPEG magic number */
439                byte[] data = new byte[contentLength];
440                cameraInputStream.readFully(data);
441
442                synchronized (m_imageDataLock) {
443                    if (m_imageData.capacity() < data.length) {
444                        m_imageData = ByteBuffer.allocate(data.length + kImageBufferAllocationIncrement);
445                    }
446
447                    m_imageData.clear();
448                    m_imageData.limit(contentLength);
449                    m_imageData.put(data);
450
451                    m_freshImage = true;
452                }
453
454                if (this.writeParameters()) {
455                    break;
456                }
457
458                /* Skip the boundary and Content-Type header */
459                cameraInputStream.readLine();
460                cameraInputStream.readLine();
461            }
462        }
463
464        m_cameraSocket.close();
465    }
466
467    /**
468     * Send a request to the camera to set all of the parameters.  This is called
469     * in the capture thread between each frame. This strategy avoids making lots
470     * of redundant HTTP requests, accounts for failed initial requests, and
471     * avoids blocking calls in the main thread unless necessary.
472     * <p>
473     * This method does nothing if no parameters have been modified since it last
474     * completely successfully.
475     *
476     * @return <code>true</code> if the stream should be restarted due to a
477     * parameter changing.
478     */
479    private boolean writeParameters() {
480        if (m_parametersDirty) {
481            String request = "GET /axis-cgi/admin/param.cgi?action=update";
482
483            synchronized (m_parametersLock) {
484                request += "&ImageSource.I0.Sensor.Brightness=" + m_brightness;
485                request += "&ImageSource.I0.Sensor.WhiteBalance=" + kWhiteBalanceStrings[m_whiteBalance.ordinal()];
486                request += "&ImageSource.I0.Sensor.ColorLevel=" + m_colorLevel;
487                request += "&ImageSource.I0.Sensor.Exposure=" + kExposureControlStrings[m_exposureControl.ordinal()];
488                request += "&ImageSource.I0.Sensor.ExposurePriority=" + m_exposurePriority;
489                request += "&Image.I0.Stream.FPS=" + m_maxFPS;
490                request += "&Image.I0.Appearance.Resolution=" + kResolutionStrings[m_resolution.ordinal()];
491                request += "&Image.I0.Appearance.Compression=" + m_compression;
492                request += "&Image.I0.Appearance.Rotation=" + kRotationStrings[m_rotation.ordinal()];
493            }
494
495            request += " HTTP/1.1\n";
496            request += "User-Agent: HTTPStreamClient\n";
497            request += "Connection: Keep-Alive\n";
498            request += "Cache-Control: no-cache\n";
499            request += "Authorization: Basic RlJDOkZSQw==\n\n";
500
501            try {
502                Socket socket = this.createCameraSocket(request);
503                socket.close();
504
505                m_parametersDirty = false;
506
507                if (m_streamDirty) {
508                    m_streamDirty = false;
509                    return true;
510                } else {
511                    return false;
512                }
513            } catch (IOException | NullPointerException e) {
514                return false;
515            }
516
517        }
518
519        return false;
520    }
521
522    /**
523     * Create a socket connected to camera
524     * Used to create a connection for reading images and setting parameters
525     *
526     * @param requestString The initial request string to send upon successful connection.
527     * @return The created socket
528     */
529    private Socket createCameraSocket(String requestString) throws IOException {
530        /* Connect to the server */
531        Socket socket = new Socket();
532        socket.connect(new InetSocketAddress(m_cameraHost, 80), 5000);
533
534        /* Send the HTTP headers */
535        OutputStream socketOutputStream = socket.getOutputStream();
536        socketOutputStream.write(requestString.getBytes());
537
538        return socket;
539    }
540
541    @Override
542    public String toString() {
543        return "AxisCamera{" +
544                "FreshImage=" + isFreshImage() +
545                ", Brightness=" + getBrightness() +
546                ", WhiteBalance=" + getWhiteBalance() +
547                ", ColorLevel=" + getColorLevel() +
548                ", ExposureControl=" + getExposureControl() +
549                ", ExposurePriority=" + getExposurePriority() +
550                ", MaxFPS=" + getMaxFPS() +
551                ", Resolution=" + getResolution() +
552                ", Compression=" + getCompression() +
553                ", Rotation=" + getRotation() +
554                '}';
555    }
556}