source: trunk/lua/chdku.lua @ 236

Revision 236, 31.6 KB checked in by reyalp, 13 months ago (diff)

work in progress live view matching chdk reyalp-ptp-live rev 1817

  • Property svn:eol-style set to native
Line 
1--[[
2 Copyright (C) 2010-2012 <reyalp (at) gmail dot com>
3  This program is free software; you can redistribute it and/or modify
4  it under the terms of the GNU General Public License version 2 as
5  published by the Free Software Foundation.
6
7  This program is distributed in the hope that it will be useful,
8  but WITHOUT ANY WARRANTY; without even the implied warranty of
9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  GNU General Public License for more details.
11
12  You should have received a copy of the GNU General Public License
13  along with this program; if not, write to the Free Software
14  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
15
16]]
17--[[
18lua helper functions for working with the chdk.* c api
19]]
20
21local chdku={}
22chdku.rlibs = require('rlibs')
23
24-- format a script message in a human readable way
25function chdku.format_script_msg(msg)
26        if msg.type == 'none' then
27                return ''
28        end
29        local r=string.format("%d:%s:",msg.script_id,msg.type)
30        -- for user messages, type is clear from value, strings quoted, others not
31        if msg.type == 'user' or msg.type == 'return' then
32                if msg.subtype == 'boolean' or msg.subtype == 'integer' or msg.subtype == 'nil' then
33                        r = r .. tostring(msg.value)
34                elseif msg.subtype == 'string' then
35                        r = r .. string.format("'%s'",msg.value)
36                else
37                        r = r .. msg.subtype .. ':' .. tostring(msg.value)
38                end
39        elseif msg.type == 'error' then
40                r = r .. msg.subtype .. ':' .. tostring(msg.value)
41        end
42        return r
43end
44
45--[[
46Camera timestamps are in seconds since Jan 1, 1970 in current camera time
47PC timestamps (linux, windows) are since Jan 1, 1970 UTC
48return offset of current PC time from UTC time, in seconds
49]]
50function chdku.ts_get_offset()
51        -- local timestamp, assumed to be seconds since unix epoch
52        local tslocal=os.time()
53        -- !*t returns a table of hours, minutes etc in UTC (without a timezone spec)
54        -- os.time turns this into a timestamp, treating as local time
55        return tslocal - os.time(os.date('!*t',tslocal))
56end
57
58--[[
59covert a timestamp from the camera to the equivalent local time on the pc
60]]
61function chdku.ts_cam2pc(tscam)
62        local tspc = tscam - chdku.ts_get_offset()
63        -- TODO
64        -- on windows, a time < 0 causes os.date to return nil
65        -- these can appear from the cam if you set 0 with utime and have a negative utc offset
66        -- since this is a bogus date anyway, just force it to zero to avoid runtime errors
67        if tspc > 0 then
68                return tspc
69        end
70        return 0
71end
72
73--[[
74covert a timestamp from the pc to the equivalent on the camera
75default to current time if none given
76]]
77function chdku.ts_pc2cam(tspc)
78        if not tspc then
79                tspc = os.time()
80        end
81        local tscam = tspc + chdku.ts_get_offset()
82        -- TODO
83        -- cameras handle < 0 times inconsistently (vxworks > 2100, dryos < 1970)
84        if tscam > 0 then
85                return tscam
86        end
87        return 0
88end
89
90--[[
91connection methods, added to the connection object
92]]
93local con_methods = {}
94--[[
95check whether this cameras model and serial number match those given
96assumes self.ptpdev is up to date
97TODO - ugly
98]]
99function con_methods:match_ptp_info(match) 
100        match = util.extend_table({model='.*',serial_number='.*'},match)
101        -- older cams don't have serial
102        local serial = ''
103        if self.ptpdev.serial_number then
104                serial = self.ptpdev.serial_number
105        end
106--      printf('model %s (%s) serial_number %s (%s)\n',ptp_info.model,match.model,ptp_info.serial_number, match.serial_number)
107        return (string.find(self.ptpdev.model,match.model) and string.find(serial,match.serial_number))
108end
109
110--[[
111return a list of remote directory contents
112dirlist[,err]=con:listdir(path,opts)
113path should be directory, without a trailing slash (except in the case of A/...)
114opts may be a table, or a string containing lua code for a table
115returns directory listing as table, or false,error
116note may return an empty table if target is not a directory
117]]
118function con_methods:listdir(path,opts) 
119        if type(opts) == 'table' then
120                opts = serialize(opts)
121        elseif type(opts) ~= 'string' and type(opts) ~= 'nil' then
122                return false, "invalid options"
123        end
124        if opts then
125                opts = ','..opts
126        else
127                opts = ''
128        end
129        local results={}
130        local i=1
131        local status,err=self:execwait("return ls('"..path.."'"..opts..")",{
132                libs='ls',
133                msgs=chdku.msg_unbatcher(results),
134        })
135        if not status then
136                return false,err
137        end
138
139        return results
140end
141
142--[[
143download files and directories
144status[,err]=con:mdownload(srcpaths,dstpath,opts)
145opts:
146        mtime=bool -- keep (default) or discard remote mtime NOTE files only for now
147other opts are passed to find_files
148]]
149function con_methods:mdownload(srcpaths,dstpath,opts)
150        if not dstpath then
151                dstpath = '.'
152        end
153        local lopts=extend_table({mtime=true},opts)
154        local ropts=extend_table({},opts)
155        ropts.dirsfirst=true
156        ropts.mtime=nil
157        local dstmode = lfs.attributes(dstpath,'mode')
158        if dstmode and dstmode ~= 'directory' then
159                return false,'mdownload: dest must be a directory'
160        end
161        local files={}
162        local status,rstatus,rerr = self:execwait('return ff_mdownload('..serialize(srcpaths)..','..serialize(ropts)..')',
163                                                                                {libs={'ff_mdownload'},msgs=chdku.msg_unbatcher(files)})
164
165        if not status then
166                return false,rstatus
167        end
168        if not rstatus then
169                return false,rerr
170        end
171
172        if #files == 0 then
173                warnf("no matching files\n");
174                return true
175        end
176
177        if not dstmode then
178                local status,err=fsutil.mkdir_m(dstpath)
179                if not status then
180                        return false,err
181                end
182        end
183
184        for i,finfo in ipairs(files) do
185                local relpath
186                local src,dst
187                src = finfo.full
188                if #finfo.path == 1 then
189                        relpath = finfo.name
190                else
191                        if #finfo.path == 2 then
192                                relpath = finfo.path[2]
193                        else
194                                relpath = fsutil.joinpath(unpack(finfo.path,2))
195                        end
196                end
197                dst=fsutil.joinpath(dstpath,relpath)
198                if finfo.st.is_dir then
199--                      printf('fsutil.mkdir_m(%s)\n',dst)
200                        local status,err = fsutil.mkdir_m(dst)
201                        if not status then
202                                return false,err
203                        end
204                else
205                        local dst_dir = fsutil.dirname(dst)
206                        if dst_dir ~= '.' then
207--                              printf('fsutil.mkdir_m(%s)\n',dst_dir)
208                                local status,err = fsutil.mkdir_m(dst_dir)
209                                if not status then
210                                        return false,err
211                                end
212                        end
213                        printf("%s->%s\n",src,dst);
214                        -- ptp download fails on zero byte files (zero size data phase, possibly other problems)
215                        if finfo.st.size > 0 then
216                                -- TODO check newer/older etc
217                                local status,err = self:download(src,dst)
218                                if not status then
219                                        return status,err
220                                end
221                        else
222                                local f,err=io.open(dst,"wb")
223                                f:close()
224                        end
225                        if lopts.mtime then
226                                status,err = lfs.touch(dst,chdku.ts_cam2pc(finfo.st.mtime));
227                                if not status then
228                                        return status,err
229                                end
230                        end
231                end
232        end
233        return true
234end
235
236--[[
237upload files and directories
238status[,err]=con:mupload(srcpaths,dstpath,opts)
239opts are as for find_files, plus
240        pretend: just print what would be done
241        mtime: preserve mtime of local files
242]]
243local function mupload_fn(self,opts)
244        local con=opts.con
245        if #self.rpath == 0 and self.cur.st.mode == 'directory' then
246                return true
247        end
248        if self.cur.name == '.' or self.cur.name == '..' then
249                return true
250        end
251        local relpath
252        local src=self.cur.full
253        if #self.cur.path == 1 then
254                relpath = self.cur.name
255        else
256                if #self.cur.path == 2 then
257                        relpath = self.cur.path[2]
258                else
259                        relpath = fsutil.joinpath(unpack(self.cur.path,2))
260                end
261        end
262        local dst=fsutil.joinpath_cam(opts.mu_dst,relpath)
263        if self.cur.st.mode == 'directory' then
264                if opts.pretend then
265                        printf('remote mkdir_m(%s)\n',dst)
266                else
267                        local status,err=con:mkdir_m(dst)
268                        if not status then
269                                return false,err
270                        end
271                end
272                opts.lastdir = dst
273        else
274                local dst_dir=fsutil.dirname_cam(dst)
275                -- cache target directory so we don't have an extra stat call for every file in that dir
276                if opts.lastdir ~= dst_dir then
277                        local st,err=con:stat(dst_dir)
278                        if st then
279                                if not st.is_dir then
280                                        return false, 'not a directory: '..dst_dir
281                                end
282                        else
283                                if opts.pretend then
284                                        printf('remote mkdir_m(%s)\n',dst_dir)
285                                else
286                                        local status,err=con:mkdir_m(dst_dir)
287                                        if not status then
288                                                return false,err
289                                        end
290                                end
291                        end
292                        opts.lastdir = dst_dir
293                end
294                -- TODO stat'ing in batches would be faster
295                st,err=con:stat(dst)
296                if st and not st.is_file then
297                        return false, 'not a file: '..dst
298                end
299                -- TODO timestamp comparison
300                printf('%s->%s\n',src,dst)
301                if not opts.pretend then
302                        local status,err = con:upload(src,dst)
303                        if not status then
304                                return false,err
305                        end
306                        if opts.mtime then
307                                -- TODO updating times in batches would be faster
308                                local status,err = con:utime(dst,chdku.ts_pc2cam(self.cur.st.modification))
309                                if not status then
310                                        return false,err
311                                end
312                        end
313                end
314        end
315        return true
316end
317
318function con_methods:mupload(srcpaths,dstpath,opts)
319        opts = util.extend_table({mtime=true},opts)
320        opts.dirsfirst=true
321        opts.mu_dst=dstpath
322        opts.con=self
323        return fsutil.find_files(srcpaths,opts,mupload_fn)
324end
325
326--[[
327delete files and directories
328opts are as for find_files, plus
329        pretend:only return file name and action, don't delete
330        skip_topdirs: top level directories passed in paths will not be removed
331                e.g. mdelete({'A/FOO'},{skip_topdirs=true}) will delete everything in FOO, but not foo itself
332        ignore_errors: ignore failed deletes
333]]
334function con_methods:mdelete(paths,opts)
335        opts=extend_table({},opts)
336        opts.dirsfirst=false -- delete directories only after recursing into
337        local results
338        local msg_handler
339        if opts.msg_handler then
340                msg_handler = opts.msg_handler
341                opts.msg_handler = nil -- don't serialize
342        else
343                results={}
344                msg_handler = chdku.msg_unbatcher(results)
345        end
346        local status,err = self:call_remote('ff_mdelete',{libs={'ff_mdelete'},msgs=msg_handler},paths,opts)
347
348        if not status then
349                return false,err
350        end
351        if results then
352                return results
353        end
354        return true
355end
356
357--[[
358wrapper for remote functions, serialize args, combine remote and local error status
359func must be a string that evaluates to a function on the camera
360returns remote function return values on success, false + message on failure
361]]
362function con_methods:call_remote(func,opts,...)
363        local args = {...}
364        local argstrs = {}
365        -- preserve nils between values (not trailing ones but shouldn't matter in most cases)
366        for i = 1,table.maxn(args) do
367                argstrs[i] = serialize(args[i])
368        end
369
370        local code = "return "..func.."("..table.concat(argstrs,',')..")"
371--      printf("%s\n",code)
372        local results = {self:execwait(code,opts)}
373        -- if local status is good, return remote
374        if results[1] then
375                -- start at 2 to discard local status
376                return unpack(results,2,table.maxn(results)) -- maxn expression preserves nils
377        end
378        -- else return local error
379        return false,results[2]
380end
381
382function con_methods:stat(path)
383        return self:call_remote('os.stat',nil,path)
384end
385
386function con_methods:utime(path,mtime,atime)
387        return self:call_remote('os.utime',nil,path,mtime,atime)
388end
389
390function con_methods:mdkir(path)
391        return self:call_remote('os.mkdir',nil,path)
392end
393
394function con_methods:remove(path)
395        return self:call_remote('os.remove',nil,path)
396end
397
398function con_methods:mkdir_m(path)
399        return self:call_remote('mkdir_m',{libs='mkdir_m'},path)
400end
401
402--[[
403sort an array of stat+name by directory status, name
404]]
405function chdku.sortdir_stat(list)
406        table.sort(list,function(a,b) 
407                        if a.is_dir and not b.is_dir then
408                                return true
409                        end
410                        if not a.is_dir and b.is_dir then
411                                return false
412                        end
413                        return a.name < b.name
414                end)
415end
416
417--[[
418read pending messages and return error from current script, if available
419]]
420function con_methods:get_error_msg()
421        while true do
422                local msg,err = self:read_msg()
423                if not msg then
424                        return false
425                end
426                if msg.type == 'none' then
427                        return false
428                end
429                if msg.type == 'error' and msg.script_id == self:get_script_id() then
430                        return msg.value
431                end
432                warnf("chdku.get_error_msg: ignoring message %s\n",chdku.format_script_msg(msg))
433        end
434end
435
436--[[
437format a remote lua error from chdku.exec using line number information
438]]
439local function format_exec_error(libs,code,errmsg)
440        local lnum=tonumber(string.match(errmsg,'^%s*:(%d+):'))
441        if not lnum then
442                print('no match '..errmsg)
443                return errmsg
444        end
445        local l = 0
446        local lprev, errlib, errlnum
447        for i,lib in ipairs(libs.list) do
448                lprev = l
449                l = l + lib.lines + 1 -- TODO we add \n after each lib when building code
450                if l >= lnum then
451                        errlib = lib
452                        errlnum = lnum - lprev
453                        break
454                end
455        end
456        if errlib then
457                return string.format("%s\nrlib %s:%d\n",errmsg,errlib.name,errlnum)
458        else
459                return string.format("%s\nuser code: %d\n",errmsg,lnum - l)
460        end
461end
462
463--[[
464read and discard all pending messages. Returns false,error if message functions fails, otherwise true
465]]
466function con_methods:flushmsgs()
467        repeat
468                local msg,err=self:read_msg()
469                if not msg then
470                        return false, err
471                end
472        until msg.type == 'none' 
473        return true
474end
475
476--[[
477return a closure to be used with as a chdku.exec msgs function, which unbatches messages msg_batcher into t
478]]
479function chdku.msg_unbatcher(t)
480        local i=1
481        return function(msg)
482                if msg.subtype ~= 'table' then
483                        return false, 'unexpected message value'
484                end
485                local chunk,err=unserialize(msg.value)
486                if err then
487                        return false, err
488                end
489                for j,v in ipairs(chunk) do
490                        t[i]=v
491                        i=i+1
492                end
493                return true
494        end
495end
496--[[
497wrapper for chdk.execlua, using optional code from rlibs
498status[,err]=con:exec("code",opts)
499opts {
500        libs={"rlib name1","rlib name2"...} -- rlib code to be prepended to "code"
501        wait=bool -- wait for script to complete, return values will be returned after status if true
502        nodefaultlib=bool -- don't automatically include default rlibs
503        clobber=bool -- if false, will check script-status and refuse to execute if script is already running
504                                -- clobbering is likely to result in crashes / memory leaks in current versions of CHDK!
505        flushmsgs=bool -- if true (default) read and silently discard any pending messages before running script
506                                        -- not applicable if clobber is true, since the running script could just spew messages indefinitely
507        -- below only apply if with wait
508        msgs={table|callback} -- table or function to receive user script messages
509        rets={table|callback} -- table or function to receive script return values, instead of returning them
510        fdata={any lua value} -- data to be passed as second argument to callbacks
511        initwait={ms|false} -- passed to wait_status, wait before first poll
512        poll={ms} -- passed to wait_status, poll interval after ramp up
513        pollstart={ms|false} -- passed to wait_status, initial poll interval, ramps up to poll
514}
515callbacks
516        status[,err] = f(message,fdata)
517        processing continues if status is true, otherwise aborts and returns err
518]]
519-- use serialize by default
520chdku.default_libs={
521        'serialize_msgs',
522}
523
524--[[
525convenience, defaults wait=true
526]]
527function con_methods:execwait(code,opts_in)
528        return self:exec(code,extend_table({wait=true,initwait=5},opts_in))
529end
530
531function con_methods:exec(code,opts_in)
532        -- setup the options
533        local opts = extend_table({flushmsgs=true},opts_in)
534        local liblist={}
535        -- add default libs, unless disabled
536        -- TODO default libs should be per connection
537        if not opts.nodefaultlib then
538                extend_table(liblist,chdku.default_libs)
539        end
540        -- allow a single lib to be given as by name
541        if type(opts.libs) == 'string' then
542                liblist={opts.libs}
543        else
544                extend_table(liblist,opts.libs)
545        end
546
547        -- check for already running script and flush messages
548        if not opts.clobber then
549                -- TODO this causes a round trip.
550                -- Could track locally if a script has been started since last script_status call showed complete/no messages
551                -- wouldn't be safe vs scripts started in cam ui
552                local status,err = self:script_status()
553                if not status then
554                        return false,err
555                end
556                if status.run then
557                        return false,"a script is already running"
558                end
559                if opts.flushmsgs and status.msg then
560                        status,err=self:flushmsgs()
561                        if not status then
562                                return false,err
563                        end
564                end
565        end
566
567        -- build the complete script from user code and rlibs
568        local libs = chdku.rlibs:build(liblist)
569        code = libs:code() .. code
570
571        -- try to start the script
572        local status,err=self:execlua(code)
573        if not status then
574                -- syntax error, try to fetch the error message
575                if err == 'syntax' then
576                        local msg = self:get_error_msg()
577                        if msg then
578                                return false,format_exec_error(libs,code,msg)
579                        end
580                end
581                --  other unspecified error, or fetching syntax/compile error message failed
582                return false,err
583        end
584
585        -- if not waiting, we're done
586        if not opts.wait then
587                return true
588        end
589
590        -- to collect return values
591        -- first result is our status
592        local results={true}
593        local i=2
594
595        -- process messages and wait for script to end
596        while true do
597                status,err=self:wait_status{
598                        msg=true,
599                        run=false,
600                        initwait=opts.initwait,
601                        poll=opts.poll,
602                        pollstart=opts.pollstart
603                }
604                if not status then
605                        return false,tostring(err)
606                end
607                if status.msg then
608                        local msg,err=self:read_msg()
609                        if not msg then
610                                return false, err
611                        end
612                        if msg.script_id ~= self:get_script_id() then
613                                warnf("chdku.exec: message from unexpected script %s\n",msg.script_id,chdku.format_script_msg(msg))
614                        elseif msg.type == 'user' then
615                                if type(opts.msgs) == 'function' then
616                                        local status,err = opts.msgs(msg,opts.fdata)
617                                        if not status then
618                                                return false,err
619                                        end
620                                elseif type(opts.msgs) == 'table' then
621                                        table.insert(opts.msgs,msg)
622                                else
623                                        warnf("chdku.exec: unexpected user message %s\n",chdku.format_script_msg(msg))
624                                end
625                        elseif msg.type == 'return' then
626                                if type(opts.rets) == 'function' then
627                                        local status,err = opts.rets(msg,opts.fdata)
628                                        if not status then
629                                                return false,err
630                                        end
631                                elseif type(opts.rets) == 'table' then
632                                        table.insert(opts.rets,msg)
633                                else
634                                        -- if serialize_msgs is not selected, table return values will be strings
635                                        if msg.subtype == 'table' and libs.map['serialize_msgs'] then
636                                                results[i] = unserialize(msg.value)
637                                        else
638                                                results[i] = msg.value
639                                        end
640                                        i=i+1
641                                end
642                        elseif msg.type == 'error' then
643                                return false, format_exec_error(libs,code,msg.value)
644                        else
645                                return false, 'unexpected message type'
646                        end
647                -- script is completed and all messages have been processed
648                elseif status.run == false then
649                        -- returns were handled by callback or table
650                        if opts.rets then
651                                return true
652                        else
653                                return unpack(results,1,table.maxn(results)) -- maxn expression preserves nils
654                        end
655                end
656        end
657end
658
659--[[
660convenience method, get a message of a specific type
661mtype=<string> - expected message type
662msubtype=<string|nil> - expected subtype, or nil for any
663munserialize=<bool> - unserialize and return the message value, only valid for user/return
664
665returns
666status,message|msg value
667status first since message value could decode to false/nil
668]]
669function con_methods:read_msg_strict(opts)
670        opts=extend_table({},opts)
671        local msg,err=self:read_msg()
672        if not msg or msg.type == 'none' then
673                return false, err
674        end
675        if msg.script_id ~= self:get_script_id() then
676                return false,'msg from unexpected script id'
677        end
678        -- TODO this discards error messages if mtype isn't error
679        if msg.type ~= opts.mtype then
680                return false,'unexpected msg type'
681        end
682        if opts.msubtype and msg.subtype ~= opts.msubtype then
683                return false,'wrong message subtype'
684        end
685        if opts.munserialize then
686                local v = util.unserialize(msg.value)
687                if opts.msubtype and type(v) ~= opts.msubtype then
688                        return false,'unserialize failed'
689                end
690                return true,v
691        end
692        return true,msg
693end
694--[[
695convenience method, wait for a single message and return it
696opts passed wait_status, and read_msg_strict
697]]
698function con_methods:wait_msg(opts)
699        opts=extend_table({},opts)
700        opts.msg=true
701        opts.run=nil
702        local status,err=self:wait_status(opts)
703        if not status then
704                return false,err
705        end
706        if status.timeout then
707                return false,'timeout'
708        end
709        if not status.msg then
710                return false,'no msg'
711        end
712        return self:read_msg_strict(opts)
713end
714--[[
715sleep until specified status is met
716status,errmsg=con:wait_status(opts)
717opts:
718{
719        -- bool values cause the function to return when the status matches the given value
720        -- if not set, status of that item is ignored
721        msg=bool
722        run=bool
723        timeout=<number> -- timeout in ms
724        poll=<number> -- polling interval in ms
725        pollstart=<number> -- if not false, start polling at pollstart, double interval each iteration until poll is reached
726        initwait=<number> -- wait N ms before first poll. If this is long enough for call to finish, saves round trip
727}
728status: table with msg and run set to last status, and timeout set if timeout expired, or false,errormessage on error
729TODO for gui, this should yield in lua, resume from timer or something
730]]
731function con_methods:wait_status(opts)
732        opts = util.extend_table({
733                poll=250,
734                pollstart=4,
735                timeout=86400000 -- 1 day
736        },opts)
737        local timeleft = opts.timeout
738        local sleeptime
739        if opts.poll < 50 then
740                opts.poll = 50
741        end
742        if opts.pollstart then
743                sleeptime = opts.pollstart
744        else
745                sleeptime = opts.poll
746        end
747        if opts.initwait then
748                sys.sleep(opts.initwait)
749                timeleft = timeleft - opts.initwait
750        end
751        while true do
752                local status,msg = self:script_status()
753                if not status then
754                        return false,msg
755                end
756                if status.run == opts.run or status.msg == opts.msg then
757                        return status
758                end
759                if timeleft > 0 then
760                        if opts.pollstart and sleeptime < opts.poll then
761                                sleeptime = sleeptime * 2
762                                if sleeptime > opts.poll then
763                                        sleeptime = opts.poll
764                                end
765                        end
766                        if timeleft < sleeptime then
767                                sleeptime = timeleft
768                        end
769                        sys.sleep(sleeptime)
770                        timeleft = timeleft - sleeptime
771                else
772                        status.timeout=true
773                        return status
774                end
775        end
776end
777
778--[[
779set usbdev, ptpdev apiver for current connection
780]]
781function con_methods:update_connection_info()
782        -- this currently can't fail, devinfo is always stored in connection object
783        self.usbdev=self:get_usb_devinfo()
784        local status,err=self:get_ptp_devinfo() 
785        if status then
786                self.ptpdev = status
787        else
788                return false,err
789        end
790        local major,minor=self:camera_api_version()
791        if not major then
792                return false,minor
793        end
794        self.apiver={major=major,minor=minor}
795        return true
796end
797--[[
798override low level connect to gather some useful information that shouldn't change over life of connection
799opts{
800        raw:bool -- just call the low level connect (saves ~40ms)
801}
802]]
803function con_methods:connect(opts)
804        opts = util.extend_table({},opts)
805        self.live = nil
806        local status,err=chdk_connection.connect(self._con)
807        if not status then
808                return false,err
809        end
810        if opts.raw then
811                return true
812        end
813        return self:update_connection_info()
814end
815
816--[[
817attempt to reconnect to the device
818opts{
819        wait=<ms> -- amount of time to wait, default 2 sec to avoid probs with dev numbers changing
820        strict=bool -- fail if model, pid or serial number changes
821}
822if strict is not set, reconnect to different device returns true, <message>
823]]
824function con_methods:reconnect(opts)
825        opts=util.extend_table({
826                wait=2000,
827                strict=true,
828        },opts)
829        if self:is_connected() then
830                self:disconnect()
831        end
832        local ptpdev = self.ptpdev
833        local usbdev = self.usbdev
834        -- appears to be needed to avoid device numbers changing (reset too soon ?)
835        sys.sleep(opts.wait)
836        local status,err = self:connect()
837        if not status then
838                return status,err
839        end
840        if ptpdev.model ~= self.ptpdev.model
841                        or ptpdev.serial_number ~= self.ptpdev.serial_number
842                        or usbdev.product_id ~= self.usbdev.product_id then
843                if opts.strict then
844                        self:disconnect()
845                        return false,'reconnected to a different device'
846                else
847                        return true,'reconnected to a different device'
848                end
849        end
850        return true
851end
852
853--[[
854all assumed to be 32 bit signed ints for the moment
855]]
856-- TODO this stuff should be broken out into a generic "bind fields to lbuf" lib
857chdku.live_base_fields={
858        'version_major',                -- Major version number
859        'version_minor',                -- Minor version number
860        'vp_max_width',                 -- Maximum viewport width (in pixels)
861        'vp_max_height',                -- Maximum viewport height (in pixels)
862        'vp_buffer_width',              -- Viewport buffer width in case buffer is wider than visible viewport (in pixels)
863        'bm_max_width',                 -- Maximum width of bitmap (in pixels)
864        'bm_max_height',                -- Maximum height of bitmap (in pixels)
865        'bm_buffer_width',              -- Bitmap buffer width in case buffer is wider than visible bitmap (in pixels)
866        'lcd_aspect_ratio',             -- 0 = 4:3, 1 = 16:9
867}
868chdku.live_base_map={}
869chdku.live_frame_fields={
870        'vp_xoffset',             -- Viewport X offset in pixels (for cameras with variable image size)
871        'vp_yoffset',             -- Viewport Y offset in pixels (for cameras with variable image size)
872        'vp_width',               -- Actual viewport width in pixels (for cameras with variable image size)
873        'vp_height',              -- Actual viewport height in pixels (for cameras with variable image size)
874        'vp_buffer_start',        -- Offset in data transferred where the viewport data starts
875        'vp_buffer_size',         -- Size of viewport data sent (in bytes)
876        'bm_buffer_start',        -- Offset in data transferred where the bitmap data starts
877        'bm_buffer_size',         -- Size of bitmap data sent (in bytes)
878        'palette_type',           -- Camera palette type
879                                                          -- (0 = no palette', 1 = 16 x 4 byte AYUV values, 2 = 16 x 4 byte AYUV values with A = 0..3, 3 = 256 x 4 byte AYUV values with A = 0..3)
880        'palette_buffer_start',   -- Offset in data transferred where the palette data starts
881        'palette_buffer_size',    -- Size of palette data sent (in bytes)
882}
883
884chdku.live2_fields={
885        'lcd_aspect_ratio',
886        'palette_type',
887        'palette_data_start',
888}
889
890chdku.live2_fb_desc_fields={
891        'logical_width',
892        'logical_height',
893
894        'buffer_width',
895        'buffer_height',
896
897        'buffer_logical_xoffset',
898        'buffer_logical_yoffset',
899
900        'visible_width',
901        'visible_height',
902
903        'visible_buffer_xoffset',
904        'visible_buffer_yoffset',
905
906        'data_start',
907}
908
909chdku.live_frame_map={}
910chdku.live2_frame_map={}
911chdku.live2_field_names={}
912
913--[[
914init name->offset mapping
915]]
916local function live_init_maps()
917        for i,name in ipairs(chdku.live_base_fields) do
918                chdku.live_base_map[name] = (i-1)*4
919        end
920        for i,name in ipairs(chdku.live_frame_fields) do
921                chdku.live_frame_map[name] = (i-1)*4
922        end
923        for i,name in ipairs(chdku.live2_fields) do
924                chdku.live2_frame_map[name] = (i-1)*4
925                table.insert(chdku.live2_field_names,name)
926        end
927        local off = #chdku.live2_fields * 4
928        for i,pfx in ipairs({'vp','bm'}) do
929                for j,name in ipairs(chdku.live2_fb_desc_fields) do
930                        chdku.live2_frame_map[pfx .. '_' .. name] = off
931                        table.insert(chdku.live2_field_names,pfx .. '_' .. name)
932                        off = off + 4;
933                end
934        end
935end
936live_init_maps()
937
938function chdku.live2_get_frame_field(frame,field)
939        if not frame then
940                return nil
941        end
942        return frame:get_i32(chdku.live2_frame_map[field])
943end
944local live2_info_meta={
945        __index=function(t,key)
946                local frame = rawget(t,'_frame')
947                if frame and chdku.live2_frame_map[key] then
948                        return chdku.live2_get_frame_field(frame,key)
949                end
950        end
951}
952function chdku.live2_wrap(frame)
953        local t={ _frame = frame}
954        setmetatable(t,live2_info_meta)
955        return t
956end
957
958function chdku.live_get_base_field(base,field)
959        if not base then
960                return nil
961        end
962        return base:get_i32(chdku.live_base_map[field])
963end
964
965function chdku.live_get_frame_field(frame,field)
966        if not frame then
967                return nil
968        end
969        return frame:get_i32(chdku.live_frame_map[field])
970end
971
972local live_info_meta = {
973        __index=function(t,key)
974                if chdku.live_base_map[key] then
975                        return chdku.live_get_base_field(t.base,key)
976                end
977                if chdku.live_frame_map[key] then
978                        return chdku.live_get_frame_field(t.frame,key)
979                end
980        end
981}
982--[[
983return a new table for con.live with appropriate methods
984]]
985local function live_info_new(init) 
986        setmetatable(init,live_info_meta)
987        return init
988end
989
990function con_methods:live_is_api_compatible()
991        -- normally larger minor would be ok, but want to only work with dev version for now
992        if con.apiver.major == 2 and con.apiver.minor == 2 then
993                return true
994        end
995end
996--[[
997set up all once - per connection items for live streaming
998opts {
999        none yet
1000}
1001]]
1002function con_methods:live_init_streaming(opts)
1003        self.live = nil
1004        -- TODO this shouldn't really be in gui_sys
1005        if not guisys.caps().LIVEVIEW then
1006                return false,'this chdkptp not compiled with liveview support'
1007        end
1008        if not self:is_connected() then
1009                return false,'not connected'
1010        end
1011        if not self:live_is_api_compatible() then
1012                return false,'api is not compatible'
1013        end
1014        local handler,err = self:get_handler(1)
1015        if not handler then
1016                return false, string.format('error getting handler: %s',tostring(err))
1017        end
1018        if handler == 0 then
1019                return false, string.format('failed to obtain handler')
1020        end
1021        local basedata, err = self:call_handler(nil,handler,0x80)
1022        if not basedata then
1023                return false, string.format('error getting basedata: %s',tostring(err))
1024        end
1025        local t = util.extend_table({},opts)
1026        t.base = basedata
1027        t.handler = handler
1028        self.live = live_info_new(t)
1029        return true
1030end
1031
1032--[[
1033get one frame of live data from the camera
1034]]
1035function con_methods:live_get_frame(what)
1036        -- TODO check what or accept it in a more friendly format
1037        if not self.live then
1038                return false,'not initialized'
1039        end
1040        local err
1041        self.live.frame, err = self:call_handler(self.live.frame,self.live.handler,what)
1042        if not self.live.frame then
1043                return false,string.format('error getting frame: %s',tostring(err))
1044        end
1045        return true
1046end
1047
1048function con_methods:live2_get_frame(what)
1049        if not self.live2 then
1050                self.live2 = chdku.live2_wrap()
1051        end
1052
1053        local frame, err = self:get_live_data(self.live2._frame,what)
1054        if frame then
1055                self.live2._frame = frame
1056                return true
1057        end
1058        return false, err
1059end
1060
1061function con_methods:live_dump_start(filename)
1062        if not self.live or not self.live.base then
1063                return false,'not initialized'
1064        end
1065        if not self:is_connected() then
1066                return false,'not connected'
1067        end
1068        if not filename then
1069                filename = string.format('chdk_%x_%s.lvdump',con.usbdev.product_id,os.date('%Y%m%d_%H%M%S'))
1070        end
1071        --printf('recording to %s\n',dumpname)
1072        self.live.dump_fh = io.open(filename,"wb")
1073        if not self.live.dump_fh then
1074                return false, 'failed to open dumpfile'
1075        end
1076        self.live.dump_sz_buf = lbuf.new(4)
1077        -- TODO error checking
1078        -- TODO write a version header + camera information ?
1079        self.live.dump_sz_buf:set_u32(0,self.live.base:len())
1080        self.live.dump_sz_buf:fwrite(self.live.dump_fh)
1081        self.live.base:fwrite(self.live.dump_fh)
1082        self.live.dump_fn = filename
1083        self.live.dump_size = self.live.base:len() + 4
1084        return true
1085end
1086
1087function con_methods:live_dump_frame()
1088        if not self.live or not self.live.dump_fh then
1089                return false,'not initialized'
1090        end
1091        if not self.live.frame then
1092                return false,'no frame'
1093        end
1094
1095        self.live.dump_sz_buf:set_u32(0,self.live.frame:len())
1096        self.live.dump_sz_buf:fwrite(self.live.dump_fh)
1097        self.live.frame:fwrite(self.live.dump_fh)
1098        self.live.dump_size = self.live.dump_size + self.live.frame:len() + 4
1099        return true
1100end
1101
1102-- TODO should ensure this is automatically called when connection is closed, or re-connected
1103function con_methods:live_dump_end()
1104        if self.live.dump_fh then
1105                self.live.dump_fh:close()
1106                self.live.dump_fh=nil
1107        end
1108end
1109
1110--[[
1111meta table for wrapped connection object
1112]]
1113local con_meta = {
1114        __index = function(t,key)
1115                return con_methods[key]
1116        end
1117}
1118
1119--[[
1120proxy connection methods from low level object to chdku
1121]]
1122local function init_connection_methods()
1123        for name,func in pairs(chdk_connection) do
1124                if con_methods[name] == nil and type(func) == 'function' then
1125                        con_methods[name] = function(self,...)
1126                                return chdk_connection[name](self._con,...)
1127                        end
1128                end
1129        end
1130end
1131
1132init_connection_methods()
1133
1134--[[
1135bool = chdku.match_device(devinfo,match)
1136attempt to find a device specified by the match table
1137{
1138        bus='bus pattern'
1139        dev='device pattern'
1140        product_id = number
1141}
1142]]
1143function chdku.match_device(devinfo,match) 
1144        --[[
1145        printf('try bus:%s (%s) dev:%s (%s) pid:%s (%s)\n',
1146                devinfo.bus, match.bus,
1147                devinfo.dev, match.dev,
1148                devinfo.product_id, tostring(match.product_id))
1149        --]]
1150        if string.find(devinfo.bus,match.bus) and string.find(devinfo.dev,match.dev) then
1151                return (match.product_id == nil or tonumber(match.product_id)==devinfo.product_id)
1152        end
1153        return false
1154end
1155--[[
1156return a connection object wrapped with chdku methods
1157devspec is a table specifying the bus and device name to connect to
1158no checking is done on the existence of the device
1159if devspec is null, a dummy connection is returned
1160
1161TODO this returns a *new* wrapper object, even
1162if one already exist for the underlying object
1163not clear if this is desirable, could cache a table of them
1164]]
1165function chdku.connection(devspec)
1166        local con = {}
1167        setmetatable(con,con_meta)
1168        con._con = chdk.connection(devspec)
1169        return con
1170end
1171
1172return chdku
Note: See TracBrowser for help on using the repository browser.