linuxcnchal_cnccontrol.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. #!/usr/bin/env python3
  2. """
  3. #
  4. # CNC-control
  5. # LinuxCNC HAL module
  6. #
  7. # Copyright (C) 2011-2023 Michael Büsch <m@bues.ch>
  8. #
  9. """
  10. import sys
  11. import os
  12. import errno
  13. import time
  14. from datetime import datetime, timedelta
  15. import hal
  16. from hal import HAL_BIT, HAL_U32, HAL_S32, HAL_FLOAT
  17. from hal import HAL_IN, HAL_OUT, HAL_RO, HAL_RW
  18. from cnccontrol_driver import *
  19. class Timekeeping:
  20. def __init__(self):
  21. self.update()
  22. def update(self):
  23. self.now = datetime.now()
  24. class BitPoke:
  25. IDLE = 0
  26. DUTY = 1
  27. def __init__(self, h, tk, pin, cycleMsec=3):
  28. self.h = h
  29. self.tk = tk
  30. self.pin = pin
  31. self.timeout = tk.now
  32. self.cycleMsec = cycleMsec
  33. h[pin] = 0
  34. def __updateTimeout(self):
  35. self.timeout = self.tk.now + timedelta(milliseconds=self.cycleMsec)
  36. def update(self):
  37. # Returns DUTY or IDLE
  38. if self.h[self.pin]:
  39. if self.tk.now >= self.timeout:
  40. self.h[self.pin] = 0
  41. self.__updateTimeout()
  42. else:
  43. if self.tk.now >= self.timeout:
  44. return BitPoke.IDLE
  45. return BitPoke.DUTY
  46. def startDuty(self):
  47. self.h[self.pin] = 1
  48. self.__updateTimeout()
  49. def state(self):
  50. return self.h[self.pin]
  51. class ValueBalance:
  52. def __init__(self, h, tk,
  53. scalePin, incPin, decPin, feedbackPin):
  54. self.h = h
  55. self.scalePin = scalePin
  56. self.incBit = BitPoke(h, tk, incPin)
  57. self.decBit = BitPoke(h, tk, decPin)
  58. self.feedbackPin = feedbackPin
  59. def balance(self, targetValue):
  60. if self.incBit.update() != BitPoke.IDLE or\
  61. self.decBit.update() != BitPoke.IDLE:
  62. return
  63. value = self.h[self.feedbackPin]
  64. if equal(value, targetValue):
  65. self.h[self.scalePin] = 0
  66. return
  67. diff = value - targetValue
  68. self.h[self.scalePin] = abs(diff)
  69. if diff > 0:
  70. self.decBit.startDuty()
  71. else:
  72. self.incBit.startDuty()
  73. class CNCControlHAL(CNCControl):
  74. def __init__(self, halName="cnccontrol"):
  75. CNCControl.__init__(self, verbose=True)
  76. self.tk = Timekeeping()
  77. self.h = hal.component(halName)
  78. self.__createHalPins()
  79. self.h.ready()
  80. self.foBalance = ValueBalance(self.h, self.tk,
  81. scalePin="feed-override.scale",
  82. incPin="feed-override.inc",
  83. decPin="feed-override.dec",
  84. feedbackPin="feed-override.value")
  85. self.incJogPlus = {}
  86. self.incJogMinus = {}
  87. for ax in ALL_AXES:
  88. self.incJogPlus[ax] = BitPoke(self.h, self.tk,
  89. "jog.%s.inc-plus" % ax)
  90. self.incJogMinus[ax] = BitPoke(self.h, self.tk,
  91. "jog.%s.inc-minus" % ax)
  92. self.programStop = BitPoke(self.h, self.tk,
  93. "program.stop", cycleMsec=100)
  94. self.spindleStart = BitPoke(self.h, self.tk, "spindle.start")
  95. self.spindleStop = BitPoke(self.h, self.tk, "spindle.stop")
  96. def __checkLinuxCNC(self):
  97. for fn in ("/tmp/linuxcnc.lock",
  98. "/tmp/linuxcnc.print"):
  99. try:
  100. os.stat(fn)
  101. return True
  102. except OSError as e:
  103. if e.errno in {errno.EPERM, errno.EACCES}:
  104. return True
  105. print("CNC-Control: LinuxCNC doesn't seem to be running")
  106. raise KeyboardInterrupt
  107. def __createHalPins(self):
  108. h = self.h
  109. # Device config
  110. h.newparam("config.twohand", HAL_BIT, HAL_RW)
  111. h.newparam("config.debug", HAL_U32, HAL_RW)
  112. h.newparam("config.debugperf", HAL_BIT, HAL_RW)
  113. h.newparam("config.usblogmsg", HAL_BIT, HAL_RW)
  114. # Machine state
  115. h.newpin("machine.on", HAL_BIT, HAL_IN)
  116. h.newpin("machine.estop.active", HAL_BIT, HAL_IN)
  117. h.newpin("machine.mode.jog", HAL_BIT, HAL_IN)
  118. h.newpin("machine.mode.mdi", HAL_BIT, HAL_IN)
  119. h.newpin("machine.mode.auto", HAL_BIT, HAL_IN)
  120. # Axis
  121. for ax in ALL_AXES:
  122. h.newparam("axis.%s.enable" % ax, HAL_BIT, HAL_RW)
  123. h.newpin("axis.%s.pos.machine-coords" % ax, HAL_FLOAT, HAL_IN)
  124. h.newpin("axis.%s.pos.user-coords" % ax, HAL_FLOAT, HAL_IN)
  125. # Jogging
  126. h.newpin("jog.velocity", HAL_FLOAT, HAL_OUT)
  127. h.newparam("jog.velocity-rapid", HAL_FLOAT, HAL_RW)
  128. for i in range(0, ControlMsgSetincrement.MAX_INDEX + 1):
  129. h.newparam("jog.increment.%d" % i, HAL_FLOAT, HAL_RW)
  130. for ax in ALL_AXES:
  131. h.newpin("jog.%s.minus" % ax, HAL_BIT, HAL_OUT)
  132. h.newpin("jog.%s.plus" % ax, HAL_BIT, HAL_OUT)
  133. h.newpin("jog.%s.inc" % ax, HAL_FLOAT, HAL_OUT)
  134. h.newpin("jog.%s.inc-plus" % ax, HAL_BIT, HAL_OUT)
  135. h.newpin("jog.%s.inc-minus" % ax, HAL_BIT, HAL_OUT)
  136. # Master spindle
  137. h.newpin("spindle.runs-bwd", HAL_BIT, HAL_IN)
  138. h.newpin("spindle.runs-fwd", HAL_BIT, HAL_IN)
  139. h.newpin("spindle.forward", HAL_BIT, HAL_OUT)
  140. h.newpin("spindle.reverse", HAL_BIT, HAL_OUT)
  141. h.newpin("spindle.start", HAL_BIT, HAL_OUT)
  142. h.newpin("spindle.stop", HAL_BIT, HAL_OUT)
  143. # Feed override
  144. h.newpin("feed-override.scale", HAL_FLOAT, HAL_OUT)
  145. h.newpin("feed-override.dec", HAL_BIT, HAL_OUT)
  146. h.newpin("feed-override.inc", HAL_BIT, HAL_OUT)
  147. h.newpin("feed-override.value", HAL_FLOAT, HAL_IN)
  148. h.newparam("feed-override.min-value", HAL_FLOAT, HAL_RW)
  149. h.newparam("feed-override.max-value", HAL_FLOAT, HAL_RW)
  150. # Program control
  151. h.newpin("program.stop", HAL_BIT, HAL_OUT)
  152. def __resetHalOutputPins(self):
  153. h = self.h
  154. # Jogging
  155. h["jog.velocity"] = 0
  156. for ax in ALL_AXES:
  157. h["jog.%s.minus" % ax] = 0
  158. h["jog.%s.plus" % ax] = 0
  159. h["jog.%s.inc" % ax] = 0
  160. h["jog.%s.inc-plus" % ax] = 0
  161. h["jog.%s.inc-minus" % ax] = 0
  162. # Master spindle
  163. h["spindle.forward"] = 0
  164. h["spindle.reverse"] = 0
  165. h["spindle.start"] = 0
  166. h["spindle.stop"] = 0
  167. # Feed override
  168. h["feed-override.dec"] = 0
  169. h["feed-override.inc"] = 0
  170. h["feed-override.scale"] = 0
  171. # Program control
  172. h["program.stop"] = 0
  173. def __deviceInitialize(self):
  174. # CNC-Control USB device connected. Initialize it.
  175. h = self.h
  176. self.deviceReset()
  177. self.setDebugging(h["config.debug"], h["config.usblogmsg"])
  178. self.setTwohandEnabled(h["config.twohand"])
  179. for i in range(0, ControlMsgSetincrement.MAX_INDEX + 1):
  180. self.setIncrementAtIndex(i, h["jog.increment.%d" % i])
  181. axes = [ax if h["axis.%s.enable" % ax] else "" for ax in ALL_AXES]
  182. self.setEnabledAxes([_f for _f in axes if _f])
  183. def __pingDevice(self):
  184. for i in range(0, 3):
  185. if self.deviceAppPing():
  186. break
  187. else:
  188. CNCCFatal.error("Failed to ping the device")
  189. def __updatePins(self):
  190. h = self.h
  191. self.setEstopState(h["machine.estop.active"])
  192. if not h["machine.on"] or\
  193. not self.deviceIsTurnedOn():
  194. self.__resetHalOutputPins()
  195. return
  196. # Halt the program, if requested
  197. if self.haveMotionHaltRequest():
  198. self.programStop.startDuty()
  199. self.programStop.update()
  200. # Update master spindle state
  201. if self.spindleStart.update() == BitPoke.IDLE and\
  202. self.spindleStop.update() == BitPoke.IDLE and\
  203. h["machine.mode.jog"]:
  204. direction = self.getSpindleCommand()
  205. if direction < 0: # backward
  206. if not h["spindle.runs-bwd"]:
  207. h["spindle.forward"] = 0
  208. h["spindle.reverse"] = 1
  209. self.spindleStart.startDuty()
  210. elif direction > 0: # forward
  211. if not h["spindle.runs-fwd"]:
  212. h["spindle.forward"] = 1
  213. h["spindle.reverse"] = 0
  214. self.spindleStart.startDuty()
  215. else: # stop
  216. if h["spindle.runs-fwd"] or h["spindle.runs-bwd"]:
  217. h["spindle.forward"] = 0
  218. h["spindle.reverse"] = 0
  219. self.spindleStop.startDuty()
  220. if h["spindle.runs-bwd"]:
  221. self.setSpindleState(-1)
  222. elif h["spindle.runs-fwd"]:
  223. self.setSpindleState(1)
  224. else:
  225. self.setSpindleState(0)
  226. # Update feed override state
  227. foValue = self.getFeedOverrideState(h["feed-override.min-value"],
  228. h["feed-override.max-value"])
  229. self.foBalance.balance(foValue)
  230. self.setFeedOverrideState(h["feed-override.value"] * 100)
  231. # Update jog states
  232. velocity = h["jog.velocity-rapid"]
  233. jogParams = {}
  234. for ax in ALL_AXES:
  235. if not h["axis.%s.enable" % ax]:
  236. continue
  237. (direction, incremental, vel) = self.getJogState(ax)
  238. if not equal(direction, 0.0):
  239. if vel < 0: # Rapid move
  240. vel = h["jog.velocity-rapid"]
  241. velocity = min(velocity, vel)
  242. jogParams[ax] = (direction, incremental)
  243. h["jog.velocity"] = velocity
  244. for ax in jogParams:
  245. self.incJogPlus[ax].update()
  246. self.incJogMinus[ax].update()
  247. if not h["machine.mode.jog"]:
  248. h["jog.%s.minus" % ax] = 0
  249. h["jog.%s.plus" % ax] = 0
  250. continue
  251. (direction, incremental) = jogParams[ax]
  252. if incremental and not equal(direction, 0.0):
  253. h["jog.%s.minus" % ax] = 0
  254. h["jog.%s.plus" % ax] = 0
  255. if not self.incJogPlus[ax].state() and\
  256. not self.incJogMinus[ax].state():
  257. h["jog.%s.inc" % ax] = abs(direction)
  258. if direction > 0:
  259. self.incJogPlus[ax].startDuty()
  260. else:
  261. self.incJogMinus[ax].startDuty()
  262. else:
  263. if direction < -0.00001: # backward
  264. h["jog.%s.plus" % ax] = 0
  265. h["jog.%s.minus" % ax] = 1
  266. elif direction > 0.00001: # forward
  267. h["jog.%s.minus" % ax] = 0
  268. h["jog.%s.plus" % ax] = 1
  269. else: # stop
  270. h["jog.%s.minus" % ax] = 0
  271. h["jog.%s.plus" % ax] = 0
  272. # Update axis states
  273. g53coords = self.wantG53Coords()
  274. for ax in ALL_AXES:
  275. if not h["axis.%s.enable" % ax]:
  276. continue
  277. if g53coords:
  278. pos = h["axis.%s.pos.machine-coords" % ax]
  279. else:
  280. pos = h["axis.%s.pos.user-coords" % ax]
  281. self.setAxisPosition(ax, pos)
  282. def __eventLoop(self):
  283. avgRuntime = 0
  284. lastRuntimePrint = -1
  285. lastPing = -1
  286. timeDebug = bool(self.h["config.debugperf"])
  287. while self.__checkLinuxCNC():
  288. self.tk.update()
  289. start = self.tk.now
  290. try:
  291. if start.second != lastPing:
  292. lastPing = start.second
  293. self.__pingDevice()
  294. self.eventWait()
  295. self.tk.update()
  296. # Update pins, even if we didn't receive an event.
  297. self.__updatePins()
  298. except CNCCFatal as e:
  299. raise # Drop out of event loop and re-probe device.
  300. except CNCCException as e:
  301. print("CNC-Control error: " + str(e))
  302. if not timeDebug:
  303. continue
  304. self.tk.update()
  305. runtime = (self.tk.now - start).microseconds
  306. avgRuntime = (avgRuntime + runtime) // 2
  307. if start.second != lastRuntimePrint:
  308. lastRuntimePrint = start.second
  309. print("Average event loop runtime = %.1f milliseconds" %\
  310. (float(avgRuntime) / 1000))
  311. def probeLoop(self):
  312. self.__resetHalOutputPins()
  313. while self.__checkLinuxCNC():
  314. try:
  315. if self.probe():
  316. self.__deviceInitialize()
  317. self.__eventLoop()
  318. else:
  319. time.sleep(0.2)
  320. except CNCCFatal as e:
  321. print("CNC-Control fatal error: " + str(e))
  322. except CNCCException as e:
  323. print("CNC-Control error: " + str(e))
  324. self.__resetHalOutputPins()
  325. def main():
  326. try:
  327. try:
  328. os.nice(-20)
  329. except OSError as e:
  330. print("WARNING: Failed to renice cnccontrol HAL module:", str(e))
  331. cncc = CNCControlHAL()
  332. cncc.probeLoop()
  333. except CNCCException as e:
  334. print("CNC-Control: Unhandled exception: " + str(e))
  335. return 1
  336. except KeyboardInterrupt as e:
  337. print("CNC-Control: shutdown")
  338. return 0
  339. if __name__ == "__main__":
  340. sys.exit(main())