Picadae hardware and control code
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.

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()