init weather_station repo

This commit is contained in:
2026-01-24 13:02:13 +08:00
commit 38023a9bf5
18 changed files with 3333 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/archives

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# WiFi天气微站
esp8266版天气信息显示设备
## 参考资料
[MicroPython remote control: mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html)

65
docs/Nanoweb.md Normal file
View File

@@ -0,0 +1,65 @@
# Nanoweb
Nanoweb is a full asynchronous web server for micropython created in order to benefit from
a correct ratio between memory size and features.
It is thus able to run on an ESP8266, ESP32, Raspberry Pico, etc...
## Features
* Completely asynchronous
* Declaration of routes via a dictionary or directly by decorator
* Management of static files (see assets_extensions)
* Callbacks functions when a new query or an error occurs
* Extraction of HTML headers
* User code dense and conci
* Routing wildcards
## Installation
You just have to copy the `nanoweb.py` file on the target (ESP32, Nano, etc...).
## Use
See the [example.py](example.py) file for an advanced example where you will be able to:
* Make a JSON response
* Use pages protected with credentials
* Upload file
* Use `DELETE` method
* Read `POST` data
And this is a simpler example:
```Python
import uasyncio
from nanoweb import Nanoweb
naw = Nanoweb()
async def api_status(request):
"""API status endpoint"""
await request.write("HTTP/1.1 200 OK\r\n")
await request.write("Content-Type: application/json\r\n\r\n")
await request.write('{"status": "running"}')
# You can declare route from the Nanoweb routes dict...
naw.routes = {
'/api/status': api_status,
}
# ... or declare route directly from the Nanoweb route decorator
@naw.route("/ping")
async def ping(request):
await request.write("HTTP/1.1 200 OK\r\n\r\n")
await request.write("pong")
loop = uasyncio.get_event_loop()
loop.create_task(naw.run())
loop.run_forever()
```
## Contribute
* Your code must respects `flake8` and `isort` tools
* Format your commits with `Commit Conventional` (https://www.conventionalcommits.org/en/v1.0.0/)

322
docs/mp_optimize_demo.md Normal file
View File

@@ -0,0 +1,322 @@
# Micropython程序优化实例
这个优化例子来自 Damien 在 pycomau 上的演讲使用MicroPython高效快速编程。
首先我们看下面的程序它在循环中翻转LED然后通过运行的时间和翻转次数计算出每秒翻转的频率。
```py
from machine import Pin
import time
led = Pin('A13')
N = 200000
t0 = time.ticks_us()
for i in range(N):
led.on()
led.off()
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / N, N / dt * 1e3))
```
我们将这段代码保存为文件led1.py然后import led1执行。在pybv10或者pyboardCN上结果是
> 3.381 sec, 16.905 usec/blink : 59.16 kblink/sec
在 MicroPython程序优化原则 中提到尽量在程序中执行功能不要在主程序中运行因此可以将LED翻转放在函数中执行。
```py
from machine import Pin
import time
led = Pin('A13')
N = 200000
def blink_simple(n):
for i in range(n):
led.on()
led.off()
def time_it(f, n):
t0 = time.ticks_us()
f(n)
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / n, n / dt * 1e3))
time_it(blink_simple, N)
```
运行后的结果是:
> 2.902 sec, 14.509 usec/blink : 68.92 kblink/sec
可以看到,我们没有做什么实质的改到,就明显提高了速度。
循环是最消耗运行时间的我们对循环中led.on()和led.off()两个动作进行优化,将它们预先载入内存,而无需循环中每次载入。
```
from machine import Pin
import time
led = Pin('A13')
N = 200000
def blink_simple(n):
on = led.on
off = led.off
for i in range(n):
on()
off()
def time_it(f, n):
t0 = time.ticks_us()
f(n)
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / n, n / dt * 1e3))
time_it(blink_simple, N)
```
运行结果是
> 1.617 sec, 8.086 usec/blink : 123.68 kblink/sec
速度提高了将近一倍。
进一步将循环中对 range(n) 也进行优化
```py
from machine import Pin
import time
led = Pin('A13')
N = 200000
def blink_simple(n):
on = led.on
off = led.off
r = range(n)
for i in r:
on()
off()
def time_it(f, n):
t0 = time.ticks_us()
f(n)
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / n, n / dt * 1e3))
time_it(blink_simple, N)
```
运行结果是
> 1.121 sec, 5.607 usec/blink : 178.35 kblink/sec
效果非常明显。
进一步对循环中的操作优化,减少循环次数
```py
from machine import Pin
import time
led = Pin('A13')
N = 200000
def blink_simple(n):
n //= 8
on = led.on
off = led.off
r = range(n)
for i in r:
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
def time_it(f, n):
t0 = time.ticks_us()
f(n)
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / n, n / dt * 1e3))
time_it(blink_simple, N)
```
速度又有明显提升。
> 0.913 sec, 4.563 usec/blink : 219.16 kblink/sec
根据MicroPython的优化功能可以将程序声明为native code本地代码它使用CPU的操作码opcode而不是字节码bytecode
```py
from machine import Pin
import time
led = Pin('A13')
N = 200000
@micropython.native
def blink_simple(n):
n //= 8
on = led.on
off = led.off
r = range(n)
for i in r:
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
on()
off()
def time_it(f, n):
t0 = time.ticks_us()
f(n)
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / n, n / dt * 1e3))
time_it(blink_simple, N)
```
结果如下
> 0.704 sec, 3.521 usec/blink : 284.00 kblink/sec
除了native还可以使用viper code模式它进一步提升了整数计算和位操作性能
```py
from machine import Pin
import time, stm
led = Pin('A13')
N = 200000
@micropython.viper
def blink_simple(n:int):
n //= 8
p = ptr16(stm.GPIOB + stm.GPIO_BSRR)
for i in range(n):
p[0] = 1 << 4
p[1] = 1 << 4
p[0] = 1 << 4
p[1] = 1 << 4
p[0] = 1 << 4
p[1] = 1 << 4
p[0] = 1 << 4
p[1] = 1 << 4
p[0] = 1 << 4
p[1] = 1 << 4
p[0] = 1 << 4
p[1] = 1 << 4
p[0] = 1 << 4
p[1] = 1 << 4
p[0] = 1 << 4
p[1] = 1 << 4
def time_it(f, n):
t0 = time.ticks_us()
f(n)
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / n, n / dt * 1e3))
time_it(blink_simple, N)
```
运行结果的确是大幅提升了性能
> 0.016 sec, 0.078 usec/blink : 12879.13 kblink/sec
最终我们还可以通过嵌入汇编方式,最大限度提升性能
```py
from machine import Pin
import time, stm
led = Pin('A13')
N = 200000
@micropython.asm_thumb
def blink_simple(r0):
lsr(r0, r0, 3)
movwt(r1, stm.GPIOB + stm.GPIO_BSRR)
mov(r2, 1 << 4)
label(loop)
strh(r2, [r1, 0])
strh(r2, [r1, 2])
strh(r2, [r1, 0])
strh(r2, [r1, 2])
strh(r2, [r1, 0])
strh(r2, [r1, 2])
strh(r2, [r1, 0])
strh(r2, [r1, 2])
strh(r2, [r1, 0])
strh(r2, [r1, 2])
strh(r2, [r1, 0])
strh(r2, [r1, 2])
strh(r2, [r1, 0])
strh(r2, [r1, 2])
strh(r2, [r1, 0])
strh(r2, [r1, 2])
sub(r0, 1)
bne(loop)
def time_it(f, n):
t0 = time.ticks_us()
f(n)
t1 = time.ticks_us()
dt = time.ticks_diff(t1, t0)
fmt = '{:5.3f} sec, {:6.3f} usec/blink : {:8.2f} kblink/sec'
print(fmt.format(dt * 1e-6, dt / n, n / dt * 1e3))
time_it(blink_simple, N)
```
运行结果是
> 0.007 sec, 0.037 usec/blink : 27322.40 kblink/sec
这个结果已经非常接近极限了。
从前面的优化顺序,可以看到我们并没有大幅修改程序,就可以极高程序的性能。实际使用中,大家可以灵活选择,提高程序的性能。

762
docs/mpremote.rst Normal file
View File

@@ -0,0 +1,762 @@
.. _mpremote:
MicroPython remote control: mpremote
====================================
The ``mpremote`` command line tool provides an integrated set of utilities to
remotely interact with, manage the filesystem on, and automate a MicroPython
device over a serial connection.
To use mpremote, first install it via ``pip``:
.. code-block:: bash
$ pip install --user mpremote
Or via `pipx <https://pypa.github.io/pipx/>`_:
.. code-block:: bash
$ pipx install mpremote
The simplest way to use this tool is just by invoking it without any arguments:
.. code-block:: bash
$ mpremote
This command automatically detects and connects to the first available USB
serial device and provides an interactive terminal that you can use to access
the REPL and your program's output. Serial ports are opened in exclusive mode,
so running a second (or third, etc) instance of ``mpremote`` will connect to
subsequent serial devices, if any are available.
Additionally ``pipx`` also allows you to directly run ``mpremote`` without
installing first:
.. code-block:: bash
$ pipx run mpremote ...args
Commands
--------
``mpremote`` supports being given a series of commands given at the command line
which will perform various actions in sequence on a remote MicroPython device.
See the :ref:`examples section <mpremote_examples>` below to get an idea of how
this works and for some common combinations of commands.
Each command is of the form ``<command name> [--options] [args...]``. For commands
that support multiple arguments (e.g. a list of files), the argument list can
be terminated with ``+``.
If no command is specified, the default command is ``repl``. Additionally, if
any command needs to access the device, and no earlier ``connect`` has been
specified, then an implicit ``connect auto`` is added.
In order to get the device into a known state for any action command
(except ``repl``), once connected ``mpremote`` will stop any running program
and soft-reset the device before running the first command. You can control
this behavior using the ``resume`` and ``soft-reset`` commands.
See :ref:`auto-connection and auto-soft-reset <mpremote_reset>` for more details.
Multiple commands can be specified and they will be run sequentially.
The full list of supported commands are:
- `connect <mpremote_command_connect>`
- `disconnect <mpremote_command_disconnect>`
- `resume <mpremote_command_resume>`
- `soft_reset <mpremote_command_soft_reset>`
- `repl <mpremote_command_repl>`
- `eval <mpremote_command_eval>`
- `exec <mpremote_command_exec>`
- `run <mpremote_command_run>`
- `fs <mpremote_command_fs>`
- `df <mpremote_command_df>`
- `edit <mpremote_command_edit>`
- `mip <mpremote_command_mip>`
- `mount <mpremote_command_mount>`
- `unmount <mpremote_command_unmount>`
- `romfs <mpremote_command_romfs>`
- `rtc <mpremote_command_rtc>`
- `sleep <mpremote_command_sleep>`
- `reset <mpremote_command_reset>`
- `bootloader <mpremote_command_bootloader>`
.. _mpremote_command_connect:
- **connect** -- connect to specified device via name:
.. code-block:: bash
$ mpremote connect <device>
``<device>`` may be one of:
- ``list``: list available devices
- ``auto``: connect to the first available USB serial port
- ``id:<serial>``: connect to the device with USB serial number
``<serial>`` (the second column from the ``connect list``
command output)
- ``port:<path>``: connect to the device with the given path (the first column
from the ``connect list`` command output
- ``rfc2217://<host>:<port>``: connect to the device using serial over TCP
(e.g. a networked serial port based on RFC2217)
- any valid device name/path, to connect to that device
**Note:** Instead of using the ``connect`` command, there are several
:ref:`pre-defined shortcuts <mpremote_shortcuts>` for common device paths. For
example the ``a0`` shortcut command is equivalent to
``connect /dev/ttyACM0`` (Linux), or ``c1`` for ``COM1`` (Windows).
**Note:** The ``auto`` option will only detect USB serial ports, i.e. a serial
port that has an associated USB VID/PID (i.e. CDC/ACM or FTDI-style
devices). Other types of serial ports will not be auto-detected.
.. _mpremote_command_disconnect:
- **disconnect** -- disconnect current device:
.. code-block:: bash
$ mpremote disconnect
After a disconnect, :ref:`auto-soft-reset <mpremote_reset>` is enabled.
.. _mpremote_command_resume:
- **resume** -- maintain existing interpreter state for subsequent commands:
.. code-block:: bash
$ mpremote resume
This disables :ref:`auto-soft-reset <mpremote_reset>`. This is useful if you
want to run a subsequent command on a board without first soft-resetting it.
.. _mpremote_command_soft_reset:
- **soft-reset** -- perform a soft-reset of the device:
.. code-block:: bash
$ mpremote soft-reset
This will clear out the Python heap and restart the interpreter. It also
prevents the subsequent command from triggering :ref:`auto-soft-reset <mpremote_reset>`.
.. _mpremote_command_repl:
- **repl** -- enter the REPL on the connected device:
.. code-block:: bash
$ mpremote repl [--options]
Options are:
- ``--escape-non-printable``, to print non-printable bytes/characters as their hex code
- ``--capture <file>``, to capture output of the REPL session to the given
file
- ``--inject-code <string>``, to specify characters to inject at the REPL when
``Ctrl-J`` is pressed. This allows you to automate a common command.
- ``--inject-file <file>``, to specify a file to inject at the REPL when
``Ctrl-K`` is pressed. This allows you to run a file (e.g. containing some
useful setup code, or even the program you are currently working on).
While the ``repl`` command running, you can use ``Ctrl-]`` or ``Ctrl-x`` to
exit.
**Note:** The name "REPL" here reflects that the common usage of this command
to access the Read Eval Print Loop that is running on the MicroPython
device. Strictly, the ``repl`` command is just functioning as a terminal
(or "serial monitor") to access the device. Because this command does not
trigger the :ref:`auto-reset behavior <mpremote_reset>`, this means that if
a program is currently running, you will first need to interrupt it with
``Ctrl-C`` to get to the REPL, which will then allow you to access program
state. You can also use ``mpremote soft-reset repl`` to get a "clean" REPL
with all program state cleared.
.. _mpremote_command_eval:
- **eval** -- evaluate and print the result of a Python expression:
.. code-block:: bash
$ mpremote eval <string>
.. _mpremote_command_exec:
- **exec** -- execute the given Python code:
.. code-block:: bash
$ mpremote exec <string>
By default, ``mpremote exec`` will display any output from the expression until it
terminates. The ``--no-follow`` flag can be specified to return immediately and leave
the device running the expression in the background.
.. _mpremote_command_run:
- **run** -- run a script from the local filesystem:
.. code-block:: bash
$ mpremote run <file.py>
This will execute the file directly from RAM on the device without copying it
to the filesystem. This is a very useful way to iterate on the development of
a single piece of code without having to worry about deploying it to the
filesystem.
By default, ``mpremote run`` will display any output from the script until it
terminates. The ``--no-follow`` flag can be specified to return immediately and leave
the device running the script in the background.
.. _mpremote_command_fs:
- **fs** -- execute filesystem commands on the device:
.. code-block:: bash
$ mpremote fs <sub-command>
``<sub-command>`` may be:
- ``cat <file..>`` to show the contents of a file or files on the device
- ``ls`` to list the current directory
- ``ls <dirs...>`` to list the given directories
- ``cp [-rf] <src...> <dest>`` to copy files
- ``rm [-r] <src...>`` to remove files or folders on the device
- ``mkdir <dirs...>`` to create directories on the device
- ``rmdir <dirs...>`` to remove directories on the device
- ``touch <file..>`` to create the files (if they don't already exist)
- ``sha256sum <file..>`` to calculate the SHA256 sum of files
- ``tree [-vsh] <dirs...>`` to print a tree of the given directories
The ``cp`` command uses a convention where a leading ``:`` represents a remote
path. Without a leading ``:`` means a local path. This is based on the
convention used by the `Secure Copy Protocol (scp) client
<https://en.wikipedia.org/wiki/Secure_copy_protocol>`_.
So for example, ``mpremote fs cp main.py :main.py`` copies ``main.py`` from
the current local directory to the remote filesystem, whereas
``mpremote fs cp :main.py main.py`` copies ``main.py`` from the device back
to the current directory.
The ``mpremote rm -r`` command accepts both relative and absolute paths.
Use ``:`` to refer to the current remote working directory (cwd) to allow a
directory tree to be removed from the device's default path (eg ``/flash``, ``/``).
Use ``-v/--verbose`` to see the files being removed.
For example:
- ``mpremote rm -r :libs`` will remove the ``libs`` directory and all its
child items from the device.
- ``mpremote rm -rv :/sd`` will remove all files from a mounted SDCard and result
in a non-blocking warning. The mount will be retained.
- ``mpremote rm -rv :/`` will remove all files on the device, including any
located in mounted vfs such as ``/sd`` or ``/flash``. After removing all folders
and files, this will also return an error to mimic unix ``rm -rf /`` behaviour.
.. warning::
There is no supported way to undelete files removed by ``mpremote rm -r :``.
Please use with caution.
The ``tree`` command will print a tree of the given directories.
Using the ``--size/-s`` option will print the size of each file, or use
``--human/-h`` to use a more human readable format.
Note: Directory size is only printed when a non-zero size is reported by the device's filesystem.
The ``-v`` option can be used to include the name of the serial device in
the output.
All other commands implicitly assume the path is a remote path, but the ``:``
can be optionally used for clarity.
All of the filesystem sub-commands take multiple path arguments, so if there
is another command in the sequence, you must use ``+`` to terminate the
arguments, e.g.
.. code-block:: bash
$ mpremote fs cp main.py :main.py + repl
This will copy the file to the device then enter the REPL. The ``+`` prevents
``"repl"`` being interpreted as a path.
The ``cp`` command supports the ``-r`` option to make a recursive copy. By
default ``cp`` will skip copying files to the remote device if the SHA256 hash
of the source and destination file matches. To force a copy regardless of the
hash use the ``-f`` option.
**Note:** For convenience, all of the filesystem sub-commands are also
:ref:`aliased as regular commands <mpremote_shortcuts>`, i.e. you can write
``mpremote cp ...`` instead of ``mpremote fs cp ...``.
.. _mpremote_command_df:
- **df** -- query device free/used space
.. code-block:: bash
$ mpremote df
The ``df`` command will print size/used/free statistics for the device
filesystem, similar to the Unix ``df`` command.
.. _mpremote_command_edit:
- **edit** -- edit a file on the device:
.. code-block:: bash
$ mpremote edit <files...>
The ``edit`` command will copy each file from the device to a local temporary
directory and then launch your editor for each file (defined by the environment
variable ``$EDITOR``). If the editor exits successfully, the updated file will
be copied back to the device.
.. _mpremote_command_mip:
- **mip** -- install packages from :term:`micropython-lib` (or GitHub) using the ``mip`` tool:
.. code-block:: bash
$ mpremote mip install <packages...>
See :ref:`packages` for more information.
.. _mpremote_command_mount:
- **mount** -- mount the local directory on the remote device:
.. code-block:: bash
$ mpremote mount [options] <local-dir>
This allows the remote device to see the local host directory as if it were
its own filesystem. This is useful for development, and avoids the need to
copy files to the device while you are working on them.
The device installs a filesystem driver, which is then mounted in the
:ref:`device VFS <filesystem>` as ``/remote``, which uses the serial
connection to ``mpremote`` as a side-channel to access files. The device
will have its current working directory (via ``os.chdir``) set to
``/remote`` so that imports and file access will occur there instead of the
default filesystem path while the mount is active.
**Note:** If the ``mount`` command is not followed by another action in the
sequence, a ``repl`` command will be implicitly added to the end of the
sequence.
During usage, Ctrl-D will trigger a soft-reset as normal, but the mount will
automatically be re-connected. If the unit has a main.py running at startup
however the remount cannot occur. In this case a raw mode soft reboot can be
used: Ctrl-A Ctrl-D to reboot, then Ctrl-B to get back to normal repl at
which point the mount will be ready.
Options are:
- ``-l``, ``--unsafe-links``: By default an error will be raised if the device
accesses a file or directory which is outside (up one or more directory levels) the
local directory that is mounted. This option disables this check for symbolic
links, allowing the device to follow symbolic links outside of the local directory.
.. _mpremote_command_unmount:
- **unmount** -- unmount the local directory from the remote device:
.. code-block:: bash
$ mpremote umount
This happens automatically when ``mpremote`` terminates, but it can be used
in a sequence to unmount an earlier mount before subsequent command are run.
.. _mpremote_command_romfs:
- **romfs** -- manage ROMFS partitions on the device:
.. code-block:: bash
$ mpremote romfs <sub-command>
``<sub-command>`` may be:
- ``romfs query`` to list all the available ROMFS partitions and their size
- ``romfs [-o <output>] build <source>`` to create a ROMFS image from the given
source directory; the default output file is the source appended by ``.romfs``
- ``romfs [-p <partition>] deploy <source>`` to deploy a ROMFS image to the device;
will also create a temporary ROMFS image if the source is a directory
The ``build`` and ``deploy`` sub-commands both support the ``-m``/``--mpy`` option
to automatically compile ``.py`` files to ``.mpy`` when creating the ROMFS image.
This option is enabled by default, but only works if the ``mpy_cross`` Python
package has been installed (eg via ``pip install mpy_cross``). If the package is
not installed then a warning is printed and ``.py`` files remain as is. Compiling
of ``.py`` files can be disabled with the ``--no-mpy`` option.
.. _mpremote_command_rtc:
- **rtc** -- set/get the device clock (RTC):
.. code-block:: bash
$ mpremote rtc
This will query the device RTC for the current time and print it as a datetime
tuple.
.. code-block:: bash
$ mpremote rtc --set
This will set the device RTC to the host PC's current time.
.. _mpremote_command_sleep:
- **sleep** -- sleep (delay) before executing the next command
.. code-block:: bash
$ mpremote sleep 0.5
This will pause execution of the command sequence for the specified duration
in seconds, e.g. to wait for the device to do something.
.. _mpremote_command_reset:
- **reset** -- hard reset the device
.. code-block:: bash
$ mpremote reset
**Note:** hard reset is equivalent to :func:`machine.reset`.
.. _mpremote_command_bootloader:
- **bootloader** enter the bootloader
.. code-block:: bash
$ mpremote bootloader
This will make the device enter its bootloader. The bootloader is port- and
board-specific (e.g. DFU on stm32, UF2 on rp2040/Pico).
.. _mpremote_reset:
Auto connection and soft-reset
------------------------------
Connection and disconnection will be done automatically at the start and end of
the execution of the tool, if such commands are not explicitly given. Automatic
connection will search for the first available USB serial device.
Once connected to a device, ``mpremote`` will automatically soft-reset the
device if needed. This clears the Python heap and restarts the interpreter,
making sure that subsequent Python code executes in a fresh environment. Auto
soft-reset is performed the first time one of the following commands are
executed: ``mount``, ``eval``, ``exec``, ``run``, ``fs``. After doing a
soft-reset for the first time, it will not be done again automatically, until a
``disconnect`` command is issued.
Auto-soft-reset behaviour can be controlled by the ``resume`` command. This
might be useful to use the ``eval`` command to inspect the state of of the
device. The ``soft-reset`` command can be used to perform an explicit soft
reset in the middle of a sequence of commands.
.. _mpremote_shortcuts:
Shortcuts
---------
Shortcuts can be defined using the macro system. Built-in shortcuts are:
- ``devs``: Alias for ``connect list``
- ``a0``, ``a1``, ``a2``, ``a3``: Aliases for ``connect /dev/ttyACMn``
- ``u0``, ``u1``, ``u2``, ``u3``: Aliases for ``connect /dev/ttyUSBn``
- ``c0``, ``c1``, ``c2``, ``c3``: Aliases for ``connect COMn``
- ``cat``, ``edit``, ``ls``, ``cp``, ``rm``, ``mkdir``, ``rmdir``, ``touch``: Aliases for ``fs <sub-command>``
Additional shortcuts can be defined in the user configuration file ``mpremote/config.py``,
located in the User Configuration Directory.
The correct location for each OS is determined using the ``platformdirs`` module.
This is typically:
- ``$XDG_CONFIG_HOME/mpremote/config.py``
- ``$HOME/.config/mpremote/config.py``
- ``$env:LOCALAPPDATA/mpremote/config.py``
The ``config.py``` file should define a dictionary named ``commands``. The keys of this dictionary are the shortcuts
and the values are either a string or a list-of-strings:
.. code-block:: python3
"c33": "connect id:334D335C3138",
The command ``c33`` is replaced by ``connect id:334D335C3138``.
.. code-block:: python3
"test": ["mount", ".", "exec", "import test"],
The command ``test`` is replaced by ``mount . exec "import test"``.
Shortcuts can also accept arguments. For example:
.. code-block:: python3
"multiply x=4 y=7": "eval x*y",
Running ``mpremote times 3 7`` will set ``x`` and ``y`` as variables on the device, then evaluate the expression ``x*y``.
An example ``config.py`` might look like:
.. code-block:: python3
commands = {
"c33": "connect id:334D335C3138", # Connect to a specific device by ID.
"bl": "bootloader", # Shorter alias for bootloader.
"double x=4": "eval x*2", # x is an argument, with default 4
"wl_scan": ["exec", """
import network
wl = network.WLAN()
wl.active(1)
for ap in wl.scan():
print(ap)
""",], # Print out nearby WiFi networks.
"wl_ipconfig": [
"exec",
"import network; sta_if = network.WLAN(network.WLAN.IF_STA); print(sta_if.ipconfig('addr4'))",
""",], # Print ip address of station interface.
"test": ["mount", ".", "exec", "import test"], # Mount current directory and run test.py.
"demo": ["run", "path/to/demo.py"], # Execute demo.py on the device.
}
.. _mpremote_examples:
Examples
--------
.. code-block:: bash
mpremote
Connect to the first available device and implicitly run the ``repl`` command.
.. code-block:: bash
mpremote a1
Connect to the device at ``/dev/ttyACM1`` (Linux) and implicitly run the
``repl`` command. See :ref:`shortcuts <mpremote_shortcuts>` above.
.. code-block:: bash
mpremote c1
Connect to the device at ``COM1`` (Windows) and implicitly run the ``repl``
command. See :ref:`shortcuts <mpremote_shortcuts>` above.
.. code-block:: bash
mpremote connect /dev/ttyUSB0
Explicitly specify which device to connect to, and as above, implicitly run the
``repl`` command.
.. code-block:: bash
mpremote a1 ls
Connect to the device at ``/dev/ttyACM0`` and then run the ``ls`` command.
It is equivalent to ``mpremote connect /dev/ttyACM1 fs ls``.
.. code-block:: bash
mpremote exec "import micropython; micropython.mem_info()"
Run the specified Python command and display any output. This is equivalent to
typing the command at the REPL prompt.
.. code-block:: bash
mpremote eval 1/2 eval 3/4
Evaluate each expression in turn and print the results.
.. code-block:: bash
mpremote a0 eval 1/2 a1 eval 3/4
Evaluate ``1/2`` on the device at ``/dev/ttyACM0``, then ``3/4`` on the
device at ``/dev/ttyACM1``, printing each result.
.. code-block:: bash
mpremote resume exec "print_state_info()" soft-reset
Connect to the device without triggering a :ref:`soft reset <soft_reset>` and
execute the ``print_state_info()`` function (e.g. to find out information about
the current program state), then trigger a soft reset.
.. code-block:: bash
mpremote reset sleep 0.5 bootloader
Hard-reset the device, wait 500ms for it to become available, then enter the
bootloader.
.. code-block:: bash
mpremote cp utils/driver.py :utils/driver.py + run test.py
Update the copy of utils/driver.py on the device, then execute the local
``test.py`` script on the device. ``test.py`` is never copied to the device
filesystem, rather it is run from RAM.
.. code-block:: bash
mpremote cp utils/driver.py :utils/driver.py + exec "import app"
Update the copy of utils/driver.py on the device, then execute app.py on the
device.
This is a common development workflow to update a single file and then re-start
your program. In this scenario, your ``main.py`` on the device would also do
``import app``.
.. code-block:: bash
mpremote cp utils/driver.py :utils/driver.py + soft-reset repl
Update the copy of utils/driver.py on the device, then trigger a soft-reset to
restart your program, and then monitor the output via the ``repl`` command.
.. code-block:: bash
mpremote cp -r utils/ :utils/ + soft-reset repl
Same as above, but update the entire utils directory first.
.. code-block:: bash
mpremote mount .
Mount the current local directory at ``/remote`` on the device and starts a
``repl`` session which will use ``/remote`` as the working directory.
.. code-block:: bash
mpremote mount . exec "import demo"
After mounting the current local directory, executes ``demo.py`` from the
mounted directory.
.. code-block:: bash
mpremote mount app run test.py
After mounting the local directory ``app`` as ``/remote`` on the device,
executes the local ``test.py`` from the host's current directory without
copying it to the filesystem.
.. code-block:: bash
mpremote mount . repl --inject-code "import demo"
After mounting the current local directory, executes ``demo.py`` from the
mounted directory each time ``Ctrl-J`` is pressed.
You will first need to press ``Ctrl-D`` to reset the interpreter state
(which will preserve the mount) before pressing ``Ctrl-J`` to re-import
``demo.py``.
.. code-block:: bash
mpremote mount app repl --inject-file demo.py
Same as above, but executes the contents of the local file demo.py at the REPL
every time ``Ctrl-K`` is pressed. As above, use Ctrl-D to reset the interpreter
state first.
.. code-block:: bash
mpremote cat boot.py
Displays the contents of ``boot.py`` on the device.
.. code-block:: bash
mpremote edit utils/driver.py
Edit ``utils/driver.py`` on the device using your local ``$EDITOR``.
.. code-block:: bash
mpremote cp :main.py .
Copy ``main.py`` from the device to the local directory.
.. code-block:: bash
mpremote cp main.py :
Copy ``main.py`` from the local directory to the device.
.. code-block:: bash
mpremote cp :a.py :b.py
Copy ``a.py`` on the device to ``b.py`` on the device.
.. code-block:: bash
mpremote cp -r dir/ :
Recursively copy the local directory ``dir`` to the remote device.
.. code-block:: bash
mpremote cp a.py b.py : + repl
Copy ``a.py`` and ``b.py`` from the local directory to the device, then run the
``repl`` command.
.. code-block:: bash
mpremote mip install aioble
Install the ``aioble`` package from :term:`micropython-lib` to the device.
See :ref:`packages`.
.. code-block:: bash
mpremote mip install github:org/repo@branch
Install the package from the specified branch at org/repo on GitHub to the
device. See :ref:`packages`.
.. code-block:: bash
mpremote mip install gitlab:org/repo@branch
Install the package from the specified branch at org/repo on GitLab to the
device. See :ref:`packages`.
.. code-block:: bash
mpremote mip install --target /flash/third-party functools
Install the ``functools`` package from :term:`micropython-lib` to the
``/flash/third-party`` directory on the device. See :ref:`packages`.

746
docs/st7789_mpy.md Normal file
View File

@@ -0,0 +1,746 @@
# ST7789 Driver for MicroPython
This driver is based on [devbis' st7789_mpy driver.](https://github.com/devbis/st7789_mpy)
I modified the original driver for one of my projects to add:
- Display Rotation.
- Scrolling
- Writing text using bitmaps converted from True Type fonts
- Drawing text using 8 and 16-bit wide bitmap fonts
- Drawing text using Hershey vector fonts
- Drawing JPGs, including a SLOW mode to draw jpg's larger than available ram
using the TJpgDec - Tiny JPEG Decompressor R0.01d. from
http://elm-chan.org/fsw/tjpgd/00index.html
- Drawing PNGs using the pngle library from https://github.com/kikuchan/pngle
- Drawing and rotating Polygons and filled Polygons.
- Tracking bounds
- Custom init capability to support st7735, ili9341, ili9342 and other displays. See the examples/configs folder for M5Stack Core, M5Stack Core2, T-DONGLE-S3 and Wio_Terminal devices.
Included are 12 bitmap fonts derived from classic pc text mode fonts, 26
Hershey vector fonts and several example programs for different devices.
## Display Configuration
Some displays may use a BGR color order or inverted colors. The `cfg_helper.py`
program can be used to determine the color order, inversion_mode, colstart, and
rowstart values needed for a display.
### Color Modes
You can test for the correct color order needed by a display by filling it with
the `st7789.RED` color and observing the actual color displayed.
- If the displayed color is RED, the settings are correct.
- If the displayed color is BLUE, `color_order` should be `st7789.BGR`.
- If the displayed color is YELLOW, `inversion_mode` should be `True.`
- If the displayed color is CYAN, `color_order` should be `st7789.BGR` and
`inversion_mode` should be `True.`
### colstart and rowstart
Some displays have a frame buffer memory larger than the physical display
matrix. In these cases, the driver must be configured with the position of the
first physical column and row pixels relative to the frame buffer. Each
rotation setting of the display may require different colstart and rowstart
values.
The driver automatically sets the `colstart` and `rowstart` values for common
135x240, 240x240, 170x320 and 240x320 displays. If the default values do not work for
your display, these values can be overridden using the `offsets` method. The
`offsets` method should be called after any `rotation` method calls.
#### 128x128 st7735 cfg_helper.py example
```
inversion_mode(False)
color_order = st7789.BGR
for rotation 0 use offset(2, 1)
for rotation 1 use offset(1, 2)
for rotation 2 use offset(2, 3)
for rotation 3 use offset(3, 2)
```
#### 128x160 st7735 cfg_helper.py example
```
inversion_mode(False)
color_order = st7789.RGB
for rotation 0 use offset(0, 0)
for rotation 1 use offset(0, 0)
for rotation 2 use offset(0, 0)
for rotation 3 use offset(0, 0)
```
## Pre-compiled firmware files
The firmware directory contains pre-compiled firmware for various devices with
the st7789 C driver and frozen python font files. See the README.md file in the
fonts folder for more information on the font files.
MicroPython MicroPython v1.20.0 compiled with ESP IDF v4.4.4 using CMake
Directory | File | Device
--------------------- | ------------ | ----------------------------------
GENERIC-7789 | firmware.bin | Generic ESP32 devices
GENERIC_SPIRAM-7789 | firmware.bin | Generic ESP32 devices with SPI Ram
GENERIC_C3 | firmware.bin | Generic ESP32-C3 devices
LOLIN_S2_MINI | firmware.bin | Wemos S2 mini
PYBV11 | firmware.dfu | Pyboard v1.1 (No PNG)
RP2 | firmware.uf2 | Raspberry Pi Pico RP2040
RP2W | firmware.uf2 | Raspberry Pi PicoW RP2040
T-DISPLAY | firmware.bin | LILYGO® TTGO T-Display
T-Watch-2020 | firmware.bin | LILYGO® T-Watch 2020
WIO_TERMINAL | firmware.bin | Seeed Wio Terminal
## Additional Modules
Module | Source
------------------ | -----------------------------------------------------------
axp202c | https://github.com/lewisxhe/AXP202X_Libraries
focaltouch | https://gitlab.com/mooond/t-watch2020-esp32-with-micropython
## Video Examples
Example | Video
--------------------- | -----------------------------------------------------------
PYBV11 hello.py | https://youtu.be/OtcERmad5ps
PYBV11 scroll.py | https://youtu.be/ro13rvaLKAc
T-DISPLAY fonts.py | https://youtu.be/2cnAhEucPD4
T-DISPLAY hello.py | https://youtu.be/z41Du4GDMSY
T-DISPLAY scroll.py | https://youtu.be/GQa-RzHLBak
T-DISPLAY roids.py | https://youtu.be/JV5fPactSPU
TWATCH-2020 draw.py | https://youtu.be/O_lDBnvH1Sw
TWATCH-2020 hello.py | https://youtu.be/Bwq39tuMoY4
TWATCH-2020 bitmap.py | https://youtu.be/DgYzgnAW2d8
TWATCH-2020 watch.py | https://youtu.be/NItKb6umMc4
This is a work in progress.
## Thanks go out to:
- https://github.com/devbis for the original driver this is based on.
- https://github.com/hklang10 for letting me know of the new mp_raise_ValueError().
- https://github.com/aleggon for finding the correct offsets for 240x240
displays and for discovering issues compiling STM32 ports.
-- Russ
## Overview
This is a driver for MicroPython to handle cheap displays based on the ST7789
chip. The driver is written in C. Firmware is provided for ESP32, ESP32 with SPIRAM,
pyboard1.1, and Raspberry Pi Pico devices.
<p align="center">
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/ST7789.jpg" alt="ST7789 display photo"/>
</p>
# Setup MicroPython Build Environment in Ubuntu 20.04.2
See the MicroPython
[README.md](https://github.com/micropython/micropython/blob/master/ports/esp32/README.md#setting-up-esp-idf-and-the-build-environment)
if you run into any build issues not directly related to the st7789 driver. The
recommended MicroPython build instructions may have changed.
Update and upgrade Ubuntu using apt-get if you are using a new install of
Ubuntu or the Windows Subsystem for Linux.
```bash
sudo apt-get -y update
sudo apt-get -y upgrade
```
Use apt-get to install the required build tools.
```bash
sudo apt-get -y install build-essential libffi-dev git pkg-config cmake virtualenv python3-pip python3-virtualenv
```
### Install a compatible esp-idf SDK
The MicroPython README.md states: "The ESP-IDF changes quickly, and MicroPython
only supports certain versions. Currently, MicroPython supports v4.0.2, v4.1.1,
and v4.2 although other IDF v4 versions may also work." I have had good luck
using IDF v4.4
Clone the esp-idf SDK repo -- this usually takes several minutes.
```bash
git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf/
git pull
```
If you already have a copy of the IDF, you can checkout a version compatible
with MicroPython and update the submodules using:
```bash
$ cd esp-idf
$ git checkout v4.4
$ git submodule update --init --recursive
```
Install the esp-idf SDK.
```bash
./install.sh
```
Source the esp-idf export.sh script to set the required environment variables.
You must source the file and not run it using ./export.sh. You will need to
source this file before compiling MicroPython.
```bash
source export.sh
cd ..
```
Clone the MicroPython repo.
```bash
git clone https://github.com/micropython/micropython.git
```
Clone the st7789 driver repo.
```bash
git clone https://github.com/russhughes/st7789_mpy.git
```
Update the git submodules and compile the MicroPython cross-compiler
```bash
cd micropython/
git submodule update --init
cd mpy-cross/
make
cd ..
cd ports/esp32
```
Copy any .py files you want to include in the firmware as frozen python modules
to the modules subdirectory in ports/esp32. Be aware there is a limit to the
flash space available. You will know you have exceeded this limit if you
receive an error message saying the code won't fit in the partition or if your
firmware continuously reboots with an error.
For example:
```bash
cp ../../../st7789_mpy/fonts/bitmap/vga1_16x16.py modules
cp ../../../st7789_mpy/fonts/truetype/NotoSans_32.py modules
cp ../../../st7789_mpy/fonts/vector/scripts.py modules
```
Build the MicroPython firmware with the driver and frozen .py files in the
modules directory. If you did not add any .py files to the modules directory,
you can leave out the FROZEN_MANIFEST and FROZEN_MPY_DIR settings.
```bash
make USER_C_MODULES=../../../../st7789_mpy/st7789/micropython.cmake FROZEN_MANIFEST="" FROZEN_MPY_DIR=$UPYDIR/modules
```
Erase and flash the firmware to your device. Set PORT= to the ESP32's usb
serial port. I could not get the USB serial port to work under the Windows
Subsystem (WSL2) for Linux. If you have the same issue, you can copy the
firmware.bin file and use the Windows esptool.py to flash your device.
```bash
make USER_C_MODULES=../../../../st7789_mpy/st7789/micropython.cmake PORT=/dev/ttyUSB0 erase
make USER_C_MODULES=../../../../st7789_mpy/st7789/micropython.cmake PORT=/dev/ttyUSB0 deploy
```
The firmware.bin file will be in the build-GENERIC directory. To flash using
the python esptool.py utility. Use pip3 to install the esptool if it's not
already installed.
```bash
pip3 install esptool
```
Set PORT= to the ESP32's USB serial port
```bash
esptool.py --port COM3 erase_flash
esptool.py --chip esp32 --port COM3 write_flash -z 0x1000 firmware.bin
```
## CMake building instructions for MicroPython 1.14 and later
for ESP32:
$ cd micropython/ports/esp32
And then compile the module with specified USER_C_MODULES dir.
$ make USER_C_MODULES=../../../../st7789_mpy/st7789/micropython.cmake
for Raspberry Pi PICO:
$ cd micropython/ports/rp2
And then compile the module with specified USER_C_MODULES dir.
$ make USER_C_MODULES=../../../st7789_mpy/st7789/micropython.cmake
## Working examples
This module was tested on ESP32, STM32 based pyboard v1.1, and the Raspberry Pi
Pico. You have to provide an `SPI` object and the pin to use for the `dc' input
of the screen.
# ESP32 Example
# To use baudrates above 26.6MHz you must use my firmware or modify the micropython
# source code to increase the SPI baudrate limit by adding SPI_DEVICE_NO_DUMMY to the
# .flag member of the spi_device_interface_config_t struct in the machine_hw_spi_init_internal.c
# file. Not doing so will cause the ESP32 to crash if you use a baudrate that is too high.
import machine
import st7789
spi = machine.SPI(2, baudrate=40000000, polarity=1, sck=machine.Pin(18), mosi=machine.Pin(23))
display = st7789.ST7789(spi, 240, 240, reset=machine.Pin(4, machine.Pin.OUT), dc=machine.Pin(2, machine.Pin.OUT))
display.init()
## Methods
- `st7789.ST7789(spi, width, height, dc, reset, cs, backlight, rotations, rotation, custom_init, color_order, inversion, options, buffer_size)`
### Required positional arguments:
- `spi` spi device
- `width` display width
- `height` display height
### Required keyword arguments:
- `dc` sets the pin connected to the display data/command selection input.
This parameter is always required.
### Optional keyword arguments:
- `reset` sets the pin connected to the display's hardware reset input. If
the displays reset pin is tied high, the `reset` parameter is not
required.
- `cs` sets the pin connected to the displays chip select input. If the
display's CS pin is tied low, the display must be the only device
connected to the SPI port. The display will always be the selected
device, and the `cs` parameter is not required.
- `backlight` sets the pin connected to the display's backlight enable
input. The display's backlight input can often be left floating or
disconnected as the backlight on some displays is always powered on and
cannot be turned off.
- `rotations` sets the orientation table. The orientation table is a list
of tuples for each `rotation` used to set the MADCTL register, display width,
display height, start_x, and start_y values.
Default `rotations` are included for the following st7789 and st7735
display sizes:
Display | Default Orientation Tables
------- | --------------------------
240x320 | [(0x00, 240, 320, 0, 0), (0x60, 320, 240, 0, 0), (0xc0, 240, 320, 0, 0), (0xa0, 320, 240, 0, 0)]
170x320 | [(0x00, 170, 320, 35, 0), (0x60, 320, 170, 0, 35), (0xc0, 170, 320, 35, 0), (0xa0, 320, 170, 0, 35)]
240x240 | [(0x00, 240, 240, 0, 0), (0x60, 240, 240, 0, 0), (0xc0, 240, 240, 0, 80), (0xa0, 240, 240, 80, 0)]
135x240 | [(0x00, 135, 240, 52, 40), (0x60, 240, 135, 40, 53), (0xc0, 135, 240, 53, 40), (0xa0, 240, 135, 40, 52)]
128x160 | [(0x00, 128, 160, 0, 0), (0x60, 160, 128, 0, 0), (0xc0, 128, 160, 0, 0), (0xa0, 160, 128, 0, 0)]
128x128 | [(0x00, 128, 128, 2, 1), (0x60, 128, 128, 1, 2), (0xc0, 128, 128, 2, 3), (0xa0, 128, 128, 3, 2)]
other | [(0x00, width, height, 0, 0)]
You may define as many rotations as you wish.
- `rotation` sets the display rotation according to the orientation table.
The default orientation table defines four counter-clockwise rotations for 240x320, 240x240,
134x240, 128x160 and 128x128 displays with the LCD's ribbon cable at the bottom of the display.
The default rotation is Portrait (0 degrees).
Index | Rotation
----- | --------
0 | Portrait (0 degrees)
1 | Landscape (90 degrees)
2 | Reverse Portrait (180 degrees)
3 | Reverse Landscape (270 degrees)
- `custom_init` List of display configuration commands to send to the display during the display init().
The list contains tuples with a bytes object, optionally followed by a delay specified in ms. The first
byte of the bytes object contains the command to send optionally followed by data bytes.
See the `examples/configs/t_dongle_s3/tft_config.py` file or an example.
- `color_order` Sets the color order used by the driver (st7789.RGB or st7789.BGR)
- `inversion` Sets the display color inversion mode if True, clears the
display color inversion mode if false.
- `options` Sets driver option flags.
Option | Description
------------- | -----------
st7789.WRAP | pixels, lines, polygons, and Hershey text will wrap around the display both horizontally and vertically.
st7789.WRAP_H | pixels, lines, polygons, and Hershey text will wrap around the display horizontally.
st7789.WRAP_V | pixels, lines, polygons, and Hershey text will wrap around the display vertically.
- `buffer_size` If a buffer_size is not specified, a dynamically allocated
buffer is created and freed as needed. If a buffer_size is set, it must
be large enough to contain the largest bitmap, font character, and
decoded JPG image used (Rows * Columns * 2 bytes, 16bit colors in RGB565
notation). Dynamic allocation is slower and can cause heap fragmentation,
so garbage collection (GC) should be enabled.
- `inversion_mode(bool)` Sets the display color inversion mode if True, clears
the display color inversion mode if False.
- `madctl(value)` Returns the current value of the MADCTL register or sets the MADCTL register if a value is passed to the
method. The MADCTL register is used to set the display rotation and color order.
#### [MADCTL constants](#madctl-constants)
Constant Name | Value | Description
---------------- | ----- | ----------------------
st7789.MADCTL_MY | 0x80 | Page Address Order
st7789_MADCTL_MX | 0x40 | Column Address Order
st7789_MADCTL_MV | 0x20 | Page/Column Order
st7789_MADCTL_ML | 0x10 | Line Address Order
st7789_MADCTL_MH | 0x04 | Display Data Latch Order
st7789_RGB | 0x00 | RGB color order
st7789_BGR | 0x08 | BGR color order
#### [MADCTL examples](#madctl-examples)
Orientation | MADCTL Values for RGB color order, for BGR color order add 0x08 to the value.
----------- | ---------------------------------------------------------------------------------
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_0.png" /> | 0x00
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_y.png" /> | 0x80 ( MADCTL_MY )
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_x.png" /> | 0x40 ( MADCTL_MX )
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_xy.png" /> | 0xC0 ( MADCTL_MX + MADCTL_MY )
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_v.png" /> | 0x20 ( MADCTL_MV )
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_vy.png" /> | 0xA0 ( MADCTL_MV + MADCTL_MY )
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_vx.png" /> | 0x60 ( MADCTL_MV + MADCTL_MX )
<img src="https://raw.githubusercontent.com/russhughes/st7789_mpy/master/docs/madctl_vxy.png" /> | 0xE0 ( MADCTL_MV + MADCTL_MX + MADCTL_MY )
- `init()`
Must be called to initialize the display.
- `on()`
Turn on the backlight pin if one was defined during init.
- `off()`
Turn off the backlight pin if one was defined during init.
- `sleep_mode(value)`
If value is True, cause the display to enter sleep mode, otherwise wake up if value is False. During sleep display content may not be preserved.
- `fill(color)`
Fill the display with the specified color.
- `pixel(x, y, color)`
Set the specified pixel to the given `color`.
- `line(x0, y0, x1, y1, color)`
Draws a single line with the provided `color` from (`x0`, `y0`) to
(`x1`, `y1`).
- `hline(x, y, length, color)`
Draws a single horizontal line with the provided `color` and `length`
in pixels. Along with `vline`, this is a fast version with fewer SPI calls.
- `vline(x, y, length, color)`
Draws a single horizontal line with the provided `color` and `length`
in pixels.
- `rect(x, y, width, height, color)`
Draws a rectangle from (`x`, `y`) with corresponding dimensions
- `fill_rect(x, y, width, height, color)`
Fill a rectangle starting from (`x`, `y`) coordinates
- `circle(x, y, r, color)`
Draws a circle with radius `r` centered at the (`x`, `y`) coordinates in the given
`color`.
- `fill_circle(x, y, r, color)`
Draws a filled circle with radius `r` centered at the (`x`, `y`) coordinates
in the given `color`.
- `blit_buffer(buffer, x, y, width, height)`
Copy bytes() or bytearray() content to the screen internal memory. Note:
every color requires 2 bytes in the array
- `text(font, s, x, y[, fg, bg])`
Write `s` (integer, string or bytes) to the display using the specified bitmap
`font` with the coordinates as the upper-left corner of the text. The optional
arguments `fg` and `bg` can set the foreground and background colors of the
text; otherwise the foreground color defaults to `WHITE`, and the background
color defaults to `BLACK`. See the `README.md` in the `fonts/bitmap` directory
for example fonts.
- `write(bitmap_font, s, x, y[, fg, bg, background_tuple, fill_flag])`
Write text to the display using the specified proportional or Monospace bitmap
font module with the coordinates as the upper-left corner of the text. The
foreground and background colors of the text can be set by the optional
arguments `fg` and `bg`, otherwise the foreground color defaults to `WHITE`
and the background color defaults to `BLACK`.
Transparency can be emulated by providing a `background_tuple` containing
(bitmap_buffer, width, height). This is the same format used by the jpg_decode
method. See examples/T-DISPLAY/clock/clock.py for an example.
See the `README.md` in the `truetype/fonts` directory for example fonts.
Returns the width of the string as printed in pixels. Accepts UTF8 encoded strings.
The `font2bitmap` utility creates compatible 1 bit per pixel bitmap modules
from Proportional or Monospaced True Type fonts. The character size,
foreground, background colors, and characters in the bitmap
module may be specified as parameters. Use the -h option for details. If you
specify a buffer_size during the display initialization, it must be large
enough to hold the widest character (HEIGHT * MAX_WIDTH * 2).
- `write_len(bitap_font, s)`
Returns the string's width in pixels if printed in the specified font.
- `draw(vector_font, s, x, y[, fg, scale])`
Draw text to the display using the specified Hershey vector font with the
coordinates as the lower-left corner of the text. The foreground color of the
text can be set by the optional argument `fg`. Otherwise the foreground color
defaults to `WHITE`. The size of the text can be scaled by specifying a
`scale` value. The `scale` value must be larger than 0 and can be a
floating-point or an integer value. The `scale` value defaults to 1.0. See
the README.md in the `vector/fonts` directory, for example fonts and the
utils directory for a font conversion program.
- `draw_len(vector_font, s[, scale])`
Returns the string's width in pixels if drawn with the specified font.
- `jpg(jpg, x, y [, method])`
Draw a `jpg` on the display with the given `x` and `y` coordinates as the
upper left corner of the image. `jpg` may be a string containing a filename
or a buffer containing the JPEG image data.
The memory required to decode and display a JPG can be considerable as a full-screen
320x240 JPG would require at least 3100 bytes for the working area + 320 * 240 * 2
bytes of ram to buffer the image. Jpg images that would require a buffer larger than
available memory can be drawn by passing `SLOW` for the `method`. The `SLOW` `method`
will draw the image one piece at a time using the Minimum Coded Unit (MCU, typically
a multiple of 8x8) of the image. The default method is `FAST`.
- `jpg_decode(jpg_filename [, x, y, width, height])`
Decode a jpg file and return it or a portion of it as a tuple composed of
(buffer, width, height). The buffer is a color565 blit_buffer compatible byte
array. The buffer will require width * height * 2 bytes of memory.
If the optional x, y, width, and height parameters are given, the buffer will
only contain the specified area of the image. See examples/T-DISPLAY/clock/clock.py
examples/T-DISPLAY/toasters_jpg/toasters_jpg.py for examples.
- `png(png_filename, x, y [, mask])`
Draw a PNG file on the display with upper left corner of the image at the given `x` and `y`
coordinates. The PNG will be clipped if it is not able to fit fully on the display. The
PNG will be drawn one line at a time. Since the driver does not contain a frame buffer,
transparency is not supported. Providing a `True` value for the `mask` parameter
will prevent pixels with a zero alpha channel value from being displayed. Drawing masked PNG's is
slower than non-masked as each visible line segment is drawn separately. For an example of using a
mask, see the alien.py program in the examples/png folder.
- `polygon_center(polygon)`
Return the center of the `polygon` as an (x, y) tuple. The `polygon` should
consist of a list of (x, y) tuples forming a closed convex polygon.
- `fill_polygon(polygon, x, y, color[, angle, center_x, center_y])`
Draw a filled `polygon` at the `x`, and `y` coordinates in the `color` given.
The polygon may be rotated `angle` radians about the `center_x` and
`center_y` point. The polygon should consist of a list of (x, y) tuples
forming a closed convex polygon.
See the TWATCH-2020 `watch.py` demo for an example.
- `polygon(polygon, x, y, color, angle, center_x, center_y)`
Draw a `polygon` at the `x`, and `y` coordinates in the `color` given. The
polygon may be rotated `angle` radians about the `center_x` and `center_y`
point. The polygon should consist of a list of (x, y) tuples forming a closed
convex polygon.
See the T-Display `roids.py` for an example.
- `bounding({status, as_rect})`
Bounding enables or disables tracking the display area that has been written
to. Initially, tracking is disabled; pass a True value to enable tracking and
False to disable it. Passing a True or False parameter will reset the current
bounding rectangle to (display_width, display_height, 0, 0).
Returns a four integer tuple containing (min_x, min_y, max_x, max_y)
indicating the area of the display that has been written to since the last
clearing.
If `as_rect` parameter is True, the returned tuple will contain (min_x,
min_y, width, height) values.
See the TWATCH-2020 `watch.py` demo for an example.
- `bitmap(bitmap, x , y [, index])`
Draw `bitmap` using the specified `x`, `y` coordinates as the upper-left
corner of the `bitmap`. The optional `index` parameter provides a method to
select from multiple bitmaps contained a `bitmap` module. The `index` is used
to calculate the offset to the beginning of the desired bitmap using the
modules HEIGHT, WIDTH, and BPP values.
The `imgtobitmap.py` utility creates compatible 1 to 8 bit per pixel bitmap
modules from image files using the Pillow Python Imaging Library.
The `monofont2bitmap.py` utility creates compatible 1 to 8 bit per pixel
bitmap modules from Monospaced True Type fonts. See the `inconsolata_16.py`,
`inconsolata_32.py` and `inconsolata_64.py` files in the `examples/lib`
folder for sample modules and the `mono_font.py` program for an example using
the generated modules.
The character sizes, bit per pixel, foreground, background colors, and the
characters to include in the bitmap module may be specified as parameters.
Use the -h option for details. Bits per pixel settings larger than one may be
used to create antialiased characters at the expense of memory use. If you
specify a buffer_size during the display initialization, it must be large
enough to hold the one character (HEIGHT * WIDTH * 2).
- `width()`
Returns the current logical width of the display. (ie a 135x240 display
rotated 90 degrees is 240 pixels wide)
- `height()`
Returns the current logical height of the display. (ie a 135x240 display
rotated 90 degrees is 135 pixels high)
- `rotation(r)`
Set the rotates the logical display in a counter-clockwise direction.
0-Portrait (0 degrees), 1-Landscape (90 degrees), 2-Inverse Portrait (180
degrees), 3-Inverse Landscape (270 degrees)
- `offset(x_start, y_start)` The memory in the ST7789 controller is configured
for a 240x320 display. When using a smaller display like a 240x240 or
135x240, an offset needs to be added to the x and y parameters so that the
pixels are written to the memory area corresponding to the visible display.
The offsets may need to be adjusted when rotating the display.
For example, the TTGO-TDisplay is 135x240 and uses the following offsets.
| Rotation | x_start | y_start |
|----------|---------|---------|
| 0 | 52 | 40 |
| 1 | 40 | 53 |
| 2 | 53 | 40 |
| 3 | 40 | 52 |
When the rotation method is called, the driver will adjust the offsets for a
135x240 or 240x240 display. Your display may require using different offset
values; if so, use the `offset` method after `rotation` to set the offset
values.
The values needed for a particular display may not be documented and may
require some experimentation to determine the correct values. One technique
is to draw a box the same size as the display and then make small changes to
the offsets until the display looks correct. See the `cfg_helper.py` program
in the examples folder for more information.
The module exposes predefined colors:
`BLACK`, `BLUE`, `RED`, `GREEN`, `CYAN`, `MAGENTA`, `YELLOW`, and `WHITE`
## Scrolling
The st7789 display controller contains a 240 by 320-pixel frame buffer used to
store the pixels for the display. For scrolling, the frame buffer consists of
three separate areas; The (`tfa`) top fixed area, the (`height`) scrolling
area, and the (`bfa`) bottom fixed area. The `tfa` is the upper portion of the
frame buffer in pixels not to scroll. The `height` is the center portion of the
frame buffer in pixels to scroll. The `bfa` is the lower portion of the frame
buffer in pixels not to scroll. These values control the ability to scroll the
entire or a part of the display.
For displays that are 320 pixels high, setting the `tfa` to 0, `height` to 320,
and `bfa` to 0 will allow scrolling of the entire display. You can set the
`tfa` and `bfa` to a non-zero value to scroll a portion of the display. `tfa` +
`height` + `bfa` = should equal 320, otherwise the scrolling mode is undefined.
Displays less than 320 pixels high, the `tfa`, `height`, and `bfa` will need to
be adjusted to compensate for the smaller LCD panel. The actual values will
vary depending on the configuration of the LCD panel. For example, scrolling
the entire 135x240 TTGO T-Display requires a `tfa` value of 40, `height` value
of 240, and `bfa` value of 40 (40+240+40=320) because the T-Display LCD shows
240 rows starting at the 40th row of the frame buffer, leaving the last 40 rows
of the frame buffer undisplayed.
Other displays like the Waveshare Pico LCD 1.3 inch 240x240 display require the
`tfa` set to 0, `height` set to 240, and `bfa` set to 80 (0+240+80=320) to
scroll the entire display. The Pico LCD 1.3 shows 240 rows starting at the 0th
row of the frame buffer, leaving the last 80 rows of the frame buffer
undisplayed.
The `vscsad` method sets the (VSSA) Vertical Scroll Start Address. The VSSA
sets the line in the frame buffer that will be the first line after the `tfa`.
The ST7789 datasheet warns:
The value of the vertical scrolling start address is absolute (with reference to the frame memory),
it must not enter the fixed area (defined by Vertical Scrolling Definition, otherwise undesirable
image will be displayed on the panel.
- `vscrdef(tfa, height, bfa)` Set the vertical scrolling parameters.
`tfa` is the top fixed area in pixels. The top fixed area is the upper
portion of the display frame buffer that will not be scrolled.
`height` is the total height in pixels of the area scrolled.
`bfa` is the bottom fixed area in pixels. The bottom fixed area is the lower
portion of the display frame buffer that will not be scrolled.
- `vscsad(vssa)` Set the vertical scroll address.
`vssa` is the vertical scroll start address in pixels. The vertical scroll
start address is the line in the frame buffer will be the first line shown
after the TFA.
## Helper functions
- `color565(r, g, b)`
Pack a color into 2-bytes rgb565 format
- `map_bitarray_to_rgb565(bitarray, buffer, width, color=WHITE, bg_color=BLACK)`
Convert a `bitarray` to the rgb565 color `buffer` suitable for blitting. Bit
1 in `bitarray` is a pixel with `color` and 0 - with `bg_color`.

19
scripts/download.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
# 连接并打印文件列表
mpremote connect /dev/tty.wchusbserial14310 ls
# 生成romfs文件系统并上传
mpremote romfs -o website.romfs build website
mpremote romfs deploy website.romfs
# 复制文件
for file in src/*.py; do
if [ -f "$file" ]; then
mpremote fs cp "$file" :
fi
done
# 打印文件列表
mpremote ls /
mpremote ls /rom

93
src/app.py Normal file
View File

@@ -0,0 +1,93 @@
# ESP8266天气站主程序
# 首先尝试连接已保存的WiFi失败则启动CaptivePortal进行配置
import gc, time, sys, machine
# 使用全局的WiFiManager实例
from wifi_manager import wifi_manager
print("尝试连接已保存的WiFi网络...")
if wifi_manager.connect():
# 连接成功
ip = wifi_manager.get_ip()
print(f"WiFi连接成功IP地址: {ip}")
# 在这里可以添加主应用程序代码
# 例如:启动天气数据获取和显示
print("启动主应用程序...")
# 示例:保持连接
try:
while True:
# 检查连接状态
if not wifi_manager.is_connected():
print("WiFi连接断开")
break
# 每3秒报告一次状态
time.sleep(3)
gc.collect()
print(f"正常运行中IP: {ip}, 可用内存: {gc.mem_free()} bytes")
except KeyboardInterrupt:
print("用户中断")
finally:
wifi_manager.disconnect()
print("应用程序结束")
else:
# 连接失败启动CaptivePortal进行WiFi配置
print("无法连接到WiFi启动CaptivePortal进行配置")
from captive_portal import CaptivePortal
# 启动CaptivePortal
portal = CaptivePortal()
try:
if portal.start():
# clear
del portal
sys.modules.pop('CaptivePortal', None)
sys.modules.pop('captive_dns', None)
sys.modules.pop('captive_http', None)
sys.modules.pop('server_base', None)
gc.collect()
# CaptivePortal成功配置并连接
print("WiFi配置成功并已连接")
# 在这里可以添加主应用程序代码
print("启动主应用程序...")
# 示例:保持连接
try:
while True:
# 检查连接状态
if not wifi_manager.is_connected():
print("WiFi连接断开")
break
# 每3秒报告一次状态
time.sleep(3)
gc.collect()
print(f"正常运行中,可用内存: {gc.mem_free()} bytes")
except KeyboardInterrupt:
print("用户中断")
finally:
wifi_manager.disconnect()
print("应用程序结束")
else:
print("CaptivePortal未能成功建立连接")
except KeyboardInterrupt:
print("用户中断")
finally:
time.sleep(3)
machine.reset()

11
src/main.py Normal file
View File

@@ -0,0 +1,11 @@
import machine, sys, time
import rom.app
try:
app.start()
except Exception as e:
print("Fatal error in main:")
sys.print_exception(e)
time.sleep(3)
machine.reset()

12
src/rom/boot.py Normal file
View File

@@ -0,0 +1,12 @@
# This file is executed on every boot (including wake-boot from deepsleep)
import esp, gc, uos, machine
esp.osdebug(None)
#uos.dupterm(None, 1) # disable REPL on UART(0)
# cpu freq = 160MHz
machine.freq(160000000)
# memory auto collect (<16KB)
gc.threshold(16384)
gc.collect()

75
src/rom/captive_dns.py Normal file
View File

@@ -0,0 +1,75 @@
import gc
import usocket as socket
from server_base import BaseServer
class DNSQuery:
def __init__(self, data):
self.data = data
self.domain = ""
# header is bytes 0-11, so question starts on byte 12
head = 12
# length of this label defined in first byte
length = data[head]
while length != 0:
label = head + 1
# add the label to the requested domain and insert a dot after
self.domain += data[label : label + length].decode("utf-8") + "."
# check if there is another label after this one
head += length + 1
length = data[head]
def answer(self, ip_addr):
# ** create the answer header **
# copy the ID from incoming request
packet = self.data[:2]
# set response flags (assume RD=1 from request)
packet += b"\x81\x80"
# copy over QDCOUNT and set ANCOUNT equal
packet += self.data[4:6] + self.data[4:6]
# set NSCOUNT and ARCOUNT to 0
packet += b"\x00\x00\x00\x00"
# ** create the answer body **
# respond with original domain name question
packet += self.data[12:]
# pointer back to domain name (at byte 12)
packet += b"\xc0\x0c"
# set TYPE and CLASS (A record and IN class)
packet += b"\x00\x01\x00\x01"
# set TTL to 60sec
packet += b"\x00\x00\x00\x3c"
# set response length to 4 bytes (to hold one IPv4 address)
packet += b"\x00\x04"
# now actually send the IP address as 4 bytes (without the "."s)
packet += bytes(map(int, ip_addr.split(".")))
gc.collect()
return packet
class DNSServer(BaseServer):
def __init__(self, poller, ip_addr):
super().__init__(poller, 53, socket.SOCK_DGRAM, "DNS Server")
self.ip_addr = ip_addr
def handle(self, sock, event, others):
# server doesn't spawn other sockets, so only respond to its own socket
if sock is not self.sock:
return
# check the DNS question, and respond with an answer
try:
data, sender = sock.recvfrom(1024)
request = DNSQuery(data)
print("Sending {:s} -> {:s}".format(request.domain, self.ip_addr))
sock.sendto(request.answer(self.ip_addr), sender)
# help MicroPython with memory management
del request
gc.collect()
except Exception as e:
print("DNS server exception:", e)

292
src/rom/captive_http.py Normal file
View File

@@ -0,0 +1,292 @@
from collections import namedtuple
import network
import uerrno
import uio
import uselect as select
import usocket as socket
from config import config
from server_base import BaseServer
from wifi_manager import wifi_manager
WriteConn = namedtuple("WriteConn", ["body", "buff", "buffmv", "write_range"])
ReqInfo = namedtuple("ReqInfo", ["type", "path", "params", "host"])
import gc
def unquote(string):
"""stripped down implementation of urllib.parse unquote_to_bytes"""
if not string:
return b""
if isinstance(string, str):
string = string.encode("utf-8")
string = string.replace(b"+", b" ")
# split into substrings on each escape character
bits = string.split(b"%")
if len(bits) == 1:
return string # there was no escape character
res = [bits[0]] # everything before the first escape character
# for each escape character, get the next two digits and convert to
for item in bits[1:]:
code = item[:2]
char = bytes([int(code, 16)]) # convert to utf-8-encoded byte
res.append(char) # append the converted character
res.append(
item[2:]
) # append anything else that occurred before the next escape character
return b"".join(res)
class HTTPServer(BaseServer):
def __init__(self, poller, local_ip):
super().__init__(poller, 80, socket.SOCK_STREAM, "HTTP Server")
if type(local_ip) is bytes:
self.local_ip = local_ip
else:
self.local_ip = local_ip.encode()
self.request = dict()
self.conns = dict()
self.routes = {
b"/": b"/rom/index.html",
b"/login": self.login,
b"/scan": self.scan_networks,
}
self.ssid = None
# queue up to 2 connection requests before refusing (ESP8266 memory optimization)
self.sock.listen(2)
self.sock.setblocking(False)
#@micropython.native
def handle(self, sock, event, others):
if sock is self.sock:
# client connecting on port 80, so spawn off a new
# socket to handle this connection
print("- Accepting new HTTP connection")
self.accept(sock)
elif event & select.POLLIN:
# socket has data to read in
print("- Reading incoming HTTP data")
self.read(sock)
elif event & select.POLLOUT:
# existing connection has space to send more data
print("- Sending outgoing HTTP data")
self.write_to(sock)
def accept(self, server_sock):
"""accept a new client request socket and register it for polling"""
try:
client_sock, addr = server_sock.accept()
except OSError as e:
if e.args[0] == uerrno.EAGAIN:
return
client_sock.setblocking(False)
client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.poller.register(client_sock, select.POLLIN)
def parse_request(self, req):
"""parse a raw HTTP request to get items of interest"""
req_lines = req.split(b"\r\n")
req_type, full_path, http_ver = req_lines[0].split(b" ")
path = full_path.split(b"?")
base_path = path[0]
query = path[1] if len(path) > 1 else None
query_params = (
{
key: val
for key, val in [param.split(b"=") for param in query.split(b"&")]
}
if query
else {}
)
host = [line.split(b": ")[1] for line in req_lines if b"Host:" in line][0]
return ReqInfo(req_type, base_path, query_params, host)
def login(self, params):
# 从URL参数中提取表单数据
ssid = unquote(params.get(b"ssid", None))
password = unquote(params.get(b"password", None))
city = unquote(params.get(b"city", None))
# 使用全局Config实例保存配置
config.set("ssid", ssid)
config.set("password", password)
config.set("city", city)
if config.write():
print("配置保存成功")
else:
print("配置保存失败,数据无效")
# 重定向local_ip
headers = (
b"HTTP/1.1 307 Temporary Redirect\r\nLocation: http://{:s}/\r\n".format(
self.local_ip
)
)
return b"", headers
def scan_networks(self, params):
"""扫描WiFi网络并返回JSON数据"""
try:
# 使用wifi_manager扫描网络
networks = wifi_manager.scan_networks()
import ujson
json_data = ujson.dumps({"networks": networks})
except Exception as e:
print(f"扫描网络时出错: {e}")
json_data = ujson.dumps({"networks": []})
headers = b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nAccess-Control-Allow-Origin: *\r\n"
return json_data.encode(), headers
def get_response(self, req):
"""generate a response body and headers, given a route"""
headers = b"HTTP/1.1 200 OK\r\n"
route = self.routes.get(req.path, None)
if type(route) is bytes:
# expect a filename, so return contents of file
return open(route, "rb"), headers
if callable(route):
# call a function, which may or may not return a response
response = route(req.params)
body = response[0] or b""
headers = response[1] or headers
return uio.BytesIO(body), headers
headers = b"HTTP/1.1 404 Not Found\r\n"
return uio.BytesIO(b""), headers
def is_valid_req(self, req):
if req.host != self.local_ip:
# force a redirect to the MCU's IP address
return False
# redirect if we don't have a route for the requested path
return req.path in self.routes
def read(self, s):
"""read in client request from socket"""
data = s.read()
if not data:
# no data in the TCP stream, so close the socket
self.close(s)
return
# add new data to the full request
sid = id(s)
self.request[sid] = self.request.get(sid, b"") + data
# check if additional data expected
if data[-4:] != b"\r\n\r\n":
# HTTP request is not finished if no blank line at the end
# wait for next read event on this socket instead
return
# get the completed request
req = self.parse_request(self.request.pop(sid))
if not self.is_valid_req(req):
headers = (
b"HTTP/1.1 307 Temporary Redirect\r\nLocation: http://{:s}/\r\n".format(
self.local_ip
)
)
body = uio.BytesIO(b"")
self.prepare_write(s, body, headers)
return
# by this point, we know the request has the correct
# host and a valid route
body, headers = self.get_response(req)
self.prepare_write(s, body, headers)
def prepare_write(self, s, body, headers):
# add newline to headers to signify transition to body
headers += "\r\n"
# TCP/IP MSS is 536 bytes, so create buffer of this size and
# initially populate with header data
buff = bytearray(headers + "\x00" * (536 - len(headers)))
# use memoryview to read directly into the buffer without copying
buffmv = memoryview(buff)
# start reading body data into the memoryview starting after
# the headers, and writing at most the remaining space of the buffer
# return the number of bytes written into the memoryview from the body
bw = body.readinto(buffmv[len(headers) :], 536 - len(headers))
# save place for next write event
c = WriteConn(body, buff, buffmv, [0, len(headers) + bw])
self.conns[id(s)] = c
# let the poller know we want to know when it's OK to write
self.poller.modify(s, select.POLLOUT)
def write_to(self, sock):
"""write the next message to an open socket"""
# get the data that needs to be written to this socket
c = self.conns[id(sock)]
if c:
# write next 536 bytes (max) into the socket
try:
bytes_written = sock.write(
c.buffmv[c.write_range[0] : c.write_range[1]]
)
except OSError:
print("cannot write to a closed socket")
self.close(sock)
return
if not bytes_written or c.write_range[1] < 536:
# either we wrote no bytes, or we wrote < TCP MSS of bytes
# so we're done with this connection
self.close(sock)
else:
# more to write, so read the next portion of the data into
# the memoryview for the next send event
self.buff_advance(c, bytes_written)
def buff_advance(self, c, bytes_written):
"""advance the writer buffer for this connection to next outgoing bytes"""
if bytes_written == c.write_range[1] - c.write_range[0]:
# wrote all the bytes we had buffered into the memoryview
# set next write start on the memoryview to the beginning
c.write_range[0] = 0
# set next write end on the memoryview to length of bytes
# read in from remainder of the body, up to TCP MSS
c.write_range[1] = c.body.readinto(c.buff, 536)
else:
# didn't read in all the bytes that were in the memoryview
# so just set next write start to where we ended the write
c.write_range[0] += bytes_written
def close(self, s):
"""close the socket, unregister from poller, and delete connection"""
s.close()
self.poller.unregister(s)
sid = id(s)
if sid in self.request:
del self.request[sid]
if sid in self.conns:
c = self.conns[sid]
# 检查body是文件对象而不是BytesIO则关闭文件
if hasattr(c.body, "close"):
c.body.close()
del self.conns[sid]
gc.collect()

133
src/rom/captive_portal.py Normal file
View File

@@ -0,0 +1,133 @@
import gc
import network
import ubinascii as binascii
import uselect as select
import utime as time
from captive_dns import DNSServer
from captive_http import HTTPServer
from config import config
from wifi_manager import wifi_manager
class CaptivePortal:
AP_IP = "192.168.4.1"
MAX_CONN_ATTEMPTS = 10
def __init__(self, essid=None):
self.local_ip = self.AP_IP
self.ap_if = network.WLAN(network.AP_IF)
if essid is None:
essid = b"ws2-%s" % binascii.hexlify(self.ap_if.config("mac")[-3:])
self.essid = essid
self.dns_server = None
self.http_server = None
self.poller = select.poll()
def start_access_point(self):
# sometimes need to turn off AP before it will come up properly
self.ap_if.active(False)
while not self.ap_if.active():
print("Waiting for access point to turn on")
self.ap_if.active(True)
time.sleep(1)
# IP address, netmask, gateway, DNS
self.ap_if.ifconfig(
(self.local_ip, "255.255.255.0", self.local_ip, self.local_ip)
)
self.ap_if.config(essid=self.essid, authmode=network.AUTH_OPEN)
print("AP mode configured:", self.ap_if.ifconfig())
def connect_to_wifi(self):
# 使用全局WiFiManager进行连接
if wifi_manager.connect():
self.local_ip = wifi_manager.get_ip()
return True
return False
def check_valid_wifi(self):
if not wifi_manager.is_connected():
if config.is_valid():
# have credentials to connect, but not yet connected
# return value based on whether the connection was successful
return self.connect_to_wifi()
# not connected, and no credentials to connect yet
return False
return True
def captive_portal(self):
print("Starting captive portal")
self.start_access_point()
if self.http_server is None:
self.http_server = HTTPServer(self.poller, self.local_ip)
print("Configured HTTP server")
if self.dns_server is None:
self.dns_server = DNSServer(self.poller, self.local_ip)
print("Configured DNS server")
try:
while True:
gc.collect()
# check for socket events and handle them
for response in self.poller.ipoll(1000):
sock, event, *others = response
is_handled = self.handle_dns(sock, event, others)
if not is_handled:
self.handle_http(sock, event, others)
if self.check_valid_wifi():
print("Connected to WiFi!")
self.http_server.stop(self.poller)
self.dns_server.stop(self.poller)
break
except KeyboardInterrupt:
print("Captive portal stopped")
self.cleanup()
return wifi_manager.is_connected()
def handle_dns(self, sock, event, others):
if sock is self.dns_server.sock:
# ignore UDP socket hangups
if event == select.POLLHUP:
return True
self.dns_server.handle(sock, event, others)
return True
return False
def handle_http(self, sock, event, others):
self.http_server.handle(sock, event, others)
def cleanup(self):
print("Cleaning up")
if self.ap_if.active():
self.ap_if.active(False)
print("Turned off access point")
if self.dns_server:
self.dns_server.stop(self.poller)
self.dns_server = None
print("Discard portal.dns_server")
if self.http_server:
self.http_server.stop(self.poller)
self.http_server = None
print("Discard portal.http_server")
gc.collect()
def try_connect_from_file(self):
if config.is_valid():
if self.connect_to_wifi():
return True
# WiFi Connection failed but keep credentials for future retries
print("连接失败但保留配置,可以稍后重试")
return False
def start(self):
# turn off station interface to force a reconnect
wifi_manager.disconnect()
if not self.try_connect_from_file():
return self.captive_portal()

91
src/rom/config.py Normal file
View File

@@ -0,0 +1,91 @@
import ujson
import uos
class Config:
"""通用配置类,用于保存各种系统配置项"""
CONFIG_FILE = "/config.json"
_instance = None
_initialized = False
def __new__(cls):
"""单一实例模式实现"""
if cls._instance is None:
cls._instance = super(Config, cls).__new__(cls)
return cls._instance
def __init__(self, ssid=None, password=None, city=None, **kwargs):
"""初始化配置,只在第一次调用时执行"""
if not self._initialized:
self.config_data = {"ssid": ssid, "password": password, "city": city}
# 添加其他可能的自定义配置项
self.config_data.update(kwargs)
self._initialized = True
# 自动加载配置文件
self.load()
def write(self):
"""将配置写入JSON格式的配置文件"""
if self.is_valid():
# 只将非None的值保存到文件
save_data = {k: v for k, v in self.config_data.items() if v is not None}
with open(self.CONFIG_FILE, "w") as f:
ujson.dump(save_data, f)
print(f"写入配置到 {self.CONFIG_FILE}")
# 写入后重新加载配置
return self.load()
return False
def load(self):
"""从配置文件加载配置"""
try:
with open(self.CONFIG_FILE, "r") as f:
loaded_data = ujson.load(f)
self.config_data.update(loaded_data)
print(f"{self.CONFIG_FILE} 加载配置")
# 如果核心配置不完整,可能需要清除文件
if not self.is_valid():
print("配置不完整,清除配置文件")
self.remove()
except (OSError, ValueError):
pass
return self
def get(self, key, default=None):
"""获取配置项"""
return self.config_data.get(key, default)
def set(self, key, value):
"""设置配置项"""
self.config_data[key] = value
def remove(self):
"""删除配置文件并重置配置"""
try:
uos.remove(self.CONFIG_FILE)
except OSError:
pass
# 保留默认的关键配置项
self.config_data = {"ssid": None, "password": None, "city": None}
def is_valid(self):
"""检查核心配置项是否有效"""
ssid = self.config_data.get("ssid")
password = self.config_data.get("password")
# 确保SSID和密码都是字符串类型且不为空
if not ssid:
return False
if not password:
return False
return True
# 全局配置实例
config = Config()

317
src/rom/index.html Normal file
View File

@@ -0,0 +1,317 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WiFi认证</title>
<style>
body {
font-family: sans-serif;
background: #3498db;
width: 100%;
text-align: center;
margin: 20px 0;
position: relative;
}
p {
font-size: 12px;
text-decoration: none;
color: #fff;
}
h1 {
font-size: 1.5em;
color: #525252;
}
.box {
background: white;
width: 40ch;
border-radius: 6px;
margin: 0 auto;
padding: 10px 0;
position: relative;
}
input[type="text"],
input[type="password"] {
background: #ecf0f1;
border: #ccc 1px solid;
border-bottom: #ccc 2px solid;
padding: 8px;
width: 80%;
color: #000;
margin-top: 10px;
font-size: 1em;
border-radius: 4px;
}
.btn {
background: #2ecc71;
width: 80%;
padding: 8px 0;
color: white;
border-radius: 4px;
border: #27ae60 1px solid;
margin: 20 auto;
font-weight: 800;
font-size: 0.9em;
cursor: pointer;
}
.btn:hover {
background: #27ae60;
}
.config-title {
font-size: 1em;
color: #525252;
margin-top: 15px;
}
.wifi-list {
width: 80%;
max-height: 200px;
overflow-y: auto;
margin: 10px auto;
text-align: left;
border: 1px solid #eee;
background: #f8f8f8;
border-radius: 4px;
padding: 5px;
}
.wifi-item {
padding: 5px;
cursor: pointer;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
justify-content: space-between;
}
.wifi-item:hover {
background: #e0e0e0;
}
.wifi-name {
flex-grow: 1;
}
.wifi-signal {
font-size: 12px;
color: #888;
}
.refresh-btn {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
cursor: pointer;
font-size: 20px;
color: #7f8c8d;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.refresh-btn:hover {
background: #ecf0f1;
}
.refresh-btn:disabled {
color: #bdc3c7;
cursor: not-allowed;
}
.wifi-icon {
position: relative;
display: inline-block;
width: 18px;
height: 12px;
margin-right: 5px;
}
.wifi-icon-bar {
position: absolute;
bottom: 0;
background: #7f8c8d;
border-radius: 1px;
}
.wifi-icon-bar:nth-child(1) {
left: 0;
width: 3px;
height: 3px;
}
.wifi-icon-bar:nth-child(2) {
left: 5px;
width: 3px;
height: 6px;
}
.wifi-icon-bar:nth-child(3) {
left: 10px;
width: 3px;
height: 9px;
}
.wifi-icon-bar:nth-child(4) {
left: 15px;
width: 3px;
height: 12px;
}
.wifi-signal-4 .wifi-icon-bar {
background: #2ecc71;
}
.wifi-signal-3 .wifi-icon-bar:nth-child(1),
.wifi-signal-3 .wifi-icon-bar:nth-child(2),
.wifi-signal-3 .wifi-icon-bar:nth-child(3) {
background: #f1c40f;
}
.wifi-signal-2 .wifi-icon-bar:nth-child(1),
.wifi-signal-2 .wifi-icon-bar:nth-child(2) {
background: #e67e22;
}
.wifi-signal-1 .wifi-icon-bar:nth-child(1) {
background: #e74c3c;
}
/* 遮罩层样式 */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: none;
z-index: 1000;
justify-content: center;
align-items: center;
}
.overlay-content {
background: white;
padding: 30px;
border-radius: 8px;
text-align: center;
max-width: 80%;
}
.spinner-small {
width: 30px;
height: 30px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
margin: 15px auto;
animation: spin 1.5s linear infinite;
}
</style>
</head>
<body>
<form action="/login" method="get" class="box">
<h1>WiFi 配置</h1>
<button
type="button"
class="refresh-btn"
onclick="refreshList()"
title="刷新WiFi列表"
>
</button>
<div id="wifiList" class="wifi-list"></div>
<input
type="text"
id="ssid"
placeholder="WiFi名称"
name="ssid"
required
/><br />
<input
type="password"
placeholder="WiFi密码"
name="password"
required
/><br />
<input
type="text"
placeholder="城市名称"
name="city"
value=""
/><br />
<button type="submit" class="btn">保存配置</button>
</form>
<!-- 遮罩层 -->
<div class="overlay" id="connectingOverlay">
<div class="overlay-content">
<h2>正在连接到WiFi</h2>
<div class="spinner-small"></div>
<p>设备正在尝试连接,请稍候...</p>
</div>
</div>
<script>
window.onload = function () {
fetchWifiList();
// 添加表单提交事件监听
document
.querySelector(".box")
.addEventListener("submit", function (e) {
// 显示遮罩层
document.getElementById(
"connectingOverlay",
).style.display = "flex";
});
};
function fetchWifiList() {
const listContainer = document.getElementById("wifiList");
listContainer.innerHTML = "Loading...";
fetch("/scan")
.then((response) => response.json())
.then((data) => {
if (data.networks) {
data.networks.sort((a, b) => b.rssi - a.rssi);
displayWifiList(data.networks);
}
})
.catch((error) => {
console.error("Error fetching WiFi list:", error);
});
}
function getSignalLevel(rssi) {
if (rssi >= -50) return 4;
if (rssi >= -60) return 3;
if (rssi >= -70) return 2;
return 1;
}
function createSignalIcon(signalLevel) {
const icon = document.createElement("span");
icon.className = `wifi-icon wifi-signal-${signalLevel}`;
for (let i = 0; i < 4; i++) {
const bar = document.createElement("span");
bar.className = "wifi-icon-bar";
icon.appendChild(bar);
}
return icon;
}
function displayWifiList(networks) {
const listContainer = document.getElementById("wifiList");
listContainer.innerHTML = "";
for (const network of networks) {
const item = document.createElement("div");
item.className = "wifi-item";
const nameContainer = document.createElement("div");
nameContainer.className = "wifi-name";
const signalLevel = getSignalLevel(network.rssi);
nameContainer.appendChild(createSignalIcon(signalLevel));
const nameText = document.createTextNode(network.ssid);
nameContainer.appendChild(nameText);
const signalContainer = document.createElement("div");
signalContainer.className = "wifi-signal";
signalContainer.textContent = `${network.rssi} dBm`;
item.appendChild(nameContainer);
item.appendChild(signalContainer);
item.onclick = function () {
document.getElementById("ssid").value = network.ssid;
};
listContainer.appendChild(item);
}
}
function refreshList() {
const btn = document.querySelector(".refresh-btn");
btn.innerHTML = "⟳";
btn.disabled = true;
fetchWifiList();
setTimeout(() => {
btn.innerHTML = "↻";
btn.disabled = false;
}, 1000);
}
</script>
</body>
</html>

196
src/rom/nanoweb.py Normal file
View File

@@ -0,0 +1,196 @@
import uasyncio as asyncio
import uerrno
__version__ = '1.0.0'
class HttpError(Exception):
pass
class Request:
url = ""
method = ""
headers = {}
route = ""
read = None
write = None
close = None
def __init__(self):
self.url = ""
self.method = ""
self.headers = {}
self.route = ""
self.read = None
self.write = None
self.close = None
async def write(request, data):
await request.write(
data.encode('ISO-8859-1') if type(data) == str else data
)
async def error(request, code, reason):
await request.write("HTTP/1.1 %s %s\r\n\r\n" % (code, reason))
await request.write("<h1>%s</h1>" % (reason))
async def send_file(request, filename, segment=64, binary=False):
try:
with open(filename, 'rb' if binary else 'r') as f:
while True:
data = f.read(segment)
if not data:
break
await request.write(data)
except OSError as e:
if e.args[0] != uerrno.ENOENT:
raise
raise HttpError(request, 404, "File Not Found")
class Nanoweb:
extract_headers = ('Authorization', 'Content-Length', 'Content-Type')
headers = {}
routes = {}
assets_extensions = ('html', 'css', 'js')
callback_request = None
callback_error = staticmethod(error)
STATIC_DIR = ''
INDEX_FILE = STATIC_DIR + '/index.html'
def __init__(self, port=80, address='0.0.0.0'):
self.port = port
self.address = address
def route(self, route):
"""Route decorator"""
def decorator(func):
self.routes[route] = func
return func
return decorator
async def generate_output(self, request, handler):
"""Generate output from handler
`handler` can be :
* dict representing the template context
* string, considered as a path to a file
* tuple where the first item is filename and the second
is the template context
* callable, the output of which is sent to the client
"""
while True:
if isinstance(handler, dict):
handler = (request.url, handler)
if isinstance(handler, str):
await write(request, "HTTP/1.1 200 OK\r\n\r\n")
await send_file(request, handler)
elif isinstance(handler, tuple):
await write(request, "HTTP/1.1 200 OK\r\n\r\n")
filename, context = handler
context = context() if callable(context) else context
try:
with open(filename, "r") as f:
for l in f:
await write(request, l.format(**context))
except OSError as e:
if e.args[0] != uerrno.ENOENT:
raise
raise HttpError(request, 404, "File Not Found")
else:
handler = await handler(request)
if handler:
# handler can returns data that can be fed back
# to the input of the function
continue
break
async def handle(self, reader, writer):
items = await reader.readline()
items = items.decode('ascii').split()
if len(items) != 3:
return
request = Request()
request.read = reader.read
request.write = writer.awrite
request.close = writer.aclose
request.method, request.url, version = items
try:
try:
if version not in ("HTTP/1.0", "HTTP/1.1"):
raise HttpError(request, 505, "Version Not Supported")
while True:
items = await reader.readline()
items = items.decode('ascii').split(":", 1)
if len(items) == 2:
header, value = items
value = value.strip()
if header in self.extract_headers:
request.headers[header] = value
elif len(items) == 1:
break
if self.callback_request:
self.callback_request(request)
if request.url in self.routes:
# 1. If current url exists in routes
request.route = request.url
await self.generate_output(request,
self.routes[request.url])
else:
# 2. Search url in routes with wildcard
for route, handler in self.routes.items():
if route == request.url \
or (route[-1] == '*' and
request.url.startswith(route[:-1])):
request.route = route
await self.generate_output(request, handler)
break
else:
# 3. Try to load index file
if request.url in ('', '/'):
await send_file(request, self.INDEX_FILE)
else:
# 4. Current url have an assets extension ?
for extension in self.assets_extensions:
if request.url.endswith('.' + extension):
await send_file(
request,
'%s%s' % (
self.STATIC_DIR,
request.url,
),
binary=True,
)
break
else:
raise HttpError(request, 404, "File Not Found")
except HttpError as e:
request, code, message = e.args
await self.callback_error(request, code, message)
except OSError as e:
# Skip ECONNRESET error (client abort request)
if e.args[0] != uerrno.ECONNRESET:
raise
finally:
await writer.aclose()
async def run(self):
return await asyncio.start_server(self.handle, self.address, self.port)

27
src/rom/server_base.py Normal file
View File

@@ -0,0 +1,27 @@
import select
import socket
class BaseServer:
"""基础服务器类为HTTP和DNS服务器提供通用功能"""
def __init__(self, poller, port, sock_type, name):
self.name = name
# create socket with correct type: stream (TCP) or datagram (UDP)
self.sock = socket.socket(socket.AF_INET, sock_type)
# register to get event updates for this socket
self.poller = poller
self.poller.register(self.sock, select.POLLIN)
addr = socket.getaddrinfo("0.0.0.0", port)[0][-1]
# allow new requests while still sending last response
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind(addr)
print(self.name, "listening on", addr)
def stop(self, poller):
poller.unregister(self.sock)
self.sock.close()
print(self.name, "stopped")

164
src/rom/wifi_manager.py Normal file
View File

@@ -0,0 +1,164 @@
import time
import network
from config import config
class WiFiManager:
"""WiFi连接管理类负责处理WiFi连接相关功能"""
MAX_CONN_ATTEMPTS = 12
def __init__(self):
self.sta_if = network.WLAN(network.STA_IF)
self.config = config
self._interface_initialized = False
def _ensure_interface_active(self):
"""确保WLAN接口处于活动状态"""
try:
if not self.sta_if.active():
self.sta_if.active(True)
time.sleep(1) # 等待接口激活
self._interface_initialized = True
return True
except Exception as e:
print(f"激活WiFi接口失败: {e}")
return False
def _safe_connect_check(self):
"""安全地检查连接状态"""
try:
return self.sta_if.isconnected()
except:
return False
def is_connected(self):
"""检查是否已连接到WiFi"""
if not self._interface_initialized:
return False
return self._safe_connect_check()
def get_ip(self):
"""获取当前IP地址"""
if self.is_connected():
try:
return self.sta_if.ifconfig()[0]
except:
return None
return None
def connect(self):
"""尝试连接到WiFi"""
# 加载配置
if not self.config.load().is_valid():
print("没有有效的WiFi配置")
return False
ssid = self.config.get("ssid")
password = self.config.get("password")
if not ssid or not password:
print("SSID或密码为空")
return False
print(f"正在尝试连接到SSID: {ssid}")
try:
# 确保接口处于活动状态
if not self._ensure_interface_active():
return False
# 如果已经连接,先断开
if self._safe_connect_check():
self.sta_if.disconnect()
time.sleep(1)
# 执行连接
self.sta_if.connect(ssid, password)
# 等待连接完成
attempts = 1
while attempts <= self.MAX_CONN_ATTEMPTS:
if self._safe_connect_check():
ip = self.get_ip()
if ip:
print(f"连接成功! IP: {ip}")
return True
print(f"连接尝试 {attempts}/{self.MAX_CONN_ATTEMPTS}...")
time.sleep(2)
attempts += 1
# 连接失败
print(f"连接失败: {ssid}")
self.clear_config()
try:
print(f"WLAN状态: {self.sta_if.status()}")
except:
pass
return False
except Exception as e:
print(f"连接过程中发生错误: {e}")
return False
def disconnect(self):
"""断开WiFi连接"""
try:
if self._safe_connect_check():
self.sta_if.disconnect()
time.sleep(1)
print("已断开WiFi连接")
except Exception as e:
print(f"断开连接时出错: {e}")
def scan_networks(self):
"""扫描可用的WiFi网络"""
try:
# 确保接口处于活动状态
if not self._ensure_interface_active():
return []
# 如果已连接,先断开
if self._safe_connect_check():
self.sta_if.disconnect()
time.sleep(1)
# 执行扫描
networks = self.sta_if.scan()
# 处理结果
result = []
for net in networks:
try:
ssid = net[0].decode("utf-8", "ignore")
if ssid: # 只添加非隐藏网络
result.append({"ssid": ssid, "rssi": net[3]})
except:
continue # 跳过有问题的网络
return result
except Exception as e:
print(f"扫描WiFi网络时出错: {e}")
return []
def reset(self):
"""重置WiFi配置"""
try:
self.config.remove()
print("WiFi配置已重置")
except Exception as e:
print(f"重置配置时出错: {e}")
def clear_config(self):
"""清除WiFi配置可选操作"""
try:
self.config.set("ssid", None)
print("WiFi配置已临时清空")
except Exception as e:
print(f"清除配置时出错: {e}")
# 全局WiFi管理器实例
wifi_manager = WiFiManager()