source: trunk/lua/cli.lua @ 78

Revision 78, 12.6 KB checked in by reyalp, 17 months ago (diff)

merge connection_rework_test branch back to trunk - note low level connection object from chdk.connection no longer has methods, use chdku.connection

  • 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
24cli.cmd_proto = {
25        get_help = function(self)
26                local namestr = self.names[1]
27                if #self.names > 1 then
28                        namestr = namestr .. " (" .. self.names[2]
29                        for i=3,#self.names do
30                                namestr = namestr .. "," .. self.names[i]
31                        end
32                        namestr = namestr .. ")"
33                end
34                return string.format("%-12s %-12s: - %s\n",namestr,self.arghelp,self.help)
35        end,
36}
37
38cli.cmd_meta = {
39        __index = function(cmd, key)
40                return cli.cmd_proto[key]
41        end,
42        __call = function(cmd,...)
43                return cmd:func(...)
44        end,
45}
46
47function cli:add_commands(cmds)
48        for i = 1, #cmds do
49                cmd = cmds[i]
50                table.insert(self.cmds,cmd)
51                if not cmd.arghelp then
52                        cmd.arghelp = ''
53                end
54                for _,name in ipairs(cmd.names) do
55                        if self.names[name] then
56                                warnf("duplicate command name %s\n",name)
57                        else
58                                self.names[name] = cmd
59                        end
60                end
61                setmetatable(cmd,cli.cmd_meta)
62        end
63end
64
65function cli:prompt()
66        if con:is_connected() then
67                local script_id = con:get_script_id()
68                if script_id then
69                        printf("con %d> ",script_id)
70                else
71                        printf("con> ")
72                end
73        else
74                printf("___> ")
75        end
76end
77
78-- execute command given by a single line
79-- returns status,message
80-- message is an error message or printable value
81function cli:execute(line)
82        -- single char shortcuts
83        local s,e,cmd = string.find(line,'^[%c%s]*([!?.#=])[%c%s]*')
84        if not cmd then
85                s,e,cmd = string.find(line,'^[%c%s]*([%w_]+)[%c%s]*')
86        end
87        if s then
88                local args = string.sub(line,e+1)
89                if self.names[cmd] then
90                        local status,msg = self.names[cmd](args)
91                        if not status and not msg then
92                                msg=cmd .. " failed"
93                        end
94                        return status,msg
95                else
96                        return false,string.format("unknown command '%s'\n",cmd)
97                end
98        elseif string.find(line,'[^%c%s]') then
99                return false, string.format("bad input '%s'\n",line)
100        end
101        -- blank input is OK
102        return true,""
103end
104
105function cli:print_status(status,msg)
106        if not status then
107                errf("%s\n",tostring(msg))
108        elseif msg and string.len(msg) ~= 0 then
109                printf("%s",msg)
110                if string.sub(msg,-1,-1) ~= '\n' then
111                        printf("\n")
112                end
113        end
114end
115
116function cli:run()
117        self:prompt()
118        for line in io.lines() do
119                self:print_status(self:execute(line))
120                if self.finished then
121                        break
122                end
123                self:prompt()
124        end
125end
126
127-- add/correct A/ as needed, replace \ with /
128function cli:make_camera_path(path)
129        if not path then
130                return 'A/'
131        end
132        -- fix slashes
133        path = string.gsub(path,'\\','/')
134        local pfx = string.sub(path,1,2)
135        if pfx == 'A/' then
136                return path
137        end
138        if pfx == 'a/' then
139                return 'A' .. string.sub(path,2,-1)
140        end
141        return 'A/' .. path
142end
143
144-- returns <str>, <remaining arg string>
145-- accepts quoted strings or space delimited
146function cli:get_string_arg(arg)
147        if type(arg) ~= 'string' then
148                return
149        end
150        local path
151        -- trim leading spaces
152        local s, e = string.find(arg,'^[%c%s]*')
153        arg = string.sub(arg,e+1)
154        -- check for quotes
155        s, e, str = string.find(arg,'^["]([^"]+)["]')
156        if s then
157                return str, string.sub(arg,e+1)
158        end
159        -- try without quotes
160        s, e, str = string.find(arg,'^([^%c%s]+)')
161        if s then
162                return str, string.sub(arg,e+1)
163        end
164        return nil
165end
166
167--[[
168t,args=cli:get_opts(args,optspec)
169optspec is an array of option letters
170returns table of option values
171plus arg string with recognized opts removed
172TODO should unify command line processing with main.lua args
173]]
174function cli:get_opts(arg,optspec)
175        local r={}
176        for i,v in ipairs(optspec) do
177                arg = string.gsub(arg,'-'..v,function()
178                        r[v]=true
179                        return ''
180                end)
181        end
182        return r,arg
183end
184
185-- returns num, <remaining arg string>
186-- num can be signed hex or decimal
187function cli:get_num_arg(arg)
188        if type(arg) ~= 'string' then
189                return
190        end
191        local hex,num
192        local s, e, neg=string.find(arg,'^[%c%s]*(-?)')
193        if not s then
194                neg = ''
195        end
196        arg = string.sub(arg,e+1)
197        s, e, hex=string.find(arg,'^(0[Xx])')
198        if s then
199                arg = string.sub(arg,e+1)
200                s, e, num=string.find(arg,'^([%x]+)')
201        else
202                hex = ''
203                s, e, num=string.find(arg,'^([%d]+)')
204        end
205        if s then
206                return tonumber(neg..hex..num), string.sub(arg,e+1)
207        end
208end
209
210cli:add_commands{
211        {
212                names={'help','h','?'},
213                arghelp = '[command]';
214                help='help on [command] or all commands',
215                func=function(self,args)
216                        if cli.names[args] then
217                                return true, cli.names[args]:get_help()
218                        end
219                        if args and args ~= "" then
220                                return false, string.format("unknown command '%s'\n",args)
221                        end
222                        msg = ""
223                        for i,c in ipairs(cli.cmds) do
224                                msg = msg .. c:get_help()
225                        end
226                        return true, msg
227                end,
228        },
229        {
230                names={'#'},
231                help='comment',
232                func=function(self,args)
233                        return true
234                end,
235        },
236        {
237                names={'exec','!'},
238                help='execute local lua',
239                arghelp='<lua code>',
240                func=function(self,args)
241                        local f,r = loadstring(args)
242                        if f then
243                                r={pcall(f)};
244                                if not r[1] then
245                                        return false, string.format("call failed:%s\n",r[2])
246                                end
247                                local s
248                                if #r > 1 then
249                                        s='=' .. serialize(r[2],{pretty=true,err_type=false,err_cycle=false})
250                                        for i = 3, #r do
251                                                s = s .. ',' .. serialize(r[i],{pretty=true,err_type=false,err_cycle=false})
252                                        end
253                                end
254                                return true, s
255                        else
256                                return false, string.format("compile failed:%s\n",r)
257                        end
258                end,
259        },
260        {
261                names={'quit','q'},
262                help='quit program',
263                func=function()
264                        cli.finished = true
265                        return true,"bye"
266                end,
267        },
268        {
269                names={'lua','l','.'},
270                help='execute remote lua',
271                arghelp='<lua code>',
272                func=function(self,args)
273                        return con:exec(args)
274                end,
275        },
276        {
277                names={'getm'},
278                help='get messages',
279                func=function(self,args)
280                        local msgs=''
281                        local msg,err
282                        while true do
283                                msg,err=con:read_msg()
284                                if type(msg) ~= 'table' then
285                                        return false,msgs..err
286                                end
287                                if msg.type == 'none' then
288                                        return true,msgs
289                                end
290                                msgs = msgs .. chdku.format_script_msg(msg) .. "\n"
291                        end
292                end,
293        },
294        {
295                names={'putm'},
296                help='send message',
297                arghelp='<msg string>',
298                func=function(self,args)
299                        return con:write_msg(args)
300                end,
301        },
302        {
303                names={'luar','='},
304                help='execute remote lua, wait for result',
305                arghelp='<lua code>',
306                func=function(self,args)
307                        local rets={}
308                        local msgs={}
309                        local status,err = con:execwait(args,{rets=rets,msgs=msgs})
310                        if not status then
311                                return false,err
312                        end
313                        local r=''
314                        for i=1,#msgs do
315                                r=r .. chdku.format_script_msg(msgs[i]) .. '\n'
316                        end
317                        for i=1,table.maxn(rets) do
318                                r=r .. chdku.format_script_msg(rets[i]) .. '\n'
319                        end
320                        return true,r
321                end,
322        },
323        {
324                -- TODO support display as words
325                names={'rmem'},
326                help=' read memory',
327                arghelp='<address> [count]',
328                func=function(self,args)
329                        local addr
330                        addr,args = cli:get_num_arg(args)
331                        local count = cli:get_num_arg(args)
332                        if not addr then
333                                return false, "bad args"
334                        end
335                        if not count then
336                                count = 1
337                        end
338                        printf("0x%x %u\n",addr,count)
339                        r,msg = con:getmem(addr,count)
340                        if not r then
341                                return false,msg
342                        end
343                        return true,hexdump(r,addr)
344                end,
345        },
346        {
347                names={'list'},
348                help='list devices',
349                func=function()
350                        local msg = ''
351                        local devs = chdk.list_usb_devices()
352                        for i,desc in ipairs(devs) do
353                                local lcon = chdku.connection(desc)
354                                local usb_info = lcon:get_usb_devinfo()
355                                local tempcon = false
356                                local status = "CONNECTED"
357                                if not lcon:is_connected() then
358                                        tempcon = true
359                                        status = "NOT CONNECTED"
360                                        lcon:connect()
361                                end
362                                local ptp_info = lcon:get_ptp_devinfo()
363                                if not ptp_info then
364                                        ptp_info = { model = "<unknown>" }
365                                end
366
367                                if lcon == con then
368                                        status = status..", CLI"
369                                end
370
371                                msg = msg .. string.format("%d: %s bus:%s dev:%s vendor:%x pid:%x [%s]\n",
372                                                                                        i, ptp_info.model,
373                                                                                        usb_info.bus, usb_info.dev,
374                                                                                        usb_info.vendor_id, usb_info.product_id,
375                                                                                        status)
376                                if tempcon then
377                                        lcon:disconnect()
378                                end
379                        end
380                        return true,msg
381                end,
382        },
383        {
384                names={'upload','u'},
385                help='upload a file to the camera',
386                arghelp="<local> [remote]",
387                func=function(self,args)
388                        local src,args = cli:get_string_arg(args)
389                        if not src then
390                                return false, "missing source"
391                        end
392                        local dst = cli:get_string_arg(args)
393                        -- no dst, use filename of source
394                        if not dst then
395                                dst = util.basename(src)
396                        -- trailing slash, append filename of source
397                        elseif string.find(dst,'[\\/]$') then
398                                dst = dst .. util.basename(src)
399                        end
400                        if not (src and dst) then
401                                return false, "bad/missing args ?"
402                        end
403                        dst = cli:make_camera_path(dst)
404                        local msg=string.format("%s->%s\n",src,dst)
405                        local r, msg2 = con:upload(src,dst)
406                        if msg2 then
407                                msg = msg .. msg2
408                        end
409                        return r, msg
410                end,
411        },
412        {
413                names={'download','d'},
414                help='download a file from the camera',
415                arghelp="<remote> [local]",
416                func=function(self,args)
417                        local src,args = cli:get_string_arg(args)
418                        if not src then
419                                return false, "missing source"
420                        end
421                        local dst = cli:get_string_arg(args)
422                        -- use final component
423                        if not dst then
424                                dst = util.basename(src)
425                        -- trailing slash, append filename of source
426                        -- TODO should use stat to figure out if target is a directory
427                        elseif string.find(dst,'[\\/]$') then
428                                dst = dst .. util.basename(src)
429                        end
430                        if not dst then
431                                return false, "bad/missing args ?"
432                        end
433                        src = cli:make_camera_path(src)
434                        local msg=string.format("%s->%s\n",src,dst)
435                        local r, msg2 = con:download(src,dst)
436                        if msg2 then
437                                msg = msg .. msg2
438                        end
439                        return r, msg
440                end,
441        },
442        {
443                names={'version','ver'},
444                help='print API versions',
445                func=function(self,args)
446                        local host_ver = string.format("host:%d.%d cam:",chdk.host_api_version())
447                        if con:is_connected() then
448                                local cam_major, cam_minor = con:camera_api_version()
449                                if not cam_major then
450                                        return false, host_ver .. string.format("error %s",cam_minor)
451                                end
452                                return true, host_ver .. string.format("%d.%d",cam_major,cam_minor)
453                        else
454                                return true, host_ver .. "not connected"
455                        end
456                end,
457        },
458        {
459                names={'connect','c'},
460                help='connect to device',
461                func=function(self,args)
462                        if con:is_connected() then
463                                con:disconnect()
464                        end
465                        -- TODO temp just connect to the default device for now
466                        local devs = chdk.list_usb_devices()
467                        if #devs > 0 then
468                                con = chdku.connection(devs[1])
469                                return con:connect()
470                        end
471                        return false,"no devices available"
472                end,
473        },
474        --[[
475        -- TODO this isn't useful - on win device name can change on reset ?
476        {
477                names={'reconnect','r'},
478                help='reconnect to current device',
479                func=function(self,args)
480                        if con:is_connected() then
481                                con:disconnect()
482                        end
483                        return con:connect()
484                end,
485        },
486        ]]
487        {
488                names={'disconnect','dis'},
489                help='disconnect from device',
490                func=function(self,args)
491                        return con:disconnect()
492                end,
493        },
494        {
495                names={'ls'},
496                help='list files/directories on camera',
497                arghelp="[-l] [path]",
498                func=function(self,args)
499                        local opts,listops
500                        opts,args=cli:get_opts(args,{'l'})
501                        local path=cli:get_string_arg(args)
502                        path = cli:make_camera_path(path)
503                        if opts.l then
504                                listopts = { stat='*' }
505                        else
506                                listopts = { stat='/' }
507                        end
508                        local list,msg = con:listdir(path,listopts)
509                        if type(list) == 'table' then
510                                local r = ''
511                                if opts.l then
512                                        -- alphabetic sort TODO sorting/grouping options
513                                        chdku.sortdir_stat(list)
514                                        for i,st in ipairs(list) do
515                                                if st.is_dir then
516                                                        r = r .. string.format("%s/\n",st.name)
517                                                else
518                                                        r = r .. string.format("%-13s %10d\n",st.name,st.size)
519                                                end
520                                        end
521                                else
522                                        table.sort(list)
523                                        for i,name in ipairs(list) do
524                                                r = r .. name .. '\n'
525                                        end
526                                end
527
528                                return true,r
529                        end
530                        return false,msg
531                end,
532        },
533        {
534                names={'reboot'},
535                help='reboot the camera',
536                arghelp="[file]",
537                func=function(self,args)
538                        local bootfile=cli:get_string_arg(args)
539                        if bootfile then
540                                bootfile = cli:make_camera_path(bootfile)
541                                bootfile = string.format("'%s'",bootfile)
542                        else
543                                bootfile = ''
544                        end
545                        -- sleep and disconnect to avoid later connection problems on some cameras
546                        -- clobber because we don't care about memory leaks
547                        local status,err=con:exec('sleep(1000);reboot('..bootfile..')',{clobber=true})
548                        if not status then
549                                return false,err
550                        end
551                        con:disconnect()
552                        -- sleep locally to avoid clobbering the reboot, and allow time for the camera to come up before trying to connect
553                        sys.sleep(3000)
554
555                        return con:connect()
556                end,
557        },
558};
559
560return cli;
Note: See TracBrowser for help on using the repository browser.