001package edu.wpi.first.wpilibj;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.io.OutputStream;
007import java.net.InetSocketAddress;
008import java.net.ServerSocket;
009import java.net.Socket;
010import java.nio.ByteBuffer;
011import java.nio.ByteOrder;
012import java.util.ArrayDeque;
013import java.util.ArrayList;
014import java.util.Deque;
015import java.util.List;
016import java.util.NoSuchElementException;
017import java.util.concurrent.atomic.AtomicBoolean;
018
019import com.ni.vision.NIVision;
020import com.ni.vision.NIVision.Image;
021import com.ni.vision.NIVision.RawData;
022import com.ni.vision.VisionException;
023
024import edu.wpi.first.wpilibj.DriverStation;
025import edu.wpi.first.wpilibj.Timer;
026import edu.wpi.first.wpilibj.vision.USBCamera;
027
028//replicates CameraServer.cpp in java lib
029
030public class CameraServer {
031
032    private static final int kPort = 1180;
033    private static final byte[] kMagicNumber = { 0x01, 0x00, 0x00, 0x00 };
034    private static final int kSize640x480 = 0;
035    private static final int kSize320x240 = 1;
036    private static final int kSize160x120 = 2;
037    private static final int kHardwareCompression = -1;
038    private static final String kDefaultCameraName = "cam1";
039    private static final int kMaxImageSize = 200000;
040    private static CameraServer server;
041
042    public static CameraServer getInstance() {
043        if (server == null) {
044            server = new CameraServer();
045        }
046        return server;
047    }
048
049    private Thread serverThread;
050    private int m_quality;
051    private boolean m_autoCaptureStarted;
052    private boolean m_hwClient = true;
053    private USBCamera m_camera;
054    private CameraData m_imageData;
055    private Deque<ByteBuffer> m_imageDataPool;
056
057    private class CameraData {
058        RawData data;
059        int start;
060        public CameraData(RawData d, int s) {
061            data = d;
062            start = s;
063        }
064    }
065
066    private CameraServer() {
067        m_quality = 50;
068        m_camera = null;
069        m_imageData = null;
070        m_imageDataPool = new ArrayDeque<>(3);
071        for (int i = 0; i < 3; i++) {
072            m_imageDataPool.addLast(ByteBuffer.allocateDirect(kMaxImageSize));
073        }
074        serverThread = new Thread(new Runnable() {
075                public void run() {
076                    try {
077                        serve();
078                    } catch (IOException e) {
079                        // do stuff here
080                    } catch (InterruptedException e) {
081                        // do stuff here
082                    }
083                }
084            });
085        serverThread.setName("CameraServer Send Thread");
086        serverThread.start();
087    }
088
089    private synchronized void setImageData(RawData data, int start) {
090        if (m_imageData != null && m_imageData.data != null) {
091            m_imageData.data.free();
092            if (m_imageData.data.getBuffer() != null)
093                m_imageDataPool.addLast(m_imageData.data.getBuffer());
094            m_imageData = null;
095        }
096        m_imageData = new CameraData(data, start);
097        notifyAll();
098    }
099
100    /**
101     * Manually change the image that is served by the MJPEG stream. This can be
102     * called to pass custom annotated images to the dashboard. Note that, for
103     * 640x480 video, this method could take between 40 and 50 milliseconds to
104     * complete.
105     *
106     * This shouldn't be called if {@link #startAutomaticCapture} is called.
107     *
108     * @param image
109     *            The IMAQ image to show on the dashboard
110     */
111    public void setImage(Image image) {
112        // handle multi-threadedness
113
114        /* Flatten the IMAQ image to a JPEG */
115
116        RawData data = NIVision.imaqFlatten(image,
117                                            NIVision.FlattenType.FLATTEN_IMAGE,
118                                            NIVision.CompressionType.COMPRESSION_JPEG, 10 * m_quality);
119        ByteBuffer buffer = data.getBuffer();
120        boolean hwClient;
121
122        synchronized (this) {
123            hwClient = m_hwClient;
124        }
125
126        /* Find the start of the JPEG data */
127        int index = 0;
128        if (hwClient) {
129            while (index < buffer.limit() - 1) {
130                if ((buffer.get(index) & 0xff) == 0xFF
131                    && (buffer.get(index + 1) & 0xff) == 0xD8)
132                    break;
133                index++;
134            }
135        }
136
137        if (buffer.limit() - index - 1 <= 2) {
138            throw new VisionException("data size of flattened image is less than 2. Try another camera! ");
139        }
140
141        setImageData(data, index);
142    }
143
144    /**
145     * Start automatically capturing images to send to the dashboard.
146     * You should call this method to just see a camera feed on the dashboard
147     * without doing any vision processing on the roboRIO. {@link #setImage}
148     * shouldn't be called after this is called.
149     * This overload calles {@link #startAutomaticCapture(String)} with the
150     * default camera name
151     */
152    public void startAutomaticCapture() {
153        startAutomaticCapture(USBCamera.kDefaultCameraName);
154    }
155
156    /**
157     * Start automatically capturing images to send to the dashboard.
158     *
159     * You should call this method to just see a camera feed on the dashboard
160     * without doing any vision processing on the roboRIO. {@link #setImage}
161     * shouldn't be called after this is called.
162     *
163     * @param cameraName
164     *            The name of the camera interface (e.g. "cam1")
165     */
166    public void startAutomaticCapture(String cameraName) {
167        USBCamera camera = new USBCamera(cameraName);
168        camera.openCamera();
169        startAutomaticCapture(camera);
170    }
171
172    public synchronized void startAutomaticCapture(USBCamera camera) {
173        if (m_autoCaptureStarted) return;
174        m_autoCaptureStarted = true;
175        m_camera = camera;
176
177        m_camera.startCapture();
178
179        Thread captureThread = new Thread(new Runnable() {
180                @Override
181                public void run() {
182                    capture();
183                }
184            });
185        captureThread.setName("Camera Capture Thread");
186        captureThread.start();
187    }
188
189    protected void capture() {
190        Image frame = NIVision.imaqCreateImage(NIVision.ImageType.IMAGE_RGB, 0);
191        while (true) {
192            boolean hwClient;
193            ByteBuffer dataBuffer = null;
194            synchronized (this) {
195                hwClient = m_hwClient;
196                if (hwClient) {
197                    dataBuffer = m_imageDataPool.removeLast();
198                }
199            }
200
201            try {
202                if (hwClient && dataBuffer != null) {
203                    // Reset the image buffer limit
204                    dataBuffer.limit(dataBuffer.capacity() - 1);
205                    m_camera.getImageData(dataBuffer);
206                    setImageData(new RawData(dataBuffer), 0);
207                } else {
208                    m_camera.getImage(frame);
209                    setImage(frame);
210                }
211            } catch (VisionException ex) {
212                DriverStation.reportError("Error when getting image from the camera: " + ex.getMessage(), true);
213                if (dataBuffer != null) {
214                    synchronized (this) {
215                        m_imageDataPool.addLast(dataBuffer);
216                        Timer.delay(.1);
217                    }
218                }
219            }
220        }
221    }
222
223
224
225    /**
226     * check if auto capture is started
227     *
228     */
229    public synchronized boolean isAutoCaptureStarted() {
230        return m_autoCaptureStarted;
231    }
232
233    /**
234     * Sets the size of the image to use. Use the public kSize constants
235     * to set the correct mode, or set it directory on a camera and call
236     * the appropriate autoCapture method
237     * @param size The size to use
238     */
239    public synchronized void setSize(int size) {
240        if (m_camera == null) return;
241        switch (size) {
242        case kSize640x480:
243            m_camera.setSize(640, 480);
244            break;
245        case kSize320x240:
246            m_camera.setSize(320, 240);
247            break;
248        case kSize160x120:
249            m_camera.setSize(160, 120);
250            break;
251        }
252    }
253
254    /**
255     * Set the quality of the compressed image sent to the dashboard
256     *
257     * @param quality
258     *            The quality of the JPEG image, from 0 to 100
259     */
260    public synchronized void setQuality(int quality) {
261        m_quality = quality > 100 ? 100 : quality < 0 ? 0 : quality;
262    }
263
264    /**
265     * Get the quality of the compressed image sent to the dashboard
266     *
267     * @return The quality, from 0 to 100
268     */
269    public synchronized int getQuality() {
270        return m_quality;
271    }
272
273    /**
274     * Run the M-JPEG server.
275     *
276     * This function listens for a connection from the dashboard in a background
277     * thread, then sends back the M-JPEG stream.
278     *
279     * @throws IOException if the Socket connection fails
280     * @throws InterruptedException if the sleep is interrupted
281     */
282    protected void serve() throws IOException, InterruptedException {
283
284        ServerSocket socket = new ServerSocket();
285        socket.setReuseAddress(true);
286        InetSocketAddress address = new InetSocketAddress(kPort);
287        socket.bind(address);
288
289        while (true) {
290            try {
291                Socket s = socket.accept();
292
293                DataInputStream is = new DataInputStream(s.getInputStream());
294                DataOutputStream os = new DataOutputStream(s.getOutputStream());
295
296                int fps = is.readInt();
297                int compression = is.readInt();
298                int size = is.readInt();
299
300                if (compression != kHardwareCompression) {
301                    DriverStation.reportError("Choose \"USB Camera HW\" on the dashboard", false);
302                    s.close();
303                    continue;
304                }
305
306                // Wait for the camera
307                synchronized (this) {
308                    System.out.println("Camera not yet ready, awaiting image");
309                    if (m_camera == null) wait();
310                    m_hwClient = compression == kHardwareCompression;
311                    if (!m_hwClient) setQuality(100 - compression);
312                    else if (m_camera != null) m_camera.setFPS(fps);
313                    setSize(size);
314                }
315
316                long period = (long) (1000 / (1.0 * fps));
317                while (true) {
318                    long t0 = System.currentTimeMillis();
319                    CameraData imageData = null;
320                    synchronized (this) {
321                        wait();
322                        imageData = m_imageData;
323                        m_imageData = null;
324                    }
325
326                    if (imageData == null) continue;
327                    // Set the buffer position to the start of the data,
328                    // and then create a new wrapper for the data at
329                    // exactly that position
330                    imageData.data.getBuffer().position(imageData.start);
331                    byte[] imageArray = new byte[imageData.data.getBuffer().remaining()];
332                    imageData.data.getBuffer().get(imageArray, 0, imageData.data.getBuffer().remaining());
333
334                    // write numbers
335                    try {
336                        os.write(kMagicNumber);
337                        os.writeInt(imageArray.length);
338                        os.write(imageArray);
339                        os.flush();
340                        long dt = System.currentTimeMillis() - t0;
341
342                        if (dt < period) {
343                            Thread.sleep(period - dt);
344                        }
345                    } catch (IOException | UnsupportedOperationException ex) {
346                        DriverStation.reportError(ex.getMessage(), true);
347                        break;
348                    } finally {
349                        imageData.data.free();
350                        if (imageData.data.getBuffer() != null) {
351                            synchronized (this) {
352                                m_imageDataPool.addLast(imageData.data.getBuffer());
353                            }
354                        }
355                    }
356                }
357            } catch (IOException ex) {
358                DriverStation.reportError(ex.getMessage(), true);
359                continue;
360            }
361        }
362    }
363}