source: trunk/lua/cli.lua @ 134

Revision 134, 24.7 KB checked in by reyalp, 16 months ago (diff)

chdku con:connect now store ptp, usb and chdk api version information in con object by default
add reconnect to chdku, checks if pid, model and serial match. affects cli reconnect and reboot
list cli output changed, missing serial number is now "nil" rather than "(none)"
reboot now takes an options -wait=<ms> and -norecon

  • Property svn:eol-style set to native
Line 
1--[[
2 Copyright (C) 2010-2011 <reyalp (at) gmail dot com>
3
4  This program is free software; you can redistribute it and/or modify
5  it under the terms of the GNU General Public License version 2 as
6  published by the Free Software Foundation.
7
8  This program is distributed in the hope that it will be useful,
9  but WITHOUT ANY WARRANTY; without even the implied warranty of
10  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11  GNU General Public License for more details.
12
13  You should have received a copy of the GNU General Public License
14  along with this program; if not, write to the Free Software
15  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16--]]
17
18local cli = {
19        cmds={},
20        names={},
21        finished = false,
22}
23
24--[[
25get command args of the form -a[=value] -bar[=value] .. [wordarg1] [wordarg2] [wordarg...]
26--]]
27local argparser = { }
28cli.argparser = argparser
29
30-- trim leading spaces
31function argparser:trimspace(str)
32        local s, e = string.find(str,'^[%c%s]*')
33        return string.sub(str,e+1)
34end
35--[[
36get a 'word' argument, either a sequence of non-white space characters, or a quoted string
37inside " \ is treated as an escape character
38return word, end position on success or false, error message
39]]
40function argparser:get_word(str)
41        local result = ''
42        local esc = false
43        local qchar = false
44        local pos = 1
45        while pos <= string.len(str) do
46                local c = string.sub(str,pos,pos)
47                -- in escape, append next character unconditionally
48                if esc then
49                        result = result .. c
50                        esc = false
51                -- inside double quote, start escape and discard backslash
52                elseif qchar == '"' and c == '\\' then
53                        esc = true
54                -- character is the current quote char, close quote and discard
55                elseif c == qchar then
56                        qchar = false
57                -- not hit a space and not inside a quote, end
58                elseif not qchar and string.match(c,"[%c%s]") then
59                        break
60                -- hit a quote and not inside a quote, enter quote and discard
61                elseif not qchar and c == '"' or c == "'" then
62                        qchar = c
63                -- anything else, copy
64                else
65                        result = result .. c
66                end
67                pos = pos + 1
68        end
69        if esc then
70                return false,"unexpected \\"
71        end
72        if qchar then
73                return false,"unclosed " .. qchar
74        end
75        return result,pos
76end
77
78function argparser:parse_words(str)
79        local words={}
80        str = self:trimspace(str)
81        while string.len(str) > 0 do
82                local w,pos = self:get_word(str)
83                if not w then
84                        return false,pos -- pos is error string
85                end
86                table.insert(words,w)
87                str = string.sub(str,pos)
88                str = self:trimspace(str)
89        end
90        return words
91end
92
93--[[
94parse a command string into switches and word arguments
95switches are in the form -swname[=value]
96word arguments are anything else
97any portion of the string may be quoted with '' or ""
98inside "", \ is treated as an escape
99on success returns table with args as array elements and switches as named elements
100on failure returns false, error
101defs defines the valid switches and their default values. Can also define default values of numeric args
102TODO enforce switch values, number of args, integrate with help
103]]
104function argparser:parse(str)
105        -- default values
106        local results=util.extend_table({},self.defs)
107        local words,errmsg=self:parse_words(str)
108        if not words then
109                return false,errmsg
110        end
111        for i, w in ipairs(words) do
112                -- look for -name
113                local s,e,swname=string.find(w,'^-(%a[%w_-]*)')
114                -- found a switch
115                if s then               
116                        if type(self.defs[swname]) == 'nil' then
117                                return false,'unknown switch '..swname
118                        end
119                        local swval
120                        -- no value
121                        if e == string.len(w) then
122                                swval = true
123                        elseif string.sub(w,e+1,e+1) == '=' then
124                                -- note, may be empty string but that's ok
125                                swval = string.sub(w,e+2)
126                        else
127                                return false,"invalid switch value "..string.sub(w,e+1)
128                        end
129                        results[swname]=swval
130                else
131                        table.insert(results,w)
132                end
133        end
134        return results
135end
136
137-- a default for comands that want the raw string
138argparser.nop = {
139        parse =function(self,str)
140                return str
141        end
142}
143
144function argparser.create(defs)
145        local r={ defs=defs }
146        return util.mt_inherit(r,argparser)
147end
148
149cli.cmd_proto = {
150        get_help = function(self)
151                local namestr = self.names[1]
152                if #self.names > 1 then
153                        namestr = namestr .. " (" .. self.names[2]
154                        for i=3,#self.names do
155                                namestr = namestr .. "," .. self.names[i]
156                        end
157                        namestr = namestr .. ")"
158                end
159                return string.format("%-12s %-12s: - %s\n",namestr,self.arghelp,self.help)
160        end,
161        get_help_detail = function(self)
162                local msg=self:get_help()
163                if self.help_detail then
164                        msg = msg..self.help_detail..'\n'
165                end
166                return msg
167        end,
168}
169
170cli.cmd_meta = {
171        __index = function(cmd, key)
172                return cli.cmd_proto[key]
173        end,
174        __call = function(cmd,...)
175                return cmd:func(...)
176        end,
177}
178
179function cli:add_commands(cmds)
180        for i = 1, #cmds do
181                cmd = cmds[i]
182                table.insert(self.cmds,cmd)
183                if not cmd.arghelp then
184                        cmd.arghelp = ''
185                end
186                if not cmd.args then
187                        cmd.args = argparser.nop
188                end
189                for _,name in ipairs(cmd.names) do
190                        if self.names[name] then
191                                warnf("duplicate command name %s\n",name)
192                        else
193                                self.names[name] = cmd
194                        end
195                end
196                setmetatable(cmd,cli.cmd_meta)
197        end
198end
199
200function cli:prompt()
201        if con:is_connected() then
202                local script_id = con:get_script_id()
203                if script_id then
204                        printf("con %d> ",script_id)
205                else
206                        printf("con> ")
207                end
208        else
209                printf("___> ")
210        end
211end
212
213-- execute command given by a single line
214-- returns status,message
215-- message is an error message or printable value
216function cli:execute(line)
217        -- single char shortcuts
218        local s,e,cmd = string.find(line,'^[%c%s]*([!.#=])[%c%s]*')
219        if not cmd then
220                s,e,cmd = string.find(line,'^[%c%s]*([%w_]+)[%c%s]*')
221        end
222        if s then
223                local args = string.sub(line,e+1)
224                if self.names[cmd] then
225                        local status,msg
226                        args,msg = self.names[cmd].args:parse(args)
227                        if not args then
228                                return false,msg
229                        end
230                        local t0=ustime.new()
231                        status,msg = self.names[cmd](args)
232                        if cli.showtime then
233                                printf("time %.4f\n",ustime.diff(t0)/1000000)
234                        end
235                        if not status and not msg then
236                                msg=cmd .. " failed"
237                        end
238                        return status,msg
239                else
240                        return false,string.format("unknown command '%s'\n",cmd)
241                end
242        elseif string.find(line,'[^%c%s]') then
243                return false, string.format("bad input '%s'\n",line)
244        end
245        -- blank input is OK
246        return true,""
247end
248
249function cli:print_status(status,msg)
250        if not status then
251                errf("%s\n",tostring(msg))
252        elseif msg and string.len(msg) ~= 0 then
253                printf("%s",msg)
254                if string.sub(msg,-1,-1) ~= '\n' then
255                        printf("\n")
256                end
257        end
258end
259
260function cli:run()
261        self:prompt()
262        for line in io.lines() do
263                self:print_status(self:execute(line))
264                if self.finished then
265                        break
266                end
267                self:prompt()
268        end
269end
270
271cli:add_commands{
272        {
273                names={'help','h'},
274                arghelp='[cmd]|[-v]',
275                args=argparser.create{v=false},
276                help='help on [cmd] or all commands',
277                help_detail=[[
278 help -v gives full help on all commands, otherwise as summary is printed
279]],
280                func=function(self,args)
281                        cmd = args[1]
282                        if cmd and cli.names[cmd] then
283                                return true, cli.names[cmd]:get_help_detail()
284                        end
285                        if cmd then
286                                return false, string.format("unknown command '%s'\n",cmd)
287                        end
288                        msg = ""
289                        for i,c in ipairs(cli.cmds) do
290                                if args.v then
291                                        msg = msg .. c:get_help_detail()
292                                else
293                                        msg = msg .. c:get_help()
294                                end
295                        end
296                        return true, msg
297                end,
298        },
299        {
300                names={'#'},
301                help='comment',
302                func=function(self,args)
303                        return true
304                end,
305        },
306        {
307                names={'exec','!'},
308                help='execute local lua',
309                arghelp='<lua code>',
310                help_detail=[[
311 Execute lua in chdkptp.
312 The global variable con accesses the current CLI connection.
313 Return values are printed in the console.
314]],
315                func=function(self,args)
316                        local f,r = loadstring(args)
317                        if f then
318                                r={pcall(f)};
319                                if not r[1] then
320                                        return false, string.format("call failed:%s\n",r[2])
321                                end
322                                local s
323                                if #r > 1 then
324                                        s='=' .. serialize(r[2],{pretty=true,err_type=false,err_cycle=false})
325                                        for i = 3, #r do
326                                                s = s .. ',' .. serialize(r[i],{pretty=true,err_type=false,err_cycle=false})
327                                        end
328                                end
329                                return true, s
330                        else
331                                return false, string.format("compile failed:%s\n",r)
332                        end
333                end,
334        },
335        {
336                names={'quit','q'},
337                help='quit program',
338                func=function()
339                        cli.finished = true
340                        return true,"bye"
341                end,
342        },
343        {
344                names={'lua','.'},
345                help='execute remote lua',
346                arghelp='<lua code>',
347                help_detail=[[
348 Execute Lua code on the camera.
349 Returns immediately after the script is started.
350 Return values or error messages can be retrieved with getm after the script is completed.
351]],
352                func=function(self,args)
353                        return con:exec(args)
354                end,
355        },
356        {
357                names={'getm'},
358                help='get messages',
359                func=function(self,args)
360                        local msgs=''
361                        local msg,err
362                        while true do
363                                msg,err=con:read_msg()
364                                if type(msg) ~= 'table' then
365                                        return false,msgs..err
366                                end
367                                if msg.type == 'none' then
368                                        return true,msgs
369                                end
370                                msgs = msgs .. chdku.format_script_msg(msg) .. "\n"
371                        end
372                end,
373        },
374        {
375                names={'putm'},
376                help='send message',
377                arghelp='<msg string>',
378                func=function(self,args)
379                        return con:write_msg(args)
380                end,
381        },
382        {
383                names={'luar','='},
384                help='execute remote lua, wait for result',
385                arghelp='<lua code>',
386                help_detail=[[
387 Execute Lua code on the camera, waiting for the script to end.
388 Return values or error messages are printed after the script completes.
389]],
390                func=function(self,args)
391                        local rets={}
392                        local msgs={}
393                        local status,err = con:execwait(args,{rets=rets,msgs=msgs})
394                        if not status then
395                                return false,err
396                        end
397                        local r=''
398                        for i=1,#msgs do
399                                r=r .. chdku.format_script_msg(msgs[i]) .. '\n'
400                        end
401                        for i=1,table.maxn(rets) do
402                                r=r .. chdku.format_script_msg(rets[i]) .. '\n'
403                        end
404                        return true,r
405                end,
406        },
407        {
408                -- TODO support display as words
409                names={'rmem'},
410                help='read memory',
411                args=argparser.create(), -- only word args
412                arghelp='<address> [count]',
413                func=function(self,args)
414                        local addr = tonumber(args[1])
415                        local count = tonumber(args[2])
416                        if not addr then
417                                return false, "bad args"
418                        end
419                        if not count then
420                                count = 1
421                        end
422
423                        r,msg = con:getmem(addr,count)
424                        if not r then
425                                return false,msg
426                        end
427                        return true,string.format("0x%x %u\n",addr,count)..hexdump(r,addr)
428                end,
429        },
430        {
431                names={'list'},
432                help='list devices',
433                help_detail=[[
434 Lists all recognized PTP devices in the following format
435  <status><num><modelname> b=<bus> d=<device> v=<usb vendor> p=<usb pid> s=<serial number>
436 status values
437  * connected, current target for CLI commands (con global variable)
438  + connected, not CLI target
439  - not connected
440 serial numbers are not available from all models
441]],
442                func=function()
443                        local msg = ''
444                        local devs = chdk.list_usb_devices()
445                        for i,desc in ipairs(devs) do
446                                local lcon = chdku.connection(desc)
447                                local tempcon = false
448                                local status = "+"
449                                if not lcon:is_connected() then
450                                        tempcon = true
451                                        status = "-"
452                                        lcon:connect()
453                                else
454                                        -- existing con wrapped in new object won't have info set
455                                        lcon:update_connection_info()
456                                end
457
458                                if lcon._con == con._con then
459                                        status = "*"
460                                end
461
462                                msg = msg .. string.format("%s%d:%s b=%s d=%s v=0x%x p=0x%x s=%s\n",
463                                                                                        status, i,
464                                                                                        tostring(lcon.ptpdev.model),
465                                                                                        lcon.usbdev.bus, lcon.usbdev.dev,
466                                                                                        tostring(lcon.usbdev.vendor_id),
467                                                                                        tostring(lcon.usbdev.product_id),
468                                                                                        tostring(lcon.ptpdev.serial_number))
469                                if tempcon then
470                                        lcon:disconnect()
471                                end
472                        end
473                        return true,msg
474                end,
475        },
476        {
477                names={'upload','u'},
478                help='upload a file to the camera',
479                arghelp="[-nolua] <local> [remote]",
480                args=argparser.create{nolua=false},
481                help_detail=[[
482 <local>  file to upload
483 [remote] destination
484   If not specified, file is uploaded to A/
485   If remote is a directory or ends in / uploaded to remote/<local file name>
486 -nolua   skip lua based checks on remote
487   Allows upload while running script
488   Prevents detecting if remote is a directory
489 Some cameras have problems with paths > 32 characters
490 Dryos cameras do not handle non 8.3 filenames well
491]],
492                func=function(self,args)
493                        local src = args[1]
494                        if not src then
495                                return false, "missing source"
496                        end
497                        if lfs.attributes(src,'mode') ~= 'file' then
498                                return false, 'src is not a file: '..src
499                        end
500
501                        local dst_dir
502                        local dst = args[2]
503                        -- no dst, use filename of source
504                        if dst then
505                                dst = fsutil.make_camera_path(dst)
506                                if string.find(dst,'[\\/]$') then
507                                        -- trailing slash, append filename of source
508                                        dst = string.sub(dst,1,-2)
509                                        if not args.nolua then
510                                                local st,err = con:stat(dst)
511                                                if not st then
512                                                        return false, 'stat dest '..dst..' failed: ' .. err
513                                                end
514                                                if not st.is_dir then
515                                                        return false, 'not a directory: '..dst
516                                                end
517                                        end
518                                        dst = fsutil.joinpath(dst,fsutil.basename(src))
519                                else
520                                        if not args.nolua then
521                                                local st = con:stat(dst)
522                                                if st and st.is_dir then
523                                                        dst = fsutil.joinpath(dst,fsutil.basename(src))
524                                                end
525                                        end
526                                end
527                        else
528                                dst = fsutil.make_camera_path(fsutil.basename(src))
529                        end
530
531                        local msg=string.format("%s->%s\n",src,dst)
532                        local r, msg2 = con:upload(src,dst)
533                        if msg2 then
534                                msg = msg .. msg2
535                        end
536                        return r, msg
537                end,
538        },
539        {
540                names={'download','d'},
541                help='download a file from the camera',
542                arghelp="[-nolua] <remote> [local]",
543                args=argparser.create{nolua=false},
544                help_detail=[[
545 <remote> file to download
546        A/ is prepended if not present
547 [local]  destination
548   If not specified, the file will be downloaded to the current directory
549   If a directory, the file will be downloaded into it
550 -nolua   skip lua based checks on remote
551   Allows download while running script
552]],
553
554                func=function(self,args)
555                        local src = args[1]
556                        if not src then
557                                return false, "missing source"
558                        end
559                        local dst = args[2]
560                        if not dst then
561                                -- no dest, use final component of source path
562                                dst = fsutil.basename(src)
563                        elseif string.match(dst,'[\\/]+$') then
564                                -- explicit / treat it as a directory
565                                dst = fsutil.joinpath(dst,fsutil.basename(src))
566                                -- and check if it is
567                                local dst_dir = fsutil.dirname(dst)
568                                -- TODO should create it
569                                if lfs.attributes(dst_dir,'mode') ~= 'directory' then
570                                        return false,'not a directory: '..dst_dir
571                                end
572                        elseif lfs.attributes(dst,'mode') == 'directory' then
573                                -- if target is a directory download into it
574                                dst = fsutil.joinpath(dst,fsutil.basename(src))
575                        end
576
577                        src = fsutil.make_camera_path(src)
578                        if not args.nolua then
579                                local src_st,err = con:stat(src)
580                                if not src_st then
581                                        return false, 'stat source '..src..' failed: ' .. err
582                                end
583                                if not src_st.is_file then
584                                        return false, src..' is not a file'
585                                end
586                        end
587                        local msg=string.format("%s->%s\n",src,dst)
588                        local r, msg2 = con:download(src,dst)
589                        if msg2 then
590                                msg = msg .. msg2
591                        end
592                        return r, msg
593                end,
594        },
595        {
596                names={'mdownload','mdl'},
597                help='download file/directories from the camera',
598                arghelp="[options] <remote, remote, ...> <target dir>",
599                args=argparser.create{
600                        fmatch=false,
601                        dmatch=false,
602                        rmatch=false,
603                        nodirs=false,
604                        maxdepth=100,
605                        nomtime=false,
606                },
607                help_detail=[[
608 <remote...> files/directories to download
609 <target dir> directory to download into
610 options:
611   -fmatch=<pattern> download only file with path/name matching <pattern>
612   -dmatch=<pattern> only create directories with path/name matching <pattern>
613   -rmatch=<pattern> only recurse into directories with path/name matching <pattern>
614   -nodirs           only create directories needed to download file 
615   -maxdepth=n       only recurse into N levels of directory
616   -nomtime                      don't preserve modification time of remote files
617 note <pattern> is a lua pattern, not a filesystem glob like *.JPG
618]],
619
620                func=function(self,args)
621                        if #args < 2 then
622                                return false,'expected source(s) and destination'
623                        end
624                        local dst=table.remove(args)
625                        local srcs={}
626                        for i,v in ipairs(args) do
627                                srcs[i]=fsutil.make_camera_path(v)
628                        end
629                        -- TODO some of these need translating, so can't pass direct
630                        local opts={
631                                fmatch=args.fmatch,
632                                dmatch=args.dmatch,
633                                rmatch=args.rmatch,
634                                dirs=not args.nodirs,
635                                maxdepth=tonumber(args.maxdepth),
636                                mtime=not args.nomtime
637                        }
638                        return con:mdownload(srcs,dst,opts)
639                end,
640        },
641        {
642                names={'mupload','mup'},
643                help='upload file/directories to the camera',
644                arghelp="[options] <local, local, ...> <target dir>",
645                args=argparser.create{
646                        fmatch=false,
647                        dmatch=false,
648                        rmatch=false,
649                        nodirs=false,
650                        maxdepth=100,
651                        pretend=false,
652                        nomtime=false,
653                },
654                help_detail=[[
655 <local...> files/directories to upload
656 <target dir> directory to upload into
657 options:
658   -fmatch=<pattern> upload only file with path/name matching <pattern>
659   -dmatch=<pattern> only create directories with path/name matching <pattern>
660   -rmatch=<pattern> only recurse into directories with path/name matching <pattern>
661   -nodirs           only create directories needed to upload file
662   -maxdepth=n       only recurse into N levels of directory
663   -pretend          print actions instead of doing them
664   -nomtime          don't preserve local modification time
665 note <pattern> is a lua pattern, not a filesystem glob like *.JPG
666]],
667
668                func=function(self,args)
669                        if #args < 2 then
670                                return false,'expected source(s) and destination'
671                        end
672                        local dst=fsutil.make_camera_path(table.remove(args))
673                        local srcs={}
674                        -- args has other stuff in it, copy array parts
675                        srcs={unpack(args)}
676                        -- TODO some of these need translating, so can't pass direct
677                        local opts={
678                                fmatch=args.fmatch,
679                                dmatch=args.dmatch,
680                                rmatch=args.rmatch,
681                                dirs=not args.nodirs,
682                                maxdepth=tonumber(args.maxdepth),
683                                pretend=args.pretend,
684                                mtime=not args.nomtime,
685                        }
686                        return con:mupload(srcs,dst,opts)
687                end,
688        },
689        {
690                names={'delete','rm'},
691                help='delete file/directories from the camera',
692                arghelp="[options] <target, target,...>",
693                args=argparser.create{
694                        fmatch=false,
695                        dmatch=false,
696                        rmatch=false,
697                        nodirs=false,
698                        maxdepth=100,
699                        pretend=false,
700                        ignore_errors=false,
701                        skip_topdirs=false,
702                },
703                help_detail=[[
704 <target...> files/directories to remote
705 options:
706   -fmatch=<pattern> upload only file with names matching <pattern>
707   -dmatch=<pattern> only delete directories with names matching <pattern>
708   -rmatch=<pattern> only recurse into directories with names matching <pattern>
709   -nodirs           don't delete drictories recursed into, only files
710   -maxdepth=n       only recurse into N levels of directory
711   -pretend          print actions instead of doing them
712   -ignore_errors    don't abort if delete fails, continue to next item
713   -skip_topdirs     don't delete directories given in command line, only contents
714 note <pattern> is a lua pattern, not a filesystem glob like *.JPG
715]],
716
717                func=function(self,args)
718                        if #args < 1 then
719                                return false,'expected at least one target'
720                        end
721                        -- args has other stuff in it, copy array parts
722                        local tgts={}
723                        for i,v in ipairs(args) do
724                                tgts[i]=fsutil.make_camera_path(v)
725                        end
726                        -- TODO some of these need translating, so can't pass direct
727                        local opts={
728                                fmatch=args.fmatch,
729                                dmatch=args.dmatch,
730                                rmatch=args.rmatch,
731                                dirs=not args.nodirs,
732                                maxdepth=tonumber(args.maxdepth),
733                                pretend=args.pretend,
734                                ignore_errors=args.ignore_errors,
735                                skip_topdirs=args.skip_topdirs,
736                        }
737                        -- TODO use msg_handler to print as they are deleted instead of all at the end
738                        local results,err = con:mdelete(tgts,opts)
739                        if not results then
740                                return false,err
741                        end
742                        for i,v in ipairs(results) do
743                                printf("%s: ",v.file)
744                                if v.status then
745                                        printf('OK')
746                                else
747                                        printf('FAILED')
748                                end
749                                if v.msg then
750                                        printf(": %s",v.msg)
751                                end
752                                printf('\n')
753                        end
754                        return true
755                end,
756        },
757        {
758                names={'mkdir'},
759                help='create directories on camera',
760                arghelp="<directory>",
761                args=argparser.create{ },
762                help_detail=[[
763 <directory> directory to create. Intermediate directories will be created as needed
764]],
765                func=function(self,args)
766                        if #args ~= 1 then
767                                return false,'expected exactly one arg'
768                        end
769                        return con:mkdir_m(fsutil.make_camera_path(args[1]))
770                end
771        },
772        {
773                names={'version','ver'},
774                help='print API versions',
775                func=function(self,args)
776                        local host_ver = string.format("host:%d.%d cam:",chdk.host_api_version())
777                        if con:is_connected() then
778                                local cam_major, cam_minor = con:camera_api_version()
779                                if not cam_major then
780                                        return false, host_ver .. string.format("error %s",cam_minor)
781                                end
782                                return true, host_ver .. string.format("%d.%d",cam_major,cam_minor)
783                        else
784                                return true, host_ver .. "not connected"
785                        end
786                end,
787        },
788        {
789                names={'connect','c'},
790                help='connect to device',
791                arghelp="[-b=<bus>] [-d=<dev>] [-p=<pid>] [-s=<serial>] [model] ",
792                args=argparser.create{
793                        b='.*',
794                        d='.*',
795                        p=false,
796                        s=false,
797                },
798               
799                help_detail=[[
800 If no options are given, connects to the first available device.
801 <pid> is the USB product ID, as a decimal or hexadecimal number.
802 All other options are treated as a Lua pattern. For alphanumerics, this is a case sensitive substring match.
803 If the serial or model are specified, a temporary connection will be made to each device
804 If <model> includes spaces, it must be quoted.
805 If multiple devices match, the first matching device will be connected.
806]],
807                func=function(self,args)
808                        local match = {}
809                        local opt_map = {
810                                b='bus',
811                                d='dev',
812                                p='product_id',
813                                s='serial_number',
814                                [1]='model',
815                        }
816                        for k,v in pairs(opt_map) do
817                                -- TODO matches expect nil
818                                if type(args[k]) == 'string' then
819                                        match[v] = args[k]
820                                end
821--                              printf('%s=%s\n',v,tostring(args[k]))
822                        end
823
824                        if con:is_connected() then
825                                con:disconnect()
826                        end
827
828                        if match.product_id and not tonumber(match.product_id) then
829                                return false,"expected number for product id"
830                        end
831                        local devices = chdk.list_usb_devices()
832                        local lcon
833                        for i, devinfo in ipairs(devices) do
834                                lcon = nil
835                                if chdku.match_device(devinfo,match) then
836                                        lcon = chdku.connection(devinfo)
837                                        -- if we are looking for model or serial, need to connect to the dev to check
838                                        if match.model or match.serial_number then
839                                                local tempcon = false
840--                                              printf('model check %s %s\n',tostring(match.model),tostring(match.serial_number))
841                                                if not lcon:is_connected() then
842                                                        lcon:connect()
843                                                        tempcon = true
844                                                else
845                                                        lcon:update_connection_info()
846                                                end
847                                                if not lcon:match_ptp_info(match) then
848                                                        if tempcon then
849                                                                lcon:disconnect()
850                                                        end
851                                                        lcon = nil
852                                                end
853                                        end
854                                        if lcon then
855                                                break
856                                        end
857                                end
858                        end
859                        if lcon then
860                                con = lcon
861                                if con:is_connected() then
862                                        return true
863                                end
864                                return con:connect()
865                        end
866                        return false,"no matching devices found"
867                end,
868        },
869        {
870                names={'reconnect','r'},
871                help='reconnect to current device',
872                -- NOTE camera may connect to a different device,
873                -- will detect and fail if serial, model or pid don't match
874                func=function(self,args)
875                        return con:reconnect()
876                end,
877        },
878        {
879                names={'disconnect','dis'},
880                help='disconnect from device',
881                func=function(self,args)
882                        return con:disconnect()
883                end,
884        },
885        {
886                names={'ls'},
887                help='list files/directories on camera',
888                args=argparser.create{l=false},
889                arghelp="[-l] [path]",
890                func=function(self,args)
891                        local listops
892                        local path=args[1]
893                        path = fsutil.make_camera_path(path)
894                        if args.l then
895                                listopts = { stat='*' }
896                        else
897                                listopts = { stat='/' }
898                        end
899                        local list,msg = con:listdir(path,listopts)
900                        if type(list) == 'table' then
901                                local r = ''
902                                if args.l then
903                                        -- alphabetic sort TODO sorting/grouping options
904                                        chdku.sortdir_stat(list)
905                                        for i,st in ipairs(list) do
906                                                local name = st.name
907                                                local size = st.size
908                                                if st.is_dir then
909                                                        name = name..'/'
910                                                        size = '<dir>'
911                                                else
912                                                end
913                                                -- print(i,name,chdku.ts_cam2pc(st.mtime))
914                                                r = r .. string.format("%s %10s %s\n",os.date('%c',chdku.ts_cam2pc(st.mtime)),tostring(size),name)
915                                        end
916                                else
917                                        table.sort(list)
918                                        for i,name in ipairs(list) do
919                                                r = r .. name .. '\n'
920                                        end
921                                end
922
923                                return true,r
924                        end
925                        return false,msg
926                end,
927        },
928        {
929                names={'reboot'},
930                help='reboot the camera',
931                arghelp="[options] [file]",
932                args=argparser.create({
933                        wait=3500,
934                        norecon=false,
935                }),
936                help_detail=[[
937 file: Optional file to boot.
938  Must be an unencoded binary or for DryOS only, an encoded .FI2
939  Format is assumed based on extension
940  If not set, firmware boots normally, loading diskboot.bin if configured
941 options:
942   -norecon  don't try to reconnect
943   -wait=<N> wait N ms before attempting to reconnect, default 3500
944]],
945                func=function(self,args)
946                        local bootfile=args[1]
947                        if bootfile then
948                                bootfile = fsutil.make_camera_path(bootfile)
949                                bootfile = string.format("'%s'",bootfile)
950                        else
951                                bootfile = ''
952                        end
953                        -- sleep and disconnect to avoid later connection problems on some cameras
954                        -- clobber because we don't care about memory leaks
955                        local status,err=con:exec('sleep(1000);reboot('..bootfile..')',{clobber=true})
956                        if not status then
957                                return false,err
958                        end
959                        if args.norecon then
960                                return true
961                        end
962                        return con:reconnect({wait=args.wait})
963                end,
964        },
965};
966
967return cli;
Note: See TracBrowser for help on using the repository browser.