led.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. led: Control the LED on a single-board computer
  20. """
  21. import sys, os, threading
  22. from collections import namedtuple
  23. from subprocess import run
  24. from .util import msg, msg_r, die, have_sudo
  25. from .color import blue, orange
  26. from .base_obj import Lockable
  27. class LEDControl:
  28. class binfo(Lockable):
  29. _reset_ok = ('trigger_reset',)
  30. def __init__(
  31. self,
  32. *,
  33. name,
  34. control,
  35. trigger = None,
  36. trigger_dfl = 'heartbeat',
  37. trigger_disable = 'none',
  38. color = 'colored'):
  39. self.name = name
  40. self.control = control
  41. self.trigger = trigger
  42. self.trigger_dfl = trigger_dfl
  43. self.trigger_reset = trigger_dfl
  44. self.trigger_disable = trigger_disable
  45. self.color = color
  46. boards = {
  47. 'raspi_pi': binfo(
  48. name = 'Raspberry Pi',
  49. control = '/sys/class/leds/led0/brightness',
  50. trigger = '/sys/class/leds/led0/trigger',
  51. trigger_dfl = 'mmc0',
  52. color = 'red'),
  53. 'orange_pi': binfo(
  54. name = 'Orange Pi (Armbian)',
  55. control = '/sys/class/leds/orangepi:red:status/brightness',
  56. color = 'red'),
  57. 'orange_pi_5': binfo(
  58. name = 'Orange Pi 5 (Armbian)',
  59. control = '/sys/class/leds/status_led/brightness',
  60. color = 'red'),
  61. 'rock_pi': binfo(
  62. name = 'Rock Pi (Armbian)',
  63. control = '/sys/class/leds/status/brightness',
  64. trigger = '/sys/class/leds/status/trigger',
  65. color = 'blue'),
  66. 'rock_5': binfo(
  67. name = 'Rock 5 (Armbian)',
  68. control = '/sys/class/leds/user-led2/brightness',
  69. trigger = '/sys/class/leds/user-led2/trigger',
  70. color = 'blue'),
  71. 'banana_pi_f3': binfo(
  72. name = 'Banana Pi F3 (Armbian)',
  73. control = '/sys/class/leds/sys-led/brightness',
  74. trigger = '/sys/class/leds/sys-led/trigger',
  75. color = 'green'),
  76. 'nano_pi_m6': binfo(
  77. name = 'Nano Pi M6 (Armbian)',
  78. control = '/sys/class/leds/user_led/brightness',
  79. trigger = '/sys/class/leds/user_led/trigger',
  80. color = 'green'),
  81. 'dummy': binfo(
  82. name = 'Fake Board',
  83. control = '/tmp/led_status',
  84. trigger = '/tmp/led_trigger'),
  85. }
  86. def __init__(self, *, enabled, simulate=False, debug=False):
  87. self.enabled = enabled
  88. self.debug = debug or simulate
  89. if not enabled:
  90. self.set = self.stop = self.noop
  91. return
  92. self.ev = threading.Event()
  93. self.led_thread = None
  94. for board_id, board in self.boards.items():
  95. if board_id == 'dummy' and not simulate:
  96. continue
  97. try:
  98. os.stat(board.control)
  99. break
  100. except FileNotFoundError:
  101. pass
  102. else:
  103. die('NoLEDSupport', 'Control files not found! LED control not supported on this system')
  104. msg(f'{board.name} board detected')
  105. if self.debug:
  106. msg(f'\n Status file: {board.control}\n Trigger file: {board.trigger}')
  107. def write_init_val(fn, init_val):
  108. if not init_val:
  109. with open(fn) as fp:
  110. init_val = fp.read().strip()
  111. with open(fn, 'w') as fp:
  112. fp.write(f'{init_val}\n')
  113. def permission_error_action(fn, desc):
  114. cmd = f'sudo chmod 0666 {fn}'
  115. if have_sudo():
  116. msg(orange(f'Running ‘{cmd}’'))
  117. run(cmd.split(), check=True)
  118. else:
  119. msg('\n{}\n{}\n{}'.format(
  120. blue(f'You do not have write access to the {desc}'),
  121. blue(f'To allow access, run the following command:\n\n {cmd}'),
  122. orange('[To prevent this message in the future, enable sudo without a password]')
  123. ))
  124. sys.exit(1)
  125. def init_state(fn, *, desc, init_val=None):
  126. try:
  127. write_init_val(fn, init_val)
  128. except PermissionError:
  129. permission_error_action(fn, desc)
  130. write_init_val(fn, init_val)
  131. # Writing to control file can alter trigger file, so read and initialize trigger file first:
  132. if board.trigger:
  133. def get_cur_state():
  134. try:
  135. with open(board.trigger) as fh:
  136. states = fh.read()
  137. except PermissionError:
  138. permission_error_action(board.trigger, 'status LED trigger file')
  139. with open(board.trigger) as fh:
  140. states = fh.read()
  141. res = [a for a in states.split() if a.startswith('[') and a.endswith(']')]
  142. return res[0][1:-1] if len(res) == 1 else None
  143. if cur_state := get_cur_state():
  144. msg(f'Saving current LED trigger state: [{cur_state}]')
  145. board.trigger_reset = cur_state
  146. else:
  147. msg('Unable to determine current LED trigger state')
  148. init_state(board.trigger, desc='status LED trigger file', init_val=board.trigger_disable)
  149. init_state(board.control, desc='status LED control file')
  150. self.board = board
  151. @classmethod
  152. def create_dummy_control_files(cls):
  153. db = cls.boards['dummy']
  154. with open(db.control, 'w') as fp:
  155. fp.write('0\n')
  156. with open(db.trigger, 'w') as fp:
  157. fp.write(db.trigger_dfl + '\n')
  158. def noop(self, *args, **kwargs):
  159. pass
  160. def ev_sleep(self, secs):
  161. self.ev.wait(secs)
  162. return self.ev.is_set()
  163. def led_loop(self, on_secs, off_secs):
  164. if self.debug:
  165. msg(f'led_loop({on_secs}, {off_secs})')
  166. if not on_secs:
  167. with open(self.board.control, 'w') as fp:
  168. fp.write('0\n')
  169. while True:
  170. if self.ev_sleep(3600):
  171. return
  172. while True:
  173. for s_time, val in ((on_secs, 255), (off_secs, 0)):
  174. if self.debug:
  175. msg_r(('^', '+')[bool(val)])
  176. with open(self.board.control, 'w') as fp:
  177. fp.write(f'{val}\n')
  178. if self.ev_sleep(s_time):
  179. if self.debug:
  180. msg('\n')
  181. return
  182. def set(self, state):
  183. lt = namedtuple('led_timings', ['on_secs', 'off_secs'])
  184. timings = {
  185. 'off': lt(0, 0),
  186. 'standby': lt(2.2, 0.2),
  187. 'busy': lt(0.06, 0.06),
  188. 'error': lt(0.5, 0.5)}
  189. if self.led_thread:
  190. self.ev.set()
  191. self.led_thread.join()
  192. self.ev.clear()
  193. if self.debug:
  194. msg(f'Setting LED state to {state!r}')
  195. self.led_thread = threading.Thread(
  196. target = self.led_loop,
  197. name = 'LED loop',
  198. args = timings[state],
  199. daemon = True)
  200. self.led_thread.start()
  201. def stop(self):
  202. self.set('off')
  203. self.ev.set()
  204. self.led_thread.join()
  205. if self.debug:
  206. msg('Stopping LED')
  207. if self.board.trigger:
  208. with open(self.board.trigger, 'w') as fp:
  209. fp.write(self.board.trigger_reset + '\n')