Python RGB Matrix games and animations https://www.xythobuz.de/ledmatrix_v2.html
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.

pico_ota.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. #!/usr/bin/env python3
  2. # Uses the Gitea API to fetch the latest revision of the project from a repo.
  3. #
  4. # Inspired by:
  5. # https://github.com/olivergregorius/micropython_ota
  6. #
  7. # ----------------------------------------------------------------------------
  8. # "THE BEER-WARE LICENSE" (Revision 42):
  9. # <xythobuz@xythobuz.de> wrote this file. As long as you retain this notice
  10. # you can do whatever you want with this stuff. If we meet some day, and you
  11. # think this stuff is worth it, you can buy me a beer in return. Thomas Buck
  12. # ----------------------------------------------------------------------------
  13. import util
  14. import sys
  15. import os
  16. import gc
  17. # to check if we're actually running on MicroPython
  18. on_pico = False
  19. try:
  20. import machine
  21. on_pico = True
  22. except Exception as e:
  23. print()
  24. if hasattr(sys, "print_exception"):
  25. sys.print_exception(e)
  26. else:
  27. print(e)
  28. print()
  29. class PicoOTA:
  30. def __init__(self, host, repo, branch = None):
  31. self.host = host
  32. self.repo = repo
  33. self.branch = branch
  34. self.get = None
  35. self.update_path = "."
  36. self.exe_path = ""
  37. self.version_file = "ota_version"
  38. self.blacklist = []
  39. self.gui = None
  40. self.text = None
  41. def ui(self, g, t):
  42. self.gui = g
  43. self.text = t
  44. if (self.gui == None) or (self.text == None):
  45. return
  46. for i in range(0, 5):
  47. self.gui.loop_start()
  48. self.text.fg = (255, 255, 0)
  49. self.text.setText("WiFi", "bitmap6")
  50. self.text.draw(0, 6 * 0, False)
  51. self.text.fg = (255, 0, 255)
  52. self.text.setText("???", "bitmap6")
  53. self.text.draw(0, 6 * 1, False)
  54. self.text.fg = (255, 0, 0)
  55. self.text.setText(str(i + 1) + " / 5", "bitmap6")
  56. self.text.draw(0, 6 * 2, False)
  57. self.text.fg = (0, 255, 0)
  58. self.text.setText("", "bitmap6")
  59. self.text.draw(0, 6 * 3, False)
  60. self.text.setText("Conn...", "bitmap6")
  61. self.text.draw(0, 6 * 4, False)
  62. self.gui.loop_end()
  63. util.connectToWiFi()
  64. self.gui.loop_start()
  65. self.text.fg = (255, 255, 0)
  66. self.text.setText("WiFi", "bitmap6")
  67. self.text.draw(0, 6 * 0, False)
  68. self.text.fg = (255, 0, 255)
  69. self.text.setText("$NAME", "bitmap6")
  70. self.text.draw(0, 6 * 1, False)
  71. self.text.fg = (255, 0, 0)
  72. self.text.setText(str(i + 1) + " / 5", "bitmap6")
  73. self.text.draw(0, 6 * 2, False)
  74. self.text.fg = (0, 255, 0)
  75. self.text.setText("Status:", "bitmap6")
  76. self.text.draw(0, 6 * 3, False)
  77. self.text.setText("$val", "bitmap6")
  78. self.text.draw(0, 6 * 4, False)
  79. self.gui.loop_end()
  80. def path(self, p):
  81. self.update_path = p
  82. def exe(self, e):
  83. self.exe_path = e
  84. def ignore(self, path):
  85. if not path in self.blacklist:
  86. self.blacklist.append(path)
  87. def fetch(self, url):
  88. # lazily initialize WiFi
  89. if self.get == None:
  90. self.get, post = util.getRequests()
  91. if self.get == None:
  92. return None
  93. try:
  94. #print("GET " + url)
  95. r = self.get(url)
  96. # explitic close on Response object not needed,
  97. # handled internally by r.content / r.text / r.json()
  98. # to avoid this automatic behaviour, first access r.content
  99. # to trigger caching it in response object, then close
  100. # socket.
  101. tmp = r.content
  102. if hasattr(r, "raw"):
  103. if r.raw != None:
  104. r.raw.close()
  105. r.raw = None
  106. return r
  107. except Exception as e:
  108. print()
  109. print(url)
  110. if hasattr(sys, "print_exception"):
  111. sys.print_exception(e)
  112. else:
  113. print(e)
  114. print()
  115. return None
  116. def get_stored_commit(self):
  117. current = "unknown"
  118. try:
  119. f = open(self.update_path + "/" + self.version_file, "r")
  120. current = f.readline().strip()
  121. f.close()
  122. except Exception as e:
  123. print()
  124. if hasattr(sys, "print_exception"):
  125. sys.print_exception(e)
  126. else:
  127. print(e)
  128. print()
  129. return current
  130. def get_previous_commit(self, commit):
  131. r = self.fetch(self.host + "/" + self.repo + "/commit/" + commit).text
  132. for line in r.splitlines():
  133. if not (self.repo + "/commit/") in line:
  134. continue
  135. line = line[line.find("/commit/") : ][8 : ][ : 40]
  136. if line != commit:
  137. return line
  138. return "unknown"
  139. def check(self, verbose = False):
  140. if self.branch == None:
  141. # get default branch
  142. r = self.fetch(self.host + "/api/v1/repos/" + self.repo).json()
  143. self.branch = r["default_branch"]
  144. if verbose:
  145. print("Selected default branch " + self.branch)
  146. # check for latest commit in branch
  147. r = self.fetch(self.host + "/api/v1/repos/" + self.repo + "/branches/" + self.branch).json()
  148. commit = r["commit"]["id"]
  149. if verbose:
  150. print("Latest commit is " + commit)
  151. current = self.get_stored_commit()
  152. if verbose:
  153. if current != commit:
  154. print("Current commit " + current + " is different!")
  155. else:
  156. print("No update required")
  157. return (current != commit, commit)
  158. def update_to_commit(self, commit, verbose = False):
  159. # list all files for a commit
  160. r = self.fetch(self.host + "/api/v1/repos/" + self.repo + "/git/trees/" + commit).json()
  161. # TODO does not support sub-folders
  162. if verbose:
  163. if len(r["tree"]) > 0:
  164. print(str(len(r["tree"])) + " files in repo:")
  165. for f in r["tree"]:
  166. if f["path"] in self.blacklist:
  167. print(" - (IGNORED) " + f["path"])
  168. else:
  169. print(" - " + f["path"])
  170. else:
  171. print("No files in repo?!")
  172. for f in r["tree"]:
  173. if f["path"] in self.blacklist:
  174. continue
  175. if (self.gui != None) and (self.text != None):
  176. self.gui.loop_start()
  177. self.text.fg = (255, 255, 0)
  178. self.text.setText("OTA", "bitmap6")
  179. self.text.draw(0, 6 * 0, False)
  180. self.text.fg = (255, 0, 255)
  181. self.text.setText(commit[ 0 : 5], "bitmap6")
  182. self.text.draw(0, 6 * 1, False)
  183. self.text.fg = (0, 255, 255)
  184. self.text.setText("Get", "bitmap6")
  185. self.text.draw(0, 6 * 2, False)
  186. self.text.fg = (0, 255, 0)
  187. self.text.setText(f["path"][0 : 5], "bitmap6")
  188. self.text.draw(0, 6 * 3, False)
  189. self.text.setText(f["path"][5 : ], "bitmap6")
  190. self.text.draw(0, 6 * 4, False)
  191. self.gui.loop_end()
  192. gc.collect()
  193. if hasattr(gc, "mem_free"):
  194. print("Collected Garbage:", gc.mem_free())
  195. # get a file from a commit
  196. r = self.fetch(self.host + "/" + self.repo + "/raw/commit/" + commit + "/" + f["path"]).text
  197. if verbose:
  198. print("Writing " + f["path"] + " to " + self.update_path)
  199. if (self.gui != None) and (self.text != None):
  200. self.gui.loop_start()
  201. self.text.fg = (255, 255, 0)
  202. self.text.setText("OTA", "bitmap6")
  203. self.text.draw(0, 6 * 0, False)
  204. self.text.fg = (255, 0, 255)
  205. self.text.setText(commit[ 0 : 5], "bitmap6")
  206. self.text.draw(0, 6 * 1, False)
  207. self.text.fg = (255, 0, 0)
  208. self.text.setText("Write", "bitmap6")
  209. self.text.draw(0, 6 * 2, False)
  210. self.text.fg = (0, 255, 0)
  211. self.text.setText(f["path"][0 : 5], "bitmap6")
  212. self.text.draw(0, 6 * 3, False)
  213. self.text.setText(f["path"][5 : ], "bitmap6")
  214. self.text.draw(0, 6 * 4, False)
  215. self.gui.loop_end()
  216. # overwrite existing file
  217. fo = open(self.update_path + "/" + f["path"], "w")
  218. fo.write(r)
  219. fo.close()
  220. if f["path"] == self.exe_path:
  221. if verbose:
  222. print("Writing " + f["path"] + " to main.py")
  223. fo = open(self.update_path + "/main.py", "w")
  224. fo.write(r)
  225. fo.close()
  226. # Write new commit id to local file
  227. f = open(self.update_path + "/" + self.version_file, "w")
  228. f.write(commit + "\n")
  229. f.close()
  230. def non_pico_ota_test(ota):
  231. if not os.path.exists("tmp"):
  232. os.makedirs("tmp")
  233. ota.path("tmp")
  234. print("Checking for updates")
  235. newer, commit = ota.check(True)
  236. print()
  237. # Just for testing
  238. previous = ota.get_previous_commit(commit)
  239. print("Previous commit (-1):", previous)
  240. previous = ota.get_previous_commit(previous)
  241. print("Previous commit (-2):", previous)
  242. print()
  243. if newer:
  244. print("Updating")
  245. ota.update_to_commit(commit, True)
  246. else:
  247. print("No update required")
  248. if on_pico:
  249. from pico import PicoText
  250. i = util.getInput()
  251. t = util.getTarget(i)
  252. s = PicoText(t)
  253. def pico_ota_run(ota):
  254. gc.collect()
  255. print("Collected Garbage:", gc.mem_free())
  256. t.loop_start()
  257. s.fg = (255, 255, 0)
  258. s.setText("OTA", "bitmap6")
  259. s.draw(0, 6 * 0, False)
  260. s.fg = (0, 255, 255)
  261. s.setText("Check", "bitmap6")
  262. s.draw(0, 6 * 2, False)
  263. t.loop_end()
  264. gc.collect()
  265. print("Collected Garbage:", gc.mem_free())
  266. print("Checking for updates")
  267. newer, commit = ota.check(True)
  268. gc.collect()
  269. print("Collected Garbage:", gc.mem_free())
  270. if newer:
  271. t.loop_start()
  272. s.fg = (255, 255, 0)
  273. s.setText("OTA", "bitmap6")
  274. s.draw(0, 6 * 0, False)
  275. s.fg = (255, 0, 255)
  276. s.setText(commit[ 0 : 5], "bitmap6")
  277. s.draw(0, 6 * 1, False)
  278. s.setText(commit[ 5 : 10], "bitmap6")
  279. s.draw(0, 6 * 2, False)
  280. s.setText(commit[10 : 15], "bitmap6")
  281. s.draw(0, 6 * 3, False)
  282. s.setText(commit[15 : 20], "bitmap6")
  283. s.draw(0, 6 * 4, False)
  284. t.loop_end()
  285. print("Updating to:", commit)
  286. ota.update_to_commit(commit, True)
  287. print("Resetting")
  288. machine.soft_reset()
  289. else:
  290. t.loop_start()
  291. s.fg = (255, 255, 0)
  292. s.setText("OTA", "bitmap6")
  293. s.draw(0, 6 * 0, False)
  294. s.fg = (0, 255, 0)
  295. s.setText("Done!", "bitmap6")
  296. s.draw(0, 6 * 1, False)
  297. s.fg = (255, 0, 255)
  298. s.setText("Boot", "bitmap6")
  299. s.draw(0, 6 * 2, False)
  300. s.fg = (0, 255, 255)
  301. s.setText("camp_pico.py"[0 : 5], "bitmap6")
  302. s.draw(0, 6 * 3, False)
  303. s.setText("camp_pico.py"[5 : ], "bitmap6")
  304. s.draw(0, 6 * 4, False)
  305. t.loop_end()
  306. fallback = False
  307. try:
  308. gc.collect()
  309. print("Collected Garbage:", gc.mem_free())
  310. print("Starting Application")
  311. import camp_pico
  312. except Exception as e:
  313. print()
  314. if hasattr(sys, "print_exception"):
  315. sys.print_exception(e)
  316. else:
  317. print(e)
  318. print()
  319. print("Falling back to previous")
  320. fallback = True
  321. # TODO this would immediately cause another update on reboot
  322. # TODO set a flag to prevent updates after fallbacks?
  323. # TODO or better, blacklist failed commit_id!
  324. #if fallback:
  325. # previous = ota.get_previous_commit(commit, True)
  326. # ota.update_to_commit(previous, True)
  327. # machine.soft_reset()
  328. ota = PicoOTA("https://git.xythobuz.de", "thomas/rgb-matrix-visualizer")
  329. # stuff not needed on Pico
  330. ota.ignore(".gitignore")
  331. ota.ignore("README.md")
  332. ota.ignore("copy.sh")
  333. ota.ignore("config.py")
  334. ota.ignore("fonts")
  335. ota.ignore("hardware")
  336. ota.ignore("images")
  337. ota.ignore("bdf.py")
  338. ota.ignore("camp_small.py")
  339. ota.ignore("gamepad.py")
  340. ota.ignore("pi.py")
  341. ota.ignore("test.py")
  342. if not on_pico:
  343. non_pico_ota_test(ota)
  344. else:
  345. ota.exe("pico_ota.py")
  346. ota.ui(t, s)
  347. pico_ota_run(ota)