{"action":"created","comment":{"url":"https://api.github.com/repos/delmic/odemis/pulls/comments/242955844","pull_request_review_id":186596804,"id":242955844,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0Mjk1NTg0NA==","diff_hunk":"@@ -0,0 +1,2116 @@\n+# -*- coding: utf-8 -*-\n+'''\n+Created on Aug 2018\n+\n+@author: Sabrina Rossberger, Delmic\n+\n+Copyright © 2018 Sabrina Rossberger, Delmic\n+\n+This file is part of Odemis.\n+\n+Odemis is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.\n+\n+Odemis 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.\n+\n+You should have received a copy of the GNU General Public License along with Odemis. If not, see http://www.gnu.org/licenses/.\n+'''\n+\n+from __future__ import division\n+\n+import Queue\n+import logging\n+from odemis import model, util\n+from odemis.model import oneway\n+import threading\n+import time\n+import socket\n+import numpy\n+import collections\n+import numbers\n+\n+\n+#  0= Boolean: Can have the values true or false. Valid entries are „true“ (true), „false“\n+#              (false), „on“ (true), „off“ (false), „yes“ (true), „no“ (false), „0“ (false), or\n+#               any other numerical value (true). On output only 0 (false) and 1 (true) is\n+#               used.\n+#  1= Numeric: A numerical value. In the case of a numerical value the minimum and\n+#              maximum value is returned.\n+#  2= List: The value is one entry in a list.\n+#  3= String: Any string can be used.\n+#  4= ExposureTime: An expression which evaluates to a time like „5ms“, „1h“, „1s“ etc. Valid\n+#                   units are ns (nanosecond), us (microsecond), ms (millisecond), s (second), m\n+#                   (minute), h(hour).\n+#  5= Display: A string which is displayed only (read only).\n+\n+PARAM_TYPE_BOOL = 0\n+PARAM_TYPE_NUMERIC = 1\n+PARAM_TYPE_LIST = 2\n+PARAM_TYPE_STRING = 3\n+PARAM_TYPE_EXPTIME = 4\n+PARAM_TYPE_DISPLAY = 5\n+\n+\n+class RemoteExError(StandardError):\n+\n+    def __init__(self, errnum, *args, **kwargs):\n+        # Needed for pickling, cf https://bugs.python.org/issue1692335 (fixed in Python 3.3)\n+        StandardError.__init__(self, errnum, *args, **kwargs)\n+        self.errnum = errnum\n+\n+    def __str__(self):\n+        errmsg = self._errordict.get(self.errnum, \"Unknown RemoteEx error.\")\n+        return \"Hamamatsu streak camera RemoteEx error %d: %s\" % (self.errnum, errmsg)\n+\n+    _errordict = {\n+            0: \"Command successfully executed.\",\n+            1: \"Invalid syntax (command must be followed by\"\n+               \"parentheses and must have the correct number\"\n+               \"and type of parameters separated by comma).\",\n+            2: \"Command or Parameters are unknown.\",\n+            3: \"Command currently not possible.\",\n+            6: \"Parameter is missing.\",\n+            7: \"Command cannot be executed.\",\n+            8: \"An error has occurred during execution.\",\n+            9: \"Data cannot be sent by TCP-IP.\",\n+            10: \"Value of a parameter is out of range.\",\n+        }\n+\n+\n+class ReadoutCamera(model.DigitalCamera):\n+    \"\"\"\n+    Represents Hamamatsu readout camera.\n+    \"\"\"\n+\n+    def __init__(self, name, role, parent, spectrograph=None, daemon=None, **kwargs):\n+        \"\"\" Initializes the Hamamatsu OrcaFlash readout camera.\n+        :parameter name: (str) as in Odemis\n+        :parameter role: (str) as in Odemis\n+        :parameter parent: class streakcamera\n+        \"\"\"\n+\n+        super(ReadoutCamera, self).__init__(name, role, parent=parent,\n+                                            daemon=daemon, **kwargs)  # init HwComponent\n+\n+        self.parent = parent\n+\n+        self._spectrograph = spectrograph\n+        if not spectrograph:\n+            logging.warning(\"No spectrograph specified. No wavelength metadata will be attached.\")\n+\n+        # Set parameters readout camera\n+        # TODO what we want to have in MD\n+        # TODO check if we need to do this or can use default HW file?\n+        parent.CamParamSet(\"Setup\", \"TimingMode\", \"Internal timing\")  # TODO internal or external?\n+        parent.CamParamSet(\"Setup\", \"TriggerMode\", 'Edge trigger')\n+        parent.CamParamSet(\"Setup\", \"TriggerSource\", 'BNC')\n+        parent.CamParamSet(\"Setup\", \"TriggerPolarity\", 'neg.')\n+        parent.CamParamSet(\"Setup\", \"ScanMode\", 'Subarray')\n+        parent.CamParamSet(\"Setup\", \"Binning\", '2 x 2')\n+        parent.CamParamSet(\"Setup\", \"VWidth\", '1016')\n+        parent.CamParamSet(\"Setup\", \"HWidth\", '1344')\n+        parent.CamParamSet(\"Setup\", \"ShowGainOffset\", 'True')\n+\n+        # TODO make nice!\n+        self.parent._getReadoutCamInfo = True\n+        cam_info = parent.CamParamGet(\"Setup\", \"CameraInfo\")\n+        try:\n+            self._hwVersion = cam_info[0][0] + \", \" + cam_info[0][1] + \", \" + cam_info[0][2]   # needs to be a string\n+        except:\n+            self._hwVersion = \"dummy\"\n+            logging.debug(\"Could not get hardware information for streak readout camera.\")\n+        self._metadata[model.MD_HW_VERSION] = self._hwVersion\n+        try:\n+            self._swVersion = cam_info[0][3] + \", \" + cam_info[0][4]   # needs to be a string\n+        except:\n+            self._swVersion = \"dummy\"\n+            logging.debug(\"Could not get software information for streak readout camera.\")\n+        self._metadata[model.MD_SW_VERSION] = self._swVersion\n+        self._metadata[model.MD_DET_TYPE] = model.MD_DT_INTEGRATING\n+\n+        # sensor size (resolution)\n+        # Note: sensor size of OrcaFlash is actually much larger (2048px x 2048px)\n+        # However, only a smaller subarea is used for operating the streak system.\n+        # x (lambda): horizontal, y (time): vertical\n+        full_res = (int(parent.CamParamGet(\"Setup\", \"HWidth\")[0]),\n+                       int(parent.CamParamGet(\"Setup\", \"VWidth\")[0]))\n+        self._metadata[model.MD_SENSOR_SIZE] = full_res\n+\n+        # 16-bit\n+        self._shape = full_res + (2 ** 16,)\n+\n+        self._binning = self._getBinning()  # used by _setResolution\n+\n+        # need to be before binning, as it is modified when changing binning\n+        resolution = (int(full_res[0]/self._binning[0]), int(full_res[1]/self._binning[1]))\n+        self.resolution = model.ResolutionVA(resolution, ((1, 1), full_res), setter=self._setResolution)\n+\n+        choices_bin = self._getReadoutCamBinningChoices()\n+        self.binning = model.VAEnumerated(self._binning, choices_bin, setter=self._setBinning)\n+        self._metadata[model.MD_BINNING] = self.binning.value\n+\n+        # physical pixel size is 6.5um x 6.5um\n+        sensor_pixelsize = (6.5e-06, 6.5e-06)\n+        self._metadata[model.MD_SENSOR_PIXEL_SIZE] = sensor_pixelsize   #self.pixelSize.value\n+\n+        # pixelsize VA is the sensor size, it does not include binning or magnification\n+        self.pixelSize = model.VigilantAttribute(sensor_pixelsize, unit=\"m\", readonly=True)\n+\n+        # multiply with mag as we use the 1/M as input in yaml file!\n+        eff_pixelsize = sensor_pixelsize[0] * self._binning[0] * self._metadata.get(model.MD_LENS_MAG, 1.0)\n+        # eff_pixelsize  # TODO think it is useful to have the eff px size attached to each image in the MD\n+\n+        # Note: no function to get current acqMode.\n+        # Note: Acquisition mode, needs to be before exposureTime!\n+        # Acquisition mode should be either \"Live\" (non-sync acq) or \"SingleLive\" (sync acq) for now.\n+        self.acqMode = \"Live\"\n+\n+        range_exp = self._getCamExpTimeRange()\n+        self._exp_time = self.GetCamExpTime()\n+        self.exposureTime = model.FloatContinuous(self._exp_time, range_exp, unit=\"s\", setter=self._setCamExpTime)\n+        self._metadata[model.MD_EXP_TIME] = self.exposureTime.value\n+        # Note: timeRange of streakunit > exposureTime readoutcam is possible and okay.\n+\n+        self.readoutRate = model.VigilantAttribute(425000000, unit=\"Hz\", readonly=True)  # MHz\n+        self._metadata[model.MD_READOUT_TIME] = 1 / self.readoutRate.value  # s\n+\n+        # spectrograph VAs after readout camera VAs\n+        if self._spectrograph:\n+            logging.debug(\"Starting streak camera with spectrograph.\")\n+            self._spectrograph.position.subscribe(self._updateWavelengthList, init=True)\n+\n+        # for synchronized acquisition\n+        self._sync_event = None\n+        self.softwareTrigger = model.Event()\n+        # queue events starting an acquisition (advantageous when event.notify is called very fast)\n+        self.queue_events = collections.deque()\n+        self._acq_sync_lock = threading.Lock()\n+\n+        # start thread, which keeps reading the dataport when an image/scaling table has arrived\n+        # after commandport thread to be able to set the RingBuffer\n+        # AcqLiveMonitor writes images to Ringbuffer, which we can read from\n+        # only works if we use \"Live\" or \"SingleLive\" mode\n+        self.parent.AcqLiveMonitor(\"RingBuffer\", \"10\")  # TODO need to be handled in case we use other acq modes\n+        self.t_image = threading.Thread(target=self._getDataFromBuffer)\n+        self.t_image.start()\n+\n+        self.data = StreakCameraDataFlow(self._start, self._stop, self._sync)\n+\n+    def _updateWavelengthList(self, _=None):\n+        \"\"\"\n+        Updates the wavelength list MD based on the current spectrograph position.\n+        \"\"\"\n+        npixels = self.resolution.value[0]  # number of pixels, horizontal is wavelength\n+        # pixelsize VA is sensor px size without binning and magnification\n+        pxs = self.pixelSize.value[0] * self.binning.value[0] * self._metadata.get(model.MD_LENS_MAG, 1.0)\n+        wll = self._spectrograph.getPixelToWavelength(npixels, pxs)\n+        self._metadata[model.MD_WL_LIST] = wll\n+\n+    def _getReadoutCamBinningChoices(self):\n+        \"\"\"\n+        Get min and max values for exposure time. Values are in order. First to fourth values see CamParamInfoEx.\n+        :return: (tuple) min and max exposure time\n+        \"\"\"\n+        choices_raw = self.parent.CamParamInfoEx(\"Setup\", \"Binning\")[4:]\n+        choices = []\n+        for choice in choices_raw:\n+            choices.append((int(choice[0]), int(choice[4])))\n+\n+        return set(choices)\n+\n+    def _getBinning(self):\n+        \"\"\"\n+        Get binning setting from camera.\n+        Convert the format provided by RemoteEx.\n+        :return: (tuple) current binning\n+        \"\"\"\n+        _binning = self.parent.CamParamGet(\"Setup\", \"Binning\")  # returns list of format e.g. [2 x 2]\n+        # convert as .resolution VA need tuple instead of list\n+        binning = int(_binning[0].split(\"x\")[0].strip(\" \")), int(_binning[0].split(\"x\")[1].strip(\" \"))\n+\n+        return binning\n+\n+    def _setBinning(self, value):\n+        \"\"\"\n+        Called when .binning VA is modified. It actually modifies the camera binning.\n+        :parameter value: (2-tuple int) binning value to set\n+        :return: current binning value\n+        \"\"\"\n+        # ResolutionVA need tuple instead of list of format [2 x 2]\n+        binning = \"%s x %s\" % (value[0], value[1])\n+\n+        # If camera is acquiring, it is essential to stop cam first and then change binning.\n+        # Currently, this only affects the Alignment tab, where camera is continuously acquiring.\n+        if self.data.active:  # Note: not thread save -> change # TODO use update_settings()\n+            self.parent.AcqStop()  # TODO check with HW!\n+            self.parent.CamParamSet(\"Setup\", \"Binning\", binning)\n+            self.parent.AcqStart(\"Live\")\n+\n+        prev_binning, self._binning = self._binning, value\n+\n+        # adapt resolution\n+        change = (prev_binning[0] / self._binning[0],\n+                  prev_binning[1] / self._binning[1])\n+        old_resolution = self.resolution.value\n+        new_res = (int(round(old_resolution[0] * change[0])),\n+                   int(round(old_resolution[1] * change[1])))\n+\n+        # fit and also ensure wl is correct (calls updateWavelengthList)\n+        self.resolution.value = new_res\n+\n+        self._metadata[model.MD_BINNING] = self._binning  # update MD\n+\n+        return self._binning\n+\n+    def _setResolution(self, _=None):\n+        \"\"\"\n+        Sets the resolution VA.\n+        So far the full field of view is always used. Therefore, resolution only changes with binning.\n+        :return: current resolution value\n+        \"\"\"\n+        # Note: we can keep it simple as long as we do not provide to change the sensor size yet...\n+        resolution = self._shape[:2]\n+        new_res = (int(resolution[0] // self._binning[0]),\n+                   int(resolution[1] // self._binning[1]))  # floor division\n+\n+        if self._spectrograph:\n+            self._updateWavelengthList()  # update WavelengthList when changing binning\n+\n+        return new_res\n+\n+    def _getCamExpTimeRange(self):\n+        \"\"\"\n+        Get min and max values for the camera exposure time.\n+        :return: tuple containing min and max exposure time\n+        \"\"\"\n+        exp = self.parent.CamParamInfoEx(\"Live\", \"Exposure\")  # returns list\n+        # Values in returned list \"exp\" are in order. 1st - 4th values see CamParamInfoEx.\n+        min_value = exp[4]\n+        max_value = exp[-1]\n+\n+        min_value_raw, min_unit = min_value.split(' ')[0:2]\n+        max_value_raw, max_unit = max_value.split(' ')[0:2]\n+\n+        min_exp = self.parent.convertUnit2Time(min_value_raw, min_unit)\n+        max_exp = self.parent.convertUnit2Time(max_value_raw, max_unit)\n+\n+        return min_exp, max_exp\n+\n+    def GetCamExpTime(self):\n+        \"\"\"\n+        Get the camera exposure time.\n+        Converts the provided value received from RemoteEx into sec.\n+        :return: (float) exposure time in sec\n+        \"\"\"\n+        exp_time_raw = self.parent.CamParamGet(\"Live\", \"Exposure\")[0].split(' ')\n+        try:\n+            exp_time = self.parent.convertUnit2Time(exp_time_raw[0], exp_time_raw[1])\n+        except Exception:\n+            raise logging.error(\"Exposure time of %s is not supported for read-out camera.\" % exp_time_raw)\n+\n+        return exp_time\n+\n+    def _setCamExpTime(self, value):\n+        \"\"\"\n+        Set the camera exposure time.\n+        Converts the time range in sec into a for RemoteEx readable format.\n+        :parameter value: (float) exposure time to be set\n+        :return: (float) current exposure time\n+        \"\"\"\n+        try:\n+            exp_time_raw = self.parent.convertTime2Unit(value)\n+        except Exception:\n+            raise logging.debug(\"Exposure time of %s sec is not supported for read-out camera.\" % value)\n+\n+        # Note: RemoteEx uses different exposure times depending on acquisition mode\n+        # If we support e.g. photon counting, we need to specify a different location in RemoteEx.\n+        # For now location is always \"Live\"\n+        self.parent.CamParamSet(\"Live\", \"Exposure\", exp_time_raw)\n+        self._metadata[model.MD_EXP_TIME] = value  # update MD\n+\n+        return value\n+\n+    def _start(self):\n+        \"\"\"\n+        Start an acquisition.\n+        \"\"\"\n+        if self._sync_event is None:  # do not care about synchronization, start acquire\n+            self.parent.StartAcquisition(self.acqMode)\n+\n+    def _stop(self):\n+        \"\"\"\n+        Stop the acquisition.\n+        \"\"\"\n+        self.parent.AcqStop()\n+        self.parent.queue_img.put(\"F\")  # Flush, to stop reading all images still in the ring buffer\n+        # Note: set MCPGain to zero after acquiring for HW safety reasons\n+        self.parent._streakunit.MCPGain.value = 0\n+\n+    def _sync(self, event):  # event = self.softwareTrigger\n+        \"\"\"\n+        Synchronize the acquisition on the given event. Every time the event is\n+          triggered, the DataFlow will start a new acquisition.\n+        Behaviour is unspecified if the acquisition is already running.  # TODO still True?\n+        event (model.Event or None): event to synchronize with. Use None to\n+          disable synchronization.\n+        The DataFlow can be synchronized only with one Event at a time.\n+        \"\"\"\n+        # if event None and sync as well -> return, or if event sync, but sync already set -> return\n+        if self._sync_event == event:\n+            return\n+\n+        if self._sync_event:  # if new event = None, unsubscribe previous event (which was softwareTrigger)\n+            self._sync_event.unsubscribe(self)\n+\n+        self._sync_event = event\n+\n+        if self._sync_event:\n+            self.acqMode = \"SingleLive\"  # TODO or use \"Live\"? and just get latest image in buffer when software trigger request it?\n+            # softwareTrigger subscribes to onEvent method: if softwareTrigger.notify() called, onEvent method called\n+            self._sync_event.subscribe(self)  # must have onEvent method\n+        else:\n+            self.acqMode = \"Live\"\n+\n+    @oneway\n+    def onEvent(self):\n+        \"\"\"\n+        Called by the Event when it is triggered  (e.g. self.softwareTrigger.notify()).\n+        \"\"\"\n+        logging.debug(\"Event triggered to start a new synchronized acquisition.\")\n+        self.queue_events.append(time.time())\n+        self.parent.queue_img.put(\"start\")\n+\n+    # override\n+    def updateMetadata(self, md):\n+        \"\"\"\n+        Update the metadata.\n+        \"\"\"\n+        super(ReadoutCamera, self).updateMetadata(md)\n+        if model.MD_LENS_MAG in md and self._spectrograph:\n+            self._updateWavelengthList()\n+\n+    def _mergeMetadata(self, md):\n+        \"\"\"\n+        Create dict containing all metadata from the children readout camera, streak unit, delay genereator\n+        and the metadata from the parent streak camera.\n+        \"\"\"\n+\n+        md_devices = [self.parent._streakunit._metadata, self.parent._delaybox._metadata]\n+\n+        for md_dev in md_devices:\n+            for key in md_dev.keys():\n+                if key not in md:\n+                    md[key] = md_dev[key]\n+                elif key in (model.MD_HW_NAME, model.MD_HW_VERSION, model.MD_SW_VERSION):\n+                    md[key] = md[key] + \", \" + md_dev[key]\n+                    # md[key].append(md_dev[key])  # TODO make nice  \", \".join(c)\n+        return md\n+\n+    def _getDataFromBuffer(self):\n+        \"\"\"\n+        This method runs in a separate thread and waits for messages in queue indicating\n+        that some data was received. The image is then received from the device via the dataport IP socket or\n+        the vertical scaling table is received, which corresponds to a time range for a single sweep.\n+        It corrects the vertical time information. The table contains the actual timestamps for each px..\n+        \"\"\"\n+\n+        # TODO need to check that there is an ringbuffer available!?\n+\n+        logging.debug(\"Starting data thread.\")\n+        time.sleep(1)\n+        is_receiving_image = False\n+\n+        try:\n+            while True:\n+\n+                if self._sync_event and not is_receiving_image:\n+                    while int(self.parent.AsyncCommandStatus()[0]):\n+                        time.sleep(0)\n+                        logging.debug(\"Asynchronous RemoteEx command still in process. Wait until finished.\")\n+                        # TODO if not finished after some time might be live mode, so StopAcq again\n+                    try:\n+                        event_time = self.queue_events.popleft()\n+                        logging.warning(\"Starting acquisition delayed by %g s.\", time.time() - event_time)\n+                        self.parent.AcqStart(self.acqMode)\n+                        is_receiving_image = True\n+                    except IndexError:\n+                        # No event (yet) => fine\n+                        pass\n+\n+                rargs = self.parent.queue_img.get(block=True)  # block until receive something\n+                logging.debug(\"Received message %s\", rargs)\n+\n+                if rargs is None:  # if message is None end the thread\n+                    return\n+\n+                # synchronized mode\n+                if self._sync_event:\n+                    if rargs == \"start\":\n+                        logging.info(\"Received event trigger\")\n+                        continue\n+                    else:\n+                        logging.info(\"Get the synchronized image.\")\n+\n+                # non-sync mode\n+                else:\n+                    while not self.parent.queue_img.empty():\n+                        # keep reading to check if there might be a newer image for display\n+                        # in case we are too slow with reading\n+                        rargs = self.parent.queue_img.get(block=False)\n+\n+                        if rargs is None:  # if message is None end the thread\n+                            return\n+                    logging.info(\"No more images in queue, so get the image.\")\n+\n+                if rargs == \"F\":  # Flush => the previous images are from the previous acquisition\n+                    logging.debug(\"Acquisiton was stopped so flush previous images.\")\n+                    continue\n+\n+                self._metadata[model.MD_ACQ_DATE] = time.time() - \\\n+                                                    (self.exposureTime.value + self._metadata[model.MD_READOUT_TIME])\n+\n+                # get the image from the buffer\n+                img_num = rargs[1]\n+                img_info = self.parent.ImgRingBufferGet(\"Data\", img_num)\n+\n+                if not img_info:  # TODO check if this ever happens in log and if not remove!\n+                    logging.warning(\"Image info received from buffer is empty!\")\n+                    continue\n+\n+                img_size = int(img_info[0]) * int(img_info[1]) * 2  # num of bytes we need to receive #TODO why 2?\n+                img_num_actual = img_info[4]\n+\n+                img = \"\"\n+                try:\n+                    while len(img) < img_size:  # wait until all bytes are received\n+                        img += self.parent._dataport.recv(img_size)\n+                except socket.timeout as msg:\n+                    logging.error(\"Did not receive an image: %s\", msg)\n+                    continue\n+\n+                image = numpy.frombuffer(img, dtype=numpy.uint16)  # convert to array\n+                image.shape = (int(img_info[1]), int(img_info[0]))\n+\n+                logging.debug(\"Requested image number %s, received image number %s from buffer.\"\n+                              % (img_num, img_num_actual))\n+\n+                # get the scaling table to correct the time axis\n+                # TODO only request scaling table if corresponding MD not available for this time range\n+                if self.parent._streakunit.streakMode.value:\n+                    # TODO some sync problem might be here if a different command is in queue\n+                    # in between ImgRingBufferGet and ImgDataGet: check again!\n+                    # request scaling table\n+                    scl_table_info = self.parent.ImgDataGet(\"current\", \"ScalingTable\", \"Vertical\")\n+\n+                    scl_table_size = int(scl_table_info[0]) * 4  # num of bytes we need to receive\n+\n+                    # receive the bytes via the dataport\n+                    tab = ''\n+                    try:\n+                        while len(tab) < scl_table_size:  # keep receiving bytes until we received all expected bytes\n+                            tab += self.parent._dataport.recv(scl_table_size)\n+                        table = numpy.frombuffer(tab, dtype=numpy.float32)  # convert to array\n+                        table_converted = table * self.parent._streakunit.timeRangeFactor  # convert to sec\n+                        self._metadata[model.MD_TIME_LIST] = table_converted\n+                    except socket.timeout as msg:\n+                        logging.error(\"Did not receive a scaling table: %s\", msg)\n+                        continue\n+                else:\n+                    if model.MD_TIME_LIST in self._metadata.keys():\n+                        self._metadata.pop(model.MD_TIME_LIST, None)\n+\n+                # update the trigger rate VA and MD for the current image","path":"src/odemis/driver/hamamatsurx.py","position":521,"original_position":521,"commit_id":"b405a956454b077759aa95a37ff14874602283d1","original_commit_id":"b405a956454b077759aa95a37ff14874602283d1","user":{"login":"pieleric","id":99444,"node_id":"MDQ6VXNlcjk5NDQ0","avatar_url":"https://avatars3.githubusercontent.com/u/99444?v=4","gravatar_id":"","url":"https://api.github.com/users/pieleric","html_url":"https://github.com/pieleric","followers_url":"https://api.github.com/users/pieleric/followers","following_url":"https://api.github.com/users/pieleric/following{/other_user}","gists_url":"https://api.github.com/users/pieleric/gists{/gist_id}","starred_url":"https://api.github.com/users/pieleric/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pieleric/subscriptions","organizations_url":"https://api.github.com/users/pieleric/orgs","repos_url":"https://api.github.com/users/pieleric/repos","events_url":"https://api.github.com/users/pieleric/events{/privacy}","received_events_url":"https://api.github.com/users/pieleric/received_events","type":"User","site_admin":false},"body":"The comment is out-dated: it only updates the MD.","created_at":"2018-12-19T15:22:25Z","updated_at":"2018-12-19T15:37:43Z","html_url":"https://github.com/delmic/odemis/pull/735#discussion_r242955844","pull_request_url":"https://api.github.com/repos/delmic/odemis/pulls/735","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/delmic/odemis/pulls/comments/242955844"},"html":{"href":"https://github.com/delmic/odemis/pull/735#discussion_r242955844"},"pull_request":{"href":"https://api.github.com/repos/delmic/odemis/pulls/735"}}},"pull_request":{"url":"https://api.github.com/repos/delmic/odemis/pulls/735","id":224224150,"node_id":"MDExOlB1bGxSZXF1ZXN0MjI0MjI0MTUw","html_url":"https://github.com/delmic/odemis/pull/735","diff_url":"https://github.com/delmic/odemis/pull/735.diff","patch_url":"https://github.com/delmic/odemis/pull/735.patch","issue_url":"https://api.github.com/repos/delmic/odemis/issues/735","number":735,"state":"open","locked":false,"title":"Hamamatsu streak camera driver: a first working version","user":{"login":"Sabrina85","id":16820066,"node_id":"MDQ6VXNlcjE2ODIwMDY2","avatar_url":"https://avatars3.githubusercontent.com/u/16820066?v=4","gravatar_id":"","url":"https://api.github.com/users/Sabrina85","html_url":"https://github.com/Sabrina85","followers_url":"https://api.github.com/users/Sabrina85/followers","following_url":"https://api.github.com/users/Sabrina85/following{/other_user}","gists_url":"https://api.github.com/users/Sabrina85/gists{/gist_id}","starred_url":"https://api.github.com/users/Sabrina85/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Sabrina85/subscriptions","organizations_url":"https://api.github.com/users/Sabrina85/orgs","repos_url":"https://api.github.com/users/Sabrina85/repos","events_url":"https://api.github.com/users/Sabrina85/events{/privacy}","received_events_url":"https://api.github.com/users/Sabrina85/received_events","type":"User","site_admin":false},"body":null,"created_at":"2018-10-19T09:28:50Z","updated_at":"2018-12-19T15:37:43Z","closed_at":null,"merged_at":null,"merge_commit_sha":"dee285d9e27b53912f211914d72fee356af6f48a","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/delmic/odemis/pulls/735/commits","review_comments_url":"https://api.github.com/repos/delmic/odemis/pulls/735/comments","review_comment_url":"https://api.github.com/repos/delmic/odemis/pulls/comments{/number}","comments_url":"https://api.github.com/repos/delmic/odemis/issues/735/comments","statuses_url":"https://api.github.com/repos/delmic/odemis/statuses/b405a956454b077759aa95a37ff14874602283d1","head":{"label":"Sabrina85:streakCamDriver","ref":"streakCamDriver","sha":"b405a956454b077759aa95a37ff14874602283d1","user":{"login":"Sabrina85","id":16820066,"node_id":"MDQ6VXNlcjE2ODIwMDY2","avatar_url":"https://avatars3.githubusercontent.com/u/16820066?v=4","gravatar_id":"","url":"https://api.github.com/users/Sabrina85","html_url":"https://github.com/Sabrina85","followers_url":"https://api.github.com/users/Sabrina85/followers","following_url":"https://api.github.com/users/Sabrina85/following{/other_user}","gists_url":"https://api.github.com/users/Sabrina85/gists{/gist_id}","starred_url":"https://api.github.com/users/Sabrina85/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Sabrina85/subscriptions","organizations_url":"https://api.github.com/users/Sabrina85/orgs","repos_url":"https://api.github.com/users/Sabrina85/repos","events_url":"https://api.github.com/users/Sabrina85/events{/privacy}","received_events_url":"https://api.github.com/users/Sabrina85/received_events","type":"User","site_admin":false},"repo":{"id":128922836,"node_id":"MDEwOlJlcG9zaXRvcnkxMjg5MjI4MzY=","name":"odemis","full_name":"Sabrina85/odemis","private":false,"owner":{"login":"Sabrina85","id":16820066,"node_id":"MDQ6VXNlcjE2ODIwMDY2","avatar_url":"https://avatars3.githubusercontent.com/u/16820066?v=4","gravatar_id":"","url":"https://api.github.com/users/Sabrina85","html_url":"https://github.com/Sabrina85","followers_url":"https://api.github.com/users/Sabrina85/followers","following_url":"https://api.github.com/users/Sabrina85/following{/other_user}","gists_url":"https://api.github.com/users/Sabrina85/gists{/gist_id}","starred_url":"https://api.github.com/users/Sabrina85/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Sabrina85/subscriptions","organizations_url":"https://api.github.com/users/Sabrina85/orgs","repos_url":"https://api.github.com/users/Sabrina85/repos","events_url":"https://api.github.com/users/Sabrina85/events{/privacy}","received_events_url":"https://api.github.com/users/Sabrina85/received_events","type":"User","site_admin":false},"html_url":"https://github.com/Sabrina85/odemis","description":"Open Delmic Microscope Software","fork":true,"url":"https://api.github.com/repos/Sabrina85/odemis","forks_url":"https://api.github.com/repos/Sabrina85/odemis/forks","keys_url":"https://api.github.com/repos/Sabrina85/odemis/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Sabrina85/odemis/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Sabrina85/odemis/teams","hooks_url":"https://api.github.com/repos/Sabrina85/odemis/hooks","issue_events_url":"https://api.github.com/repos/Sabrina85/odemis/issues/events{/number}","events_url":"https://api.github.com/repos/Sabrina85/odemis/events","assignees_url":"https://api.github.com/repos/Sabrina85/odemis/assignees{/user}","branches_url":"https://api.github.com/repos/Sabrina85/odemis/branches{/branch}","tags_url":"https://api.github.com/repos/Sabrina85/odemis/tags","blobs_url":"https://api.github.com/repos/Sabrina85/odemis/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Sabrina85/odemis/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Sabrina85/odemis/git/refs{/sha}","trees_url":"https://api.github.com/repos/Sabrina85/odemis/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Sabrina85/odemis/statuses/{sha}","languages_url":"https://api.github.com/repos/Sabrina85/odemis/languages","stargazers_url":"https://api.github.com/repos/Sabrina85/odemis/stargazers","contributors_url":"https://api.github.com/repos/Sabrina85/odemis/contributors","subscribers_url":"https://api.github.com/repos/Sabrina85/odemis/subscribers","subscription_url":"https://api.github.com/repos/Sabrina85/odemis/subscription","commits_url":"https://api.github.com/repos/Sabrina85/odemis/commits{/sha}","git_commits_url":"https://api.github.com/repos/Sabrina85/odemis/git/commits{/sha}","comments_url":"https://api.github.com/repos/Sabrina85/odemis/comments{/number}","issue_comment_url":"https://api.github.com/repos/Sabrina85/odemis/issues/comments{/number}","contents_url":"https://api.github.com/repos/Sabrina85/odemis/contents/{+path}","compare_url":"https://api.github.com/repos/Sabrina85/odemis/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Sabrina85/odemis/merges","archive_url":"https://api.github.com/repos/Sabrina85/odemis/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Sabrina85/odemis/downloads","issues_url":"https://api.github.com/repos/Sabrina85/odemis/issues{/number}","pulls_url":"https://api.github.com/repos/Sabrina85/odemis/pulls{/number}","milestones_url":"https://api.github.com/repos/Sabrina85/odemis/milestones{/number}","notifications_url":"https://api.github.com/repos/Sabrina85/odemis/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Sabrina85/odemis/labels{/name}","releases_url":"https://api.github.com/repos/Sabrina85/odemis/releases{/id}","deployments_url":"https://api.github.com/repos/Sabrina85/odemis/deployments","created_at":"2018-04-10T11:35:32Z","updated_at":"2018-11-06T16:54:23Z","pushed_at":"2018-12-19T15:21:47Z","git_url":"git://github.com/Sabrina85/odemis.git","ssh_url":"git@github.com:Sabrina85/odemis.git","clone_url":"https://github.com/Sabrina85/odemis.git","svn_url":"https://github.com/Sabrina85/odemis","homepage":null,"size":190612,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"gpl-2.0","name":"GNU General Public License v2.0","spdx_id":"GPL-2.0","url":"https://api.github.com/licenses/gpl-2.0","node_id":"MDc6TGljZW5zZTg="},"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"delmic:master","ref":"master","sha":"0a31e2e116ca1edfb541ce1c9dfd5b903314ae46","user":{"login":"delmic","id":1955637,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE5NTU2Mzc=","avatar_url":"https://avatars2.githubusercontent.com/u/1955637?v=4","gravatar_id":"","url":"https://api.github.com/users/delmic","html_url":"https://github.com/delmic","followers_url":"https://api.github.com/users/delmic/followers","following_url":"https://api.github.com/users/delmic/following{/other_user}","gists_url":"https://api.github.com/users/delmic/gists{/gist_id}","starred_url":"https://api.github.com/users/delmic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/delmic/subscriptions","organizations_url":"https://api.github.com/users/delmic/orgs","repos_url":"https://api.github.com/users/delmic/repos","events_url":"https://api.github.com/users/delmic/events{/privacy}","received_events_url":"https://api.github.com/users/delmic/received_events","type":"Organization","site_admin":false},"repo":{"id":4987197,"node_id":"MDEwOlJlcG9zaXRvcnk0OTg3MTk3","name":"odemis","full_name":"delmic/odemis","private":false,"owner":{"login":"delmic","id":1955637,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE5NTU2Mzc=","avatar_url":"https://avatars2.githubusercontent.com/u/1955637?v=4","gravatar_id":"","url":"https://api.github.com/users/delmic","html_url":"https://github.com/delmic","followers_url":"https://api.github.com/users/delmic/followers","following_url":"https://api.github.com/users/delmic/following{/other_user}","gists_url":"https://api.github.com/users/delmic/gists{/gist_id}","starred_url":"https://api.github.com/users/delmic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/delmic/subscriptions","organizations_url":"https://api.github.com/users/delmic/orgs","repos_url":"https://api.github.com/users/delmic/repos","events_url":"https://api.github.com/users/delmic/events{/privacy}","received_events_url":"https://api.github.com/users/delmic/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/delmic/odemis","description":"Open Delmic Microscope Software","fork":false,"url":"https://api.github.com/repos/delmic/odemis","forks_url":"https://api.github.com/repos/delmic/odemis/forks","keys_url":"https://api.github.com/repos/delmic/odemis/keys{/key_id}","collaborators_url":"https://api.github.com/repos/delmic/odemis/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/delmic/odemis/teams","hooks_url":"https://api.github.com/repos/delmic/odemis/hooks","issue_events_url":"https://api.github.com/repos/delmic/odemis/issues/events{/number}","events_url":"https://api.github.com/repos/delmic/odemis/events","assignees_url":"https://api.github.com/repos/delmic/odemis/assignees{/user}","branches_url":"https://api.github.com/repos/delmic/odemis/branches{/branch}","tags_url":"https://api.github.com/repos/delmic/odemis/tags","blobs_url":"https://api.github.com/repos/delmic/odemis/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/delmic/odemis/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/delmic/odemis/git/refs{/sha}","trees_url":"https://api.github.com/repos/delmic/odemis/git/trees{/sha}","statuses_url":"https://api.github.com/repos/delmic/odemis/statuses/{sha}","languages_url":"https://api.github.com/repos/delmic/odemis/languages","stargazers_url":"https://api.github.com/repos/delmic/odemis/stargazers","contributors_url":"https://api.github.com/repos/delmic/odemis/contributors","subscribers_url":"https://api.github.com/repos/delmic/odemis/subscribers","subscription_url":"https://api.github.com/repos/delmic/odemis/subscription","commits_url":"https://api.github.com/repos/delmic/odemis/commits{/sha}","git_commits_url":"https://api.github.com/repos/delmic/odemis/git/commits{/sha}","comments_url":"https://api.github.com/repos/delmic/odemis/comments{/number}","issue_comment_url":"https://api.github.com/repos/delmic/odemis/issues/comments{/number}","contents_url":"https://api.github.com/repos/delmic/odemis/contents/{+path}","compare_url":"https://api.github.com/repos/delmic/odemis/compare/{base}...{head}","merges_url":"https://api.github.com/repos/delmic/odemis/merges","archive_url":"https://api.github.com/repos/delmic/odemis/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/delmic/odemis/downloads","issues_url":"https://api.github.com/repos/delmic/odemis/issues{/number}","pulls_url":"https://api.github.com/repos/delmic/odemis/pulls{/number}","milestones_url":"https://api.github.com/repos/delmic/odemis/milestones{/number}","notifications_url":"https://api.github.com/repos/delmic/odemis/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/delmic/odemis/labels{/name}","releases_url":"https://api.github.com/repos/delmic/odemis/releases{/id}","deployments_url":"https://api.github.com/repos/delmic/odemis/deployments","created_at":"2012-07-11T09:49:00Z","updated_at":"2018-12-19T13:32:28Z","pushed_at":"2018-12-19T15:21:50Z","git_url":"git://github.com/delmic/odemis.git","ssh_url":"git@github.com:delmic/odemis.git","clone_url":"https://github.com/delmic/odemis.git","svn_url":"https://github.com/delmic/odemis","homepage":null,"size":190411,"stargazers_count":15,"watchers_count":15,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":15,"mirror_url":null,"archived":false,"open_issues_count":33,"license":{"key":"gpl-2.0","name":"GNU General Public License v2.0","spdx_id":"GPL-2.0","url":"https://api.github.com/licenses/gpl-2.0","node_id":"MDc6TGljZW5zZTg="},"forks":15,"open_issues":33,"watchers":15,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/delmic/odemis/pulls/735"},"html":{"href":"https://github.com/delmic/odemis/pull/735"},"issue":{"href":"https://api.github.com/repos/delmic/odemis/issues/735"},"comments":{"href":"https://api.github.com/repos/delmic/odemis/issues/735/comments"},"review_comments":{"href":"https://api.github.com/repos/delmic/odemis/pulls/735/comments"},"review_comment":{"href":"https://api.github.com/repos/delmic/odemis/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/delmic/odemis/pulls/735/commits"},"statuses":{"href":"https://api.github.com/repos/delmic/odemis/statuses/b405a956454b077759aa95a37ff14874602283d1"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"pieleric"}}