picadae calibration programs
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. ##| Copyright: (C) 2019-2020 Kevin Larke <contact AT larke DOT org>
  2. ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
  3. import sys,os,argparse,types,logging,select,time,json
  4. from datetime import datetime
  5. import multiprocessing
  6. from multiprocessing import Process, Pipe
  7. from picadae_api import Picadae
  8. from AudioDevice import AudioDevice
  9. from MidiDevice import MidiDevice
  10. from result import Result
  11. from common import parse_yaml_cfg
  12. from plot_seq import form_resample_pulse_time_list
  13. from plot_seq_1 import get_resample_points_wrap
  14. from plot_seq import form_final_pulse_list
  15. from rt_note_analysis import RT_Analyzer
  16. from keyboard import Keyboard
  17. from calibrate import Calibrate
  18. from rms_analysis import rms_analyze_one_rt_note_wrap
  19. from MidiFilePlayer import MidiFilePlayer
  20. class AttackPulseSeq:
  21. """ Sequence a fixed pitch over a list of attack pulse lengths."""
  22. def __init__(self, cfg, audio, api, noteDurMs=1000, pauseDurMs=1000 ):
  23. self.cfg = cfg
  24. self.audio = audio
  25. self.api = api
  26. self.outDir = None # directory to write audio file and results
  27. self.pitch = None # pitch to paly
  28. self.pulseUsL = [] # one onset pulse length in microseconds per sequence element
  29. self.noteDurMs = noteDurMs # duration of each chord in milliseconds
  30. self.pauseDurMs = pauseDurMs # duration between end of previous note and start of next
  31. self.holdDutyPctL= None # hold voltage duty cycle table [ (minPulseSeqUsec,dutyCyclePct) ]
  32. self.holdDutyPctD= None # { us:dutyPct } for each us in self.pulseUsL
  33. self.silentNoteN = None
  34. self.pulse_idx = 0 # Index of next pulse
  35. self.state = None # 'note_on','note_off'
  36. self.prevHoldDutyPct = None
  37. self.next_ms = 0 # Time of next event (note-on or note_off)
  38. self.eventTimeL = [] # Onset/offset time of each note [ [onset_ms,offset_ms] ] (used to locate the note in the audio file)
  39. self.beginMs = 0
  40. self.playOnlyFl = False
  41. self.rtAnalyzer = RT_Analyzer()
  42. def start( self, ms, outDir, pitch, pulseUsL, holdDutyPctL, holdDutyPctD, playOnlyFl=False ):
  43. self.outDir = outDir # directory to write audio file and results
  44. self.pitch = pitch # note to play
  45. self.pulseUsL = pulseUsL # one onset pulse length in microseconds per sequence element
  46. self.holdDutyPctL = holdDutyPctL
  47. self.holdDutyPctD = holdDutyPctD
  48. self.silentNoteN = 0
  49. self.pulse_idx = 0
  50. self.state = 'note_on'
  51. self.prevHoldDutyPct = None
  52. self.next_ms = ms + 500 # wait for 500ms to play the first note (this will guarantee that there is some empty space in the audio file before the first note)
  53. self.eventTimeL = [[0,0] for _ in range(len(pulseUsL))] # initialize the event time
  54. self.beginMs = ms
  55. self.playOnlyFl = playOnlyFl
  56. # kpl if not playOnlyFl:
  57. self.audio.record_enable(True) # start recording audio
  58. self.tick(ms) # play the first note
  59. def stop(self, ms):
  60. self._send_note_off() # be sure that all notes are actually turn-off
  61. # kpl if not self.playOnlyFl:
  62. self.audio.record_enable(False) # stop recording audio
  63. self._disable() # disable this sequencer
  64. if not self.playOnlyFl:
  65. self._write() # write the results
  66. def is_enabled(self):
  67. return self.state is not None
  68. def tick(self, ms):
  69. # if next event time has arrived
  70. if self.is_enabled() and ms >= self.next_ms:
  71. # if waiting to turn note on
  72. if self.state == 'note_on':
  73. self._note_on(ms)
  74. # if waiting to turn a note off
  75. elif self.state == 'note_off':
  76. self._note_off(ms)
  77. self._count_silent_notes()
  78. self.pulse_idx += 1
  79. # if all notes have been played
  80. if self.pulse_idx >= len(self.pulseUsL): # or self.silentNoteN >= self.cfg.maxSilentNoteCount:
  81. self.stop(ms)
  82. else:
  83. assert(0)
  84. def _count_silent_notes( self ):
  85. annBegMs = self.eventTimeL[ self.pulse_idx ][0]
  86. annEndMs = self.eventTimeL[ self.pulse_idx ][1]
  87. minDurMs = self.cfg.silentNoteMinDurMs
  88. maxPulseUs = self.cfg.silentNoteMaxPulseUs
  89. resD = rms_analyze_one_rt_note_wrap( self.audio, annBegMs, annEndMs, self.pitch, self.pauseDurMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
  90. print( " %4.1f db %4i ms %i" % (resD['hm']['db'], resD['hm']['durMs'], self.pulse_idx))
  91. if resD is not None and resD['hm']['durMs'] < minDurMs and self.pulseUsL[ self.pulse_idx ] < maxPulseUs:
  92. self.silentNoteN += 1
  93. print("SILENT", self.silentNoteN)
  94. else:
  95. self.silentNoteN = 0
  96. return self.silentNoteN
  97. def _get_duty_cycle( self, pulseUsec ):
  98. return self.holdDutyPctD[ pulseUsec ]
  99. dutyPct = self.holdDutyPctL[0][1]
  100. for refUsec,refDuty in self.holdDutyPctL:
  101. if pulseUsec < refUsec:
  102. break
  103. dutyPct = refDuty
  104. return dutyPct
  105. def _set_duty_cycle( self, pitch, pulseUsec ):
  106. dutyPct = self._get_duty_cycle( pulseUsec )
  107. if dutyPct != self.prevHoldDutyPct:
  108. self.api.set_pwm_duty( pitch, dutyPct )
  109. print("Hold Duty:",dutyPct)
  110. self.prevHoldDutyPct = dutyPct
  111. def _note_on( self, ms ):
  112. self.eventTimeL[ self.pulse_idx ][0] = self.audio.buffer_sample_ms().value
  113. self.next_ms = ms + self.noteDurMs
  114. self.state = 'note_off'
  115. pulse_usec = int(self.pulseUsL[ self.pulse_idx ])
  116. self._set_duty_cycle( self.pitch, pulse_usec )
  117. self.api.note_on_us( self.pitch, pulse_usec )
  118. print("note-on:",self.pitch, self.pulse_idx, pulse_usec)
  119. def _note_off( self, ms ):
  120. self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value
  121. self.next_ms = ms + self.pauseDurMs
  122. self.state = 'note_on'
  123. if self.playOnlyFl:
  124. begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
  125. endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
  126. self.rtAnalyzer.analyze_note( self.audio, self.pitch, begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
  127. self._send_note_off()
  128. def _send_note_off( self ):
  129. self.api.note_off( self.pitch )
  130. #print("note-off:",self.pitch,self.pulse_idx)
  131. def _disable(self):
  132. self.state = None
  133. def _write( self ):
  134. d = {
  135. "pulseUsL":self.pulseUsL,
  136. "pitch":self.pitch,
  137. "noteDurMs":self.noteDurMs,
  138. "pauseDurMs":self.pauseDurMs,
  139. "holdDutyPctL":self.holdDutyPctL,
  140. "eventTimeL":self.eventTimeL,
  141. "beginMs":self.beginMs
  142. }
  143. print("Writing: ", self.outDir )
  144. outDir = os.path.expanduser(self.outDir)
  145. if not os.path.isdir(outDir):
  146. os.mkdir(outDir)
  147. with open(os.path.join( outDir, "seq.json" ),"w") as f:
  148. f.write(json.dumps( d ))
  149. self.audio.write_buffer( os.path.join( outDir, "audio.wav" ) )
  150. class CalibrateKeys:
  151. def __init__(self, cfg, audioDev, api):
  152. self.cfg = cfg
  153. self.seq = AttackPulseSeq( cfg, audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs )
  154. self.pulseUsL = None
  155. self.pitchL = None
  156. self.pitch_idx = -1
  157. def start( self, ms, pitchL, pulseUsL, playOnlyFl=False ):
  158. if len(pitchL) > 0:
  159. self.pulseUsL = pulseUsL
  160. self.pitchL = pitchL
  161. self.pitch_idx = -1
  162. self._start_next_note( ms, playOnlyFl )
  163. def stop( self, ms ):
  164. self.pitch_idx = -1
  165. self.seq.stop(ms)
  166. def is_enabled( self ):
  167. return self.pitch_idx >= 0
  168. def tick( self, ms ):
  169. if self.is_enabled():
  170. self.seq.tick(ms)
  171. # if the sequencer is done playing
  172. if not self.seq.is_enabled():
  173. self._start_next_note( ms, self.seq.playOnlyFl ) # ... else start the next sequence
  174. return None
  175. def _start_next_note( self, ms, playOnlyFl ):
  176. self.pitch_idx += 1
  177. # if the last note in pitchL has been played ...
  178. if self.pitch_idx >= len(self.pitchL):
  179. self.stop(ms) # ... then we are done
  180. else:
  181. pitch = self.pitchL[ self.pitch_idx ]
  182. # be sure that the base directory exists
  183. baseDir = os.path.expanduser( cfg.outDir )
  184. if not os.path.isdir( baseDir ):
  185. os.mkdir( baseDir )
  186. outDir = os.path.join(baseDir, str(pitch) )
  187. if not os.path.isdir(outDir):
  188. os.mkdir(outDir)
  189. # get the next available output directory id
  190. outDir_id = self._calc_next_out_dir_id( outDir )
  191. print(outDir_id,outDir)
  192. # if this is not the first time this note has been sampled then get the resample locations
  193. if (outDir_id == 0) or self.cfg.useFullPulseListFl:
  194. self.pulseUsL = self.cfg.full_pulseL
  195. else:
  196. #self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
  197. self.pulseUsL = get_resample_points_wrap( baseDir, pitch, self.cfg.analysisArgs )
  198. holdDutyPctL = self.cfg.calibrateArgs['holdDutyPctD'][pitch]
  199. if playOnlyFl:
  200. self.pulseUsL,_,holdDutyPctL = form_final_pulse_list( outDir, pitch, self.cfg.analysisArgs, take_id=None )
  201. noteN = cfg.analysisArgs['auditionNoteN']
  202. self.pulseUsL = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ]
  203. else:
  204. outDir = os.path.join( outDir, str(outDir_id) )
  205. if not os.path.isdir(outDir):
  206. os.mkdir(outDir)
  207. #------------------------
  208. j = 0
  209. holdDutyPctD = {}
  210. for us in self.pulseUsL:
  211. if j+1<len(holdDutyPctL) and us >= holdDutyPctL[j+1][0]:
  212. j += 1
  213. holdDutyPctD[ us ] = holdDutyPctL[j][1]
  214. #------------------------
  215. if self.cfg.reversePulseListFl:
  216. self.pulseUsL = [ us for us in reversed(self.pulseUsL) ]
  217. # start the sequencer
  218. self.seq.start( ms, outDir, pitch, self.pulseUsL, holdDutyPctL, holdDutyPctD, playOnlyFl )
  219. def _calc_next_out_dir_id( self, outDir ):
  220. id = 0
  221. while os.path.isdir( os.path.join(outDir,"%i" % id)):
  222. id += 1
  223. return id
  224. # This is the main application API it is running in a child process.
  225. class App:
  226. def __init__(self ):
  227. self.cfg = None
  228. self.audioDev = None
  229. self.api = None
  230. self.cal_keys = None
  231. self.keyboard = None
  232. self.calibrate = None
  233. self.midiFilePlayer = None
  234. def setup( self, cfg ):
  235. self.cfg = cfg
  236. self.audioDev = AudioDevice()
  237. self.midiDev = MidiDevice()
  238. res = None
  239. #
  240. # TODO: unify the result error handling
  241. # (the API and the audio device return two diferent 'Result' types
  242. #
  243. if hasattr(cfg,'audio'):
  244. res = self.audioDev.setup(**cfg.audio)
  245. if not res:
  246. self.audio_dev_list(0)
  247. else:
  248. self.audioDev = None
  249. if True:
  250. if hasattr(cfg,'midi'):
  251. res = self.midiDev.setup(**cfg.midi)
  252. if not res:
  253. self.midi_dev_list(0)
  254. else:
  255. self.midiDev = None
  256. self.api = Picadae( key_mapL=cfg.key_mapL)
  257. # wait for the letter 'a' to come back from the serial port
  258. api_res = self.api.wait_for_serial_sync(timeoutMs=cfg.serial_sync_timeout_ms)
  259. # did the serial port sync fail?
  260. if not api_res:
  261. res.set_error("Serial port sync failed.")
  262. else:
  263. print("Serial port sync'ed")
  264. self.cal_keys = CalibrateKeys( cfg, self.audioDev, self.api )
  265. self.keyboard = Keyboard( cfg, self.audioDev, self.api )
  266. self.calibrate = None #Calibrate( cfg.calibrateArgs, self.audioDev, self.midiDev, self.api )
  267. self.midiFilePlayer = MidiFilePlayer( cfg, self.api, self.midiDev, cfg.midiFileFn )
  268. return res
  269. def tick( self, ms ):
  270. if self.audioDev is not None:
  271. self.audioDev.tick(ms)
  272. if self.cal_keys:
  273. self.cal_keys.tick(ms)
  274. if self.keyboard:
  275. self.keyboard.tick(ms)
  276. if self.calibrate:
  277. self.calibrate.tick(ms)
  278. if self.midiFilePlayer:
  279. self.midiFilePlayer.tick(ms)
  280. def audio_dev_list( self, ms ):
  281. portL = self.audioDev.get_port_list( True )
  282. for port in portL:
  283. print("chs:%4i label:%s" % (port['chN'],port['label']))
  284. def midi_dev_list( self, ms ):
  285. d = self.midiDev.get_port_list( True )
  286. for port in d['listL']:
  287. print("IN:",port)
  288. d = self.midiDev.get_port_list( False )
  289. for port in d['listL']:
  290. print("OUT:",port)
  291. def calibrate_keys_start( self, ms, pitchRangeL ):
  292. pitchL = [ pitch for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
  293. self.cal_keys.start( ms, pitchL, cfg.full_pulseL )
  294. def play_keys_start( self, ms, pitchRangeL ):
  295. chordL = [ [pitch] for pitch in range(pitchRangeL[0], pitchRangeL[1]+1)]
  296. self.cal_keys.start( ms, chordL, cfg.full_pulseL, playOnlyFl=True )
  297. def keyboard_start_pulse_idx( self, ms, argL ):
  298. pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
  299. self.keyboard.start( ms, pitchL, argL[2], None )
  300. def keyboard_repeat_pulse_idx( self, ms, argL ):
  301. self.keyboard.repeat( ms, argL[0], None )
  302. def keyboard_start_target_db( self, ms, argL ):
  303. pitchL = [ pitch for pitch in range(argL[0], argL[1]+1)]
  304. self.keyboard.start( ms, pitchL, None, argL[2] )
  305. def keyboard_repeat_target_db( self, ms, argL ):
  306. self.keyboard.repeat( ms, None, argL[0] )
  307. def calibrate_start( self, ms, argL ):
  308. self.calibrate.start(ms)
  309. def calibrate_play( self, ms, argL ):
  310. self.calibrate.play(ms)
  311. def calibrate_keys_stop( self, ms ):
  312. self.cal_keys.stop(ms)
  313. self.keyboard.stop(ms)
  314. self.calibrate.stop(ms)
  315. def midi_file_player_start( self, ms ):
  316. self.midiFilePlayer.start(ms)
  317. def midi_file_player_stop( self, ms ):
  318. self.midiFilePlayer.stop(ms)
  319. def pedal_down( self, ms ):
  320. print("pedal_down")
  321. self.midiDev.send_controller(64, 100 )
  322. def pedal_up( self, ms ):
  323. print("pedal_up");
  324. self.midiDev.send_controller(64, 0 )
  325. def quit( self, ms ):
  326. if self.api:
  327. self.api.close()
  328. def _send_error( pipe, res ):
  329. if res is None:
  330. return
  331. if res.msg:
  332. pipe.send( [{"type":"error", 'value':res.msg}] )
  333. def _send_error_msg( pipe, msg ):
  334. _send_error( pipe, Result(None,msg))
  335. def _send_quit( pipe ):
  336. pipe.send( [{ 'type':'quit' }] )
  337. # This is the application engine async. process loop
  338. def app_event_loop_func( pipe, cfg ):
  339. multiprocessing.get_logger().info("App Proc Started.")
  340. # create the asynchronous application object
  341. app = App()
  342. res = app.setup(cfg)
  343. # if the app did not initialize successfully
  344. if not res:
  345. _send_error( pipe, res )
  346. _send_quit(pipe)
  347. return
  348. dt0 = datetime.now()
  349. ms = 0
  350. while True:
  351. # have any message arrived from the parent process?
  352. if pipe.poll():
  353. msg = None
  354. try:
  355. msg = pipe.recv()
  356. except EOFError:
  357. return
  358. if not hasattr(app,msg.type):
  359. _send_error_msg( pipe, "Unknown message type:'%s'." % (msg.type) )
  360. else:
  361. # get the command handler function in 'app'
  362. func = getattr(app,msg.type)
  363. ms = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
  364. # call the command handler
  365. if msg.value:
  366. res = func( ms, msg.value )
  367. else:
  368. res = func( ms )
  369. # handle any errors returned from the commands
  370. _send_error( pipe, res )
  371. # if a 'quit' msg was recived then break out of the loop
  372. if msg.type == 'quit':
  373. _send_quit(pipe)
  374. break
  375. # give some time to the system
  376. time.sleep(0.05)
  377. # calc the tick() time stamp
  378. ms = int(round( (datetime.now() - dt0).total_seconds() * 1000.0) )
  379. # tick the app
  380. app.tick( ms )
  381. class AppProcess(Process):
  382. def __init__(self,cfg):
  383. self.parent_end, child_end = Pipe()
  384. super(AppProcess, self).__init__(target=app_event_loop_func,name="AppProcess",args=(child_end,cfg))
  385. self.doneFl = False
  386. def send(self, d):
  387. # This function is called by the parent process to send an arbitrary msg to the App process
  388. self.parent_end.send( types.SimpleNamespace(**d) )
  389. return None
  390. def recv(self):
  391. # This function is called by the parent process to receive lists of child messages.
  392. msgL = None
  393. if not self.doneFl and self.parent_end.poll():
  394. msgL = self.parent_end.recv()
  395. for msg in msgL:
  396. if msg['type'] == 'quit':
  397. self.doneFl = True
  398. return msgL
  399. def isdone(self):
  400. return self.doneFl
  401. class Shell:
  402. def __init__( self, cfg ):
  403. self.appProc = None
  404. self.parseD = {
  405. 'q':{ "func":'quit', "minN":0, "maxN":0, "help":"quit"},
  406. '?':{ "func":"_help", "minN":0, "maxN":0, "help":"Print usage text."},
  407. 'a':{ "func":"audio_dev_list", "minN":0, "maxN":0, "help":"List the audio devices."},
  408. 'm':{ "func":"midi_dev_list", "minN":0, "maxN":0, "help":"List the MIDI devices."},
  409. 'c':{ "func":"calibrate_keys_start", "minN":1, "maxN":2, "help":"Calibrate a range of keys. "},
  410. 'd':{ "func":"calibrate_start", "minN":1, "maxN":1, "help":"Calibrate based on fixed db levels. "},
  411. 'D':{ "func":"calibrate_play", "minN":1, "maxN":1, "help":"Play back last calibration."},
  412. 's':{ "func":"calibrate_keys_stop", "minN":0, "maxN":0, "help":"Stop key calibration"},
  413. 'p':{ "func":"play_keys_start", "minN":1, "maxN":2, "help":"Play current calibration"},
  414. 'k':{ "func":"keyboard_start_pulse_idx", "minN":3, "maxN":3, "help":"Play pulse index across keyboard"},
  415. 'r':{ "func":"keyboard_repeat_pulse_idx", "minN":1, "maxN":1, "help":"Repeat pulse index across keyboard with new pulse_idx"},
  416. 'K':{ "func":"keyboard_start_target_db", "minN":3, "maxN":3, "help":"Play db across keyboard"},
  417. 'R':{ "func":"keyboard_repeat_target_db", "minN":1, "maxN":1, "help":"Repeat db across keyboard with new pulse_idx"},
  418. 'F':{ "func":"midi_file_player_start", "minN":0, "maxN":0, "help":"Play the MIDI file."},
  419. 'f':{ "func":"midi_file_player_stop", "minN":0, "maxN":0, "help":"Stop the MIDI file."},
  420. 'P':{ "func":"pedal_down", "minN":0, "maxN":0, "help":"Pedal down."},
  421. 'U':{ "func":"pedal_up", "minN":0, "maxN":0, "help":"Pedal up."},
  422. }
  423. def _help( self, _=None ):
  424. for k,d in self.parseD.items():
  425. s = "{} = {}".format( k, d['help'] )
  426. print(s)
  427. return None
  428. def _syntaxError( self, msg ):
  429. return Result(None,"Syntax Error: " + msg )
  430. def _exec_cmd( self, tokL ):
  431. if len(tokL) <= 0:
  432. return None
  433. opcode = tokL[0]
  434. if opcode not in self.parseD:
  435. return self._syntaxError("Unknown opcode: '{}'.".format(opcode))
  436. d = self.parseD[ opcode ]
  437. func_name = d['func']
  438. func = None
  439. # find the function associated with this command
  440. if hasattr(self, func_name ):
  441. func = getattr(self, func_name )
  442. try:
  443. # convert the parameter list into integers
  444. argL = [ int(tokL[i]) for i in range(1,len(tokL)) ]
  445. except:
  446. return self._syntaxError("Unable to create integer arguments.")
  447. # validate the count of command args
  448. if d['minN'] != -1 and (d['minN'] > len(argL) or len(argL) > d['maxN']):
  449. return self._syntaxError("Argument count mismatch. {} is out of range:{} to {}".format(len(argL),d['minN'],d['maxN']))
  450. # call the command function
  451. if func:
  452. result = func(*argL)
  453. else:
  454. result = self.appProc.send( { 'type':func_name, 'value':argL } )
  455. return result
  456. def run( self ):
  457. # create the API object
  458. self.appProc = AppProcess(cfg)
  459. self.appProc.start()
  460. print("'q'=quit '?'=help")
  461. time_out_secs = 1
  462. # this is the shell main loop
  463. while True:
  464. # wait for keyboard activity
  465. i, o, e = select.select( [sys.stdin], [], [], time_out_secs )
  466. if i:
  467. # read the command
  468. s = sys.stdin.readline().strip()
  469. # tokenize the command
  470. tokL = s.split(' ')
  471. # execute the command
  472. result = self._exec_cmd( tokL )
  473. # if this is the 'quit' command
  474. if tokL[0] == 'q':
  475. break
  476. # check for msg's from the async application process
  477. if self._handle_app_msgs( self.appProc.recv() ):
  478. break
  479. # wait for the appProc to complete
  480. while not self.appProc.isdone():
  481. self.appProc.recv() # drain the AppProc() as it shutdown
  482. time.sleep(0.1)
  483. def _handle_app_msgs( self, msgL ):
  484. quitAppFl = False
  485. if msgL:
  486. for msg in msgL:
  487. if msg:
  488. if msg['type'] == 'error':
  489. print("Error: {}".format(msg['value']))
  490. elif msg['type'] == 'quit':
  491. quitAppFl = True
  492. else:
  493. print(msg)
  494. return quitAppFl
  495. def parse_args():
  496. """Parse the command line arguments."""
  497. descStr = """Picadae auto-calibrate."""
  498. logL = ['debug','info','warning','error','critical']
  499. ap = argparse.ArgumentParser(description=descStr)
  500. ap.add_argument("-c","--config", default="p_ac.yml", help="YAML configuration file.")
  501. ap.add_argument("-l","--log_level",choices=logL, default="warning", help="Set logging level: debug,info,warning,error,critical. Default:warning")
  502. return ap.parse_args()
  503. if __name__ == "__main__":
  504. logging.basicConfig()
  505. #mplog = multiprocessing.log_to_stderr()
  506. #mplog.setLevel(logging.INFO)
  507. args = parse_args()
  508. cfg = parse_yaml_cfg(args.config)
  509. shell = Shell(cfg)
  510. shell.run()