예제 #1
0
class ChdkDevice(object):
    def __init__(self, device_info):
        """ Create a new device instance and connect to the CHDK device.

        :param device_info:   Information about device to connect to
        :type device_info:    :class:`DeviceInfo`
        """
        self.info = device_info
        self._lua = LuaContext()
        self._lua.globals.devspec = self.info._asdict()
        self._lua.pexecute("""
        con = chdku.connection({bus = devspec.bus_num,
                                dev = devspec.device_num})
        con:connect()
        """)
        self._con = self._lua.globals.con

    @property
    def is_connected(self):
        return self._lua.call("con:is_connected")

    @property
    def mode(self):
        """ The current mode of the device, one of `record` or `play`. """
        is_record, is_video, _ = self.lua_execute('return get_mode()')
        return 'record' if is_record else 'play'

    def switch_mode(self, mode):
        """ Change the mode of the device, must be one of `record` or `play`.
        """
        if mode not in ('play', 'record'):
            raise ValueError("`mode` must be one of 'play' or 'record'")
        if self.mode == mode:
            return
        mode_num = int(mode == 'record')
        status, error = self.lua_execute("""
            switch_mode_usb(%d)
            local i = 0
            while (get_mode() and 1 or 0) ~= %d and i < 300 do
                sleep(10)
                i = i + 1
            end
            if (get_mode() and 1 or 0) ~= %d then
                return false, 'switch failed'
            end
            return true, ""
            """ % (mode_num, mode_num, mode_num))
        if not status:
            raise RuntimeError('Could not switch mode')

    def _parse_message(self, raw_msg):
        value = raw_msg.value
        if raw_msg.subtype == 'table':
            value = parse_table(self._lua.eval(raw_msg.value))
        return Message(type=raw_msg.type,
                       script_id=raw_msg.script_id,
                       value=value)

    def get_messages(self):
        """ Get all messages from device buffer

        :return:    Messages
        :rtype:     generator, yields :class:`Message`
        """
        while True:
            raw_msg = self._con.read_msg(self._con)
            if raw_msg.type == 'none':
                raise StopIteration()
            yield self._parse_message(raw_msg)

    def send_message(self, message, script_id=None):
        """ Send a message to the device

        :param message:     Message to be sent
        :type message:      str/unicode
        :param script_id:   ID of script that the message should be sent to,
                            defaults to the most recently started script
        :type script_id:    int/None
        """
        if script_id:
            self._lua.call("con:write_msg", message, script_id)
        else:
            self._lua.call("con:write_msg", message)

    def lua_execute(self, lua_code, wait=True, do_return=True, remote_libs=[]):
        """ Execute Lua code on the device.

        :param lua_code:    Lua code to execute
        :type lua_code:     str/unicode
        :param wait:        Block until code has finished executing
        :type wait:         bool
        :param do_return:   Return value of lua code, only if `wait=True`
        :type do_return:    bool
        :param remote_libs: Additional code modules from `rlibs.lua` (see
                            chdkptp source) that should be uploaded along with
                            the specified code
        :type remote_libs:  List of str/unicode with names of modules from
                            `rlibs.lua`

        :rtype:             bool/int/unicode/dict/tuple
        """
        # TODO: This should all really work with LuaContext.call, but for some
        # reason it f***s up the return values .-/
        remote_libs = "{%s}" % ", ".join("'%s'" % lib for lib in remote_libs)
        if not wait:
            self._lua.pexecute("con:exec([[%s]], {libs=%s})" %
                               (lua_code, remote_libs))
            return None
        if do_return and "return" not in lua_code:
            if ";" not in lua_code[:-1] and "\n" not in lua_code:
                lua_code = "return " + lua_code
            else:
                raise ValueError(
                    "`do_return` was specified, but no return statement was"
                    " specified in the supplied `lua_code`. Please change your"
                    " script so that it returns the value you want.")
        # NOTE: Because of the frequency of curly braces, we prefer old-style
        # string formatting in this case, since this saves us quite a bit of
        # escaping
        lua_rvals, msgs = self._lua.pexecute("""
            local rvals = {}
            local msgs = {}
            con:execwait([[%s]], {rets=rvals, msgs=msgs, libs=%s})
            return {rvals, msgs}
            """ % (lua_code, remote_libs)).values()
        if not do_return:
            return None
        return_values = []
        for rv in lua_rvals.values():
            return_values.append(self._parse_message(rv).value)
        if len(return_values) == 1:
            return return_values[0]
        else:
            return tuple(return_values)

    def kill_scripts(self, flush=True):
        """ Terminate any running script on the device.

        :param flush:   Discard script messages
        :type flush:    bool
        """
        self._lua.call("con:exec",
                       "",
                       flush_cam_msgs=flush,
                       flush_host_msgs=flush,
                       clobber=True)
        self._lua.call("con:wait_status", run=False)

    def upload_file(self, local_path, remote_path='A/', skip_checks=False):
        """ Upload a file to the device.

        :param local_paths:     Path to a local file
        :type local_paths:      str/unicode
        :param remote_path:     Target path on the device
        :type remote_path:      str/unicode
        :param skip_checks:     Skip sanity checks on the device, required if
                                a script is running on the device while
                                uploading.
        """
        # TODO: Test!
        local_path = os.path.abspath(local_path)
        remote_path = util.to_camerapath(remote_path)
        if os.path.isdir(local_path):
            raise ValueError("`local_path` must be a file, not a directory.")
        if not skip_checks:
            if remote_path.endswith("/"):
                try:
                    status = parse_table(
                        self._lua.call("con:stat", remote_path))
                except LuaError:
                    status = {'is_dir': False}
                if not status['is_dir']:
                    raise ValueError("Remote path '{0}' is not a directory. "
                                     "Please leave out the trailing slash if "
                                     "you are refering to a file")
                remote_path = os.path.join(remote_path,
                                           os.path.basename(local_path))
        self._lua.call("con:upload", local_path, remote_path)

    def batch_upload(self, local_paths, remote_path='A/'):
        """ Upload multiple files/directories to the device.

        :param local_paths:     Multiple locals paths
        :type local_paths:      collection of str/unicode
        :param remote_path:     Target path on the device
        :type remote_path:      str/unicode
        """
        remote_path = util.to_camerapath(remote_path)
        local_paths = [os.path.abspath(p) for p in local_paths]
        self._lua.call("con:mupload",
                       self._lua.table(*local_paths),
                       remote_path,
                       dirs=True,
                       mtime=True,
                       maxdepth=100)

    def download_file(self, remote_path, local_path=None):
        """ Download a single file from the device.

        If no local path is specified, the file's content is returned as a
        bytestring.

        :param remote_path: Path on the device. The leading 'A/' is optional,
                            it will be automatically prepended if not
                            specified
        :type remote_path:  str/unicode
        :param local_path:  (Optional) local path to store file under.
        :type local_path:   str/unicode
        :return:            If `local_path` was not specified, the file content
                            as a bytestring, otherwise None
        :rtype:             str/None
        """
        remote_path = util.to_camerapath(remote_path)
        path = local_path or tempfile.mkstemp()[1]
        self._lua.call("con:download", remote_path, path)
        if not local_path:
            with open(path, 'rb') as fp:
                rval = fp.read()
            os.unlink(path)
            return rval

    def batch_download(self, remote_paths, local_path='./', overwrite=False):
        """ Download multiple files/directories from the device.

        :param remote_paths:    Multiple paths on the device. The leading
                                'A/' is optional, it will be automatically
                                prepended if not specified
        :type remote_paths:     collection of str/unicode
        :param local_path:      Target path on the local file system
        :type local_path:       str/unicode
        :param overwrite:       Overwrite existing files
        :type overwrite:        bool
        """
        remote_paths = [util.to_camerapath(p) for p in remote_paths]
        local_path = os.path.abspath(local_path)
        self._lua.call("con:mdownload",
                       self._lua.table(*remote_paths),
                       local_path,
                       maxdepth=100,
                       batchsize=20,
                       dbgmem=False,
                       overwrite=overwrite)

    def delete_files(self, *remote_paths):
        """ Delete one or more files/directories from the device.

        :param remote_paths:    One or more paths on the device. The leading
                                'A/' is optional, it will be automatically
                                prepended if not specified
        """
        self._con.mdelete(self._con, self._lua.table(*remote_paths),
                          self._lua.table(skip_topdirs=True))

    def list_files(self, remote_path='A/DCIM', detailed=False):
        """ Get directory listing for a path on the device.

        :param remote_path: Path on the device
        :type remote_path:  str/unicode
        :param detailed:    Return detailed information about each file/dir
        :type detailed:     bool
        :return:            All files and directories in the path
        """
        remote_path = util.to_camerapath(remote_path)
        flist = self._lua.call("con:listdir",
                               remote_path,
                               dirsonly=False,
                               stat="*" if detailed else "/")
        if not detailed:
            return [os.path.join(remote_path, p) for p in flist.values()]
        else:
            return [
                tuple(os.path.join(remote_path,
                                   dict(info.items())['name']),
                      {k: v
                       for k, v in info.items() if k != 'name'})
                for info in flist.values()
            ]

    def mkdir(self, remote_path):
        """ Create a directory on the device.
        Intermediate directories will be created as needed.

        :param remote_path: Path on the device
        :type remote_path:  str/unicode
        """
        remote_path = util.to_camerapath(remote_path)
        self._lua.call("con:mkdir_m", remote_path)

    def reconnect(self, wait=2000):
        """ Reset the connection to the device.

        :param wait:        Time in miliseconds to wait before attempting
                            to reconnect
        :type wait:         int
        """
        self._lua.call("con:reconnect", wait=wait, strict=True)

    def reboot(self, wait=3500, bootfile=None):
        """ Reboot the device.

        :param wait:        Time in miliseconds to wait before attempting
                            to reconnect
        :type wait:         int
        :param bootfile:    Optional file to boot. Must be the path to an
                            existing file on the device that is either an
                            unencoded binary or (for DryOS) an encoded .FI2
        :type bootfile:     str/unicode
        """
        if bootfile:
            bootfile = util.to_camerapath(bootfile)
        self.lua_execute("sleep(1000); reboot('{0}')".format(bootfile),
                         clobber=True)
        self.reconnect(wait)

    def get_frames(self, format='ppm', scaled=None):
        """ Get a generator that yields frames from the device's viewport.

        :param format:      Target format for frames, if `None` the raw image
                            data is returned
        :type format:       One of 'ppm', 'jpg', 'png'
        :param scaled:      The raw image has the wrong aspect ratio, with
                            this flag this can be corrected on the device,
                            which results in some quality degradation, but
                            is very fast.
                            Defaults to `True` when format is 'ppm', otherwise
                            `False`.
        :type scaled:       bool
        :return:            Generator that yields bytestrings with frame data
                            in the specified format
        """
        if format not in ('ppm', 'jpg', 'png'):
            raise ValueError("`format` has to be one of 'ppm', 'jpg' or 'png'")
        if scaled is None:
            scaled = (format == 'ppm')
        while True:
            imgdata = self._lua.eval("""
                function(skip)
                    local frame = con:get_live_data(nil, 1)
                    local pimg = liveimg.get_viewport_pimg(nil, frame, skip)
                    local lb = pimg:to_lbuf_packed_rgb(nil)
                    local header = string.format('P6\\n%d\\n%d\\n%d\\n',
                                                pimg:width(), pimg:height(),
                                                255)
                    return header .. lb:string()
                end
            """)(scaled)
            if format == 'ppm':
                yield imgdata
            else:
                try:
                    from PIL import Image
                except ImportError:
                    raise RuntimeError(
                        "To convert into JPEG or PNG, please install the "
                        "`pillow` package.")
                img = Image.open(StringIO.StringIO(imgdata))
                width, height = img.size
                img.resize((width / 2, height))
                imgdata = img.tobytes('PNG' if format == 'png' else 'JPEG')
                yield imgdata

    def shoot(self, **kwargs):
        """ Shoot a picture

        For all arguments where `None` is a legal type, it signifies that the
        current value from the camera should be used and not be overriden.

        :param shutter_speed:   Shutter speed in APEX96 (default: None)
        :type shutter_speed:    int/float/None
        :param real_iso:        Canon 'real' ISO (default: None)
        :type real_iso:         int/float/None
        :param market_iso:      Canon 'market' ISO (default: None)
        :type market_iso:       int/float/None
        :param aperture:        Aperture value in APEX96 (default: None)
        :type aperture:         int/float/None
        :param isomode:         Must conform to ISO value in Canon UI, shooting
                                mode must have manual ISO (default: None)
        :type isomode:          int/None
        :param nd_filter:       Toggle Neutral Density filter (default: None)
        :type nd_filter:        boolean/None
        :param distance:        Subject distance. If specified as an integer,
                                the value is interpreted as the distance in
                                milimeters. You can also pass a string that
                                contains a number followed by one of the
                                following units: 'mm', 'cm', 'm', 'ft' or 'in'
                                (default: None)
        :type distance:         str/unicode/int
        :param dng:             Dump raw framebuffer in DNG format
                                (default: False)
        :type dng:              boolean
        :param wait:            Wait for capture to complete (default: True)
        :type wait:             boolean
        :param download_after:  Download and return image data after capture
                                (default: False)
        :type download_after:   boolean
        :param remove_after:    Remove image data after shooting
                                (default: False)
        :type remove_after:     boolean
        :param stream:          Stream and return image data directly from
                                device (will not be saved on camera storage)
                                (default: True)
        :type stream:           boolean
        """
        self._validate_shoot_args()
        options = self._lua.globals.util.serialize(
            self._lua.table(**self._parse_shoot_args(**kwargs)))

        if not kwargs.get('stream', True):
            return self._shoot_nonstreaming(
                options,
                wait=kwargs.get('wait', True),
                download=kwargs.get('download_after', False),
                remove=kwargs.get('remove_after', False))
        else:
            return self._shoot_streaming(options, dng=kwargs.get('dng', False))

    def _shoot_nonstreaming(self,
                            options,
                            wait=True,
                            download=False,
                            remove=False):
        if not wait:
            self.lua_execute("rlib_shoot(%s)" % options,
                             wait=False,
                             remote_libs=['rlib_shoot'])
            return
        status = self.lua_execute("return rlib_shoot(%s)" % options,
                                  remote_libs=['serialize_msgs', 'rlib_shoot'])
        # TODO: Check for errors
        img_path = "{0}/IMG_{1:04}.JPG".format(status['dir'], status['exp'])
        rval = None
        if download:
            rval = self.download_file(img_path)
        if remove:
            self.delete_files(img_path)
        return rval

    def _shoot_streaming(self, options, dng=False):
        self.lua_execute("return rs_init(%s)" % options,
                         remote_libs=['rs_shoot_init'])
        # TODO: Check for errors
        self.lua_execute("rs_shoot(%s)" % options,
                         remote_libs=['rs_shoot'],
                         wait=False)
        rcopts = {}
        img_data = self._lua.table()
        if dng:
            dng_info = self._lua.table(lstart=0, lcount=0, badpix=0)
            rcopts['dng_hdr'] = self._lua.globals.chdku.rc_handler_store(
                self._lua.eval("""
                function(dng_info)
                    return function(chunk)
                        dng_info.hdr=chunk.data
                    end
                end
                """)(dng_info))
            rcopts['raw'] = self._lua.eval("""
                function(dng_info, img_data)
                    return function(lcon, hdata)
                        local status, raw = lcon:capture_get_chunk_pcall(
                            hdata.id)
                        if not status then
                            return false, raw
                        end
                        table.insert(img_data, {data=dng_info.hdr})
                        local status, err = chdku.rc_process_dng(dng_info,
                                                                raw)
                        if status then
                            table.insert(img_data, {data=dng_info.thumb})
                            table.insert(img_data, raw)
                        end
                        return status, err
                    end
                end
                """)(dng_info, img_data)
        else:
            rcopts['jpg'] = self._lua.globals.chdku.rc_handler_store(img_data)
        self._con.capture_get_data_pcall(self._con, self._lua.table(**rcopts))
        self._con.wait_status_pcall(self._con,
                                    self._lua.table(run=False, timeout=30000))
        # TODO: Check for error
        # TODO: Check for timeout
        self.lua_execute('init_usb_capture(0)')
        # NOTE: We can't touch the chunk data from Python or else the
        # Lua runtime segfaults, so we let Lua take care of assembling
        # the output data
        return self._lua.eval("""
            function(chunks)
                local size = 0
                for i, c in ipairs(chunks) do
                    size = size + c.size
                end
                local buf = lbuf.new(size)
                local offset = 0
                for i, c in ipairs(chunks) do
                    if c.offset ~= nil then
                        offset = c.offset
                    end
                    buf:fill(c.data, offset, 1)
                    offset = offset + c.size
                end
                return buf:string()
            end
            """)(img_data)

    def _validate_shoot_args(self, **kwargs):
        for arg in ('shutter_speed', 'real_iso', 'market_iso', 'aperture',
                    'isomode'):
            if kwargs.get(arg, None) is not None and not isinstance(
                    kwargs.get(arg, None), Number):
                raise ValueError("`{0}` must be an number".format(arg))
        if sum(1 for x in ('real_iso', 'market_iso', 'isomode')
               if kwargs.get(x, None) is not None) > 1:
            raise ValueError("Only one of `real_iso`, `market_iso` or "
                             "`isomode` can be set.")
        if kwargs.get('nd_filter', None) not in (True, False, None):
            raise ValueError("`nd_filter` must be one of True (swung in), "
                             "False (swung out) or None (camera default)")
        bad_distance = (
            'distance' in kwargs
            and not (isinstance(kwargs.get('distance', None), Number)
                     or DISTANCE_RE.match(kwargs.get('distance', None))))
        if bad_distance:
            raise ValueError("`distance` must be an integer (= value in "
                             "milimeter) or a string with a suffix that is "
                             "either `m`, `cm`, `mm`, `ft` or `in`.")
        action_after = any(
            kwargs.get(x, False)
            for x in ('stream', 'download_after', 'remove_after'))
        if not kwargs.get('wait', True) and action_after:
            raise ValueError("Cannot stream, remove/download after when "
                             "`wait` is `False`")
        dng_download = (not kwargs.get('stream', True)
                        and kwargs.get('dng', False)
                        and (kwargs.get('download_after', False)
                             or kwargs.get('remove_after', False)))
        if dng_download:
            raise NotImplementedError(
                "Non-streaming capture with subsequent download/removal is "
                "only supported for JPEG at the moment.")

    def _parse_shoot_args(self, **kwargs):
        options = {}
        if kwargs.get('aperture', None) is not None:
            options['av'] = kwargs.get('aperture', None)
        if kwargs.get('real_iso', None) is not None:
            options['sv'] = kwargs.get('real_iso', None)
        if kwargs.get('market_iso', None) is not None:
            options['svm'] = kwargs.get('market_iso', None)
        if kwargs.get('isomode', None) is not None:
            options['isomode'] = int(kwargs.get('isomode', None))
        if kwargs.get('shutter_speed', None) is not None:
            options['tv'] = kwargs.get('shutter_speed', None)
        if kwargs.get('nd_filter', None):
            options['nd'] = 1 if kwargs.get('nd_filter', None) else 2
        if kwargs.get('distance', None) is not None:
            if not isinstance(kwargs.get('distance', None), Number):
                value, unit = DISTANCE_RE.match(kwargs.get('distance',
                                                           None)).groups()
                options['sd'] = round(DISTANCE_FACTORS[unit] * float(value))
            else:
                options['sd'] = round(kwargs.get('distance', None))
        if kwargs.get('dng', False):
            options['dng'] = 1
        if kwargs.get('dng', False) or kwargs.get('raw', False):
            options['raw'] = 1
        if kwargs.get('stream', True):
            if kwargs.get('dng', False):
                options['fformat'] = 6
            elif kwargs.get('raw', False):
                options['fformat'] = 4
            else:
                options['fformat'] = 1
        else:
            options['info'] = True
        return options
예제 #2
0
파일: device.py 프로젝트: duerig/chdkptp.py
class ChdkDevice(object):
    def __init__(self, device_info):
        """ Create a new device instance and connect to the CHDK device.

        :param device_info:   Information about device to connect to
        :type device_info:    :class:`DeviceInfo`
        """
        self.info = device_info
        self._lua = LuaContext()
        self._lua.globals.devspec = self.info._asdict()
        self._lua.pexecute(
            """
        con = chdku.connection({bus = devspec.bus_num,
                                dev = devspec.device_num})
        con:connect()
        """
        )
        self._con = self._lua.globals.con

    @property
    def is_connected(self):
        return self._lua.call("con:is_connected")

    @property
    def mode(self):
        """ The current mode of the device, one of `record` or `play`. """
        is_record, is_video, _ = self.lua_execute("sleep(50); return get_mode()")
        return "record" if is_record else "play"

    def switch_mode(self, mode):
        """ Change the mode of the device, must be one of `record` or `play`.
        """
        if mode not in ("play", "record"):
            raise ValueError("`mode` must be one of 'play' or 'record'")
        if self.mode == mode:
            return
        mode_num = int(mode == "record")
        status, error = self.lua_execute(
            """
            sleep(50)
            switch_mode_usb(%d)
            local i = 0
            while (get_mode() and 1 or 0) ~= %d and i < 300 do
                sleep(10)
                i = i + 1
            end
            if (get_mode() and 1 or 0) ~= %d then
                return false, 'switch failed'
            end
            return true, ""
            """
            % (mode_num, mode_num, mode_num)
        )
        if not status:
            raise RuntimeError("Could not switch mode")

    def _parse_message(self, raw_msg):
        value = raw_msg.value
        if raw_msg.subtype == "table":
            value = parse_table(self._lua.eval(raw_msg.value))
        return Message(type=raw_msg.type, script_id=raw_msg.script_id, value=value)

    def get_messages(self):
        """ Get all messages from device buffer

        :return:    Messages
        :rtype:     generator, yields :class:`Message`
        """
        while True:
            raw_msg = self._con.read_msg(self._con)
            if raw_msg.type == "none":
                raise StopIteration()
            yield self._parse_message(raw_msg)

    def send_message(self, message, script_id=None):
        """ Send a message to the device

        :param message:     Message to be sent
        :type message:      str/unicode
        :param script_id:   ID of script that the message should be sent to,
                            defaults to the most recently started script
        :type script_id:    int/None
        """
        if script_id:
            self._lua.call("con:write_msg", message, script_id)
        else:
            self._lua.call("con:write_msg", message)

    def lua_execute(self, lua_code, wait=True, do_return=True, remote_libs=[]):
        """ Execute Lua code on the device.

        :param lua_code:    Lua code to execute
        :type lua_code:     str/unicode
        :param wait:        Block until code has finished executing
        :type wait:         bool
        :param do_return:   Return value of lua code, only if `wait=True`
        :type do_return:    bool
        :param remote_libs: Additional code modules from `rlibs.lua` (see
                            chdkptp source) that should be uploaded along with
                            the specified code
        :type remote_libs:  List of str/unicode with names of modules from
                            `rlibs.lua`

        :rtype:             bool/int/unicode/dict/tuple
        """
        # TODO: This should all really work with LuaContext.call, but for some
        # reason it f***s up the return values .-/
        remote_libs = "{%s}" % ", ".join("'%s'" % lib for lib in remote_libs)
        if not wait:
            self._lua.pexecute("con:exec([[%s]], {libs=%s})" % (lua_code, remote_libs))
            return None
        if do_return and "return" not in lua_code:
            if ";" not in lua_code[:-1] and "\n" not in lua_code:
                lua_code = "return " + lua_code
            else:
                raise ValueError(
                    "`do_return` was specified, but no return statement was"
                    " specified in the supplied `lua_code`. Please change your"
                    " script so that it returns the value you want."
                )
        # NOTE: Because of the frequency of curly braces, we prefer old-style
        # string formatting in this case, since this saves us quite a bit of
        # escaping
        lua_rvals, msgs = self._lua.pexecute(
            """
            local rvals = {}
            local msgs = {}
            con:execwait([[%s]], {rets=rvals, msgs=msgs, libs=%s})
            return {rvals, msgs}
            """
            % (lua_code, remote_libs)
        ).values()
        if not do_return:
            return None
        return_values = []
        for rv in lua_rvals.values():
            return_values.append(self._parse_message(rv).value)
        if len(return_values) == 1:
            return return_values[0]
        else:
            return tuple(return_values)

    def kill_scripts(self, flush=True):
        """ Terminate any running script on the device.

        :param flush:   Discard script messages
        :type flush:    bool
        """
        self._lua.call("con:exec", "", flush_cam_msgs=flush, flush_host_msgs=flush, clobber=True)
        self._lua.call("con:wait_status", run=False)

    def upload_file(self, local_path, remote_path="A/", skip_checks=False):
        """ Upload a file to the device.

        :param local_paths:     Path to a local file
        :type local_paths:      str/unicode
        :param remote_path:     Target path on the device
        :type remote_path:      str/unicode
        :param skip_checks:     Skip sanity checks on the device, required if
                                a script is running on the device while
                                uploading.
        """
        # TODO: Test!
        local_path = os.path.abspath(local_path)
        remote_path = util.to_camerapath(remote_path)
        if os.path.isdir(local_path):
            raise ValueError("`local_path` must be a file, not a directory.")
        if not skip_checks:
            if remote_path.endswith("/"):
                try:
                    status = parse_table(self._lua.call("con:stat", remote_path))
                except LuaError:
                    status = {"is_dir": False}
                if not status["is_dir"]:
                    raise ValueError(
                        "Remote path '{0}' is not a directory. "
                        "Please leave out the trailing slash if "
                        "you are refering to a file"
                    )
                remote_path = os.path.join(remote_path, os.path.basename(local_path))
        self._lua.call("con:upload", local_path, remote_path)

    def batch_upload(self, local_paths, remote_path="A/"):
        """ Upload multiple files/directories to the device.

        :param local_paths:     Multiple locals paths
        :type local_paths:      collection of str/unicode
        :param remote_path:     Target path on the device
        :type remote_path:      str/unicode
        """
        remote_path = util.to_camerapath(remote_path)
        local_paths = [os.path.abspath(p) for p in local_paths]
        self._lua.call("con:mupload", self._lua.table(*local_paths), remote_path, dirs=True, mtime=True, maxdepth=100)

    def download_file(self, remote_path, local_path=None):
        """ Download a single file from the device.

        If no local path is specified, the file's content is returned as a
        bytestring.

        :param remote_path: Path on the device. The leading 'A/' is optional,
                            it will be automatically prepended if not
                            specified
        :type remote_path:  str/unicode
        :param local_path:  (Optional) local path to store file under.
        :type local_path:   str/unicode
        :return:            If `local_path` was not specified, the file content
                            as a bytestring, otherwise None
        :rtype:             str/None
        """
        remote_path = util.to_camerapath(remote_path)
        path = local_path or tempfile.mkstemp()[1]
        self._lua.call("con:download", remote_path, path)
        if not local_path:
            with open(path, "rb") as fp:
                rval = fp.read()
            os.unlink(path)
            return rval

    def batch_download(self, remote_paths, local_path="./", overwrite=False):
        """ Download multiple files/directories from the device.

        :param remote_paths:    Multiple paths on the device. The leading
                                'A/' is optional, it will be automatically
                                prepended if not specified
        :type remote_paths:     collection of str/unicode
        :param local_path:      Target path on the local file system
        :type local_path:       str/unicode
        :param overwrite:       Overwrite existing files
        :type overwrite:        bool
        """
        remote_paths = [util.to_camerapath(p) for p in remote_paths]
        local_path = os.path.abspath(local_path)
        self._lua.call(
            "con:mdownload",
            self._lua.table(*remote_paths),
            local_path,
            maxdepth=100,
            batchsize=20,
            dbgmem=False,
            overwrite=overwrite,
        )

    def delete_files(self, *remote_paths):
        """ Delete one or more files/directories from the device.

        :param remote_paths:    One or more paths on the device. The leading
                                'A/' is optional, it will be automatically
                                prepended if not specified
        """
        self._con.mdelete(self._con, self._lua.table(*remote_paths), self._lua.table(skip_topdirs=True))

    def list_files(self, remote_path="A/DCIM", detailed=False):
        """ Get directory listing for a path on the device.

        :param remote_path: Path on the device
        :type remote_path:  str/unicode
        :param detailed:    Return detailed information about each file/dir
        :type detailed:     bool
        :return:            All files and directories in the path
        """
        remote_path = util.to_camerapath(remote_path)
        flist = self._lua.call("con:listdir", remote_path, dirsonly=False, stat="*" if detailed else "/")
        if not detailed:
            return [os.path.join(remote_path, p) for p in flist.values()]
        else:
            return [
                tuple(
                    os.path.join(remote_path, dict(info.items())["name"]),
                    {k: v for k, v in info.items() if k != "name"},
                )
                for info in flist.values()
            ]

    def mkdir(self, remote_path):
        """ Create a directory on the device.
        Intermediate directories will be created as needed.

        :param remote_path: Path on the device
        :type remote_path:  str/unicode
        """
        remote_path = util.to_camerapath(remote_path)
        self._lua.call("con:mkdir_m", remote_path)

    def reconnect(self, wait=2000):
        """ Reset the connection to the device.

        :param wait:        Time in miliseconds to wait before attempting
                            to reconnect
        :type wait:         int
        """
        self._lua.call("con:reconnect", wait=wait, strict=True)

    def reboot(self, wait=3500, bootfile=None):
        """ Reboot the device.

        :param wait:        Time in miliseconds to wait before attempting
                            to reconnect
        :type wait:         int
        :param bootfile:    Optional file to boot. Must be the path to an
                            existing file on the device that is either an
                            unencoded binary or (for DryOS) an encoded .FI2
        :type bootfile:     str/unicode
        """
        if bootfile:
            bootfile = util.to_camerapath(bootfile)
        self.lua_execute("sleep(1000); reboot('{0}')".format(bootfile), clobber=True)
        self.reconnect(wait)

    def get_frames(self, format="ppm", scaled=None):
        """ Get a generator that yields frames from the device's viewport.

        :param format:      Target format for frames, if `None` the raw image
                            data is returned
        :type format:       One of 'ppm', 'jpg', 'png'
        :param scaled:      The raw image has the wrong aspect ratio, with
                            this flag this can be corrected on the device,
                            which results in some quality degradation, but
                            is very fast.
                            Defaults to `True` when format is 'ppm', otherwise
                            `False`.
        :type scaled:       bool
        :return:            Generator that yields bytestrings with frame data
                            in the specified format
        """
        if format not in ("ppm", "jpg", "png"):
            raise ValueError("`format` has to be one of 'ppm', 'jpg' or 'png'")
        if scaled is None:
            scaled = format == "ppm"
        while True:
            imgdata = self._lua.eval(
                """
                function(skip)
                    local frame = con:get_live_data(nil, 1)
                    local pimg = liveimg.get_viewport_pimg(nil, frame, skip)
                    local lb = pimg:to_lbuf_packed_rgb(nil)
                    local header = string.format('P6\\n%d\\n%d\\n%d\\n',
                                                pimg:width(), pimg:height(),
                                                255)
                    return header .. lb:string()
                end
            """
            )(scaled)
            if format == "ppm":
                yield imgdata
            else:
                try:
                    from PIL import Image
                except ImportError:
                    raise RuntimeError("To convert into JPEG or PNG, please install the " "`pillow` package.")
                img = Image.open(StringIO.StringIO(imgdata))
                width, height = img.size
                img.resize((width / 2, height))
                imgdata = img.tobytes("PNG" if format == "png" else "JPEG")
                yield imgdata

    def shoot(self, **kwargs):
        """ Shoot a picture

        For all arguments where `None` is a legal type, it signifies that the
        current value from the camera should be used and not be overriden.

        :param shutter_speed:   Shutter speed in APEX96 (default: None)
        :type shutter_speed:    int/float/None
        :param real_iso:        Canon 'real' ISO (default: None)
        :type real_iso:         int/float/None
        :param market_iso:      Canon 'market' ISO (default: None)
        :type market_iso:       int/float/None
        :param aperture:        Aperture value in APEX96 (default: None)
        :type aperture:         int/float/None
        :param isomode:         Must conform to ISO value in Canon UI, shooting
                                mode must have manual ISO (default: None)
        :type isomode:          int/None
        :param nd_filter:       Toggle Neutral Density filter (default: None)
        :type nd_filter:        boolean/None
        :param distance:        Subject distance. If specified as an integer,
                                the value is interpreted as the distance in
                                milimeters. You can also pass a string that
                                contains a number followed by one of the
                                following units: 'mm', 'cm', 'm', 'ft' or 'in'
                                (default: None)
        :type distance:         str/unicode/int
        :param dng:             Dump raw framebuffer in DNG format
                                (default: False)
        :type dng:              boolean
        :param wait:            Wait for capture to complete (default: True)
        :type wait:             boolean
        :param download_after:  Download and return image data after capture
                                (default: False)
        :type download_after:   boolean
        :param remove_after:    Remove image data after shooting
                                (default: False)
        :type remove_after:     boolean
        :param stream:          Stream and return image data directly from
                                device (will not be saved on camera storage)
                                (default: True)
        :type stream:           boolean
        """
        self._validate_shoot_args()
        options = self._lua.globals.util.serialize(self._lua.table(**self._parse_shoot_args(**kwargs)))

        if not kwargs.get("stream", True):
            return self._shoot_nonstreaming(
                options,
                wait=kwargs.get("wait", True),
                download=kwargs.get("download_after", False),
                remove=kwargs.get("remove_after", False),
            )
        else:
            return self._shoot_streaming(options, dng=kwargs.get("dng", False))

    def _shoot_nonstreaming(self, options, wait=True, download=False, remove=False):
        if not wait:
            self.lua_execute("rlib_shoot(%s)" % options, wait=False, remote_libs=["rlib_shoot"])
            return
        status = self.lua_execute("return rlib_shoot(%s)" % options, remote_libs=["serialize_msgs", "rlib_shoot"])
        # TODO: Check for errors
        img_path = "{0}/IMG_{1:04}.JPG".format(status["dir"], status["exp"])
        rval = None
        if download:
            rval = self.download_file(img_path)
        if remove:
            self.delete_files(img_path)
        return rval

    def _shoot_streaming(self, options, dng=False):
        self.lua_execute("return rs_init(%s)" % options, remote_libs=["rs_shoot_init"])
        # TODO: Check for errors
        self.lua_execute("rs_shoot(%s)" % options, remote_libs=["rs_shoot"], wait=False)
        rcopts = {}
        img_data = self._lua.table()
        if dng:
            dng_info = self._lua.table(lstart=0, lcount=0, badpix=0)
            rcopts["dng_hdr"] = self._lua.globals.chdku.rc_handler_store(
                self._lua.eval(
                    """
                function(dng_info)
                    return function(chunk)
                        dng_info.hdr=chunk.data
                    end
                end
                """
                )(dng_info)
            )
            rcopts["raw"] = self._lua.eval(
                """
                function(dng_info, img_data)
                    return function(lcon, hdata)
                        local status, raw = lcon:capture_get_chunk_pcall(
                            hdata.id)
                        if not status then
                            return false, raw
                        end
                        table.insert(img_data, {data=dng_info.hdr})
                        local status, err = chdku.rc_process_dng(dng_info,
                                                                raw)
                        if status then
                            table.insert(img_data, {data=dng_info.thumb})
                            table.insert(img_data, raw)
                        end
                        return status, err
                    end
                end
                """
            )(dng_info, img_data)
        else:
            rcopts["jpg"] = self._lua.globals.chdku.rc_handler_store(img_data)
        self._con.capture_get_data_pcall(self._con, self._lua.table(**rcopts))
        self._con.wait_status_pcall(self._con, self._lua.table(run=False, timeout=30000))
        # TODO: Check for error
        # TODO: Check for timeout
        self.lua_execute("init_usb_capture(0)")
        # NOTE: We can't touch the chunk data from Python or else the
        # Lua runtime segfaults, so we let Lua take care of assembling
        # the output data
        return self._lua.eval(
            """
            function(chunks)
                local size = 0
                for i, c in ipairs(chunks) do
                    size = size + c.size
                end
                local buf = lbuf.new(size)
                local offset = 0
                for i, c in ipairs(chunks) do
                    if c.offset ~= nil then
                        offset = c.offset
                    end
                    buf:fill(c.data, offset, 1)
                    offset = offset + c.size
                end
                return buf:string()
            end
            """
        )(img_data)

    def _validate_shoot_args(self, **kwargs):
        for arg in ("shutter_speed", "real_iso", "market_iso", "aperture", "isomode"):
            if kwargs.get(arg, None) is not None and not isinstance(kwargs.get(arg, None), Number):
                raise ValueError("`{0}` must be an number".format(arg))
        if sum(1 for x in ("real_iso", "market_iso", "isomode") if kwargs.get(x, None) is not None) > 1:
            raise ValueError("Only one of `real_iso`, `market_iso` or " "`isomode` can be set.")
        if kwargs.get("nd_filter", None) not in (True, False, None):
            raise ValueError(
                "`nd_filter` must be one of True (swung in), " "False (swung out) or None (camera default)"
            )
        bad_distance = "distance" in kwargs and not (
            isinstance(kwargs.get("distance", None), Number) or DISTANCE_RE.match(kwargs.get("distance", None))
        )
        if bad_distance:
            raise ValueError(
                "`distance` must be an integer (= value in "
                "milimeter) or a string with a suffix that is "
                "either `m`, `cm`, `mm`, `ft` or `in`."
            )
        action_after = any(kwargs.get(x, False) for x in ("stream", "download_after", "remove_after"))
        if not kwargs.get("wait", True) and action_after:
            raise ValueError("Cannot stream, remove/download after when " "`wait` is `False`")
        dng_download = (
            not kwargs.get("stream", True)
            and kwargs.get("dng", False)
            and (kwargs.get("download_after", False) or kwargs.get("remove_after", False))
        )
        if dng_download:
            raise NotImplementedError(
                "Non-streaming capture with subsequent download/removal is " "only supported for JPEG at the moment."
            )

    def _parse_shoot_args(self, **kwargs):
        options = {}
        if kwargs.get("aperture", None) is not None:
            options["av"] = kwargs.get("aperture", None)
        if kwargs.get("real_iso", None) is not None:
            options["sv"] = kwargs.get("real_iso", None)
        if kwargs.get("market_iso", None) is not None:
            options["svm"] = kwargs.get("market_iso", None)
        if kwargs.get("isomode", None) is not None:
            options["isomode"] = int(kwargs.get("isomode", None))
        if kwargs.get("shutter_speed", None) is not None:
            options["tv"] = kwargs.get("shutter_speed", None)
        if kwargs.get("nd_filter", None):
            options["nd"] = 1 if kwargs.get("nd_filter", None) else 2
        if kwargs.get("distance", None) is not None:
            if not isinstance(kwargs.get("distance", None), Number):
                value, unit = DISTANCE_RE.match(kwargs.get("distance", None)).groups()
                options["sd"] = round(DISTANCE_FACTORS[unit] * float(value))
            else:
                options["sd"] = round(kwargs.get("distance", None))
        if kwargs.get("dng", False):
            options["dng"] = 1
        if kwargs.get("dng", False) or kwargs.get("raw", False):
            options["raw"] = 1
        if kwargs.get("stream", True):
            if kwargs.get("dng", False):
                options["fformat"] = 6
            elif kwargs.get("raw", False):
                options["fformat"] = 4
            else:
                options["fformat"] = 1
        else:
            options["info"] = True
        return options