picadae calibration programs
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

p_ac.py 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  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. print("Hold Delay from end of attack:",cfg.defaultHoldDelayUsecs)
  59. self.api.set_hold_delay(pitch,cfg.defaultHoldDelayUsecs)
  60. self.tick(ms) # play the first note
  61. def stop(self, ms):
  62. self._send_note_off() # be sure that all notes are actually turn-off
  63. # kpl if not self.playOnlyFl:
  64. self.audio.record_enable(False) # stop recording audio
  65. self._disable() # disable this sequencer
  66. if not self.playOnlyFl:
  67. self._write() # write the results
  68. def is_enabled(self):
  69. return self.state is not None
  70. def tick(self, ms):
  71. # if next event time has arrived
  72. if self.is_enabled() and ms >= self.next_ms:
  73. # if waiting to turn note on
  74. if self.state == 'note_on':
  75. self._note_on(ms)
  76. # if waiting to turn a note off
  77. elif self.state == 'note_off':
  78. self._note_off(ms)
  79. self._count_silent_notes()
  80. self.pulse_idx += 1
  81. # if all notes have been played
  82. if self.pulse_idx >= len(self.pulseUsL): # or self.silentNoteN >= self.cfg.maxSilentNoteCount:
  83. self.stop(ms)
  84. else:
  85. assert(0)
  86. def _count_silent_notes( self ):
  87. annBegMs = self.eventTimeL[ self.pulse_idx ][0]
  88. annEndMs = self.eventTimeL[ self.pulse_idx ][1]
  89. minDurMs = self.cfg.silentNoteMinDurMs
  90. maxPulseUs = self.cfg.silentNoteMaxPulseUs
  91. resD = rms_analyze_one_rt_note_wrap( self.audio, annBegMs, annEndMs, self.pitch, self.pauseDurMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
  92. print( " %4.1f db %4i ms %i" % (resD['hm']['db'], resD['hm']['durMs'], self.pulse_idx))
  93. if resD is not None and resD['hm']['durMs'] < minDurMs and self.pulseUsL[ self.pulse_idx ] < maxPulseUs:
  94. self.silentNoteN += 1
  95. print("SILENT", self.silentNoteN)
  96. else:
  97. self.silentNoteN = 0
  98. return self.silentNoteN
  99. def _get_duty_cycle( self, pulseUsec ):
  100. return self.holdDutyPctD[ pulseUsec ]
  101. def _set_duty_cycle( self, pitch, pulseUsec ):
  102. dutyPct = self._get_duty_cycle( pulseUsec )
  103. if dutyPct != self.prevHoldDutyPct:
  104. self.api.set_pwm_duty( pitch, dutyPct )
  105. print("Hold Duty:",dutyPct)
  106. self.prevHoldDutyPct = dutyPct
  107. def _note_on( self, ms ):
  108. self.eventTimeL[ self.pulse_idx ][0] = self.audio.buffer_sample_ms().value
  109. self.next_ms = ms + self.noteDurMs
  110. self.state = 'note_off'
  111. pulse_usec = int(self.pulseUsL[ self.pulse_idx ])
  112. self._set_duty_cycle( self.pitch, pulse_usec )
  113. self.api.note_on_us( self.pitch, pulse_usec )
  114. print("note-on:",self.pitch, self.pulse_idx, pulse_usec)
  115. def _note_off( self, ms ):
  116. self.eventTimeL[ self.pulse_idx ][1] = self.audio.buffer_sample_ms().value
  117. self.next_ms = ms + self.pauseDurMs
  118. self.state = 'note_on'
  119. if self.playOnlyFl:
  120. begTimeMs = self.eventTimeL[ self.pulse_idx ][0]
  121. endTimeMs = self.eventTimeL[ self.pulse_idx ][1]
  122. self.rtAnalyzer.analyze_note( self.audio, self.pitch, begTimeMs, endTimeMs, self.cfg.analysisArgs['rmsAnalysisArgs'] )
  123. self._send_note_off()
  124. def _send_note_off( self ):
  125. self.api.note_off( self.pitch )
  126. #print("note-off:",self.pitch,self.pulse_idx)
  127. def _disable(self):
  128. self.state = None
  129. def _write( self ):
  130. d = {
  131. "pulseUsL":self.pulseUsL,
  132. "pitch":self.pitch,
  133. "noteDurMs":self.noteDurMs,
  134. "pauseDurMs":self.pauseDurMs,
  135. "holdDutyPctL":self.holdDutyPctL,
  136. "eventTimeL":self.eventTimeL,
  137. "beginMs":self.beginMs
  138. }
  139. print("Writing: ", self.outDir )
  140. outDir = os.path.expanduser(self.outDir)
  141. if not os.path.isdir(outDir):
  142. os.mkdir(outDir)
  143. with open(os.path.join( outDir, "seq.json" ),"w") as f:
  144. f.write(json.dumps( d ))
  145. self.audio.write_buffer( os.path.join( outDir, "audio.wav" ) )
  146. class CalibrateKeys:
  147. def __init__(self, cfg, audioDev, api):
  148. self.cfg = cfg
  149. self.seq = AttackPulseSeq( cfg, audioDev, api, noteDurMs=cfg.noteDurMs, pauseDurMs=cfg.pauseDurMs )
  150. self.pulseUsL = None
  151. self.pitchL = None
  152. self.pitch_idx = -1
  153. def start( self, ms, pitchL, pulseUsL, playOnlyFl=False ):
  154. if len(pitchL) > 0:
  155. self.pulseUsL = pulseUsL
  156. self.pitchL = pitchL
  157. self.pitch_idx = -1
  158. self._start_next_note( ms, playOnlyFl )
  159. def stop( self, ms ):
  160. self.pitch_idx = -1
  161. self.seq.stop(ms)
  162. def is_enabled( self ):
  163. return self.pitch_idx >= 0
  164. def tick( self, ms ):
  165. if self.is_enabled():
  166. self.seq.tick(ms)
  167. # if the sequencer is done playing
  168. if not self.seq.is_enabled():
  169. self._start_next_note( ms, self.seq.playOnlyFl ) # ... else start the next sequence
  170. return None
  171. def _start_next_note( self, ms, playOnlyFl ):
  172. self.pitch_idx += 1
  173. # if the last note in pitchL has been played ...
  174. if self.pitch_idx >= len(self.pitchL):
  175. self.stop(ms) # ... then we are done
  176. else:
  177. pitch = self.pitchL[ self.pitch_idx ]
  178. # be sure that the base directory exists
  179. baseDir = os.path.expanduser( cfg.outDir )
  180. if not os.path.isdir( baseDir ):
  181. os.mkdir( baseDir )
  182. outDir = os.path.join(baseDir, str(pitch) )
  183. if not os.path.isdir(outDir):
  184. os.mkdir(outDir)
  185. # get the next available output directory id
  186. outDir_id = self._calc_next_out_dir_id( outDir )
  187. print(outDir_id,outDir)
  188. # if this is not the first time this note has been sampled then get the resample locations
  189. if (outDir_id == 0) or self.cfg.useFullPulseListFl:
  190. self.pulseUsL = self.cfg.full_pulseL
  191. else:
  192. #self.pulseUsL,_,_ = form_resample_pulse_time_list( outDir, self.cfg.analysisArgs )
  193. self.pulseUsL = get_resample_points_wrap( baseDir, pitch, self.cfg.analysisArgs )
  194. holdDutyPctL = self.cfg.calibrateArgs['holdDutyPctD'][pitch]
  195. if playOnlyFl:
  196. self.pulseUsL,_,holdDutyPctL = form_final_pulse_list( baseDir, pitch, self.cfg.analysisArgs, take_id=None )
  197. noteN = cfg.analysisArgs['auditionNoteN']
  198. self.pulseUsL = [ self.pulseUsL[ int(round(i*126.0/(noteN-1)))] for i in range(noteN) ]
  199. else:
  200. outDir = os.path.join( outDir, str(outDir_id) )
  201. if not os.path.isdir(outDir):
  202. os.mkdir(outDir)
  203. #------------------------
  204. j = 0
  205. holdDutyPctD = {}
  206. for us in self.pulseUsL:
  207. if j+1<len(holdDutyPctL) and us >= holdDutyPctL[j+1][0]:
  208. j += 1
  209. holdDutyPctD[ us ] = holdDutyPctL[j][1]
  210. #------------------------
  211. if self.cfg.reversePulseListFl:
  212. self.pulseUsL = [ us for us in reversed(self.pulseUsL) ]
  213. # start the sequencer
  214. self.seq.start( ms, outDir, pitch, self.pulseUsL, holdDutyPctL, holdDutyPctD, playOnlyFl )
  215. def _calc_next_out_dir_id( self, outDir ):
  216. id = 0
  217. while os.path.isdir( os.path.join(outDir,"%i" % id)):
  218. id += 1
  219. return id
  220. # This is the main application API it is running in a child process.
  221. class App:
  222. def __init__(self ):
  223. self.cfg = None
  224. self.audioDev = None
  225. self.api = None
  226. self.cal_keys = None
  227. self.keyboard = None
  228. self.calibrate = None
  229. self.midiFilePlayer = None
  230. def setup( self, cfg ):
  231. self.cfg = cfg
  232. self.audioDev = AudioDevice()
  233. self.midiDev = MidiDevice()
  234. res = None
  235. #
  236. # TODO: unify the result error handling
  237. # (the API and the audio device return two diferent 'Result' types
  238. #
  239. if hasattr(cfg,'audio'):
  240. res = self.audioDev.setup(**cfg.audio)
  241. if not res:
  242. self.audio_dev_list(0)
  243. else:
  244. print("The cfg file has no audio stanza. No audio device will be initialized.")
  245. self.audioDev = None
  246. if True:
  247. if hasattr(cfg,'midi'):
  248. res = self.midiDev.setup(**cfg.midi)
  249. if not res:
  250. self.midi_dev_list(0)
  251. else:
  252. self.midiDev = None
  253. self.api = Picadae( key_mapL=cfg.key_mapL)
  254. # wait for the letter 'a' to come back from the serial port
  255. api_res = self.api.wait_for_serial_sync(timeoutMs=cfg.serial_sync_timeout_ms)
  256. # did the serial port sync fail?
  257. if not api_res:
  258. res.set_error("Serial port sync failed.")
  259. else:
  260. print("Serial port sync'ed")
  261. self.cal_keys = CalibrateKeys( cfg, self.audioDev, self.api )
  262. self.keyboard = Keyboard( cfg, self.audioDev, self.api )
  263. self.calibrate = None #Calibrate( cfg.calibrateArgs, self.audioDev, self.midiDev, self.api )
  264. self.midiFilePlayer = MidiFilePlayer( cfg, self.api, self.midiDev, cfg.midiFileFn )
  265. return res
  266. def tick( self, ms ):
  267. if self.audioDev is not None:
  268. self.audioDev.tick(ms)
  269. if self.cal_keys:
  270. self.cal_keys.tick(ms)
  271. if self.keyboard:
  272. self.keyboard.tick(ms)
  273. if self.calibrate:
  274. self.calibrate.tick(ms)
  275. if self.midiFilePlayer:
  276. self.midiFilePlayer.tick(ms)
  277. def audio_dev_list( self, ms ):
  278. if self.audioDev is not None:
  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. def create_logger():
  502. import multiprocessing, logging
  503. logger = multiprocessing.get_logger()
  504. logger.setLevel(logging.INFO)
  505. formatter = logging.Formatter(\
  506. '[%(asctime)s| %(levelname)s| %(processName)s] %(message)s')
  507. handler = logging.FileHandler('/home/kevin/temp/p_ac_log.txt')
  508. handler.setFormatter(formatter)
  509. # this bit will make sure you won't have
  510. # duplicated messages in the output
  511. if not len(logger.handlers):
  512. logger.addHandler(handler)
  513. return logger
  514. if __name__ == "__main__":
  515. from multiprocessing import Pool
  516. logger = create_logger()
  517. logger.info('Starting pooling')
  518. #p = Pool()
  519. #logging.basicConfig()
  520. #mplog = multiprocessing.log_to_stderr()
  521. #mplog.setLevel(logging.INFO)
  522. args = parse_args()
  523. cfg = parse_yaml_cfg(args.config)
  524. shell = Shell(cfg)
  525. shell.run()