From 8423f6d2d787e99affb52b0cd9cfa1a0c4df4888 Mon Sep 17 00:00:00 2001 From: "Peter D. Gray" Date: Wed, 20 Dec 2023 16:15:01 -0500 Subject: [PATCH] introduce battery.py --- shared/actions.py | 2 +- shared/battery.py | 175 ++++++++++++++++++++++++++++++++++++ shared/flow.py | 11 +-- shared/lcd_display.py | 23 +++-- shared/main.py | 2 +- shared/manifest_q1.py | 1 + shared/q1.py | 132 +-------------------------- unix/headless.py | 2 + unix/sim_boot.py | 2 +- unix/variant/manifest.py | 2 +- unix/variant/sim_battery.py | 16 ++++ unix/variant/sim_q1.py | 16 ---- 12 files changed, 216 insertions(+), 168 deletions(-) create mode 100644 shared/battery.py create mode 100644 unix/variant/sim_battery.py delete mode 100644 unix/variant/sim_q1.py diff --git a/shared/actions.py b/shared/actions.py index 006489d4..57933088 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -792,7 +792,7 @@ async def start_login_sequence(): except: pass if version.has_battery: - from q1 import batt_idle_logout + from battery import batt_idle_logout IMPT.start_task('b-idle', batt_idle_logout()) # maybe show a nickname before we do anything diff --git a/shared/battery.py b/shared/battery.py new file mode 100644 index 00000000..06f948cb --- /dev/null +++ b/shared/battery.py @@ -0,0 +1,175 @@ +# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# battery.py - Q-specific code related to batteries and monitoring that. +# +# NOTE: Lots of hardware overlap with Mk4, so see mk4.py too! +# +from imptask import IMPT +import uasyncio as asyncio +from machine import Pin + +# value must exist in battery_idle_timeout_chooser() choices +DEFAULT_BATT_IDLE_TIMEOUT = const(30*60) + +# 0..255 brightness value for when on batteries +DEFAULT_BATT_BRIGHTNESS = const(200) + +nbat_pin = Pin('NOT_BATTERY') + +def setup_battery(): + # setup and monitor things. + setup_adc() + + IMPT.start_task('battery', batt_monitor_task()) + + #nbat_pin.irq(_nbatt_irq, Pin.IRQ_FALLING|Pin.IRQ_RISING) + +async def batt_monitor_task(): + last_lvl = None + + while 1: + # slowly track battery level + await asyncio.sleep(5) + + lvl = get_batt_threshold() + + if lvl != last_lvl: + from glob import dis + + dis.draw_status(bat=lvl) + last_lvl = lvl + +def setup_adc(): + # configure VREF source as internally generated + import uctypes + + VREF_LAYOUT = { + "CSR": 0 | uctypes.UINT32, + "CCR": 4 | uctypes.UINT32, + } + VREFBUF_CSR = 0x40010030 + + vref = uctypes.struct(VREFBUF_CSR, VREF_LAYOUT) + vref.CSR = 0x05 # VRS=1, HIZ=0, ENVR=1 2.5v ref + + # could delay here until reads back as 0x9 (VRR==1) + # but no need + +def get_batt_level(): + # return voltage from batteries, as a float + # - will only work on battery power, else return None + # - reads a bit low (3.3v in => 2.7v here) + try: + from machine import ADC, Pin + except ImportError: + # simulator + return 2.99 + + if nbat_pin() == 1: + # not getting power from batteries, so don't know level + return None + + adc = ADC(Pin('VIN_SENSE')) + avg = sum(adc.read_u16() for i in range(13)) / 13.0 + + return round((avg / 65535.0) * 2.5 * 2, 1) + +def get_batt_threshold(): + # return 0=empty, 1=low, 2=75% 3=full or None if no bat + volts = get_batt_level() + if volts is None: + return None + if volts <= 2.1: + return 0 + if volts <= 3.0: + return 1 + if volts <= 4.0: + return 2 + return 3 + +def brightness_chooser(): + from glob import settings, dis + + bright = settings.get('bright', DEFAULT_BATT_BRIGHTNESS) + + ch = [ '25%', '50%', '60%', '70%', '80% (default)', '90%','100%'] + va = [ 64, 128, 153, 180, DEFAULT_BATT_BRIGHTNESS, 230, 255] + + try: + which = va.index(bright) + except ValueError: + which = DEFAULT_BATT_BRIGHTNESS + + def _set(idx, text): + settings.set('bright', va[idx]) + dis.set_lcd_brightness() + + def _preview(idx): + dis.set_lcd_brightness(tmp_override=va[idx]) + + return which, ch, _set, _preview + +def battery_idle_timeout_chooser(): + from glob import settings + + timeout = settings.get('batt_to', DEFAULT_BATT_IDLE_TIMEOUT) # in seconds + + ch = [ + ' 30 seconds', + ' 60 seconds', + ' 2 minutes', + ' 5 minutes', + '10 minutes', + '15 minutes', + '30 minutes', + ' 1 hour', + ' 4 hours', + ' Never' ] + va = [ 30, 60, 2*60, 5*60, 10*60, 15*60, 30*60, + 3600, 4*3600, 0 ] + + try: + which = va.index(timeout) + except ValueError: + which = 0 + + def _set(idx, text): + settings.set('batt_to', va[idx]) + + return which, ch, _set + + +async def batt_idle_logout(): + # long-running task to power down when idle too long. + # - even before login + import glob + from uasyncio import sleep_ms + from glob import settings + import utime + + while not glob.hsm_active: + await sleep_ms(5000) + + if get_batt_level() == None: + # on USB power + continue + + last = glob.numpad.last_event_time + if not last: + continue + + dt = utime.ticks_diff(utime.ticks_ms(), last) + + # they may have changed setting recently + timeout = settings.get('batt_to', DEFAULT_BATT_IDLE_TIMEOUT)*1000 # ms + + if timeout and dt > timeout: + # user has been idle for too long: do a logout (and powerdown) + print("Batt Idle!") + + from actions import logout_now + await logout_now() + return # not reached + + +# EOF diff --git a/shared/flow.py b/shared/flow.py index 17210537..188a7d1b 100644 --- a/shared/flow.py +++ b/shared/flow.py @@ -34,10 +34,9 @@ else: hsm_feature = lambda: False make_users_menu = lambda: [] -trick_pin_menu = TrickPinMenu.make_menu - +# Battery related items if version.has_battery: - from q1 import battery_idle_timeout_chooser, brightness_chooser + from battery import battery_idle_timeout_chooser, brightness_chooser else: battery_idle_timeout_chooser = None brightness_chooser = None @@ -93,7 +92,7 @@ with the Coldcard.''', LoginPrefsMenu = [ # xxxxxxxxxxxxxxxx MenuItem('Change Main PIN', f=main_pin_changer), - NonDefaultMenuItem('Trick PINs', 'tp', menu=trick_pin_menu), + NonDefaultMenuItem('Trick PINs', 'tp', menu=TrickPinMenu.make_menu), NonDefaultMenuItem('Set Nickname', 'nick', prelogin=True, f=pick_nickname), NonDefaultMenuItem('Scramble Keys', 'rngk', prelogin=True, f=pick_scramble), NonDefaultMenuItem('Kill Key', 'kbtn', prelogin=True, f=pick_killkey), @@ -349,9 +348,11 @@ EmptyWallet = [ NormalSystem = [ # xxxxxxxxxxxxxxxx MenuItem('Ready To Sign', f=ready2sign, shortcut='r'), + MenuItem('Passphrase', f=start_b39_pw, predicate=bip39_passphrase_active, shortcut='p'), MenuItem('Scan Any QR Code', predicate=lambda: version.has_qr, shortcut=KEY_QR, f=scan_any_qr, arg=(False, True)), - MenuItem('Passphrase', f=start_b39_pw, predicate=bip39_passphrase_active, shortcut='p'), + MenuItem('NFC Tools', predicate=lambda: nfc_enabled() and version.has_qwerty, + menu=NFCToolsMenu, shortcut=KEY_NFC), MenuItem('Start HSM Mode', f=start_hsm_menu_item, predicate=hsm_policy_available), MenuItem("Address Explorer", f=address_explore, shortcut='x'), MenuItem('Type Passwords', f=password_entry, shortcut='t', diff --git a/shared/lcd_display.py b/shared/lcd_display.py index 03517d7e..42157188 100644 --- a/shared/lcd_display.py +++ b/shared/lcd_display.py @@ -64,14 +64,10 @@ def get_sys_status(): # Read current values for all status-bar items # - normally we update as we go along. # - return a dict - from q1 import get_batt_threshold + from battery import get_batt_threshold rv = dict(shift=0, caps=0, symbol=0) - b = get_batt_threshold() - if b is None: - rv['plugged'] = True - else: - rv['bat'] = b + rv['bat'] = get_batt_threshold() from stash import bip39_passphrase rv['bip39'] = int(bool(bip39_passphrase)) @@ -149,7 +145,7 @@ class Display: # Call when battery changes state, or if you want max for a bit (QR display) # - call w/o args to get back to state we're supposed to be in. from glob import settings - from q1 import get_batt_threshold, DEFAULT_BATT_BRIGHTNESS + from battery import get_batt_threshold, DEFAULT_BATT_BRIGHTNESS if tmp_override is not None: self.dis.backlight.intensity(tmp_override) @@ -182,11 +178,12 @@ class Display: b_x = 290 if 'bat' in kws: - self.image(b_x, 0, 'bat_%d' % kws['bat']) - self.set_lcd_brightness(True) - if 'plugged' in kws: - self.image(b_x, 0, 'plugged') - self.set_lcd_brightness(False) + if kws['bat'] is None: + self.image(b_x, 0, 'plugged') + self.set_lcd_brightness(False) + else: + self.image(b_x, 0, 'bat_%d' % kws['bat']) + self.set_lcd_brightness(True) if 'bip39' in kws: self.image(102, 0, 'bip39_%d' % kws['bip39']) @@ -502,7 +499,7 @@ class Display: def set_brightness(self, val): # - was only used by HSM ux code - # - QR code display brightness could be done in show_qr_data() + # - QR code display brightness is done in show_qr_data() now # - see self.set_lcd_brightness() return diff --git a/shared/main.py b/shared/main.py index 58aa980b..81729162 100644 --- a/shared/main.py +++ b/shared/main.py @@ -16,7 +16,7 @@ assert not glob.dis, "main reimport" # this makes the GC run when larger objects are free in an attempt to reduce fragmentation. gc.threshold(4096) -# useful for debug: start serial port early +# useful for debug: start serial port early, when possible try: from h import * import ckcc diff --git a/shared/manifest_q1.py b/shared/manifest_q1.py index 42a54ed3..299b8460 100644 --- a/shared/manifest_q1.py +++ b/shared/manifest_q1.py @@ -15,6 +15,7 @@ freeze_as_mpy('', [ 'ndef.py', 'trick_pins.py', 'ux_q1.py', + 'battery.py', ], opt=0) # Optimize data-like files, since no need to debug them. diff --git a/shared/q1.py b/shared/q1.py index 18803a02..5fe0fe97 100644 --- a/shared/q1.py +++ b/shared/q1.py @@ -33,137 +33,9 @@ def init0(): except: pass try: - setup_adc() + import battery + battery.setup_battery() #print('Batt volt: %s' % get_batt_level()) except: pass -def setup_adc(): - # configure VREF source as internal 2.5v - VREF_LAYOUT = { - "CSR": 0 | uctypes.UINT32, - "CCR": 4 | uctypes.UINT32, - } - VREFBUF_CSR = 0x40010030 - - vref = uctypes.struct(VREFBUF_CSR, VREF_LAYOUT) - vref.CSR = 0x01 # VRS=0, HIZ=0, ENVR=1 - - # could delay here until reads back as 0x9 (VRR==1) - # but no need - -def get_batt_level(): - # return voltage from batteries, as a float - # - will only work on battery power, else return None - try: - from machine import ADC, Pin - except ImportError: - # simulator - return 2.99 - - if Pin('NOT_BATTERY')() == 1: - # not getting power from batteries, so don't know level - return None - - adc = ADC(Pin('VIN_SENSE')) - avg = sum(adc.read_u16() for i in range(10)) / 10.0 - - return round((avg / 65535.0) * 2.5 * 2, 2) - -def get_batt_threshold(): - # return 0=empty, 1=low, 2=75% 3=full or None if no bat - # TODO check these ranges - volts = get_batt_level() - if volts is None: - return None - if volts <= 3.0: - return 0 - if volts <= 3.5: - return 1 - return 3 if volts > 4.5 else 2 - -def brightness_chooser(): - from glob import settings, dis - - bright = settings.get('bright', DEFAULT_BATT_BRIGHTNESS) - - ch = [ '25%', '50%', '60%', '70%', '80% (default)', '90%','100%'] - va = [ 64, 128, 153, 180, DEFAULT_BATT_BRIGHTNESS, 230, 255] - - try: - which = va.index(bright) - except ValueError: - which = DEFAULT_BATT_BRIGHTNESS - - def _set(idx, text): - settings.set('bright', va[idx]) - dis.set_lcd_brightness() - - def _preview(idx): - dis.set_lcd_brightness(tmp_override=va[idx]) - - return which, ch, _set, _preview - -def battery_idle_timeout_chooser(): - from glob import settings - - timeout = settings.get('batt_to', DEFAULT_BATT_IDLE_TIMEOUT) # in seconds - - ch = [ - ' 30 seconds', - ' 60 seconds', - ' 2 minutes', - ' 5 minutes', - '10 minutes', - '15 minutes', - '30 minutes', - ' 1 hour', - ' 4 hours', - ' Never' ] - va = [ 30, 60, 2*60, 5*60, 10*60, 15*60, 30*60, - 3600, 4*3600, 0 ] - - try: - which = va.index(timeout) - except ValueError: - which = 0 - - def _set(idx, text): - settings.set('batt_to', va[idx]) - - return which, ch, _set - - -async def batt_idle_logout(): - # long-running task to power down when idle too long. - # - even before login - import glob - from uasyncio import sleep_ms - from glob import settings - import utime - - while not glob.hsm_active: - await sleep_ms(5000) - - if get_batt_level() == None: - # on USB power - continue - - last = glob.numpad.last_event_time - if not last: - continue - - dt = utime.ticks_diff(utime.ticks_ms(), last) - - # they may have changed setting recently - timeout = settings.get('batt_to', DEFAULT_BATT_IDLE_TIMEOUT)*1000 # ms - - if timeout and dt > timeout: - # user has been idle for too long: do a logout (and powerdown) - print("Batt Idle!") - - from actions import logout_now - await logout_now() - return # not reached - - # EOF diff --git a/unix/headless.py b/unix/headless.py index c8d2fb39..48fc1d14 100755 --- a/unix/headless.py +++ b/unix/headless.py @@ -35,6 +35,8 @@ def start(): import atexit atexit.register(cleanup) + # XXX obsolete w/ Q changes? + os.chdir('./work') cc_cmd = ['../coldcard-mpy', '-X', 'heapsize=9m', diff --git a/unix/sim_boot.py b/unix/sim_boot.py index 8443de7d..9b99a839 100644 --- a/unix/sim_boot.py +++ b/unix/sim_boot.py @@ -33,7 +33,7 @@ if '--sflash' not in sys.argv: # Install various hacks and workarounds import mk4 import sim_mk4 -import sim_q1 +import sim_battery import sim_psram import sim_vdisk diff --git a/unix/variant/manifest.py b/unix/variant/manifest.py index 84d91c53..4569d65b 100644 --- a/unix/variant/manifest.py +++ b/unix/variant/manifest.py @@ -8,7 +8,7 @@ freeze_as_mpy('', [ 'os.py', 'pyb.py', 'sim_mk4.py', - 'sim_q1.py', + 'sim_battery.py', 'sim_scanner.py', 'sim_nfc.py', 'sim_psram.py', diff --git a/unix/variant/sim_battery.py b/unix/variant/sim_battery.py new file mode 100644 index 00000000..3aca31cc --- /dev/null +++ b/unix/variant/sim_battery.py @@ -0,0 +1,16 @@ +# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC. +# +# sim_battery.py - Simulate Q specific code related to batteries. +# +import battery + +battery.setup_battery = lambda: None + +battery.setup_adc = lambda: None + +def mock_get_batt_level(): + return 3.69 + +battery.get_batt_level = mock_get_batt_level + +# EOF diff --git a/unix/variant/sim_q1.py b/unix/variant/sim_q1.py deleted file mode 100644 index 06648818..00000000 --- a/unix/variant/sim_q1.py +++ /dev/null @@ -1,16 +0,0 @@ -# (c) Copyright 2023 by Coinkite Inc. This file is covered by license found in COPYING-CC. -# -# sim_q14.py - Simulate Q1 specific code, not needed on other devices. -# -# - shared/q1.py calls mk4.init0 so no need to replace that -# -import q1 - -q1.setup_adc = lambda: None - -def mock_get_batt_level(): - return 3.69 - -q1.get_batt_level = mock_get_batt_level - -# EOF