Port snapshot tool from IMX

Change-Id: Ibf0ebb92d1f51e79c14bd624ec7bae37b3258c77
diff --git a/debian/excelsior-tools.install b/debian/excelsior-tools.install
index 6422483..842f46e 100644
--- a/debian/excelsior-tools.install
+++ b/debian/excelsior-tools.install
@@ -1 +1,2 @@
 pinout /usr/bin
+snapshot /usr/bin
diff --git a/snapshot b/snapshot
new file mode 100755
index 0000000..0aac41d
--- /dev/null
+++ b/snapshot
@@ -0,0 +1,289 @@
+#!/usr/bin/env python3
+
+import argparse
+import contextlib
+import fcntl
+import os
+import select
+import sys
+import termios
+import time
+import threading
+
+import gi
+gi.require_version('Gst', '1.0')
+gi.require_version('GstBase', '1.0')
+
+from functools import partial
+from gi.repository import GLib, GObject, Gst, GstBase
+from PIL import Image
+
+Gst.init(None)
+
+FILENAME_PREFIX = 'img'
+FILENAME_SUFFIX = '.png'
+AF_SYSFS_NODE = '/sys/module/ov5645_camera_mipi_v2/parameters/ov5645_af'
+CAMERA_INIT_QUERY_SYSFS_NODE = '/sys/module/ov5645_camera_mipi_v2/parameters/ov5645_initialized'
+HDMI_SYSFS_NODE = '/sys/class/drm/card0/card0-HDMI-A-1/status'
+
+# No of initial frames to throw away before camera has stabilized
+SCRAP_FRAMES = 1
+
+SRC_WIDTH = 2592
+SRC_HEIGHT = 1944
+SRC_RATE = '15/1'
+SRC_ELEMENT = 'v4l2src device=/dev/video0'
+
+SINK_WIDTH = 2592
+SINK_HEIGHT = 1944
+SINK_ELEMENT = ('appsink name=appsink sync=false emit-signals=true '
+                'max-buffers=1 drop=true')
+SCREEN_SINK = 'glimagesink sync=false'
+FAKE_SINK = 'fakesink sync=false'
+
+SRC_CAPS = 'video/x-raw,format=YUY2,width={width},height={height},framerate={rate}'
+SINK_CAPS = 'video/x-raw,format=RGB,width={width},height={height}'
+LEAKY_Q = 'queue max-size-buffers=1 leaky=downstream'
+
+PIPELINE = '''
+    {src_element} ! {src_caps} ! {leaky_q} ! tee name=t
+    t. ! {leaky_q} ! {screen_sink}
+    t. ! {leaky_q} ! videoconvert ! {sink_caps} ! {sink_element}
+    '''
+
+def monitor_connected():
+  with open(HDMI_SYSFS_NODE, 'r') as hdmi_status:
+    status = hdmi_status.read()
+    return (status.rstrip() == 'connected')
+
+def on_bus_message(bus, message, loop):
+  t = message.type
+  if t == Gst.MessageType.EOS:
+    loop.quit()
+  elif t == Gst.MessageType.WARNING:
+    err, debug = message.parse_warning()
+    sys.stderr.write('Warning: %s: %s\n' % (err, debug))
+  elif t == Gst.MessageType.ERROR:
+    err, debug = message.parse_error()
+    sys.stderr.write('Error: %s: %s\n' % (err, debug))
+    loop.quit()
+  return True
+
+
+def on_new_sample(sink, snapinfo):
+
+  if not snapinfo.save_frame():
+    # Throw away the frame
+    return Gst.FlowReturn.OK
+
+  sample = sink.emit('pull-sample')
+
+  buf = sample.get_buffer()
+  result, mapinfo = buf.map(Gst.MapFlags.READ)
+  if result:
+    imgfile = snapinfo.get_filename()
+    print('Saving image: ' + imgfile)
+    caps = sample.get_caps()
+    width = caps.get_structure(0).get_value('width')
+    height = caps.get_structure(0).get_value('height')
+    img = Image.frombytes('RGB', (width, height), mapinfo.data, 'raw')
+    img.save(imgfile)
+    img.close()
+  buf.unmap(mapinfo)
+  return Gst.FlowReturn.OK
+
+
+def run_pipeline(snapinfo):
+  src_caps = SRC_CAPS.format(width=SRC_WIDTH, height=SRC_HEIGHT, rate=SRC_RATE)
+  sink_caps = SINK_CAPS.format(width=SINK_WIDTH, height=SINK_HEIGHT)
+
+  if snapinfo.oneshot or not monitor_connected():
+    screen_sink = FAKE_SINK
+  else:
+    screen_sink = SCREEN_SINK
+
+  pipeline = PIPELINE.format(
+      leaky_q=LEAKY_Q,
+      src_element=SRC_ELEMENT,
+      src_caps=src_caps,
+      sink_caps=sink_caps,
+      sink_element=SINK_ELEMENT,
+      screen_sink=screen_sink)
+
+  pipeline = Gst.parse_launch(pipeline)
+  appsink = pipeline.get_by_name('appsink')
+  appsink.connect('new-sample', partial(on_new_sample, snapinfo=snapinfo))
+
+  loop = GLib.MainLoop()
+
+  # Set up a pipeline bus watch to catch errors.
+  bus = pipeline.get_bus()
+  bus.add_signal_watch()
+  bus.connect('message', on_bus_message, loop)
+
+  # Connect the loop to the snaphelper
+  snapinfo.connect_loop(loop)
+
+  # Run pipeline.
+  pipeline.set_state(Gst.State.PLAYING)
+
+  try:
+    loop.run()
+  except:
+    pass
+
+  # Clean up.
+  pipeline.set_state(Gst.State.NULL)
+  while GLib.MainContext.default().iteration(False):
+    pass
+
+
+@contextlib.contextmanager
+def setup_keyboard():
+  fd = sys.stdin.fileno()
+  termattr = termios.tcgetattr(fd)
+  orgattr = list(termattr)
+  termattr[3] = termattr[3] & ~(termios.ICANON | termios.ECHO)
+  termios.tcsetattr(fd, termios.TCSANOW, termattr)
+  orgflags = fcntl.fcntl(fd, fcntl.F_GETFL)
+  fcntl.fcntl(fd, fcntl.F_SETFL, orgflags | os.O_NONBLOCK)
+
+  try:
+    yield
+  finally:
+    termios.tcsetattr(fd, termios.TCSAFLUSH, orgattr)
+    fcntl.fcntl(fd, fcntl.F_SETFL, orgflags)
+
+class SnapHelper:
+
+  def __init__(self, sysfs, prefix='img', oneshot=True, suffix='jpg'):
+    self.prefix = prefix
+    self.oneshot = oneshot
+    self.suffix = suffix
+    self.snap_it = oneshot
+    self.num = 0
+    self.scrapframes = SCRAP_FRAMES
+    self.sysfs = sysfs
+    self.loop = None
+
+    if not oneshot:
+      self.pipe_r, self.pipe_w = os.pipe()
+      self.thread = threading.Thread(target=self.read_keyboard)
+      self.thread.daemon = True
+
+  def get_filename(self):
+    while True:
+      filename = self.prefix + str(self.num).zfill(4) + '.' + self.suffix
+      self.num = self.num + 1
+      if not os.path.exists(filename):
+        break
+
+    return filename
+
+  def read_keyboard(self):
+    print('Press space to take a snap, r to refocus, or q to quit')
+    with setup_keyboard():
+      while True:
+        read_fd, _, _ = select.select([sys.stdin, self.pipe_r], [], [])
+        if self.pipe_r in read_fd:
+          break
+        c = sys.stdin.read()
+        if c == ' ':
+          self.snap_it = True
+        if c == 'r':
+          self.refocus()
+        if c == 'q':
+          while not self.loop.is_running():
+            time.sleep(0.01)
+          self.loop.quit()
+          break
+
+  def exit_keyboard_thread(self):
+    try:
+      os.close(self.pipe_w)
+    except:
+      pass
+
+  def check_af(self):
+    try:
+      self.sysfs.seek(0)
+      v = self.sysfs.read()
+      if int(v) != 0x10:
+        print('NO Focus')
+    except:
+      pass
+
+  def refocus(self):
+    try:
+      self.sysfs.write('1')
+      self.sysfs.flush()
+    except:
+      pass
+
+  def save_frame(self):
+    # We always want to throw away the initial frames to let the
+    # camera stabilize. This seemed empirically to be the right number
+    # when running on desktop.
+    if self.scrapframes > 0:
+      if self.scrapframes == SCRAP_FRAMES:
+        self.refocus()
+      self.scrapframes = self.scrapframes - 1
+      return False
+
+    if self.snap_it:
+      self.check_af()
+      self.snap_it = False
+      retval = True
+    else:
+      retval = False
+
+    if self.oneshot:
+      self.loop.quit()
+
+    return retval
+
+  def connect_loop(self, loop):
+    self.loop = loop
+    self.thread.start()
+
+
+def main(arguments):
+  parser = argparse.ArgumentParser('Take camera snapshots')
+  parser.add_argument(
+      '--prefix',
+      '-p',
+      dest='prefix',
+      help='Filename prefix',
+      default=FILENAME_PREFIX)
+  parser.add_argument(
+      '--oneshot',
+      dest='oneshot',
+      action='store_true',
+      help='One shot vs. interactive mode')
+  parser.add_argument(
+      '--format',
+      '-f',
+      dest='suffix',
+      default='jpg',
+      choices=('jpg', 'bmp', 'png'),
+      help='Format to save, default is JPEG')
+  args = parser.parse_args()
+
+  try:
+    with open(CAMERA_INIT_QUERY_SYSFS_NODE) as init_file:
+      init_file.seek(0)
+      init = init_file.read()
+      if int(init) != 1:
+        raise Exception('Cannot find ov5645 CSI camera, ' +
+                  'check that your camera is connected')
+    with open(AF_SYSFS_NODE, 'w+') as sysfs:
+      snap = SnapHelper(sysfs, args.prefix, args.oneshot, args.suffix)
+      run_pipeline(snap)
+      snap.exit_keyboard_thread()
+  except Exception as ex:
+    print(ex)
+
+
+if __name__ == '__main__':
+  main(sys.argv)
+  sys.exit()