No Description
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.

CatToy.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. from wifi import Wifi
  2. from config import Config
  3. from toy import Toy
  4. from log import LogDup
  5. import random
  6. import time
  7. from machine import Timer
  8. import os
  9. stdio_data = LogDup()
  10. os.dupterm(stdio_data)
  11. print("Initializing CatToy...")
  12. t = Toy()
  13. max_laser_power = 0.1
  14. limits = [
  15. # pan_min, pan_max, tilt_min, tilt_max, name
  16. (84, 120, 53, 76, '3d printer tower, top'),
  17. (84, 120, 53, 76, 'office desk, front right'),
  18. (t.maximum_limits[0], t.maximum_limits[1], t.maximum_limits[2], t.maximum_limits[3], 'maximum'),
  19. ]
  20. minimumBatteryVoltage = 3.25 * 2.0
  21. maximumBatteryVoltage = 4.2 * 2.0
  22. timerRunning = False
  23. timerData = None
  24. outlineIndex = 0
  25. buttonSelection = 0
  26. ledPattern = None
  27. patternIndex = 0
  28. patternTime = time.ticks_ms()
  29. buttonTime = None
  30. repeatTimer = Timer()
  31. buttonTimer = Timer()
  32. ledTimer = Timer()
  33. random.seed()
  34. def buildPage(header, footer):
  35. html = """<!DOCTYPE html>
  36. <html>
  37. <head>
  38. <title>Cat Toy</title>
  39. </head>
  40. <body>
  41. <h1>Cat Toy</h1>
  42. %s
  43. <h2>Laser Pointer</h2>
  44. <form action="/laser" method="get">
  45. Turn power:
  46. <input type="range" name="s" min="0" max="100" step="1" value="%d">
  47. <input type="submit" value="Set"><br>
  48. </form>
  49. <form action="/laser" method="get">
  50. Or just turn it:
  51. <input type="submit" name="s" value="Off">
  52. </form>
  53. <h2>Pan and Tilt Servos</h2>
  54. <form action="/servos" method="get">
  55. Pan: <input type="text" name="s1"><br>
  56. Tilt: <input type="text" name="s2"><br>
  57. <input type="submit" value="Move">
  58. </form>
  59. <form action="/random_move" method="get">
  60. <input type="submit" name="s" value="Go to random position">
  61. </form>
  62. <h2>Auto Play</h2>
  63. <form action="/repeat" method="get">
  64. Limits:
  65. <select name="limit">
  66. %s
  67. </select><br>
  68. Steps: <input type="text" name="steps" value="100"><br>
  69. Duration: <input type="text" name="duration" value="1000">ms<br>
  70. <input type="submit" name="s" value="Random">
  71. <input type="submit" name="s" value="Outline"><br>
  72. Status: %s
  73. </form>
  74. <h2>Status</h2>
  75. <p><b>Battery:</b> %s
  76. %s
  77. <h2>Console</h2>
  78. <pre>%s</pre>
  79. </body>
  80. </html>
  81. """
  82. sl = ""
  83. for pan_min, pan_max, tilt_min, tilt_max, name in limits:
  84. val = name.replace(' ', '_').replace(',', '').lower()
  85. sl += '<option value="' + val + '">' + name + '</option>'
  86. sl += '<option value="">None</option>'
  87. status = "No program running"
  88. if timerRunning:
  89. status = "Program in progress"
  90. vb = t.getBatteryVoltage()
  91. pb = (vb - minimumBatteryVoltage) / (maximumBatteryVoltage - minimumBatteryVoltage) * 100.0
  92. pb = max(min(pb, 100), 0)
  93. battery = str(vb) + "V (" + str(pb) + "%)"
  94. page = html % (header, int(max_laser_power * 100.0), sl, status, battery, footer, stdio_data.data.decode("utf-8"))
  95. return page
  96. def rootCallback(request):
  97. pan_min, pan_max, tilt_min, tilt_max = t.maximum_limits
  98. return buildPage(
  99. '<p>Welcome to the Cat Toy interface by <a href="https://www.xythobuz.de">xythobuz</a>.</p>',
  100. "<p><b>Limits:</b> tMin={} tMax={} pMin={} pMax={}</p>".format(tilt_min, tilt_max, pan_min, pan_max)
  101. )
  102. def servoCallback(request):
  103. if t.getBatteryVoltage() < minimumBatteryVoltage:
  104. stopRepeat()
  105. t.free()
  106. return buildPage(
  107. '<p>Error: Battery Voltage too low. <b>Please charge them!</b></p>',
  108. '<p><a href="/">Back to main page</a></p>'
  109. )
  110. q = request.find("/servos?")
  111. p1 = request.find("s1=")
  112. p2 = request.find("s2=")
  113. if (q < 0) or (p1 < 0) or (p2 < 0):
  114. print("servo query error: q={} p1={} p2={}".format(q, p1, p2))
  115. return buildPage(
  116. '<p>Error: no servo arguments found in URL query string.</p>',
  117. '<p><a href="/">Back to main page</a></p>'
  118. )
  119. servos = [p1, p2]
  120. result = []
  121. for p in servos:
  122. pe = request.find("&s", p)
  123. if (pe < 0) or (p + 3 >= pe) or (pe - (p + 3) > 3):
  124. pe = request.find(" HTTP", p)
  125. if (pe < 0) or (p + 3 >= pe) or (pe - (p + 3) > 3):
  126. print("servo query error: p={} pe={}".format(p, pe))
  127. return buildPage(
  128. '<p>Error parsing query string.</p>',
  129. '<p><a href="/">Back to main page</a></p>'
  130. )
  131. r = request[p + 3 : pe]
  132. s = int(r)
  133. result.append(s)
  134. print("servos: pan={} tilt={}", result[0], result[1])
  135. t.angle(t.pan, result[0])
  136. t.angle(t.tilt, result[1])
  137. return buildPage(
  138. '<p>Servos move to s1=' + str(result[0]) + ' s2=' + str(result[1]) + '.</p>',
  139. '<p><a href="/">Back to main page</a></p>'
  140. )
  141. def laserCallback(request):
  142. if t.getBatteryVoltage() < minimumBatteryVoltage:
  143. stopRepeat()
  144. t.free()
  145. return buildPage(
  146. '<p>Error: Battery Voltage too low. <b>Please charge them!</b></p>',
  147. '<p><a href="/">Back to main page</a></p>'
  148. )
  149. value = 0.0
  150. text = "off"
  151. if request.find("?s=") == 10:
  152. pe = request.find(" HTTP", 10)
  153. r = request[13 : pe]
  154. if r != "Off":
  155. value = int(r)
  156. text = "to " + str(r) + '%'
  157. print("laser: {}%".format(value))
  158. t.laser(value / 100.0)
  159. return buildPage(
  160. '<p>Laser turned ' + text + '!</p>',
  161. '<p><a href="/">Back to main page</a></p>'
  162. )
  163. def randomMoveCallback(request):
  164. if t.getBatteryVoltage() < minimumBatteryVoltage:
  165. stopRepeat()
  166. t.free()
  167. return buildPage(
  168. '<p>Error: Battery Voltage too low. <b>Please charge them!</b></p>',
  169. '<p><a href="/">Back to main page</a></p>'
  170. )
  171. tilt = random.randint(t.tilt_min, t.tilt_max)
  172. pan = random.randint(t.pan_min, t.pan_max)
  173. print("random: tilt={} pan={}".format(tilt, pan))
  174. t.angle(t.tilt, tilt)
  175. t.angle(t.pan, pan)
  176. return buildPage(
  177. '<p>Random move to pan={} tilt={}</p>'.format(pan, tilt),
  178. '<p><a href="/">Back to main page</a></p>'
  179. )
  180. def doMove(pan_min, pan_max, tilt_min, tilt_max, dur):
  181. tilt = random.randint(tilt_min, tilt_max)
  182. pan = random.randint(pan_min, pan_max)
  183. print("random move: tilt={} pan={} duration={}".format(tilt, pan, dur))
  184. t.angle(t.tilt, tilt)
  185. t.angle(t.pan, pan)
  186. def doOutline(pan_min, pan_max, tilt_min, tilt_max, dur):
  187. global outlineIndex
  188. points = [
  189. (pan_min, tilt_min),
  190. (pan_min, tilt_max),
  191. (pan_max, tilt_max),
  192. (pan_max, tilt_min)
  193. ]
  194. outlineIndex = (outlineIndex + 1) % 4
  195. pan, tilt = points[outlineIndex]
  196. print("outline move: tilt={} pan={} duration={}".format(tilt, pan, dur))
  197. t.angle(t.tilt, tilt)
  198. t.angle(t.pan, pan)
  199. def timerCallback(unused):
  200. global timerRunning, timerData, repeatTimer
  201. if t.getBatteryVoltage() < minimumBatteryVoltage:
  202. print("Abort due to low battery voltage: " + str(t.getBatteryVoltage()))
  203. stopRepeat()
  204. t.free()
  205. return
  206. if not timerRunning:
  207. return
  208. pan_min, pan_max, tilt_min, tilt_max, steps, duration, outline = timerData
  209. dur = duration
  210. if not outline:
  211. if dur < 200:
  212. dur = random.randint(200, 2000)
  213. else:
  214. dur = random.randint(200, duration)
  215. else:
  216. if dur < 200:
  217. dur = 500
  218. if steps > 0:
  219. steps -= 1
  220. if not outline:
  221. doMove(pan_min, pan_max, tilt_min, tilt_max, dur)
  222. else:
  223. doOutline(pan_min, pan_max, tilt_min, tilt_max, dur)
  224. repeatTimer.init(period = dur, mode = Timer.ONE_SHOT, callback = timerCallback)
  225. else:
  226. timerRunning = False
  227. t.laser(0.0)
  228. timerData = (pan_min, pan_max, tilt_min, tilt_max, steps, duration, outline)
  229. def startRepeat(pan_min, pan_max, tilt_min, tilt_max, steps, duration, outline):
  230. global timerRunning, timerData
  231. timerData = (pan_min, pan_max, tilt_min, tilt_max, steps, duration, outline)
  232. if not timerRunning:
  233. timerRunning = True
  234. t.laser(max_laser_power)
  235. timerCallback(None)
  236. def stopRepeat():
  237. global timerRunning, timerData
  238. timerRunning = False
  239. t.laser(0.0)
  240. def repeatCallback(request):
  241. if t.getBatteryVoltage() < minimumBatteryVoltage:
  242. stopRepeat()
  243. t.free()
  244. return buildPage(
  245. '<p>Error: Battery Voltage too low. <b>Please charge them!</b></p>',
  246. '<p><a href="/">Back to main page</a></p>'
  247. )
  248. q = request.find("/repeat?")
  249. pl = request.find("limit=", q)
  250. ps = request.find("steps=", pl)
  251. pd = request.find("duration=", ps)
  252. pp = request.find("s=", pd)
  253. if (q < 0) or (pl < 0) or (ps < 0) or (pd < 0) or (pp < 0):
  254. print("repeat query error: q={} pl={} ps={} pd={} pp={}".format(q, pl, ps, pd, pp))
  255. return buildPage(
  256. '<p>Error: no repeat arguments found in URL query string.</p>',
  257. '<p><a href="/">Back to main page</a></p>'
  258. )
  259. data = [("limit=", pl), ("steps=", ps), ("duration=", pd), ("s=", pp)]
  260. result = []
  261. for s, p in data:
  262. #print(p)
  263. pe = request.find("&", p)
  264. #print(pe)
  265. if (pe < 0) or (p + len(s) > pe) or (pe - (p + 3) > 40):
  266. pe = request.find(" HTTP", p)
  267. #print(pe)
  268. if (pe < 0) or (p + len(s) > pe) or (pe - (p + 3) > 40):
  269. print("repeat query error: p={} pe={}".format(p, pe))
  270. return buildPage(
  271. '<p>Error parsing query string.</p>',
  272. '<p><a href="/">Back to main page</a></p>'
  273. )
  274. r = request[p + len(s) : pe]
  275. result.append(r)
  276. #print()
  277. print("repeat: limit={} steps={} duration={} s={}".format(result[0], result[1], result[2], result[3]))
  278. if len(result[0]) == 0:
  279. stopRepeat()
  280. return buildPage(
  281. '<p>Stopped repeated automatic moves!</p>',
  282. '<p><a href="/">Back to main page</a></p>'
  283. )
  284. outline = False
  285. if result[3].lower() == "outline":
  286. outline = True
  287. for pan_min, pan_max, tilt_min, tilt_max, name in limits:
  288. val = name.replace(' ', '_').replace(',', '').lower()
  289. if result[0] == val:
  290. startRepeat(pan_min, pan_max, tilt_min, tilt_max, int(result[1]), int(result[2]), outline)
  291. break
  292. return buildPage(
  293. '<p>Starting moves with limit={} steps={} duration={}</p>'.format(result[0], result[1], result[2]),
  294. '<p><a href="/">Back to main page</a></p>'
  295. )
  296. def buttonCallback(state):
  297. global timerRunning, buttonSelection, buttonTime
  298. if state:
  299. buttonTime = time.ticks_ms()
  300. elif buttonTime != None:
  301. if time.ticks_diff(time.ticks_ms(), buttonTime) <= 500:
  302. buttonSelection = (buttonSelection + 1) % len(limits)
  303. print("Selection: " + str(buttonSelection + 1))
  304. else:
  305. if not timerRunning:
  306. pan_min, pan_max, tilt_min, tilt_max, name = limits[buttonSelection]
  307. print("Start pattern " + name)
  308. startRepeat(pan_min, pan_max, tilt_min, tilt_max, 200, 2500, False)
  309. else:
  310. print("Stop")
  311. stopRepeat()
  312. def ledStatus():
  313. global timerRunning, buttonSelection, ledPattern, patternIndex, patternTime
  314. patternMode = False
  315. if t.getBatteryVoltage() < minimumBatteryVoltage:
  316. patternMode = True
  317. pattern = [ 100, 100 ]
  318. elif timerRunning:
  319. t.status(True)
  320. else:
  321. patternMode = True
  322. pattern = [ 300 ] * (buttonSelection * 2 + 1) + [ 1500 ]
  323. if patternMode:
  324. if pattern != ledPattern:
  325. ledPattern = pattern
  326. patternIndex = 0
  327. patternTime = time.ticks_ms()
  328. if time.ticks_diff(time.ticks_ms(), patternTime) >= ledPattern[patternIndex]:
  329. t.status(patternIndex % 2 == 1)
  330. patternIndex = (patternIndex + 1) % len(ledPattern)
  331. patternTime = time.ticks_ms()
  332. def buttonTimerCallback(timer):
  333. global buttonTimer
  334. t.poll(buttonCallback)
  335. buttonTimer.init(period = 25, mode = Timer.ONE_SHOT, callback = buttonTimerCallback)
  336. def ledTimerCallback(timer):
  337. global ledTimer
  338. ledStatus()
  339. ledTimer.init(period = 100, mode = Timer.ONE_SHOT, callback = ledTimerCallback)
  340. print("Starting Timers...")
  341. buttonTimerCallback(None)
  342. ledTimerCallback(None)
  343. print("Initializing WiFi...")
  344. w = Wifi(Config.networks)
  345. w.add_handler("/", rootCallback)
  346. w.add_handler("/servos", servoCallback)
  347. w.add_handler("/laser", laserCallback)
  348. w.add_handler("/random_move", randomMoveCallback)
  349. w.add_handler("/repeat", repeatCallback)
  350. w.listen()
  351. print("Ready!")