| 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 | |
|---|
| 18 | local cli = { |
|---|
| 19 | cmds={}, |
|---|
| 20 | names={}, |
|---|
| 21 | finished = false, |
|---|
| 22 | } |
|---|
| 23 | |
|---|
| 24 | --[[ |
|---|
| 25 | get command args of the form -a[=value] -bar[=value] .. [wordarg1] [wordarg2] [wordarg...] |
|---|
| 26 | --]] |
|---|
| 27 | local argparser = { } |
|---|
| 28 | cli.argparser = argparser |
|---|
| 29 | |
|---|
| 30 | -- trim leading spaces |
|---|
| 31 | function argparser:trimspace(str) |
|---|
| 32 | local s, e = string.find(str,'^[%c%s]*') |
|---|
| 33 | return string.sub(str,e+1) |
|---|
| 34 | end |
|---|
| 35 | --[[ |
|---|
| 36 | get a 'word' argument, either a sequence of non-white space characters, or a quoted string |
|---|
| 37 | inside " \ is treated as an escape character |
|---|
| 38 | return word, end position on success or false, error message |
|---|
| 39 | ]] |
|---|
| 40 | function 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 |
|---|
| 76 | end |
|---|
| 77 | |
|---|
| 78 | function 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 |
|---|
| 91 | end |
|---|
| 92 | |
|---|
| 93 | --[[ |
|---|
| 94 | parse a command string into switches and word arguments |
|---|
| 95 | switches are in the form -swname[=value] |
|---|
| 96 | word arguments are anything else |
|---|
| 97 | any portion of the string may be quoted with '' or "" |
|---|
| 98 | inside "", \ is treated as an escape |
|---|
| 99 | on success returns table with args as array elements and switches as named elements |
|---|
| 100 | on failure returns false, error |
|---|
| 101 | defs defines the valid switches and their default values. Can also define default values of numeric args |
|---|
| 102 | TODO enforce switch values, number of args, integrate with help |
|---|
| 103 | ]] |
|---|
| 104 | function 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 |
|---|
| 135 | end |
|---|
| 136 | |
|---|
| 137 | -- a default for comands that want the raw string |
|---|
| 138 | argparser.nop = { |
|---|
| 139 | parse =function(self,str) |
|---|
| 140 | return str |
|---|
| 141 | end |
|---|
| 142 | } |
|---|
| 143 | |
|---|
| 144 | function argparser.create(defs) |
|---|
| 145 | local r={ defs=defs } |
|---|
| 146 | return util.mt_inherit(r,argparser) |
|---|
| 147 | end |
|---|
| 148 | |
|---|
| 149 | cli.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 | |
|---|
| 170 | cli.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 | |
|---|
| 179 | function 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 |
|---|
| 198 | end |
|---|
| 199 | |
|---|
| 200 | function 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 |
|---|
| 211 | end |
|---|
| 212 | |
|---|
| 213 | -- execute command given by a single line |
|---|
| 214 | -- returns status,message |
|---|
| 215 | -- message is an error message or printable value |
|---|
| 216 | function 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,"" |
|---|
| 247 | end |
|---|
| 248 | |
|---|
| 249 | function 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 |
|---|
| 258 | end |
|---|
| 259 | |
|---|
| 260 | function 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 |
|---|
| 269 | end |
|---|
| 270 | |
|---|
| 271 | cli: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 | |
|---|
| 967 | return cli; |
|---|