source: trunk/lua/cli.lua @ 135

Revision 135, 24.9 KB checked in by reyalp, 16 months ago (diff)

use xpcall to trap errors in cli commands, show stack trace by default
add simple table average function table_amean to util

  • Property svn:eol-style set to native
RevLine 
[2]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
[93]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
[2]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,
[92]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,
[2]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
[93]186                if not cmd.args then
187                        cmd.args = argparser.nop
188                end
[2]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()
[60]201        if con:is_connected() then
[58]202                local script_id = con:get_script_id()
[2]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
[92]218        local s,e,cmd = string.find(line,'^[%c%s]*([!.#=])[%c%s]*')
[2]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
[93]225                        local status,msg
226                        args,msg = self.names[cmd].args:parse(args)
227                        if not args then
228                                return false,msg
229                        end
[135]230                        local cstatus
[119]231                        local t0=ustime.new()
[135]232                        cstatus,status,msg = xpcall(
233                                function()
234                                        return self.names[cmd](args)
235                                end,
236                                util.err_traceback)
[119]237                        if cli.showtime then
238                                printf("time %.4f\n",ustime.diff(t0)/1000000)
239                        end
[135]240                        if not cstatus then
241                                return false,status
242                        end
[51]243                        if not status and not msg then
244                                msg=cmd .. " failed"
245                        end
246                        return status,msg
[2]247                else 
248                        return false,string.format("unknown command '%s'\n",cmd)
249                end
250        elseif string.find(line,'[^%c%s]') then
251                return false, string.format("bad input '%s'\n",line)
252        end
253        -- blank input is OK
254        return true,""
255end
256
[78]257function cli:print_status(status,msg) 
258        if not status then
259                errf("%s\n",tostring(msg))
260        elseif msg and string.len(msg) ~= 0 then
261                printf("%s",msg)
262                if string.sub(msg,-1,-1) ~= '\n' then
263                        printf("\n")
264                end
265        end
266end
267
[2]268function cli:run()
269        self:prompt()
270        for line in io.lines() do
[78]271                self:print_status(self:execute(line))
[2]272                if self.finished then
273                        break
274                end
275                self:prompt()
276        end
277end
278
279cli:add_commands{
280        {
[92]281                names={'help','h'},
282                arghelp='[cmd]|[-v]',
[93]283                args=argparser.create{v=false},
[92]284                help='help on [cmd] or all commands',
285                help_detail=[[
286 help -v gives full help on all commands, otherwise as summary is printed
287]],
[2]288                func=function(self,args) 
[93]289                        cmd = args[1]
290                        if cmd and cli.names[cmd] then
291                                return true, cli.names[cmd]:get_help_detail()
[2]292                        end
[93]293                        if cmd then
294                                return false, string.format("unknown command '%s'\n",cmd)
[2]295                        end
296                        msg = ""
297                        for i,c in ipairs(cli.cmds) do
[93]298                                if args.v then
[92]299                                        msg = msg .. c:get_help_detail()
300                                else
301                                        msg = msg .. c:get_help()
302                                end
[2]303                        end
304                        return true, msg
305                end,
306        },
307        {
308                names={'#'},
309                help='comment',
310                func=function(self,args) 
311                        return true
312                end,
313        },
314        {
315                names={'exec','!'},
316                help='execute local lua',
317                arghelp='<lua code>',
[92]318                help_detail=[[
319 Execute lua in chdkptp.
320 The global variable con accesses the current CLI connection.
321 Return values are printed in the console.
322]],
[2]323                func=function(self,args) 
324                        local f,r = loadstring(args)
325                        if f then
[135]326                                r={xpcall(f,util.err_traceback)}
[2]327                                if not r[1] then 
328                                        return false, string.format("call failed:%s\n",r[2])
329                                end
330                                local s
331                                if #r > 1 then
[18]332                                        s='=' .. serialize(r[2],{pretty=true,err_type=false,err_cycle=false})
[2]333                                        for i = 3, #r do
[18]334                                                s = s .. ',' .. serialize(r[i],{pretty=true,err_type=false,err_cycle=false})
[2]335                                        end
336                                end
337                                return true, s
338                        else
339                                return false, string.format("compile failed:%s\n",r)
340                        end
341                end,
342        },
343        {
344                names={'quit','q'},
345                help='quit program',
346                func=function() 
347                        cli.finished = true
348                        return true,"bye"
349                end,
350        },
351        {
[92]352                names={'lua','.'},
[2]353                help='execute remote lua',
354                arghelp='<lua code>',
[92]355                help_detail=[[
356 Execute Lua code on the camera.
357 Returns immediately after the script is started.
358 Return values or error messages can be retrieved with getm after the script is completed.
359]],
[2]360                func=function(self,args) 
[58]361                        return con:exec(args)
[2]362                end,
363        },
364        {
365                names={'getm'},
366                help='get messages',
367                func=function(self,args) 
368                        local msgs=''
369                        local msg,err
370                        while true do
[58]371                                msg,err=con:read_msg()
[2]372                                if type(msg) ~= 'table' then 
373                                        return false,msgs..err
374                                end
375                                if msg.type == 'none' then
376                                        return true,msgs
377                                end
378                                msgs = msgs .. chdku.format_script_msg(msg) .. "\n"
379                        end
380                end,
381        },
382        {
383                names={'putm'},
384                help='send message',
385                arghelp='<msg string>',
386                func=function(self,args) 
[58]387                        return con:write_msg(args)
[2]388                end,
389        },
390        {
391                names={'luar','='},
392                help='execute remote lua, wait for result',
393                arghelp='<lua code>',
[92]394                help_detail=[[
395 Execute Lua code on the camera, waiting for the script to end.
396 Return values or error messages are printed after the script completes.
397]],
[2]398                func=function(self,args) 
[31]399                        local rets={}
400                        local msgs={}
[58]401                        local status,err = con:execwait(args,{rets=rets,msgs=msgs})
[2]402                        if not status then
[31]403                                return false,err
[2]404                        end
[31]405                        local r=''
406                        for i=1,#msgs do
407                                r=r .. chdku.format_script_msg(msgs[i]) .. '\n'
408                        end
409                        for i=1,table.maxn(rets) do
410                                r=r .. chdku.format_script_msg(rets[i]) .. '\n'
411                        end
412                        return true,r
[2]413                end,
414        },
415        {
416                -- TODO support display as words
417                names={'rmem'},
[92]418                help='read memory',
[93]419                args=argparser.create(), -- only word args
[2]420                arghelp='<address> [count]',
421                func=function(self,args) 
[93]422                        local addr = tonumber(args[1])
423                        local count = tonumber(args[2])
[2]424                        if not addr then
425                                return false, "bad args"
426                        end
427                        if not count then
428                                count = 1
429                        end
[89]430
[58]431                        r,msg = con:getmem(addr,count)
[2]432                        if not r then
433                                return false,msg
434                        end
[89]435                        return true,string.format("0x%x %u\n",addr,count)..hexdump(r,addr)
[2]436                end,
437        },
438        {
439                names={'list'},
440                help='list devices',
[92]441                help_detail=[[
442 Lists all recognized PTP devices in the following format
443  <status><num><modelname> b=<bus> d=<device> v=<usb vendor> p=<usb pid> s=<serial number>
444 status values
445  * connected, current target for CLI commands (con global variable)
446  + connected, not CLI target
447  - not connected
448 serial numbers are not available from all models
449]],
[2]450                func=function() 
451                        local msg = ''
[78]452                        local devs = chdk.list_usb_devices()
453                        for i,desc in ipairs(devs) do
454                                local lcon = chdku.connection(desc)
455                                local tempcon = false
[90]456                                local status = "+"
[78]457                                if not lcon:is_connected() then
458                                        tempcon = true
[92]459                                        status = "-"
[78]460                                        lcon:connect()
[134]461                                else
462                                        -- existing con wrapped in new object won't have info set
463                                        lcon:update_connection_info()
[78]464                                end
465
[90]466                                if lcon._con == con._con then
467                                        status = "*"
[78]468                                end
469
[87]470                                msg = msg .. string.format("%s%d:%s b=%s d=%s v=0x%x p=0x%x s=%s\n",
471                                                                                        status, i,
[134]472                                                                                        tostring(lcon.ptpdev.model),
473                                                                                        lcon.usbdev.bus, lcon.usbdev.dev,
474                                                                                        tostring(lcon.usbdev.vendor_id),
475                                                                                        tostring(lcon.usbdev.product_id),
476                                                                                        tostring(lcon.ptpdev.serial_number))
[78]477                                if tempcon then
478                                        lcon:disconnect()
479                                end
[2]480                        end
481                        return true,msg
482                end,
483        },
484        {
485                names={'upload','u'},
486                help='upload a file to the camera',
[98]487                arghelp="[-nolua] <local> [remote]",
488                args=argparser.create{nolua=false},
[92]489                help_detail=[[
[98]490 <local>  file to upload
491 [remote] destination
492   If not specified, file is uploaded to A/
493   If remote is a directory or ends in / uploaded to remote/<local file name>
494 -nolua   skip lua based checks on remote
495   Allows upload while running script
496   Prevents detecting if remote is a directory
497 Some cameras have problems with paths > 32 characters
498 Dryos cameras do not handle non 8.3 filenames well
[92]499]],
[2]500                func=function(self,args) 
[93]501                        local src = args[1]
[55]502                        if not src then
503                                return false, "missing source"
504                        end
[98]505                        if lfs.attributes(src,'mode') ~= 'file' then
506                                return false, 'src is not a file: '..src
507                        end
508
509                        local dst_dir
[93]510                        local dst = args[2]
[2]511                        -- no dst, use filename of source
[98]512                        if dst then
[115]513                                dst = fsutil.make_camera_path(dst)
[98]514                                if string.find(dst,'[\\/]$') then
515                                        -- trailing slash, append filename of source
516                                        dst = string.sub(dst,1,-2)
517                                        if not args.nolua then
518                                                local st,err = con:stat(dst)
519                                                if not st then
520                                                        return false, 'stat dest '..dst..' failed: ' .. err
521                                                end
522                                                if not st.is_dir then
523                                                        return false, 'not a directory: '..dst
524                                                end
525                                        end
[111]526                                        dst = fsutil.joinpath(dst,fsutil.basename(src))
[98]527                                else
528                                        if not args.nolua then
529                                                local st = con:stat(dst)
530                                                if st and st.is_dir then
[111]531                                                        dst = fsutil.joinpath(dst,fsutil.basename(src))
[98]532                                                end
533                                        end
534                                end
535                        else
[115]536                                dst = fsutil.make_camera_path(fsutil.basename(src))
[2]537                        end
[98]538
[2]539                        local msg=string.format("%s->%s\n",src,dst)
[58]540                        local r, msg2 = con:upload(src,dst)
[2]541                        if msg2 then
542                                msg = msg .. msg2
543                        end
544                        return r, msg
545                end,
546        },
547        {
548                names={'download','d'},
549                help='download a file from the camera',
[98]550                arghelp="[-nolua] <remote> [local]",
551                args=argparser.create{nolua=false},
[92]552                help_detail=[[
[98]553 <remote> file to download
554        A/ is prepended if not present
555 [local]  destination
556   If not specified, the file will be downloaded to the current directory
557   If a directory, the file will be downloaded into it
558 -nolua   skip lua based checks on remote
559   Allows download while running script
[92]560]],
561
[2]562                func=function(self,args) 
[93]563                        local src = args[1]
[55]564                        if not src then
565                                return false, "missing source"
566                        end
[93]567                        local dst = args[2]
[2]568                        if not dst then
[96]569                                -- no dest, use final component of source path
[111]570                                dst = fsutil.basename(src)
[96]571                        elseif string.match(dst,'[\\/]+$') then
572                                -- explicit / treat it as a directory
[111]573                                dst = fsutil.joinpath(dst,fsutil.basename(src))
[96]574                                -- and check if it is
[111]575                                local dst_dir = fsutil.dirname(dst)
[96]576                                -- TODO should create it
577                                if lfs.attributes(dst_dir,'mode') ~= 'directory' then
578                                        return false,'not a directory: '..dst_dir
579                                end
580                        elseif lfs.attributes(dst,'mode') == 'directory' then
581                                -- if target is a directory download into it
[111]582                                dst = fsutil.joinpath(dst,fsutil.basename(src))
[2]583                        end
[96]584
[115]585                        src = fsutil.make_camera_path(src)
[98]586                        if not args.nolua then
[96]587                                local src_st,err = con:stat(src)
588                                if not src_st then
589                                        return false, 'stat source '..src..' failed: ' .. err
590                                end
591                                if not src_st.is_file then
592                                        return false, src..' is not a file'
593                                end
[2]594                        end
595                        local msg=string.format("%s->%s\n",src,dst)
[58]596                        local r, msg2 = con:download(src,dst)
[2]597                        if msg2 then
598                                msg = msg .. msg2
599                        end
600                        return r, msg
601                end,
602        },
603        {
[117]604                names={'mdownload','mdl'},
605                help='download file/directories from the camera',
606                arghelp="[options] <remote, remote, ...> <target dir>",
607                args=argparser.create{
608                        fmatch=false,
609                        dmatch=false,
610                        rmatch=false,
611                        nodirs=false,
612                        maxdepth=100,
[126]613                        nomtime=false,
[117]614                },
615                help_detail=[[
616 <remote...> files/directories to download
617 <target dir> directory to download into
618 options:
[126]619   -fmatch=<pattern> download only file with path/name matching <pattern>
620   -dmatch=<pattern> only create directories with path/name matching <pattern>
621   -rmatch=<pattern> only recurse into directories with path/name matching <pattern>
622   -nodirs           only create directories needed to download file 
[117]623   -maxdepth=n       only recurse into N levels of directory
[126]624   -nomtime                      don't preserve modification time of remote files
[117]625 note <pattern> is a lua pattern, not a filesystem glob like *.JPG
626]],
627
628                func=function(self,args) 
629                        if #args < 2 then
630                                return false,'expected source(s) and destination'
631                        end
632                        local dst=table.remove(args)
633                        local srcs={}
634                        for i,v in ipairs(args) do
635                                srcs[i]=fsutil.make_camera_path(v)
636                        end
637                        -- TODO some of these need translating, so can't pass direct
638                        local opts={
639                                fmatch=args.fmatch,
640                                dmatch=args.dmatch,
641                                rmatch=args.rmatch,
642                                dirs=not args.nodirs,
643                                maxdepth=tonumber(args.maxdepth),
[126]644                                mtime=not args.nomtime
[117]645                        }
646                        return con:mdownload(srcs,dst,opts)
647                end,
648        },
649        {
650                names={'mupload','mup'},
651                help='upload file/directories to the camera',
652                arghelp="[options] <local, local, ...> <target dir>",
653                args=argparser.create{
654                        fmatch=false,
655                        dmatch=false,
656                        rmatch=false,
657                        nodirs=false,
658                        maxdepth=100,
659                        pretend=false,
[126]660                        nomtime=false,
[117]661                },
662                help_detail=[[
663 <local...> files/directories to upload
664 <target dir> directory to upload into
665 options:
[126]666   -fmatch=<pattern> upload only file with path/name matching <pattern>
667   -dmatch=<pattern> only create directories with path/name matching <pattern>
668   -rmatch=<pattern> only recurse into directories with path/name matching <pattern>
669   -nodirs           only create directories needed to upload file
[117]670   -maxdepth=n       only recurse into N levels of directory
671   -pretend          print actions instead of doing them
[126]672   -nomtime          don't preserve local modification time
[117]673 note <pattern> is a lua pattern, not a filesystem glob like *.JPG
674]],
675
676                func=function(self,args) 
677                        if #args < 2 then
678                                return false,'expected source(s) and destination'
679                        end
680                        local dst=fsutil.make_camera_path(table.remove(args))
681                        local srcs={}
682                        -- args has other stuff in it, copy array parts
683                        srcs={unpack(args)}
684                        -- TODO some of these need translating, so can't pass direct
685                        local opts={
686                                fmatch=args.fmatch,
687                                dmatch=args.dmatch,
688                                rmatch=args.rmatch,
689                                dirs=not args.nodirs,
690                                maxdepth=tonumber(args.maxdepth),
691                                pretend=args.pretend,
[126]692                                mtime=not args.nomtime,
[117]693                        }
694                        return con:mupload(srcs,dst,opts)
695                end,
696        },
697        {
698                names={'delete','rm'},
699                help='delete file/directories from the camera',
700                arghelp="[options] <target, target,...>",
701                args=argparser.create{
702                        fmatch=false,
703                        dmatch=false,
704                        rmatch=false,
705                        nodirs=false,
706                        maxdepth=100,
707                        pretend=false,
708                        ignore_errors=false,
709                        skip_topdirs=false,
710                },
711                help_detail=[[
712 <target...> files/directories to remote
713 options:
714   -fmatch=<pattern> upload only file with names matching <pattern>
715   -dmatch=<pattern> only delete directories with names matching <pattern>
716   -rmatch=<pattern> only recurse into directories with names matching <pattern>
717   -nodirs           don't delete drictories recursed into, only files
718   -maxdepth=n       only recurse into N levels of directory
719   -pretend          print actions instead of doing them
720   -ignore_errors    don't abort if delete fails, continue to next item
721   -skip_topdirs     don't delete directories given in command line, only contents
722 note <pattern> is a lua pattern, not a filesystem glob like *.JPG
723]],
724
725                func=function(self,args) 
726                        if #args < 1 then
727                                return false,'expected at least one target'
728                        end
729                        -- args has other stuff in it, copy array parts
730                        local tgts={}
731                        for i,v in ipairs(args) do
732                                tgts[i]=fsutil.make_camera_path(v)
733                        end
734                        -- TODO some of these need translating, so can't pass direct
735                        local opts={
736                                fmatch=args.fmatch,
737                                dmatch=args.dmatch,
738                                rmatch=args.rmatch,
739                                dirs=not args.nodirs,
740                                maxdepth=tonumber(args.maxdepth),
741                                pretend=args.pretend,
742                                ignore_errors=args.ignore_errors,
743                                skip_topdirs=args.skip_topdirs,
744                        }
745                        -- TODO use msg_handler to print as they are deleted instead of all at the end
746                        local results,err = con:mdelete(tgts,opts)
747                        if not results then
748                                return false,err
749                        end
750                        for i,v in ipairs(results) do
751                                printf("%s: ",v.file)
752                                if v.status then
753                                        printf('OK')
754                                else
755                                        printf('FAILED')
756                                end
757                                if v.msg then
758                                        printf(": %s",v.msg)
759                                end
760                                printf('\n')
761                        end
762                        return true
763                end,
764        },
765        {
[118]766                names={'mkdir'},
767                help='create directories on camera',
768                arghelp="<directory>",
769                args=argparser.create{ },
770                help_detail=[[
771 <directory> directory to create. Intermediate directories will be created as needed
772]],
773                func=function(self,args)
774                        if #args ~= 1 then
775                                return false,'expected exactly one arg'
776                        end
777                        return con:mkdir_m(fsutil.make_camera_path(args[1]))
778                end
779        },
780        {
[2]781                names={'version','ver'},
782                help='print API versions',
783                func=function(self,args) 
784                        local host_ver = string.format("host:%d.%d cam:",chdk.host_api_version())
[60]785                        if con:is_connected() then
[58]786                                local cam_major, cam_minor = con:camera_api_version()
[2]787                                if not cam_major then
788                                        return false, host_ver .. string.format("error %s",cam_minor)
789                                end
790                                return true, host_ver .. string.format("%d.%d",cam_major,cam_minor)
791                        else
792                                return true, host_ver .. "not connected"
793                        end
794                end,
795        },
796        {
797                names={'connect','c'},
[78]798                help='connect to device',
[93]799                arghelp="[-b=<bus>] [-d=<dev>] [-p=<pid>] [-s=<serial>] [model] ",
800                args=argparser.create{
801                        b='.*',
802                        d='.*',
803                        p=false,
804                        s=false,
805                },
806               
[92]807                help_detail=[[
808 If no options are given, connects to the first available device.
[93]809 <pid> is the USB product ID, as a decimal or hexadecimal number.
[92]810 All other options are treated as a Lua pattern. For alphanumerics, this is a case sensitive substring match.
811 If the serial or model are specified, a temporary connection will be made to each device
812 If <model> includes spaces, it must be quoted.
813 If multiple devices match, the first matching device will be connected.
814]],
[2]815                func=function(self,args) 
[93]816                        local match = {}
[85]817                        local opt_map = {
818                                b='bus',
819                                d='dev',
820                                p='product_id',
821                                s='serial_number',
[93]822                                [1]='model',
[85]823                        }
[93]824                        for k,v in pairs(opt_map) do
825                                -- TODO matches expect nil
826                                if type(args[k]) == 'string' then
827                                        match[v] = args[k]
828                                end
829--                              printf('%s=%s\n',v,tostring(args[k]))
830                        end
831
[60]832                        if con:is_connected() then
833                                con:disconnect()
[58]834                        end
[93]835
[92]836                        if match.product_id and not tonumber(match.product_id) then
837                                return false,"expected number for product id"
838                        end
[85]839                        local devices = chdk.list_usb_devices()
840                        local lcon
841                        for i, devinfo in ipairs(devices) do
842                                lcon = nil
843                                if chdku.match_device(devinfo,match) then
844                                        lcon = chdku.connection(devinfo)
[88]845                                        -- if we are looking for model or serial, need to connect to the dev to check
[85]846                                        if match.model or match.serial_number then
[88]847                                                local tempcon = false
[85]848--                                              printf('model check %s %s\n',tostring(match.model),tostring(match.serial_number))
849                                                if not lcon:is_connected() then
850                                                        lcon:connect()
851                                                        tempcon = true
[134]852                                                else
853                                                        lcon:update_connection_info()
[85]854                                                end
855                                                if not lcon:match_ptp_info(match) then
856                                                        if tempcon then
857                                                                lcon:disconnect()
858                                                        end
859                                                        lcon = nil
860                                                end
861                                        end
862                                        if lcon then
863                                                break
864                                        end
865                                end
866                        end
867                        if lcon then
868                                con = lcon
869                                if con:is_connected() then
870                                        return true
871                                end
[78]872                                return con:connect()
873                        end
[85]874                        return false,"no matching devices found"
[78]875                end,
876        },
877        {
878                names={'reconnect','r'},
879                help='reconnect to current device',
[134]880                -- NOTE camera may connect to a different device,
881                -- will detect and fail if serial, model or pid don't match
[78]882                func=function(self,args) 
[134]883                        return con:reconnect()
[2]884                end,
885        },
886        {
887                names={'disconnect','dis'},
888                help='disconnect from device',
889                func=function(self,args) 
[58]890                        return con:disconnect()
[2]891                end,
892        },
[16]893        {
894                names={'ls'},
895                help='list files/directories on camera',
[93]896                args=argparser.create{l=false},
[16]897                arghelp="[-l] [path]",
898                func=function(self,args) 
[93]899                        local listops
900                        local path=args[1]
[115]901                        path = fsutil.make_camera_path(path)
[93]902                        if args.l then
[17]903                                listopts = { stat='*' }
904                        else
905                                listopts = { stat='/' }
906                        end
[58]907                        local list,msg = con:listdir(path,listopts)
[16]908                        if type(list) == 'table' then
[45]909                                local r = ''
[93]910                                if args.l then
[17]911                                        -- alphabetic sort TODO sorting/grouping options
[45]912                                        chdku.sortdir_stat(list)
913                                        for i,st in ipairs(list) do
[125]914                                                local name = st.name
915                                                local size = st.size
[17]916                                                if st.is_dir then
[125]917                                                        name = name..'/'
918                                                        size = '<dir>'
[16]919                                                else
920                                                end
[126]921                                                -- print(i,name,chdku.ts_cam2pc(st.mtime))
[125]922                                                r = r .. string.format("%s %10s %s\n",os.date('%c',chdku.ts_cam2pc(st.mtime)),tostring(size),name)
[16]923                                        end
[17]924                                else
925                                        table.sort(list)
926                                        for i,name in ipairs(list) do
927                                                r = r .. name .. '\n'
928                                        end
[16]929                                end
[17]930
[16]931                                return true,r
932                        end
933                        return false,msg
934                end,
935        },
[51]936        {
937                names={'reboot'},
938                help='reboot the camera',
[134]939                arghelp="[options] [file]",
940                args=argparser.create({
941                        wait=3500,
942                        norecon=false,
943                }),
[92]944                help_detail=[[
[134]945 file: Optional file to boot.
946  Must be an unencoded binary or for DryOS only, an encoded .FI2
947  Format is assumed based on extension
948  If not set, firmware boots normally, loading diskboot.bin if configured
949 options:
950   -norecon  don't try to reconnect
951   -wait=<N> wait N ms before attempting to reconnect, default 3500
[92]952]],
[51]953                func=function(self,args) 
[93]954                        local bootfile=args[1]
[51]955                        if bootfile then
[115]956                                bootfile = fsutil.make_camera_path(bootfile)
[51]957                                bootfile = string.format("'%s'",bootfile)
958                        else
959                                bootfile = ''
960                        end
961                        -- sleep and disconnect to avoid later connection problems on some cameras
962                        -- clobber because we don't care about memory leaks
[58]963                        local status,err=con:exec('sleep(1000);reboot('..bootfile..')',{clobber=true})
[51]964                        if not status then
965                                return false,err
966                        end
[134]967                        if args.norecon then
968                                return true
969                        end
970                        return con:reconnect({wait=args.wait})
[51]971                end,
972        },
[2]973};
974
975return cli;
Note: See TracBrowser for help on using the repository browser.