picadae calibration programs
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

p_ac.py 24KB

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