Picadae hardware and control code
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

picadae_cmd.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. ##| Copyright: (C) 2018-2020 Kevin Larke <contact AT larke DOT org>
  2. ##| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file.
  3. import os,sys,argparse,yaml,types,select,serial,logging,time
  4. from multiprocessing import Process, Pipe
  5. # Message header id's for messages passed between the application
  6. # process and the microcontroller and video processes
  7. QUIT_MSG = 0xffff
  8. DATA_MSG = 0xfffe
  9. ERROR_MSG = 0xfffd
  10. def _reset_port(port):
  11. port.reset_input_buffer()
  12. port.reset_output_buffer()
  13. port.send_break()
  14. #self.reportResetFl = True
  15. #self.reportStatusFl = True
  16. def _serial_process_func( serial_dev, baud, sensor_count, pipe ):
  17. reset_N = 0
  18. drop_N = 0
  19. noSync_N = 0
  20. with serial.Serial(serial_dev, baud) as port:
  21. while True:
  22. # get the count of available bytes in the serial port buffer
  23. bytes_waiting_N = port.in_waiting
  24. # if no serial port bytes are available then sleep ....
  25. if bytes_waiting_N == 0:
  26. time.sleep(0.01) # ... for 10 ms
  27. else: # read the serial port ...
  28. v = port.read(bytes_waiting_N)
  29. pipe.send((DATA_MSG,v)) # ... and send it to the parent
  30. msg = None
  31. if pipe.poll(): # non-blocking check for parent process messages
  32. try:
  33. msg = pipe.recv()
  34. except EOFError:
  35. break
  36. # if an incoming message was received
  37. if msg != None:
  38. # this is a shutdown msg
  39. if msg[0] == QUIT_MSG:
  40. pipe.send(msg) # ... send quit msg back
  41. break
  42. # this is a data xmit msg
  43. elif msg[0] == DATA_MSG:
  44. port.write(msg[1])
  45. class SessionProcess(Process):
  46. def __init__(self,target,name,args=()):
  47. self.parent_end, child_end = Pipe()
  48. super(SessionProcess, self).__init__(target=target,name=name,args=args + (child_end,))
  49. self.doneFl = False
  50. def quit(self):
  51. # send quit msg to the child process
  52. self.parent_end.send((QUIT_MSG,0))
  53. def send(self,msg_id,value):
  54. # send a msg to the child process
  55. self.parent_end.send((msg_id,value))
  56. def recv(self):
  57. x = None
  58. if not self.doneFl and self.parent_end.poll():
  59. x = self.parent_end.recv()
  60. if x[0] == QUIT_MSG:
  61. self.doneFl = True
  62. return x
  63. def isDone(self):
  64. return self.doneFl
  65. class SerialProcess(SessionProcess):
  66. def __init__(self,serial_device,baud,foo):
  67. super(SerialProcess, self).__init__(_serial_process_func,"Serial",args=(serial_device,baud,foo))
  68. class App:
  69. def __init__( self, cfg ):
  70. self.cfg = cfg
  71. self.serialProc = SerialProcess(cfg.serial_dev,cfg.serial_baud,0)
  72. def _update( self, quittingFl=False ):
  73. if self.serialProc.isDone():
  74. return False
  75. while True:
  76. msg = serialProc.recv()
  77. # no serial msg's were received
  78. if msg is None:
  79. break
  80. if msg[0] == DATA_MSG:
  81. print("in:",msg[1])
  82. def _parse_error( self, msg, cmd_str=None ):
  83. if cmd_str:
  84. msg += " Command:{}".format(cmd_str)
  85. return (None,msg)
  86. def _parse_int( self, token, var_label, min_value, max_value ):
  87. # convert the i2c destination address to an integer
  88. try:
  89. int_value = int(token)
  90. except ValueError:
  91. return self._parse_error("Synax error: '{}' is not a legal integer.".format(token))
  92. # validate the i2c address value
  93. if min_value > int_value or int_value > max_value:
  94. return self._parse_error("Syntax error: '{}' {} out of range 0 to {}.".format(token,int_value,max_value))
  95. return (int_value,None)
  96. def parse_app_cmd( self, cmd_str ):
  97. """
  98. Command syntax <opcode> <remote_i2c_addr> <value>
  99. """
  100. op_tok_idx = 0
  101. i2c_tok_idx = 1
  102. val_tok_idx = 2
  103. cmdD = {
  104. 'p':{ 'reg':0, 'n':1, 'min':0, 'max':4 }, # timer pre-scalar: sets timer tick rate
  105. 't':{ 'reg':1, 'n':2, 'min':0, 'max':10e7 }, # microseconds
  106. 'd':{ 'reg':3, 'n':1, 'min':0, 'max':100 }, # pwm duty cylce (0-100%)
  107. 'f':{ 'reg':4, 'n':1, 'min':1, 'max':5 }, # pwm frequency divider 1=1,2=8,3=64,4=256,5=1024
  108. }
  109. cmd_str = cmd_str.strip()
  110. tokenL = cmd_str.split(' ')
  111. # validate the counf of tokens
  112. if len(tokenL) != 3:
  113. return self._parse_error("Syntax error: Invalid token count.",cmd_str)
  114. opcode = tokenL[op_tok_idx]
  115. # validate the opcode
  116. if opcode not in cmdD:
  117. return self._parse_error("Syntax error: Invalid opcode.",cmd_str)
  118. # convert the i2c destination address to an integer
  119. i2c_addr, msg = self._parse_int( tokenL[i2c_tok_idx], "i2c address", 0,127 )
  120. if i2c_addr is None:
  121. return (None,msg)
  122. d = cmdD[ opcode ]
  123. # get the value
  124. value, msg = self._parse_int( tokenL[val_tok_idx], "command value", d['min'], d['max'] )
  125. if value is None:
  126. return (value,msg)
  127. dataL = [ value ]
  128. if opcode == 't':
  129. coarse = int(value/(32*254))
  130. fine = int((value - coarse*32*254)/32)
  131. print(coarse,fine)
  132. dataL = [ coarse, fine ]
  133. elif opcode == 'd':
  134. dataL = [ int(value * 255 / 100.0) ]
  135. cmd_bV = bytearray( [ ord('w'), i2c_addr, d['reg'], len(dataL) ] + dataL )
  136. if False:
  137. print('cmd_bV:')
  138. for x in cmd_bV:
  139. print(int(x))
  140. return (cmd_bV,None)
  141. def parse_cmd( self, cmd_str ):
  142. op_tok_idx = 0
  143. i2c_tok_idx = 1
  144. reg_tok_idx = 2
  145. rdn_tok_idx = 3
  146. cmd_str = cmd_str.strip()
  147. # if this is a high level command
  148. if cmd_str[0] not in ['r','w']:
  149. return self.parse_app_cmd( cmd_str )
  150. # convert the command string to tokens
  151. tokenL = cmd_str.split(' ')
  152. # no commands require fewer than 4 tokens
  153. if len(tokenL) < 4:
  154. return self._parse_error("Syntax error: Missing tokens.")
  155. # get the command opcode
  156. op_code = tokenL[ op_tok_idx ]
  157. # validate the opcode
  158. if op_code not in [ 'r','w']:
  159. return self._parse_error("Unrecognized opcode: {}".format( op_code ))
  160. # validate the token count given the opcode
  161. if op_code == 'r' and len(tokenL) != 4:
  162. return self._parse_error("Syntax error: Illegal read syntax.")
  163. if op_code == 'w' and len(tokenL) < 4:
  164. return self._parse_error("Syntax error: Illegal write command too short.")
  165. # convert the i2c destination address to an integer
  166. i2c_addr, msg = self._parse_int( tokenL[i2c_tok_idx], "i2c address", 0,127 )
  167. if i2c_addr is None:
  168. return (None,msg)
  169. reg_addr, msg = self._parse_int( tokenL[reg_tok_idx], "reg address", 0, 255 )
  170. if reg_addr is None:
  171. return (None,msg)
  172. dataL = []
  173. # parse and validate the count of bytes to read
  174. if op_code == 'r':
  175. op_byteN, msg = self._parse_int( tokenL[ rdn_tok_idx ], "read byte count", 0, 255 )
  176. if op_byteN is None:
  177. return (None,msg)
  178. # parse and validate the values to write
  179. elif op_code == 'w':
  180. for j,i in enumerate(range(reg_tok_idx+1,len(tokenL))):
  181. value, msg = self._parse_int( tokenL[i], "write value: %i" % (j), 0, 255 )
  182. if value is None:
  183. return (None,msg)
  184. dataL.append(value)
  185. op_byteN = len(dataL)
  186. # form the command into a byte array
  187. cmd_bV = bytearray( [ ord(op_code), i2c_addr, reg_addr, op_byteN ] + dataL )
  188. # s = ""
  189. # for i in range(len(cmd_bV)):
  190. # s += "%i " % (cmd_bV[i])
  191. # print(s)
  192. return (cmd_bV,None)
  193. def run( self ):
  194. self.serialProc.start()
  195. print("'quit' to exit")
  196. time_out_secs = 1
  197. while True:
  198. i, o, e = select.select( [sys.stdin], [], [], time_out_secs )
  199. if (i):
  200. s = sys.stdin.readline().strip()
  201. if s == 'quit' or s == 'q':
  202. break
  203. cmd_bV,err_msg = self.parse_cmd(s)
  204. if cmd_bV is None:
  205. print(err_msg)
  206. else:
  207. self.serialProc.send( DATA_MSG, cmd_bV )
  208. else:
  209. # wait timed out
  210. msg = self.serialProc.recv()
  211. # if a serial msg was received
  212. if msg is not None and msg[0] == DATA_MSG:
  213. str = ""
  214. for i in range(len(msg[1])):
  215. str += "{} ".format(int(msg[1][i]))
  216. print("ser:",str)
  217. self.serialProc.quit()
  218. def parse_args():
  219. """Parse the command line arguments."""
  220. descStr = """Picadae auto-calibrate."""
  221. logL = ['debug','info','warning','error','critical']
  222. ap = argparse.ArgumentParser(description=descStr)
  223. ap.add_argument("-s","--setup", default="picadae_cmd.yml", help="YAML configuration file.")
  224. ap.add_argument("-c","--cmd", nargs="*", help="Give a command as multiple tokens")
  225. ap.add_argument("-r","--run", help="Run a named command list from the setup file.")
  226. ap.add_argument("-l","--log_level",choices=logL, default="warning", help="Set logging level: debug,info,warning,error,critical. Default:warning")
  227. return ap.parse_args()
  228. def parse_yaml_cfg( fn ):
  229. """Parse the YAML configuration file."""
  230. cfg = None
  231. with open(fn,"r") as f:
  232. cfgD = yaml.load(f, Loader=yaml.FullLoader)
  233. cfg = types.SimpleNamespace(**cfgD['picadae_cmd'])
  234. return cfg
  235. if __name__ == "__main__":
  236. args = parse_args()
  237. cfg = parse_yaml_cfg( args.setup )
  238. app = App(cfg)
  239. app.run()