init weather_station repo
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/archives
|
||||||
7
README.md
Normal file
7
README.md
Normal 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
65
docs/Nanoweb.md
Normal 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
322
docs/mp_optimize_demo.md
Normal 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
762
docs/mpremote.rst
Normal 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
746
docs/st7789_mpy.md
Normal 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
19
scripts/download.sh
Executable 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
93
src/app.py
Normal 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
11
src/main.py
Normal 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
12
src/rom/boot.py
Normal 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
75
src/rom/captive_dns.py
Normal 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
292
src/rom/captive_http.py
Normal 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
133
src/rom/captive_portal.py
Normal 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
91
src/rom/config.py
Normal 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
317
src/rom/index.html
Normal 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
196
src/rom/nanoweb.py
Normal 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
27
src/rom/server_base.py
Normal 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
164
src/rom/wifi_manager.py
Normal 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()
|
||||||
Reference in New Issue
Block a user