| 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 | status,msg = self.names[cmd](args) |
|---|
| 231 | if not status and not msg then |
|---|
| 232 | msg=cmd .. " failed" |
|---|
| 233 | end |
|---|
| 234 | return status,msg |
|---|
| 235 | else |
|---|
| 236 | return false,string.format("unknown command '%s'\n",cmd) |
|---|
| 237 | end |
|---|
| 238 | elseif string.find(line,'[^%c%s]') then |
|---|
| 239 | return false, string.format("bad input '%s'\n",line) |
|---|
| 240 | end |
|---|
| 241 | -- blank input is OK |
|---|
| 242 | return true,"" |
|---|
| 243 | end |
|---|
| 244 | |
|---|
| 245 | function cli:print_status(status,msg) |
|---|
| 246 | if not status then |
|---|
| 247 | errf("%s\n",tostring(msg)) |
|---|
| 248 | elseif msg and string.len(msg) ~= 0 then |
|---|
| 249 | printf("%s",msg) |
|---|
| 250 | if string.sub(msg,-1,-1) ~= '\n' then |
|---|
| 251 | printf("\n") |
|---|
| 252 | end |
|---|
| 253 | end |
|---|
| 254 | end |
|---|
| 255 | |
|---|
| 256 | function cli:run() |
|---|
| 257 | self:prompt() |
|---|
| 258 | for line in io.lines() do |
|---|
| 259 | self:print_status(self:execute(line)) |
|---|
| 260 | if self.finished then |
|---|
| 261 | break |
|---|
| 262 | end |
|---|
| 263 | self:prompt() |
|---|
| 264 | end |
|---|
| 265 | end |
|---|
| 266 | |
|---|
| 267 | -- add/correct A/ as needed, replace \ with / |
|---|
| 268 | function cli:make_camera_path(path) |
|---|
| 269 | if not path then |
|---|
| 270 | return 'A/' |
|---|
| 271 | end |
|---|
| 272 | -- fix slashes |
|---|
| 273 | path = string.gsub(path,'\\','/') |
|---|
| 274 | local pfx = string.sub(path,1,2) |
|---|
| 275 | if pfx == 'A/' then |
|---|
| 276 | return path |
|---|
| 277 | end |
|---|
| 278 | if pfx == 'a/' then |
|---|
| 279 | return 'A' .. string.sub(path,2,-1) |
|---|
| 280 | end |
|---|
| 281 | return 'A/' .. path |
|---|
| 282 | end |
|---|
| 283 | |
|---|
| 284 | cli:add_commands{ |
|---|
| 285 | { |
|---|
| 286 | names={'help','h'}, |
|---|
| 287 | arghelp='[cmd]|[-v]', |
|---|
| 288 | args=argparser.create{v=false}, |
|---|
| 289 | help='help on [cmd] or all commands', |
|---|
| 290 | help_detail=[[ |
|---|
| 291 | help -v gives full help on all commands, otherwise as summary is printed |
|---|
| 292 | ]], |
|---|
| 293 | func=function(self,args) |
|---|
| 294 | cmd = args[1] |
|---|
| 295 | if cmd and cli.names[cmd] then |
|---|
| 296 | return true, cli.names[cmd]:get_help_detail() |
|---|
| 297 | end |
|---|
| 298 | if cmd then |
|---|
| 299 | return false, string.format("unknown command '%s'\n",cmd) |
|---|
| 300 | end |
|---|
| 301 | msg = "" |
|---|
| 302 | for i,c in ipairs(cli.cmds) do |
|---|
| 303 | if args.v then |
|---|
| 304 | msg = msg .. c:get_help_detail() |
|---|
| 305 | else |
|---|
| 306 | msg = msg .. c:get_help() |
|---|
| 307 | end |
|---|
| 308 | end |
|---|
| 309 | return true, msg |
|---|
| 310 | end, |
|---|
| 311 | }, |
|---|
| 312 | { |
|---|
| 313 | names={'#'}, |
|---|
| 314 | help='comment', |
|---|
| 315 | func=function(self,args) |
|---|
| 316 | return true |
|---|
| 317 | end, |
|---|
| 318 | }, |
|---|
| 319 | { |
|---|
| 320 | names={'exec','!'}, |
|---|
| 321 | help='execute local lua', |
|---|
| 322 | arghelp='<lua code>', |
|---|
| 323 | help_detail=[[ |
|---|
| 324 | Execute lua in chdkptp. |
|---|
| 325 | The global variable con accesses the current CLI connection. |
|---|
| 326 | Return values are printed in the console. |
|---|
| 327 | ]], |
|---|
| 328 | func=function(self,args) |
|---|
| 329 | local f,r = loadstring(args) |
|---|
| 330 | if f then |
|---|
| 331 | r={pcall(f)}; |
|---|
| 332 | if not r[1] then |
|---|
| 333 | return false, string.format("call failed:%s\n",r[2]) |
|---|
| 334 | end |
|---|
| 335 | local s |
|---|
| 336 | if #r > 1 then |
|---|
| 337 | s='=' .. serialize(r[2],{pretty=true,err_type=false,err_cycle=false}) |
|---|
| 338 | for i = 3, #r do |
|---|
| 339 | s = s .. ',' .. serialize(r[i],{pretty=true,err_type=false,err_cycle=false}) |
|---|
| 340 | end |
|---|
| 341 | end |
|---|
| 342 | return true, s |
|---|
| 343 | else |
|---|
| 344 | return false, string.format("compile failed:%s\n",r) |
|---|
| 345 | end |
|---|
| 346 | end, |
|---|
| 347 | }, |
|---|
| 348 | { |
|---|
| 349 | names={'quit','q'}, |
|---|
| 350 | help='quit program', |
|---|
| 351 | func=function() |
|---|
| 352 | cli.finished = true |
|---|
| 353 | return true,"bye" |
|---|
| 354 | end, |
|---|
| 355 | }, |
|---|
| 356 | { |
|---|
| 357 | names={'lua','.'}, |
|---|
| 358 | help='execute remote lua', |
|---|
| 359 | arghelp='<lua code>', |
|---|
| 360 | help_detail=[[ |
|---|
| 361 | Execute Lua code on the camera. |
|---|
| 362 | Returns immediately after the script is started. |
|---|
| 363 | Return values or error messages can be retrieved with getm after the script is completed. |
|---|
| 364 | ]], |
|---|
| 365 | func=function(self,args) |
|---|
| 366 | return con:exec(args) |
|---|
| 367 | end, |
|---|
| 368 | }, |
|---|
| 369 | { |
|---|
| 370 | names={'getm'}, |
|---|
| 371 | help='get messages', |
|---|
| 372 | func=function(self,args) |
|---|
| 373 | local msgs='' |
|---|
| 374 | local msg,err |
|---|
| 375 | while true do |
|---|
| 376 | msg,err=con:read_msg() |
|---|
| 377 | if type(msg) ~= 'table' then |
|---|
| 378 | return false,msgs..err |
|---|
| 379 | end |
|---|
| 380 | if msg.type == 'none' then |
|---|
| 381 | return true,msgs |
|---|
| 382 | end |
|---|
| 383 | msgs = msgs .. chdku.format_script_msg(msg) .. "\n" |
|---|
| 384 | end |
|---|
| 385 | end, |
|---|
| 386 | }, |
|---|
| 387 | { |
|---|
| 388 | names={'putm'}, |
|---|
| 389 | help='send message', |
|---|
| 390 | arghelp='<msg string>', |
|---|
| 391 | func=function(self,args) |
|---|
| 392 | return con:write_msg(args) |
|---|
| 393 | end, |
|---|
| 394 | }, |
|---|
| 395 | { |
|---|
| 396 | names={'luar','='}, |
|---|
| 397 | help='execute remote lua, wait for result', |
|---|
| 398 | arghelp='<lua code>', |
|---|
| 399 | help_detail=[[ |
|---|
| 400 | Execute Lua code on the camera, waiting for the script to end. |
|---|
| 401 | Return values or error messages are printed after the script completes. |
|---|
| 402 | ]], |
|---|
| 403 | func=function(self,args) |
|---|
| 404 | local rets={} |
|---|
| 405 | local msgs={} |
|---|
| 406 | local status,err = con:execwait(args,{rets=rets,msgs=msgs}) |
|---|
| 407 | if not status then |
|---|
| 408 | return false,err |
|---|
| 409 | end |
|---|
| 410 | local r='' |
|---|
| 411 | for i=1,#msgs do |
|---|
| 412 | r=r .. chdku.format_script_msg(msgs[i]) .. '\n' |
|---|
| 413 | end |
|---|
| 414 | for i=1,table.maxn(rets) do |
|---|
| 415 | r=r .. chdku.format_script_msg(rets[i]) .. '\n' |
|---|
| 416 | end |
|---|
| 417 | return true,r |
|---|
| 418 | end, |
|---|
| 419 | }, |
|---|
| 420 | { |
|---|
| 421 | -- TODO support display as words |
|---|
| 422 | names={'rmem'}, |
|---|
| 423 | help='read memory', |
|---|
| 424 | args=argparser.create(), -- only word args |
|---|
| 425 | arghelp='<address> [count]', |
|---|
| 426 | func=function(self,args) |
|---|
| 427 | local addr = tonumber(args[1]) |
|---|
| 428 | local count = tonumber(args[2]) |
|---|
| 429 | if not addr then |
|---|
| 430 | return false, "bad args" |
|---|
| 431 | end |
|---|
| 432 | if not count then |
|---|
| 433 | count = 1 |
|---|
| 434 | end |
|---|
| 435 | |
|---|
| 436 | r,msg = con:getmem(addr,count) |
|---|
| 437 | if not r then |
|---|
| 438 | return false,msg |
|---|
| 439 | end |
|---|
| 440 | return true,string.format("0x%x %u\n",addr,count)..hexdump(r,addr) |
|---|
| 441 | end, |
|---|
| 442 | }, |
|---|
| 443 | { |
|---|
| 444 | names={'list'}, |
|---|
| 445 | help='list devices', |
|---|
| 446 | help_detail=[[ |
|---|
| 447 | Lists all recognized PTP devices in the following format |
|---|
| 448 | <status><num><modelname> b=<bus> d=<device> v=<usb vendor> p=<usb pid> s=<serial number> |
|---|
| 449 | status values |
|---|
| 450 | * connected, current target for CLI commands (con global variable) |
|---|
| 451 | + connected, not CLI target |
|---|
| 452 | - not connected |
|---|
| 453 | serial numbers are not available from all models |
|---|
| 454 | ]], |
|---|
| 455 | func=function() |
|---|
| 456 | local msg = '' |
|---|
| 457 | local devs = chdk.list_usb_devices() |
|---|
| 458 | for i,desc in ipairs(devs) do |
|---|
| 459 | local lcon = chdku.connection(desc) |
|---|
| 460 | local usb_info = lcon:get_usb_devinfo() |
|---|
| 461 | local tempcon = false |
|---|
| 462 | local status = "+" |
|---|
| 463 | if not lcon:is_connected() then |
|---|
| 464 | tempcon = true |
|---|
| 465 | status = "-" |
|---|
| 466 | lcon:connect() |
|---|
| 467 | end |
|---|
| 468 | local ptp_info = lcon:get_ptp_devinfo() |
|---|
| 469 | if not ptp_info then |
|---|
| 470 | ptp_info = { model = "<unknown>" } |
|---|
| 471 | end |
|---|
| 472 | if not ptp_info.serial_number then |
|---|
| 473 | ptp_info.serial_number ='(none)' |
|---|
| 474 | end |
|---|
| 475 | |
|---|
| 476 | if lcon._con == con._con then |
|---|
| 477 | status = "*" |
|---|
| 478 | end |
|---|
| 479 | |
|---|
| 480 | msg = msg .. string.format("%s%d:%s b=%s d=%s v=0x%x p=0x%x s=%s\n", |
|---|
| 481 | status, i, |
|---|
| 482 | ptp_info.model, |
|---|
| 483 | usb_info.bus, usb_info.dev, |
|---|
| 484 | usb_info.vendor_id, usb_info.product_id, |
|---|
| 485 | ptp_info.serial_number) |
|---|
| 486 | if tempcon then |
|---|
| 487 | lcon:disconnect() |
|---|
| 488 | end |
|---|
| 489 | end |
|---|
| 490 | return true,msg |
|---|
| 491 | end, |
|---|
| 492 | }, |
|---|
| 493 | { |
|---|
| 494 | names={'upload','u'}, |
|---|
| 495 | help='upload a file to the camera', |
|---|
| 496 | arghelp="[-nolua] <local> [remote]", |
|---|
| 497 | args=argparser.create{nolua=false}, |
|---|
| 498 | help_detail=[[ |
|---|
| 499 | <local> file to upload |
|---|
| 500 | [remote] destination |
|---|
| 501 | If not specified, file is uploaded to A/ |
|---|
| 502 | If remote is a directory or ends in / uploaded to remote/<local file name> |
|---|
| 503 | -nolua skip lua based checks on remote |
|---|
| 504 | Allows upload while running script |
|---|
| 505 | Prevents detecting if remote is a directory |
|---|
| 506 | Some cameras have problems with paths > 32 characters |
|---|
| 507 | Dryos cameras do not handle non 8.3 filenames well |
|---|
| 508 | ]], |
|---|
| 509 | func=function(self,args) |
|---|
| 510 | local src = args[1] |
|---|
| 511 | if not src then |
|---|
| 512 | return false, "missing source" |
|---|
| 513 | end |
|---|
| 514 | if lfs.attributes(src,'mode') ~= 'file' then |
|---|
| 515 | return false, 'src is not a file: '..src |
|---|
| 516 | end |
|---|
| 517 | |
|---|
| 518 | local dst_dir |
|---|
| 519 | local dst = args[2] |
|---|
| 520 | -- no dst, use filename of source |
|---|
| 521 | if dst then |
|---|
| 522 | dst = cli:make_camera_path(dst) |
|---|
| 523 | if string.find(dst,'[\\/]$') then |
|---|
| 524 | -- trailing slash, append filename of source |
|---|
| 525 | dst = string.sub(dst,1,-2) |
|---|
| 526 | if not args.nolua then |
|---|
| 527 | local st,err = con:stat(dst) |
|---|
| 528 | if not st then |
|---|
| 529 | return false, 'stat dest '..dst..' failed: ' .. err |
|---|
| 530 | end |
|---|
| 531 | if not st.is_dir then |
|---|
| 532 | return false, 'not a directory: '..dst |
|---|
| 533 | end |
|---|
| 534 | end |
|---|
| 535 | dst = fsutil.joinpath(dst,fsutil.basename(src)) |
|---|
| 536 | else |
|---|
| 537 | if not args.nolua then |
|---|
| 538 | local st = con:stat(dst) |
|---|
| 539 | if st and st.is_dir then |
|---|
| 540 | dst = fsutil.joinpath(dst,fsutil.basename(src)) |
|---|
| 541 | end |
|---|
| 542 | end |
|---|
| 543 | end |
|---|
| 544 | else |
|---|
| 545 | dst = cli:make_camera_path(fsutil.basename(src)) |
|---|
| 546 | end |
|---|
| 547 | |
|---|
| 548 | local msg=string.format("%s->%s\n",src,dst) |
|---|
| 549 | local r, msg2 = con:upload(src,dst) |
|---|
| 550 | if msg2 then |
|---|
| 551 | msg = msg .. msg2 |
|---|
| 552 | end |
|---|
| 553 | return r, msg |
|---|
| 554 | end, |
|---|
| 555 | }, |
|---|
| 556 | { |
|---|
| 557 | names={'download','d'}, |
|---|
| 558 | help='download a file from the camera', |
|---|
| 559 | arghelp="[-nolua] <remote> [local]", |
|---|
| 560 | args=argparser.create{nolua=false}, |
|---|
| 561 | help_detail=[[ |
|---|
| 562 | <remote> file to download |
|---|
| 563 | A/ is prepended if not present |
|---|
| 564 | [local] destination |
|---|
| 565 | If not specified, the file will be downloaded to the current directory |
|---|
| 566 | If a directory, the file will be downloaded into it |
|---|
| 567 | -nolua skip lua based checks on remote |
|---|
| 568 | Allows download while running script |
|---|
| 569 | ]], |
|---|
| 570 | |
|---|
| 571 | func=function(self,args) |
|---|
| 572 | local src = args[1] |
|---|
| 573 | if not src then |
|---|
| 574 | return false, "missing source" |
|---|
| 575 | end |
|---|
| 576 | local dst = args[2] |
|---|
| 577 | if not dst then |
|---|
| 578 | -- no dest, use final component of source path |
|---|
| 579 | dst = fsutil.basename(src) |
|---|
| 580 | elseif string.match(dst,'[\\/]+$') then |
|---|
| 581 | -- explicit / treat it as a directory |
|---|
| 582 | dst = fsutil.joinpath(dst,fsutil.basename(src)) |
|---|
| 583 | -- and check if it is |
|---|
| 584 | local dst_dir = fsutil.dirname(dst) |
|---|
| 585 | -- TODO should create it |
|---|
| 586 | if lfs.attributes(dst_dir,'mode') ~= 'directory' then |
|---|
| 587 | return false,'not a directory: '..dst_dir |
|---|
| 588 | end |
|---|
| 589 | elseif lfs.attributes(dst,'mode') == 'directory' then |
|---|
| 590 | -- if target is a directory download into it |
|---|
| 591 | dst = fsutil.joinpath(dst,fsutil.basename(src)) |
|---|
| 592 | end |
|---|
| 593 | |
|---|
| 594 | src = cli:make_camera_path(src) |
|---|
| 595 | if not args.nolua then |
|---|
| 596 | local src_st,err = con:stat(src) |
|---|
| 597 | if not src_st then |
|---|
| 598 | return false, 'stat source '..src..' failed: ' .. err |
|---|
| 599 | end |
|---|
| 600 | if not src_st.is_file then |
|---|
| 601 | return false, src..' is not a file' |
|---|
| 602 | end |
|---|
| 603 | end |
|---|
| 604 | local msg=string.format("%s->%s\n",src,dst) |
|---|
| 605 | local r, msg2 = con:download(src,dst) |
|---|
| 606 | if msg2 then |
|---|
| 607 | msg = msg .. msg2 |
|---|
| 608 | end |
|---|
| 609 | return r, msg |
|---|
| 610 | end, |
|---|
| 611 | }, |
|---|
| 612 | { |
|---|
| 613 | names={'version','ver'}, |
|---|
| 614 | help='print API versions', |
|---|
| 615 | func=function(self,args) |
|---|
| 616 | local host_ver = string.format("host:%d.%d cam:",chdk.host_api_version()) |
|---|
| 617 | if con:is_connected() then |
|---|
| 618 | local cam_major, cam_minor = con:camera_api_version() |
|---|
| 619 | if not cam_major then |
|---|
| 620 | return false, host_ver .. string.format("error %s",cam_minor) |
|---|
| 621 | end |
|---|
| 622 | return true, host_ver .. string.format("%d.%d",cam_major,cam_minor) |
|---|
| 623 | else |
|---|
| 624 | return true, host_ver .. "not connected" |
|---|
| 625 | end |
|---|
| 626 | end, |
|---|
| 627 | }, |
|---|
| 628 | { |
|---|
| 629 | names={'connect','c'}, |
|---|
| 630 | help='connect to device', |
|---|
| 631 | arghelp="[-b=<bus>] [-d=<dev>] [-p=<pid>] [-s=<serial>] [model] ", |
|---|
| 632 | args=argparser.create{ |
|---|
| 633 | b='.*', |
|---|
| 634 | d='.*', |
|---|
| 635 | p=false, |
|---|
| 636 | s=false, |
|---|
| 637 | }, |
|---|
| 638 | |
|---|
| 639 | help_detail=[[ |
|---|
| 640 | If no options are given, connects to the first available device. |
|---|
| 641 | <pid> is the USB product ID, as a decimal or hexadecimal number. |
|---|
| 642 | All other options are treated as a Lua pattern. For alphanumerics, this is a case sensitive substring match. |
|---|
| 643 | If the serial or model are specified, a temporary connection will be made to each device |
|---|
| 644 | If <model> includes spaces, it must be quoted. |
|---|
| 645 | If multiple devices match, the first matching device will be connected. |
|---|
| 646 | ]], |
|---|
| 647 | func=function(self,args) |
|---|
| 648 | local match = {} |
|---|
| 649 | local opt_map = { |
|---|
| 650 | b='bus', |
|---|
| 651 | d='dev', |
|---|
| 652 | p='product_id', |
|---|
| 653 | s='serial_number', |
|---|
| 654 | [1]='model', |
|---|
| 655 | } |
|---|
| 656 | for k,v in pairs(opt_map) do |
|---|
| 657 | -- TODO matches expect nil |
|---|
| 658 | if type(args[k]) == 'string' then |
|---|
| 659 | match[v] = args[k] |
|---|
| 660 | end |
|---|
| 661 | -- printf('%s=%s\n',v,tostring(args[k])) |
|---|
| 662 | end |
|---|
| 663 | |
|---|
| 664 | if con:is_connected() then |
|---|
| 665 | con:disconnect() |
|---|
| 666 | end |
|---|
| 667 | |
|---|
| 668 | if match.product_id and not tonumber(match.product_id) then |
|---|
| 669 | return false,"expected number for product id" |
|---|
| 670 | end |
|---|
| 671 | local devices = chdk.list_usb_devices() |
|---|
| 672 | local lcon |
|---|
| 673 | for i, devinfo in ipairs(devices) do |
|---|
| 674 | lcon = nil |
|---|
| 675 | if chdku.match_device(devinfo,match) then |
|---|
| 676 | lcon = chdku.connection(devinfo) |
|---|
| 677 | -- if we are looking for model or serial, need to connect to the dev to check |
|---|
| 678 | if match.model or match.serial_number then |
|---|
| 679 | local tempcon = false |
|---|
| 680 | -- printf('model check %s %s\n',tostring(match.model),tostring(match.serial_number)) |
|---|
| 681 | if not lcon:is_connected() then |
|---|
| 682 | lcon:connect() |
|---|
| 683 | tempcon = true |
|---|
| 684 | end |
|---|
| 685 | if not lcon:match_ptp_info(match) then |
|---|
| 686 | if tempcon then |
|---|
| 687 | lcon:disconnect() |
|---|
| 688 | end |
|---|
| 689 | lcon = nil |
|---|
| 690 | end |
|---|
| 691 | end |
|---|
| 692 | if lcon then |
|---|
| 693 | break |
|---|
| 694 | end |
|---|
| 695 | end |
|---|
| 696 | end |
|---|
| 697 | if lcon then |
|---|
| 698 | con = lcon |
|---|
| 699 | if con:is_connected() then |
|---|
| 700 | return true |
|---|
| 701 | end |
|---|
| 702 | return con:connect() |
|---|
| 703 | end |
|---|
| 704 | return false,"no matching devices found" |
|---|
| 705 | end, |
|---|
| 706 | }, |
|---|
| 707 | { |
|---|
| 708 | names={'reconnect','r'}, |
|---|
| 709 | help='reconnect to current device', |
|---|
| 710 | -- TODO depends on camera coming back on current dev/bus, not guaranteed |
|---|
| 711 | -- caching model/serial could help |
|---|
| 712 | func=function(self,args) |
|---|
| 713 | if con:is_connected() then |
|---|
| 714 | con:disconnect() |
|---|
| 715 | end |
|---|
| 716 | -- appears to be needed to avoid device numbers changing (reset too soon ?) |
|---|
| 717 | sys.sleep(2000) |
|---|
| 718 | return con:connect() |
|---|
| 719 | end, |
|---|
| 720 | }, |
|---|
| 721 | { |
|---|
| 722 | names={'disconnect','dis'}, |
|---|
| 723 | help='disconnect from device', |
|---|
| 724 | func=function(self,args) |
|---|
| 725 | return con:disconnect() |
|---|
| 726 | end, |
|---|
| 727 | }, |
|---|
| 728 | { |
|---|
| 729 | names={'ls'}, |
|---|
| 730 | help='list files/directories on camera', |
|---|
| 731 | args=argparser.create{l=false}, |
|---|
| 732 | arghelp="[-l] [path]", |
|---|
| 733 | func=function(self,args) |
|---|
| 734 | local listops |
|---|
| 735 | local path=args[1] |
|---|
| 736 | path = cli:make_camera_path(path) |
|---|
| 737 | if args.l then |
|---|
| 738 | listopts = { stat='*' } |
|---|
| 739 | else |
|---|
| 740 | listopts = { stat='/' } |
|---|
| 741 | end |
|---|
| 742 | local list,msg = con:listdir(path,listopts) |
|---|
| 743 | if type(list) == 'table' then |
|---|
| 744 | local r = '' |
|---|
| 745 | if args.l then |
|---|
| 746 | -- alphabetic sort TODO sorting/grouping options |
|---|
| 747 | chdku.sortdir_stat(list) |
|---|
| 748 | for i,st in ipairs(list) do |
|---|
| 749 | if st.is_dir then |
|---|
| 750 | r = r .. string.format("%s/\n",st.name) |
|---|
| 751 | else |
|---|
| 752 | r = r .. string.format("%-13s %10d\n",st.name,st.size) |
|---|
| 753 | end |
|---|
| 754 | end |
|---|
| 755 | else |
|---|
| 756 | table.sort(list) |
|---|
| 757 | for i,name in ipairs(list) do |
|---|
| 758 | r = r .. name .. '\n' |
|---|
| 759 | end |
|---|
| 760 | end |
|---|
| 761 | |
|---|
| 762 | return true,r |
|---|
| 763 | end |
|---|
| 764 | return false,msg |
|---|
| 765 | end, |
|---|
| 766 | }, |
|---|
| 767 | { |
|---|
| 768 | names={'reboot'}, |
|---|
| 769 | help='reboot the camera', |
|---|
| 770 | arghelp="[file]", |
|---|
| 771 | args=argparser.create(), |
|---|
| 772 | help_detail=[[ |
|---|
| 773 | [file] is an optional file to boot. |
|---|
| 774 | If not given, the normal boot process is used. |
|---|
| 775 | The file may be an unencoded binary or on DryOS only, an encoded .FI2 |
|---|
| 776 | chdkptp attempts to reconnect to the camera after it boots. |
|---|
| 777 | ]], |
|---|
| 778 | -- TODO reconnect depends on camera coming back on current dev/bus, not guaranteed |
|---|
| 779 | -- caching model/serial could help |
|---|
| 780 | func=function(self,args) |
|---|
| 781 | local bootfile=args[1] |
|---|
| 782 | if bootfile then |
|---|
| 783 | bootfile = cli:make_camera_path(bootfile) |
|---|
| 784 | bootfile = string.format("'%s'",bootfile) |
|---|
| 785 | else |
|---|
| 786 | bootfile = '' |
|---|
| 787 | end |
|---|
| 788 | -- sleep and disconnect to avoid later connection problems on some cameras |
|---|
| 789 | -- clobber because we don't care about memory leaks |
|---|
| 790 | local status,err=con:exec('sleep(1000);reboot('..bootfile..')',{clobber=true}) |
|---|
| 791 | if not status then |
|---|
| 792 | return false,err |
|---|
| 793 | end |
|---|
| 794 | con:disconnect() |
|---|
| 795 | -- sleep locally to avoid clobbering the reboot, and allow time for the camera to come up before trying to connect |
|---|
| 796 | sys.sleep(3000) |
|---|
| 797 | |
|---|
| 798 | return con:connect() |
|---|
| 799 | end, |
|---|
| 800 | }, |
|---|
| 801 | }; |
|---|
| 802 | |
|---|
| 803 | return cli; |
|---|