xytronic_simulator.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. #!/usr/bin/env python3
  2. """
  3. # Xytronic LF-1600
  4. # Open Source firmware
  5. # Simulator
  6. #
  7. # Copyright (c) 2018 Michael Buesch <m@bues.ch>
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License as published by
  11. # the Free Software Foundation; either version 2 of the License, or
  12. # (at your option) any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. """
  23. import pyxytronic
  24. import time
  25. import sys
  26. class Simulator(object):
  27. CURR_DIV = 10.0
  28. ADC_TEMP = 1
  29. ADC_CURRENT = 2
  30. PWM_CURRENT = 1
  31. FIXPT_SIZE = 24
  32. FIXPT_SHIFT = 6
  33. SETTINGS = {
  34. "temp_k[0].kp" : "fixpt_t",
  35. "temp_k[0].ki" : "fixpt_t",
  36. "temp_k[0].kd" : "fixpt_t",
  37. "temp_k[0].d_decay_div" : "fixpt_t",
  38. "temp_k[1].kp" : "fixpt_t",
  39. "temp_k[1].ki" : "fixpt_t",
  40. "temp_k[1].kd" : "fixpt_t",
  41. "temp_k[1].d_decay_div" : "fixpt_t",
  42. "temp_k[2].kp" : "fixpt_t",
  43. "temp_k[2].ki" : "fixpt_t",
  44. "temp_k[2].kd" : "fixpt_t",
  45. "temp_k[2].d_decay_div" : "fixpt_t",
  46. "temp_idle_setpoint" : "fixpt_t",
  47. "temp_setpoint" : "fixpt_t",
  48. "temp_setpoint_active" : "uint8_t",
  49. "temp_adj" : "fixpt_t",
  50. "serial" : "uint8_t",
  51. }
  52. def __init__(self):
  53. self.xy = pyxytronic
  54. self.__uartbuf = bytearray()
  55. self.__reset_debuginterface()
  56. self.__runHook = []
  57. self.stats_ena()
  58. def addRunHook(self, hook):
  59. self.__runHook.append(hook)
  60. def run(self):
  61. self.xy.simulator_mainloop_once()
  62. self.__handle_debuginterface()
  63. for hook in self.__runHook:
  64. hook();
  65. def stats_ena(self, mainloop_stats_ena=True):
  66. self.xy.simulator_stats_ena(mainloop_stats_ena)
  67. def setting_get(self, name):
  68. value = self.xy.simulator_setting_read(name)
  69. if self.SETTINGS[name] == "fixpt_t":
  70. value = self.__fixptToFloat(value)
  71. return value
  72. def setting_set(self, name, value):
  73. if self.SETTINGS[name] == "fixpt_t":
  74. value = self.__floatToFixpt(value)
  75. self.xy.simulator_setting_write(name, value)
  76. def pwm_current_get(self):
  77. value, maxValue = self.xy.simulator_pwm_get(self.PWM_CURRENT)
  78. amps = self.__scale(raw=value,
  79. raw_lo=maxValue,
  80. raw_hi=0,
  81. phys_lo=0.0,
  82. phys_hi=5.0)
  83. return amps
  84. def adc_temp_set(self, degree):
  85. value = self.__unscale(phys=degree,
  86. phys_lo=150.0,
  87. phys_hi=480.0,
  88. raw_lo=210,
  89. raw_hi=411)
  90. self.xy.simulator_adc_set(self.ADC_TEMP, value)
  91. def adc_current_set(self, amps):
  92. value = self.__unscale(phys=amps,
  93. phys_lo=0.0,
  94. phys_hi=1.1,
  95. raw_lo=0,
  96. raw_hi=65)
  97. self.xy.simulator_adc_set(self.ADC_CURRENT, value)
  98. def __do_scale(self, raw, raw_lo, raw_hi, phys_lo, phys_hi):
  99. # (phys_hi - phys_lo) * (raw - raw_lo)
  100. # ret = -------------------------------------- + phys_lo
  101. # raw_hi - raw_lo
  102. a = (phys_hi - phys_lo) * (raw - raw_lo)
  103. b = raw_hi - raw_lo
  104. ret = (a / b) + phys_lo
  105. return ret
  106. def __scale(self, raw, raw_lo, raw_hi, phys_lo, phys_hi):
  107. return float(self.__do_scale(int(raw), int(raw_lo), int(raw_hi),
  108. float(phys_lo), float(phys_hi)))
  109. def __unscale(self, phys, phys_lo, phys_hi, raw_lo, raw_hi):
  110. return int(round(self.__do_scale(float(phys), float(phys_lo), float(phys_hi),
  111. int(raw_lo), int(raw_hi))))
  112. def __uart_tx_get_line(self):
  113. data = self.xy.simulator_uart_get_tx()
  114. if data:
  115. self.__uartbuf += data
  116. i = self.__uartbuf.find(b'\r\n')
  117. if i >= 0:
  118. line = self.__uartbuf[:i]
  119. del self.__uartbuf[:i+2]
  120. if line:
  121. return line.decode("utf-8", "ignore").strip()
  122. return ""
  123. @classmethod
  124. def __floatToFixpt(cls, f):
  125. f = float(f)
  126. if f < 0.0:
  127. return int((f * float(1 << cls.FIXPT_SHIFT)) - 0.5)
  128. return int((f * float(1 << cls.FIXPT_SHIFT)) + 0.5)
  129. @classmethod
  130. def __fixptToFloat(cls, fixpt):
  131. mask = (1 << cls.FIXPT_SIZE) - 1
  132. upperMask = (mask >> cls.FIXPT_SHIFT) << cls.FIXPT_SHIFT
  133. lowerMask = mask ^ upperMask
  134. # sign
  135. if fixpt & (1 << (cls.FIXPT_SIZE - 1)):
  136. fixpt = -((~fixpt + 1) & mask)
  137. fact = -1 if fixpt < 0 else 1
  138. fixpt *= fact
  139. f = float((fixpt & upperMask) >> cls.FIXPT_SHIFT)
  140. f += float(fixpt & lowerMask) / float(lowerMask + 1)
  141. f *= fact
  142. return f
  143. def __reset_debuginterface(self):
  144. self.dbg_currentRealR = 0.0
  145. self.dbg_currentUsedR = 0.0
  146. self.dbg_currentRState = 0
  147. self.dbg_currentY = 0.0
  148. self.dbg_tempR = 0.0
  149. self.dbg_tempY1 = 0.0
  150. self.dbg_tempY2 = 0.0
  151. self.dbg_measCurr = 0
  152. self.dbg_filtCurr = 0
  153. self.dbg_measTemp = 0
  154. self.dbg_boostMode = 0
  155. self.dbg_pidTempE = 0.0
  156. self.dbg_pidTempP = 0.0
  157. self.dbg_pidTempI = 0.0
  158. self.dbg_pidTempD = 0.0
  159. self.dbg_pidTempPrevE = 0.0
  160. self.dbg_pidCurrE = 0.0
  161. self.dbg_pidCurrP = 0.0
  162. self.dbg_pidCurrI = 0.0
  163. self.dbg_pidCurrD = 0.0
  164. self.dbg_pidCurrPrevE = 0.0
  165. @classmethod
  166. def __parseInt(cls, valStr, valIdent):
  167. try:
  168. val = int(valStr, 10)
  169. except ValueError:
  170. cls.error("Invalid %s" % valIdent)
  171. return 0
  172. return val
  173. @classmethod
  174. def __parseFixpt(cls, valStr, valIdent):
  175. val = cls.__parseInt(valStr, valIdent)
  176. val = float(val) / (1 << 6)
  177. return val
  178. def __handle_debuginterface(self):
  179. line = self.__uart_tx_get_line()
  180. if not line:
  181. return
  182. if line == "st":
  183. self.__reset_debuginterface()
  184. return
  185. i = line.find(':')
  186. if i < 0:
  187. self.error("Time stamp not found")
  188. return
  189. try:
  190. timeStamp = int(line[:i], 16)
  191. except ValueError:
  192. self.error("Invalid time stamp format")
  193. return
  194. line = line[i+1:].strip()
  195. elems = line.split()
  196. if len(elems) < 2 or len(elems) > 3:
  197. self.error("Unknown format: %s" % line)
  198. return
  199. if elems[0] == "cr1":
  200. self.dbg_currentRealR = self.__parseFixpt(elems[1], "cr1") / self.CURR_DIV
  201. return
  202. elif elems[0] == "cr2":
  203. self.dbg_currentUsedR = self.__parseFixpt(elems[1], "cr2") / self.CURR_DIV
  204. return
  205. elif elems[0] == "rs":
  206. self.dbg_currentRState = self.__parseInt(elems[1], "rs")
  207. return
  208. elif elems[0] == "cy":
  209. self.dbg_currentY = self.__parseFixpt(elems[1], "cy") / self.CURR_DIV
  210. return
  211. elif elems[0] == "tr":
  212. self.dbg_tempR = self.__parseFixpt(elems[1], "tr")
  213. return
  214. elif elems[0] == "ty1":
  215. self.dbg_tempY1 = self.__parseFixpt(elems[1], "ty1")
  216. return
  217. elif elems[0] == "ty2":
  218. self.dbg_tempY2 = self.__parseFixpt(elems[1], "ty2") / self.CURR_DIV
  219. return
  220. elif elems[0] == "tb":
  221. self.dbg_boostMode = self.__parseInt(elems[1], "tb")
  222. return
  223. elif elems[0] == "mc":
  224. self.dbg_measCurr = self.__parseInt(elems[1], "mc")
  225. return
  226. elif elems[0] == "fc":
  227. self.dbg_filtCurr = self.__parseInt(elems[1], "fc")
  228. return
  229. elif elems[0] == "mt":
  230. self.dbg_measTemp = self.__parseInt(elems[1], "mt")
  231. return
  232. elif elems[0] == "pid-t" and len(elems) == 3:
  233. if elems[1] == "e":
  234. self.dbg_pidTempE = self.__parseFixpt(elems[2], "pid-t e")
  235. return
  236. elif elems[1] == "p":
  237. self.dbg_pidTempP = self.__parseFixpt(elems[2], "pid-t p")
  238. return
  239. elif elems[1] == "i":
  240. self.dbg_pidTempI = self.__parseFixpt(elems[2], "pid-t i")
  241. return
  242. elif elems[1] == "d":
  243. self.dbg_pidTempD = self.__parseFixpt(elems[2], "pid-t d")
  244. return
  245. elif elems[1] == "pe":
  246. self.dbg_pidTempPrevE = self.__parseFixpt(elems[2], "pid-t pe")
  247. return
  248. elif elems[0] == "pid-c" and len(elems) == 3:
  249. if elems[1] == "e":
  250. self.dbg_pidCurrE = self.__parseFixpt(elems[2], "pid-c e")
  251. return
  252. elif elems[1] == "p":
  253. self.dbg_pidCurrP = self.__parseFixpt(elems[2], "pid-c p")
  254. return
  255. elif elems[1] == "i":
  256. self.dbg_pidCurrI = self.__parseFixpt(elems[2], "pid-c i")
  257. return
  258. elif elems[1] == "d":
  259. self.dbg_pidCurrD = self.__parseFixpt(elems[2], "pid-c d")
  260. return
  261. elif elems[1] == "pe":
  262. self.dbg_pidCurrPrevE = self.__parseFixpt(elems[2], "pid-c pe")
  263. return
  264. self.error("Unknown elem: %s" % elems[0])
  265. @classmethod
  266. def error(cls, msg):
  267. print(msg)
  268. @classmethod
  269. def info(cls, msg):
  270. print(msg)
  271. class IronTempSimulator(object):
  272. def __init__(self, sim):
  273. self.sim = sim
  274. self.sim.addRunHook(self.__run)
  275. def __run(self):
  276. self.sim.adc_temp_set(100)#TODO
  277. class IronCurrentSimulator(object):
  278. def __init__(self, sim):
  279. self.sim = sim
  280. self.sim.addRunHook(self.__run)
  281. def __run(self):
  282. curY = self.sim.pwm_current_get()
  283. self.sim.adc_current_set(4)#TODO
  284. def main():
  285. pyxytronic.simulator_init()
  286. try:
  287. sim = Simulator()
  288. tempSim = IronTempSimulator(sim)
  289. curSim = IronCurrentSimulator(sim)
  290. while 1:
  291. sim.run()
  292. time.sleep(0.0005)
  293. finally:
  294. pyxytronic.simulator_exit()
  295. return 0
  296. if __name__ == "__main__":
  297. sys.exit(main())