457 lines
13 KiB
Python
457 lines
13 KiB
Python
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
|
|
#
|
|
# files.py - MicroSD and related functions.
|
|
#
|
|
import pyb, ckcc, os, sys, utime, glob
|
|
from uerrno import ENOENT
|
|
|
|
async def needs_microsd():
|
|
# Standard msg shown if no SD card detected when we need one.
|
|
from ux import ux_show_story
|
|
return await ux_show_story("Please insert a MicroSD card before attempting this operation.")
|
|
|
|
def is_dir(fname):
|
|
if os.stat(fname)[0] & 0x4000:
|
|
return True
|
|
return False
|
|
|
|
def _try_microsd():
|
|
# Power up, mount the SD card, return False if we can't for some reason.
|
|
# - we know card is there already, and mux set appropriately
|
|
#
|
|
sd = pyb.SDCard()
|
|
|
|
if ckcc.is_simulator():
|
|
return True
|
|
|
|
try:
|
|
# already mounted and ready?
|
|
st = os.statvfs('/sd')
|
|
return True
|
|
except OSError:
|
|
pass
|
|
|
|
try:
|
|
sd.power(1)
|
|
os.mount(sd, '/sd', readonly=0, mkfs=0)
|
|
st = os.statvfs('/sd')
|
|
|
|
return True
|
|
|
|
except OSError as exc:
|
|
# corrupt or unformated SD card (or something)
|
|
#sys.print_exception(exc)
|
|
return False
|
|
|
|
def wipe_flash_filesystem(do_rebuild=True):
|
|
# erase and re-format the flash filesystem (/flash/**)
|
|
import ckcc, pyb
|
|
from glob import dis
|
|
|
|
dis.fullscreen('Erasing...')
|
|
os.umount('/flash')
|
|
|
|
# from extmod/vfs.h
|
|
BP_IOCTL_SEC_COUNT = (4)
|
|
BP_IOCTL_SEC_SIZE = (5)
|
|
|
|
# block-level erase
|
|
fl = pyb.Flash(start=0) # start=0 does magic things
|
|
bsize = fl.ioctl(BP_IOCTL_SEC_SIZE, 0)
|
|
assert bsize == 512
|
|
bcount = fl.ioctl(BP_IOCTL_SEC_COUNT, 0)
|
|
|
|
blk = bytearray(bsize)
|
|
ckcc.rng_bytes(blk)
|
|
|
|
for n in range(bcount):
|
|
fl.writeblocks(n, blk)
|
|
dis.progress_sofar(n, bcount)
|
|
|
|
if not do_rebuild:
|
|
return
|
|
|
|
# rebuild and mount /flash
|
|
dis.fullscreen('Rebuilding...')
|
|
|
|
# no need to erase, we just put new FS on top
|
|
import mk4
|
|
mk4.make_flash_fs()
|
|
|
|
# re-store current settings
|
|
from glob import settings
|
|
settings.save()
|
|
|
|
def wipe_microsd_card():
|
|
# Erase and re-format SD card. Not secure erase, because that is too slow.
|
|
import ckcc, pyb
|
|
from glob import dis
|
|
from version import num_sd_slots
|
|
|
|
if not CardSlot.is_inserted():
|
|
return
|
|
|
|
try:
|
|
# just in case
|
|
os.umount('/sd')
|
|
except:
|
|
pass
|
|
|
|
if num_sd_slots == 2:
|
|
# pick slot with a card or default A if both installed
|
|
slot_b = (CardSlot.sd_detect2() == 0)
|
|
if CardSlot.sd_detect() == 0:
|
|
slot_b = False
|
|
CardSlot.mux(1 if slot_b else 0) # top slot = A
|
|
active_led = CardSlot.active_led2 if slot_b else CardSlot.active_led1
|
|
else:
|
|
active_led = CardSlot.active_led
|
|
slot_b = False
|
|
|
|
try:
|
|
active_led.on()
|
|
|
|
sd = pyb.SDCard()
|
|
assert sd
|
|
|
|
# power cycle so card details (like size) are re-read from current card
|
|
sd.power(0)
|
|
sd.power(1)
|
|
|
|
dis.fullscreen('Formatting...')
|
|
cutoff = 1024 # arbitrary
|
|
blk = bytearray(512)
|
|
ckcc.rng_bytes(blk)
|
|
|
|
for bnum in range(cutoff):
|
|
sd.writeblocks(bnum, blk)
|
|
dis.progress_bar_show(bnum/cutoff)
|
|
|
|
# remount, with newfs option -- this does the formating (very quick)
|
|
os.mount(sd, '/sd', readonly=0, mkfs=1)
|
|
|
|
# done, cleanup
|
|
os.umount('/sd')
|
|
finally:
|
|
active_led.off()
|
|
|
|
# important: turn off power
|
|
sd = pyb.SDCard()
|
|
sd.power(0)
|
|
|
|
if slot_b:
|
|
# optional?
|
|
CardSlot.mux(0) # top slot = A
|
|
|
|
def dfu_parse(fd):
|
|
# do just a little parsing of DFU headers, to find start/length of main binary
|
|
# - not trying to support anything but what ../stm32/Makefile will generate
|
|
# - see external/micropython/tools/pydfu.py for details
|
|
# - works sequentially only
|
|
import struct
|
|
from ucollections import namedtuple
|
|
|
|
fd.seek(0)
|
|
|
|
def consume(xfd, tname, fmt, names):
|
|
# Parses the struct defined by `fmt` from `data`, stores the parsed fields
|
|
# into a named tuple using `names`. Returns the named tuple.
|
|
size = struct.calcsize(fmt)
|
|
here = xfd.read(size)
|
|
ty = namedtuple(tname, names.split())
|
|
values = struct.unpack(fmt, here)
|
|
return ty(*values)
|
|
|
|
dfu_prefix = consume(fd, 'DFU', '<5sBIB', 'signature version size targets')
|
|
|
|
#print('dfu: ' + repr(dfu_prefix))
|
|
|
|
assert dfu_prefix.signature == b'DfuSe', "Not a DFU file (bad magic)"
|
|
|
|
for idx in range(dfu_prefix.targets):
|
|
|
|
prefix = consume(fd, 'Target', '<6sBI255s2I',
|
|
'signature altsetting named name size elements')
|
|
|
|
#print("target%d: %r" % (idx, prefix))
|
|
|
|
for ei in range(prefix.elements):
|
|
# Decode target prefix
|
|
# < little endian
|
|
# I uint32_t element address
|
|
# I uint32_t element size
|
|
elem = consume(fd, 'Element', '<2I', 'addr size')
|
|
|
|
#print("target%d: %r" % (ei, elem))
|
|
|
|
# assume bootloader at least 32k, and targeting flash.
|
|
assert elem.addr >= 0x8008000, "Bad address?"
|
|
|
|
return fd.tell(), elem.size
|
|
|
|
|
|
class CardMissingError(RuntimeError):
|
|
pass
|
|
|
|
class CardSlot:
|
|
# Manage access to the SDCard h/w resources
|
|
last_change = None
|
|
active_led = None
|
|
|
|
@classmethod
|
|
def setup(cls):
|
|
# Watch the SD card-detect signal line... but very noisy
|
|
# - this is called a few seconds after system startup
|
|
|
|
from pyb import ExtInt
|
|
from machine import Pin
|
|
from version import num_sd_slots
|
|
|
|
def card_change(_):
|
|
# Careful: these can come fast and furious!
|
|
cls.last_change = utime.ticks_ms()
|
|
|
|
cls.last_change = utime.ticks_ms()
|
|
|
|
if num_sd_slots == 2:
|
|
# Q has luxurious dual slots
|
|
cls.mux = Pin('SD_MUX', Pin.OUT, value=0)
|
|
cls.sd_detect2 = Pin('SD_DETECT2', Pin.IN, pull=Pin.PULL_UP)
|
|
cls.irq2 = ExtInt(cls.sd_detect2, ExtInt.IRQ_RISING_FALLING, Pin.PULL_UP, card_change)
|
|
|
|
cls.active_led2 = Pin('SD_ACTIVE2', Pin.OUT)
|
|
cls.active_led1 = Pin('SD_ACTIVE', Pin.OUT)
|
|
cls.active_led = cls.active_led1
|
|
else:
|
|
cls.mux = None
|
|
cls.active_led = Pin('SD_ACTIVE', Pin.OUT)
|
|
|
|
cls.sd_detect = Pin('SD_DETECT', Pin.IN, pull=Pin.PULL_UP)
|
|
cls.irq = ExtInt(cls.sd_detect, ExtInt.IRQ_RISING_FALLING, Pin.PULL_UP, card_change)
|
|
|
|
@classmethod
|
|
def is_inserted(cls):
|
|
# Sense is inverted on Mk4, and true on Q.
|
|
if cls.mux:
|
|
return (cls.sd_detect() == 0) or (cls.sd_detect2() == 0)
|
|
else:
|
|
return cls.sd_detect() == 1
|
|
|
|
@classmethod
|
|
def both_inserted(cls):
|
|
# Predicate: Is there a card in both slots? If only one slot exists, return None
|
|
if cls.mux:
|
|
return (cls.sd_detect() == 0) and (cls.sd_detect2() == 0)
|
|
# return None (implicit)
|
|
|
|
def __init__(self, force_vdisk=False, readonly=False, slot_b=None):
|
|
self.mountpt = None
|
|
self.force_vdisk = force_vdisk
|
|
self.readonly = readonly
|
|
self.wrote_files = set()
|
|
if self.mux:
|
|
use_b_slot = False # default A if both installed, or none
|
|
sa, sb = self.sd_detect() == 0, self.sd_detect2() == 0
|
|
if slot_b and sb:
|
|
# user choose B and B is present - proceed
|
|
use_b_slot = True
|
|
elif not slot_b and not sa and sb:
|
|
# user didn't choose B, but A is not present and B is
|
|
# write to B
|
|
use_b_slot = True
|
|
|
|
self.mux(1 if use_b_slot else 0) # top slot = A
|
|
self.active_led = self.active_led2 if use_b_slot else self.active_led1
|
|
|
|
def __enter__(self):
|
|
# maybe use our virtual disk in preference to SD Card
|
|
if glob.VD and (self.force_vdisk or not self.is_inserted()):
|
|
self.mountpt = glob.VD.mount(self.readonly)
|
|
return self
|
|
|
|
if not self.is_inserted():
|
|
# bugfix on Q: #618
|
|
raise CardMissingError
|
|
|
|
# Get ready!
|
|
self.active_led.on()
|
|
|
|
# busy wait for card pin to debounce/settle
|
|
while 1:
|
|
since = utime.ticks_diff(utime.ticks_ms(), self.last_change)
|
|
if since > 50:
|
|
break
|
|
utime.sleep_ms(5)
|
|
|
|
# attempt to use micro SD
|
|
ok = _try_microsd()
|
|
|
|
if not ok:
|
|
self._recover()
|
|
|
|
raise CardMissingError
|
|
|
|
self.mountpt = self.get_sd_root() # probably /sd
|
|
|
|
return self
|
|
|
|
def __exit__(self, *a):
|
|
if self.mountpt == self.get_sd_root():
|
|
self._recover()
|
|
elif glob.VD:
|
|
glob.VD.unmount(self.wrote_files, self.readonly)
|
|
|
|
self.mountpt = None
|
|
|
|
# just in case?
|
|
if self.mux:
|
|
self.mux(0)
|
|
|
|
return False
|
|
|
|
def open(self, fname, mode='r', **kw):
|
|
# open a file for read/write
|
|
# - track new files for virtdisk case
|
|
if 'w' in mode:
|
|
assert not self.readonly
|
|
self.wrote_files.add(fname)
|
|
|
|
return open(fname, mode, **kw)
|
|
|
|
def _recover(self):
|
|
# done using the microSD -- unpower it
|
|
self.active_led.off()
|
|
|
|
try:
|
|
assert self.mountpt == '/sd'
|
|
os.umount('/sd')
|
|
except: pass
|
|
|
|
# turn off power to slot
|
|
sd = pyb.SDCard()
|
|
sd.power(0)
|
|
|
|
def get_sd_root(self):
|
|
# get the path to the SD card
|
|
if ckcc.is_simulator():
|
|
return ckcc.get_sim_root_dirs()[1]
|
|
else:
|
|
return '/sd'
|
|
|
|
def get_paths(self):
|
|
# (full) paths to check on the card
|
|
#root = self.get_sd_root()
|
|
#return [root]
|
|
return [self.mountpt]
|
|
|
|
def is_dir(self, fname):
|
|
return is_dir(self.abs_path(fname))
|
|
|
|
def abs_path(self, fname):
|
|
return self.mountpt + "/" + fname
|
|
|
|
def get_id_hash(self):
|
|
# hash over card config and serial # details
|
|
# - stupidly it's over the repr of a functions' result
|
|
import ngu
|
|
|
|
info = pyb.SDCard().info()
|
|
assert info
|
|
|
|
if len(info) == 3:
|
|
# expected in v4
|
|
csd_cid = pyb.SDCard().ident()
|
|
info = tuple(list(info) + list(csd_cid))
|
|
|
|
return ngu.hash.sha256s(repr(info))
|
|
|
|
@staticmethod
|
|
def exists(fname):
|
|
try:
|
|
os.stat(fname)
|
|
except OSError as e:
|
|
if e.args[0] == ENOENT:
|
|
return False
|
|
return True
|
|
|
|
def pick_filename(self, pattern, path=None, overwrite=False):
|
|
# given foo.txt, return a full path to filesystem, AND
|
|
# a nice shortened version of the filename for display to user
|
|
# - assuming we will write to it, so cannot exist
|
|
# - return None,None if no SD card or can't mount, etc.
|
|
# - no UI here please
|
|
import ure
|
|
|
|
assert self.mountpt # else: we got used out of context mgr
|
|
|
|
# put it back where we found it
|
|
path = path or (self.mountpt + '/')
|
|
|
|
assert '/' not in pattern
|
|
assert '.' in pattern
|
|
|
|
basename, ext = pattern.rsplit('.', 1)
|
|
ext = '.' + ext
|
|
|
|
# try w/o any number first
|
|
fname = path + basename + ext
|
|
|
|
if overwrite or not self.exists(fname):
|
|
return fname, basename + ext
|
|
|
|
# look for existing numbered files, even if some are deleted, and pick next
|
|
# highest filename
|
|
highest = 1
|
|
pat = ure.compile(basename + r'-(\d+)' + ext)
|
|
|
|
for fn in os.listdir(path):
|
|
m = pat.match(fn)
|
|
if not m: continue
|
|
highest = max(highest, int(m.group(1)))
|
|
|
|
fname = path + basename + ('-%d'% (highest+1)) + ext
|
|
|
|
return fname, fname[len(path):]
|
|
|
|
def securely_blank_file(self, full_path):
|
|
# input PSBT file no longer required; so delete it
|
|
# - blank with zeros
|
|
# - rename to garbage (to hide filename after undelete)
|
|
# - delete
|
|
# - ok if file missing already (card maybe have been swapped)
|
|
#
|
|
# NOTE: we know the FAT filesystem code is simple, see
|
|
# ../external/micropython/extmod/vfs_fat.[ch]
|
|
|
|
self.wrote_files.discard(full_path)
|
|
|
|
path, basename = full_path.rsplit('/', 1)
|
|
|
|
try:
|
|
blk = bytes(64)
|
|
|
|
with open(full_path, 'r+b') as fd:
|
|
size = fd.seek(0, 2)
|
|
fd.seek(0)
|
|
|
|
# blank it
|
|
for i in range((size // len(blk)) + 1):
|
|
fd.write(blk)
|
|
|
|
assert fd.seek(0, 1) >= size
|
|
|
|
# probably pointless, but why not:
|
|
os.sync()
|
|
|
|
except OSError as exc:
|
|
# missing file is okay
|
|
if exc.args[0] == ENOENT: return
|
|
raise
|
|
|
|
# rename it and delete
|
|
new_name = path + '/' + ('x'*len(basename))
|
|
os.rename(full_path, new_name)
|
|
os.remove(new_name)
|
|
|
|
# EOF
|