4 Commits 3bcbde514c ... b4898b9aef

Author SHA1 Message Date
  The MMGen Project b4898b9aef add LED support for Rock 5, Banana Pi F3 11 months ago
  The MMGen Project fb17a3b2c9 led.py: improve initialization logic, add interactive test 11 months ago
  The MMGen Project e0c93606e1 led.py: make `binfo` a class 11 months ago
  The MMGen Project d39e1e5cd4 led.py: `binfo.status` -> `binfo.control` 11 months ago
4 changed files with 145 additions and 55 deletions
  1. 1 1
      mmgen/data/version
  2. 85 52
      mmgen/led.py
  3. 2 2
      test/cmdtest_d/ct_autosign.py
  4. 57 0
      test/misc/led.py

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-15.1.dev10
+15.1.dev11

+ 85 - 52
mmgen/led.py

@@ -26,36 +26,49 @@ 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:
 
-	binfo = namedtuple('board_info', ['name', 'status', 'trigger', 'trigger_states'])
+	class binfo(Lockable):
+		_reset_ok = ('trigger_reset',)
+
+		def __init__(self, name, control, trigger=None, trigger_dfl='heartbeat', trigger_disable='none'):
+			self.name = name
+			self.control = control
+			self.trigger = trigger
+			self.trigger_dfl = trigger_dfl
+			self.trigger_reset = trigger_dfl
+			self.trigger_disable = trigger_disable
+
 	boards = {
 		'raspi_pi': binfo(
 			name    = 'Raspberry Pi',
-			status  = '/sys/class/leds/led0/brightness',
+			control = '/sys/class/leds/led0/brightness',
 			trigger = '/sys/class/leds/led0/trigger',
-			trigger_states = ('none', 'mmc0')),
+			trigger_dfl = 'mmc0'),
 		'orange_pi': binfo(
 			name    = 'Orange Pi (Armbian)',
-			status  = '/sys/class/leds/orangepi:red:status/brightness',
-			trigger = None,
-			trigger_states = None),
+			control = '/sys/class/leds/orangepi:red:status/brightness'),
 		'orange_pi_5': binfo(
 			name    = 'Orange Pi 5 (Armbian)',
-			status  = '/sys/class/leds/status_led/brightness',
-			trigger = None,
-			trigger_states = None),
+			control = '/sys/class/leds/status_led/brightness'),
 		'rock_pi': binfo(
 			name    = 'Rock Pi (Armbian)',
-			status  = '/sys/class/leds/status/brightness',
-			trigger = '/sys/class/leds/status/trigger',
-			trigger_states = ('none', 'heartbeat')),
+			control = '/sys/class/leds/status/brightness',
+			trigger = '/sys/class/leds/status/trigger'),
+		'rock_5': binfo(
+			name    = 'Rock 5 (Armbian)',
+			control = '/sys/class/leds/user-led2/brightness',
+			trigger = '/sys/class/leds/user-led2/trigger'),
+		'banana_pi_f3': binfo(
+			name    = 'Banana Pi F3 (Armbian)',
+			control = '/sys/class/leds/sys-led/brightness',
+			trigger = '/sys/class/leds/sys-led/trigger'),
 		'dummy': binfo(
-			name    = 'Fake',
-			status  = '/tmp/led_status',
-			trigger = '/tmp/led_trigger',
-			trigger_states = ('none', 'original_value')),
+			name    = 'Fake Board',
+			control = '/tmp/led_status',
+			trigger = '/tmp/led_trigger'),
 	}
 
 	def __init__(self, enabled, simulate=False, debug=False):
@@ -74,58 +87,78 @@ class LEDControl:
 			if board_id == 'dummy' and not simulate:
 				continue
 			try:
-				os.stat(board.status)
-			except:
-				pass
-			else:
+				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.status}\n  Trigger file: {board.trigger}')
-
-		def check_access(fn, desc, init_val=None):
-
-			def write_init_val(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')
-
+			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(init_val)
+				write_init_val(fn, init_val)
 			except PermissionError:
-				cmd = f'sudo chmod 0666 {fn}'
-				if have_sudo():
-					msg(orange(f'Running ‘{cmd}’'))
-					run(cmd.split(), check=True)
-					write_init_val(init_val)
-				else:
-					msg('\n{}\n{}\n{}'.format(
-						blue(f'You do not have access to the {desc} file'),
-						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)
-
-		check_access(board.status, desc='status LED control')
+				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:
-			check_access(board.trigger, desc='LED trigger', init_val=board.trigger_states[0])
+			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.status, 'w') as fp:
+		with open(db.control, 'w') as fp:
 			fp.write('0\n')
 		with open(db.trigger, 'w') as fp:
-			fp.write(db.trigger_states[1]+'\n')
+			fp.write(db.trigger_dfl + '\n')
 
 	def noop(self, *args, **kwargs):
 		pass
@@ -140,7 +173,7 @@ class LEDControl:
 			msg(f'led_loop({on_secs}, {off_secs})')
 
 		if not on_secs:
-			with open(self.board.status, 'w') as fp:
+			with open(self.board.control, 'w') as fp:
 				fp.write('0\n')
 			while True:
 				if self.ev_sleep(3600):
@@ -150,7 +183,7 @@ class LEDControl:
 			for s_time, val in ((on_secs, 255), (off_secs, 0)):
 				if self.debug:
 					msg_r(('^', '+')[bool(val)])
-				with open(self.board.status, 'w') as fp:
+				with open(self.board.control, 'w') as fp:
 					fp.write(f'{val}\n')
 				if self.ev_sleep(s_time):
 					if self.debug:
@@ -192,4 +225,4 @@ class LEDControl:
 
 		if self.board.trigger:
 			with open(self.board.trigger, 'w') as fp:
-				fp.write(self.board.trigger_states[1]+'\n')
+				fp.write(self.board.trigger_reset + '\n')

+ 2 - 2
test/cmdtest_d/ct_autosign.py

@@ -91,7 +91,7 @@ class CmdTestAutosignBase(CmdTestBase):
 	def __del__(self):
 		if hasattr(self, 'have_dummy_control_files'):
 			db = LEDControl.boards['dummy']
-			for fn in (db.status, db.trigger):
+			for fn in (db.control, db.trigger):
 				run(f'sudo rm -f {fn}'.split(), check=True)
 
 		if hasattr(self, 'txdev'):
@@ -609,7 +609,7 @@ class CmdTestAutosign(CmdTestAutosignBase):
 			LEDControl.create_dummy_control_files()
 			db = LEDControl.boards['dummy']
 			usrgrp = {'linux': 'root:root', 'darwin': 'root:wheel'}[sys.platform]
-			for fn in (db.status, db.trigger): # trigger the auto-chmod feature
+			for fn in (db.control, db.trigger): # trigger the auto-chmod feature
 				run(f'sudo chmod 644 {fn}'.split(), check=True)
 				run(f'sudo chown {usrgrp} {fn}'.split(), check=True)
 			self.have_dummy_control_files = True

+ 57 - 0
test/misc/led.py

@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+import sys, os, atexit
+pn = os.path.abspath(os.path.dirname(sys.argv[0]))
+parpar = os.path.dirname(os.path.dirname(pn))
+os.chdir(parpar)
+sys.path[0] = os.curdir
+
+from mmgen.cfg import Config
+from mmgen.util import msg
+from mmgen.ui import keypress_confirm
+from mmgen.led import LEDControl
+
+opts_data = {
+	'text': {
+		'desc': 'Interactively test LED functionality',
+		'usage': 'command',
+		'options': """
+-h, --help     Print this help message
+""",
+	}
+}
+
+cfg = Config(opts_data=opts_data)
+
+def confirm_or_exit(prompt):
+	if not keypress_confirm(cfg, f'{prompt}.  OK?', default_yes=True):
+		msg('Exiting at user request')
+		sys.exit(1)
+
+confirm_or_exit('This script will interactively test LED functionality')
+
+led = LEDControl(enabled=True)
+
+atexit.register(led.stop)
+
+confirm_or_exit('LED should now be turned off')
+
+led.set('busy')
+
+confirm_or_exit('LED should now be signaling busy (rapid flashing)')
+
+led.set('standby')
+
+confirm_or_exit('LED should now be signaling standby (slow flashing)')
+
+led.set('error')
+
+confirm_or_exit('LED should now be signaling error (insistent flashing)')
+
+led.set('off')
+
+confirm_or_exit('LED should now be turned off')
+
+led.stop()
+
+confirm_or_exit(f'LED should now be in its original state [trigger={led.board.trigger_reset}]')