202 lines
9.8 KiB
Python
202 lines
9.8 KiB
Python
"""
|
|
The AbstractBlockDev class is a template for the design of block device classes,
|
|
MicroPython does not actually provide that class.
|
|
|
|
In the type stubs this is implemented as a Protocol, which is a Python 3.8 feature that allows for more flexible type checking.
|
|
|
|
It has been moved to the _mpy_shed type library for convinience, but may be relocated to the `vfs` stub module in the future.
|
|
|
|
- vfs.*(...)
|
|
- os.AbstractBlockDev(...)
|
|
- pyb.AbstractBlockDev(...)
|
|
- pyb.Flash(...)
|
|
- (u)os.AbstractBlockDev(...)
|
|
- esp32.Partition.ioctl
|
|
https://docs.micropython.org/en/v1.24.0/reference/filesystem.html?highlight=abstractblockdev
|
|
- https://docs.micropython.org/en/v1.24.0/library/vfs.html#vfs.AbstractBlockDev
|
|
- https://docs.micropython.org/en/v1.20.0/library/os.html?highlight=abstractblockdev#block-devices
|
|
- https://docs.micropython.org/en/v1.20.0/library/pyb.html?highlight=abstractblockdev#block-devices
|
|
- https://docs.micropython.org/en/latest/library/esp32.html#esp32.Partition.ioctl
|
|
"""
|
|
|
|
from typing import Any, Final, Literal, Protocol, Tuple, Type, final, overload, runtime_checkable
|
|
|
|
from _typeshed import AnyStr_co, Incomplete, structseq
|
|
from typing_extensions import TypeAlias, TypeVar
|
|
|
|
# TODO: improve the typechecking implementation if possible
|
|
_OldAbstractReadOnlyBlockDev: TypeAlias = Any
|
|
_OldAbstractBlockDev: TypeAlias = Any
|
|
|
|
# documented as AbstractBlockDev in the vfs module documentation
|
|
@runtime_checkable
|
|
class _BlockDeviceProtocol(Protocol):
|
|
"""
|
|
Block devices
|
|
-------------
|
|
|
|
A block device is an object which implements the block protocol. This enables a
|
|
device to support MicroPython filesystems. The physical hardware is represented
|
|
by a user defined class. The :class:`AbstractBlockDev` class is a template for
|
|
the design of such a class: MicroPython does not actually provide that class,
|
|
but an actual block device class must implement the methods described below.
|
|
|
|
A concrete implementation of this class will usually allow access to the
|
|
memory-like functionality of a piece of hardware (like flash memory). A block
|
|
device can be formatted to any supported filesystem and mounted using ``os``
|
|
methods.
|
|
|
|
See :ref:`filesystem` for example implementations of block devices using the
|
|
two variants of the block protocol described below.
|
|
|
|
.. _block-device-interface:
|
|
|
|
Simple and extended interface
|
|
.............................
|
|
|
|
There are two compatible signatures for the ``readblocks`` and ``writeblocks``
|
|
methods (see below), in order to support a variety of use cases. A given block
|
|
device may implement one form or the other, or both at the same time. The second
|
|
form (with the offset parameter) is referred to as the "extended interface".
|
|
|
|
Some filesystems (such as littlefs) that require more control over write
|
|
operations, for example writing to sub-block regions without erasing, may require
|
|
that the block device supports the extended interface.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
"""
|
|
Construct a block device object. The parameters to the constructor are
|
|
dependent on the specific block device.
|
|
"""
|
|
|
|
@overload
|
|
def readblocks(self, block_num: int, buf: bytearray, /) -> bool:
|
|
"""
|
|
The first form reads aligned, multiples of blocks.
|
|
Starting at the block given by the index *block_num*, read blocks from
|
|
the device into *buf* (an array of bytes).
|
|
The number of blocks to read is given by the length of *buf*,
|
|
which will be a multiple of the block size.
|
|
"""
|
|
|
|
@overload
|
|
def readblocks(self, block_num: int, buf: bytearray, offset: int, /) -> bool:
|
|
"""
|
|
The second form allows reading at arbitrary locations within a block,
|
|
and arbitrary lengths.
|
|
Starting at block index *block_num*, and byte offset within that block
|
|
of *offset*, read bytes from the device into *buf* (an array of bytes).
|
|
The number of bytes to read is given by the length of *buf*.
|
|
"""
|
|
|
|
@overload
|
|
def writeblocks(self, block_num: int, buf: bytes | bytearray, /) -> None:
|
|
"""
|
|
The first form writes aligned, multiples of blocks, and requires that the
|
|
blocks that are written to be first erased (if necessary) by this method.
|
|
Starting at the block given by the index *block_num*, write blocks from
|
|
*buf* (an array of bytes) to the device.
|
|
The number of blocks to write is given by the length of *buf*,
|
|
which will be a multiple of the block size.
|
|
|
|
The second form allows writing at arbitrary locations within a block,
|
|
and arbitrary lengths. Only the bytes being written should be changed,
|
|
and the caller of this method must ensure that the relevant blocks are
|
|
erased via a prior ``ioctl`` call.
|
|
Starting at block index *block_num*, and byte offset within that block
|
|
of *offset*, write bytes from *buf* (an array of bytes) to the device.
|
|
The number of bytes to write is given by the length of *buf*.
|
|
|
|
Note that implementations must never implicitly erase blocks if the offset
|
|
argument is specified, even if it is zero.
|
|
"""
|
|
|
|
@overload
|
|
def writeblocks(self, block_num: int, buf: bytes | bytearray, offset: int, /) -> None:
|
|
"""
|
|
The first form writes aligned, multiples of blocks, and requires that the
|
|
blocks that are written to be first erased (if necessary) by this method.
|
|
Starting at the block given by the index *block_num*, write blocks from
|
|
*buf* (an array of bytes) to the device.
|
|
The number of blocks to write is given by the length of *buf*,
|
|
which will be a multiple of the block size.
|
|
|
|
The second form allows writing at arbitrary locations within a block,
|
|
and arbitrary lengths. Only the bytes being written should be changed,
|
|
and the caller of this method must ensure that the relevant blocks are
|
|
erased via a prior ``ioctl`` call.
|
|
Starting at block index *block_num*, and byte offset within that block
|
|
of *offset*, write bytes from *buf* (an array of bytes) to the device.
|
|
The number of bytes to write is given by the length of *buf*.
|
|
|
|
Note that implementations must never implicitly erase blocks if the offset
|
|
argument is specified, even if it is zero.
|
|
"""
|
|
|
|
@overload
|
|
def ioctl(self, op: Literal[4, 5], arg: int) -> int:
|
|
"""
|
|
Control the block device and query its parameters. The operation to
|
|
perform is given by *op* which is one of the following integers:
|
|
|
|
- 1 -- initialise the device (*arg* is unused)
|
|
- 2 -- shutdown the device (*arg* is unused)
|
|
- 3 -- sync the device (*arg* is unused)
|
|
- 4 -- get a count of the number of blocks, should return an integer
|
|
(*arg* is unused)
|
|
- 5 -- get the number of bytes in a block, should return an integer,
|
|
or ``None`` in which case the default value of 512 is used
|
|
(*arg* is unused)
|
|
- 6 -- erase a block, *arg* is the block number to erase
|
|
|
|
As a minimum ``ioctl(4, ...)`` must be intercepted; for littlefs
|
|
``ioctl(6, ...)`` must also be intercepted. The need for others is
|
|
hardware dependent.
|
|
|
|
Prior to any call to ``writeblocks(block, ...)`` littlefs issues
|
|
``ioctl(6, block)``. This enables a device driver to erase the block
|
|
prior to a write if the hardware requires it. Alternatively a driver
|
|
might intercept ``ioctl(6, block)`` and return 0 (success). In this case
|
|
the driver assumes responsibility for detecting the need for erasure.
|
|
|
|
Unless otherwise stated ``ioctl(op, arg)`` can return ``None``.
|
|
Consequently an implementation can ignore unused values of ``op``. Where
|
|
``op`` is intercepted, the return value for operations 4 and 5 are as
|
|
detailed above. Other operations should return 0 on success and non-zero
|
|
for failure, with the value returned being an ``OSError`` errno code.
|
|
"""
|
|
|
|
@overload
|
|
def ioctl(self, op: Literal[1, 2, 3, 6], arg: int) -> int | None:
|
|
"""
|
|
Control the block device and query its parameters. The operation to
|
|
perform is given by *op* which is one of the following integers:
|
|
|
|
- 1 -- initialise the device (*arg* is unused)
|
|
- 2 -- shutdown the device (*arg* is unused)
|
|
- 3 -- sync the device (*arg* is unused)
|
|
- 4 -- get a count of the number of blocks, should return an integer
|
|
(*arg* is unused)
|
|
- 5 -- get the number of bytes in a block, should return an integer,
|
|
or ``None`` in which case the default value of 512 is used
|
|
(*arg* is unused)
|
|
- 6 -- erase a block, *arg* is the block number to erase
|
|
|
|
As a minimum ``ioctl(4, ...)`` must be intercepted; for littlefs
|
|
``ioctl(6, ...)`` must also be intercepted. The need for others is
|
|
hardware dependent.
|
|
|
|
Prior to any call to ``writeblocks(block, ...)`` littlefs issues
|
|
``ioctl(6, block)``. This enables a device driver to erase the block
|
|
prior to a write if the hardware requires it. Alternatively a driver
|
|
might intercept ``ioctl(6, block)`` and return 0 (success). In this case
|
|
the driver assumes responsibility for detecting the need for erasure.
|
|
|
|
Unless otherwise stated ``ioctl(op, arg)`` can return ``None``.
|
|
Consequently an implementation can ignore unused values of ``op``. Where
|
|
``op`` is intercepted, the return value for operations 4 and 5 are as
|
|
detailed above. Other operations should return 0 on success and non-zero
|
|
for failure, with the value returned being an ``OSError`` errno code.
|
|
"""
|