| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- #!/usr/bin/env python3
- #
- # MMGen Wallet, a terminal-based cryptocurrency wallet
- # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
- #
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
- """
- led: Control the LED on a single-board computer
- """
- import sys, os, threading
- from collections import namedtuple
- from subprocess import run
- from .util import msg, msg_r, die, have_sudo
- from .color import blue, orange
- from .base_obj import Lockable
- class LEDControl:
- class binfo(Lockable):
- _reset_ok = ('trigger_reset',)
- def __init__(
- self,
- *,
- name,
- control,
- trigger = None,
- trigger_dfl = 'heartbeat',
- trigger_disable = 'none',
- color = 'colored'):
- self.name = name
- self.control = control
- self.trigger = trigger
- self.trigger_dfl = trigger_dfl
- self.trigger_reset = trigger_dfl
- self.trigger_disable = trigger_disable
- self.color = color
- boards = {
- 'raspi_pi': binfo(
- name = 'Raspberry Pi',
- control = '/sys/class/leds/led0/brightness',
- trigger = '/sys/class/leds/led0/trigger',
- trigger_dfl = 'mmc0',
- color = 'red'),
- 'orange_pi': binfo(
- name = 'Orange Pi (Armbian)',
- control = '/sys/class/leds/orangepi:red:status/brightness',
- color = 'red'),
- 'orange_pi_5': binfo(
- name = 'Orange Pi 5 (Armbian)',
- control = '/sys/class/leds/status_led/brightness',
- color = 'red'),
- 'rock_pi': binfo(
- name = 'Rock Pi (Armbian)',
- control = '/sys/class/leds/status/brightness',
- trigger = '/sys/class/leds/status/trigger',
- color = 'blue'),
- 'rock_5': binfo(
- name = 'Rock 5 (Armbian)',
- control = '/sys/class/leds/user-led2/brightness',
- trigger = '/sys/class/leds/user-led2/trigger',
- color = 'blue'),
- 'banana_pi_f3': binfo(
- name = 'Banana Pi F3 (Armbian)',
- control = '/sys/class/leds/sys-led/brightness',
- trigger = '/sys/class/leds/sys-led/trigger',
- color = 'green'),
- 'nano_pi_m6': binfo(
- name = 'Nano Pi M6 (Armbian)',
- control = '/sys/class/leds/user_led/brightness',
- trigger = '/sys/class/leds/user_led/trigger',
- color = 'green'),
- 'dummy': binfo(
- name = 'Fake Board',
- control = '/tmp/led_status',
- trigger = '/tmp/led_trigger'),
- }
- def __init__(self, *, enabled, simulate=False, debug=False):
- self.enabled = enabled
- self.debug = debug or simulate
- if not enabled:
- self.set = self.stop = self.noop
- return
- self.ev = threading.Event()
- self.led_thread = None
- for board_id, board in self.boards.items():
- if board_id == 'dummy' and not simulate:
- continue
- try:
- os.stat(board.control)
- break
- except FileNotFoundError:
- pass
- else:
- die('NoLEDSupport', 'Control files not found! LED control not supported on this system')
- msg(f'{board.name} board detected')
- if self.debug:
- msg(f'\n Status file: {board.control}\n Trigger file: {board.trigger}')
- def write_init_val(fn, init_val):
- if not init_val:
- with open(fn) as fp:
- init_val = fp.read().strip()
- with open(fn, 'w') as fp:
- fp.write(f'{init_val}\n')
- def permission_error_action(fn, desc):
- cmd = f'sudo chmod 0666 {fn}'
- if have_sudo():
- msg(orange(f'Running ‘{cmd}’'))
- run(cmd.split(), check=True)
- else:
- msg('\n{}\n{}\n{}'.format(
- blue(f'You do not have write access to the {desc}'),
- blue(f'To allow access, run the following command:\n\n {cmd}'),
- orange('[To prevent this message in the future, enable sudo without a password]')
- ))
- sys.exit(1)
- def init_state(fn, *, desc, init_val=None):
- try:
- write_init_val(fn, init_val)
- except PermissionError:
- permission_error_action(fn, desc)
- write_init_val(fn, init_val)
- # Writing to control file can alter trigger file, so read and initialize trigger file first:
- if board.trigger:
- def get_cur_state():
- try:
- with open(board.trigger) as fh:
- states = fh.read()
- except PermissionError:
- permission_error_action(board.trigger, 'status LED trigger file')
- with open(board.trigger) as fh:
- states = fh.read()
- res = [a for a in states.split() if a.startswith('[') and a.endswith(']')]
- return res[0][1:-1] if len(res) == 1 else None
- if cur_state := get_cur_state():
- msg(f'Saving current LED trigger state: [{cur_state}]')
- board.trigger_reset = cur_state
- else:
- msg('Unable to determine current LED trigger state')
- init_state(board.trigger, desc='status LED trigger file', init_val=board.trigger_disable)
- init_state(board.control, desc='status LED control file')
- self.board = board
- @classmethod
- def create_dummy_control_files(cls):
- db = cls.boards['dummy']
- with open(db.control, 'w') as fp:
- fp.write('0\n')
- with open(db.trigger, 'w') as fp:
- fp.write(db.trigger_dfl + '\n')
- def noop(self, *args, **kwargs):
- pass
- def ev_sleep(self, secs):
- self.ev.wait(secs)
- return self.ev.is_set()
- def led_loop(self, on_secs, off_secs):
- if self.debug:
- msg(f'led_loop({on_secs}, {off_secs})')
- if not on_secs:
- with open(self.board.control, 'w') as fp:
- fp.write('0\n')
- while True:
- if self.ev_sleep(3600):
- return
- while True:
- for s_time, val in ((on_secs, 255), (off_secs, 0)):
- if self.debug:
- msg_r(('^', '+')[bool(val)])
- with open(self.board.control, 'w') as fp:
- fp.write(f'{val}\n')
- if self.ev_sleep(s_time):
- if self.debug:
- msg('\n')
- return
- def set(self, state):
- lt = namedtuple('led_timings', ['on_secs', 'off_secs'])
- timings = {
- 'off': lt(0, 0),
- 'standby': lt(2.2, 0.2),
- 'busy': lt(0.06, 0.06),
- 'error': lt(0.5, 0.5)}
- if self.led_thread:
- self.ev.set()
- self.led_thread.join()
- self.ev.clear()
- if self.debug:
- msg(f'Setting LED state to {state!r}')
- self.led_thread = threading.Thread(
- target = self.led_loop,
- name = 'LED loop',
- args = timings[state],
- daemon = True)
- self.led_thread.start()
- def stop(self):
- self.set('off')
- self.ev.set()
- self.led_thread.join()
- if self.debug:
- msg('Stopping LED')
- if self.board.trigger:
- with open(self.board.trigger, 'w') as fp:
- fp.write(self.board.trigger_reset + '\n')
|