8000 Merge pull request #6528 from tannewt/cp_webserver · crackmonkey/circuitpython@63873e2 · GitHub 8000
[go: up one dir, main page]

Skip to content

Commit 63873e2

Browse files
authored
Merge pull request adafruit#6528 from tannewt/cp_webserver
Add web workflow to ESP boards
2 parents 819b5d1 + a59b52b commit 63873e2

File tree

29 files changed

+2102
-92
lines changed

29 files changed

+2102
-92
lines changed

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Full Table of Contents
2222
supported_ports.rst
2323
troubleshooting.rst
2424
drivers.rst
25+
workflows
2526
environment.rst
2627

2728
.. toctree::

docs/workflows.md

Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
# Workflows
2+
3+
Workflows are the process used to 1) manipulate files on the CircuitPython device and 2) interact
4+
with the serial connection to CircuitPython. The serial connection is usually used to access the
5+
REPL.
6+
7+
Starting with CircuitPython 3.x we moved to a USB-only workflow. Prior to that, we used the serial
8+
connection alone to do the whole workflow. In CircuitPython 7.x, a BLE workflow was added with the
9+
advantage of working with mobile devices. CircuitPython 8.x added a web workflow that works over the
10+
local network (usually Wi-Fi) and a web browser. Other clients can also use the Web REST API. Boards
11+
should clearly document which workflows are supported.
12+
13+
Code for workflows lives in `supervisor/shared`.
14+
15+
The workflow APIs are documented here.
16+
17+
## USB
18+
19+
These USB interfaces are enabled by default on boards with USB support. They are usable once the
20+
device has been plugged into a host.
21+
22+
### CIRCUITPY drive
23+
CircuitPython exposes a standard mass storage (MSC) interface to enable file manipulation over a
24+
standard interface. This interface works underneath the file system at the block level so using it
25+
excludes other types of workflows from manipulating the file system at the same time.
26+
27+
### CDC serial
28+
CircuitPython exposes one CDC USB interface for CircuitPython serial. This is a standard serial
29+
USB interface.
30+
31+
TODO: Document how it designates itself from the user CDC.
32+
33+
Setting baudrate 1200 and disconnecting will reboot into a bootloader. (Used by Arduino to trigger
34+
a reset into bootloader.)
35+
36+
## BLE
37+
38+
The BLE workflow is enabled for nRF boards. By default, to prevent malicious access, it is disabled.
39+
To connect to the BLE workflow, press the reset button while the status led blinks blue quickly
40+
after the safe mode blinks. The board will restart and broadcast the file transfer service UUID
41+
(`0xfebb`) along with the board's [Creation IDs](https://github.com/creationid/creators). This
42+
public broadcast is done at a lower transmit level so the devices must be closer. On connection, the
43+
device will need to pair and bond. Once bonded, the device will broadcast whenever disconnected
44+
using a rotating key rather than a static one. Non-bonded devices won't be able to resolve it. After
45+
connection, the central device can discover two default services. One for file transfer and one for
46+
CircuitPython specifically that includes serial characteristics.
47+
48+
### File Transfer API
49+
50+
CircuitPython uses [an open File Transfer API](https://github.com/adafruit/Adafruit_CircuitPython_BLE_File_Transfer)
51+
to enable file system access.
52+
53+
### CircuitPython Service
54+
55+
The base UUID for the CircuitPython service is `ADAFXXXX-4369-7263-7569-7450794686e`. The `XXXX` is
56+
replaced by the four specific digits below. The service itself is `0001`.
57+
58+
#### TX - `0002` / RX - `0003`
59+
60+
These characteristic work just like the Nordic Uart Service (NUS) but have different UUIDs to prevent
61+
conflicts with user created NUS services.
62+
63+
#### Version - `0100`
64+
Read-only characteristic that returns the UTF-8 encoded version string.
65+
66+
## Web
67+
68+
The web workflow is depends on adding Wi-Fi credentials into the `/.env` file. The keys are
69+
`CIRCUITPY_WIFI_SSID` and `CIRCUITPY_WIFI_PASSWORD`. Once these are defined, CircuitPython will
70+
automatically connect to the network and start the webserver used for the workflow. The webserver
71+
is on port 80. It also enables MDNS.
72+
73+
Here is an example `/.env`:
74+
75+
```bash
76+
# To auto-connect to Wi-Fi
77+
CIRCUITPY_WIFI_SSID='scottswifi'
78+
CIRCUITPY_WIFI_PASSWORD='secretpassword'
79+
80+
# To enable modifying files from the web. Change this too!
81+
CIRCUITPY_WEB_API_PASSWORD='passw0rd'
82+
```
83+
84+
MDNS is used to resolve [`circuitpython.local`](http://circuitpython.local) to a device specific
85+
hostname of the form `cpy-XXXXXX.local`. The `XXXXXX` is based on network MAC address. The device
86+
also provides the MDNS service with service type `_circuitpython` and protocol `_tcp`.
87+
88+
### HTTP
89+
The web server is HTTP 1.1 and may use chunked responses so that it doesn't need to precompute
90+
content length.
91+
92+
The API generally consists of an HTTP method such as GET or PUT and a path. Requests and responses
93+
also have headers. Responses will contain a status code and status text such as `404 Not Found`.
94+
This API tries to use standard status codes to encode the status of the various operations. The
95+
[Mozilla Developer Network HTTP docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP)
96+
are a great reference.
97+
98+
#### Examples
99+
The examples use `curl`, a common command line program for issuing HTTP requests. The examples below
100+
use `circuitpython.local` as the easiest way to work. If you have multiple active devices, you'll
101+
want to use the specific `cpy-XXXXXX.local` version.
102+
103+
The examples also use `passw0rd` as the password placeholder. Replace it with your password before
104+
running the example.
105+
106+
### `/`
107+
The root welcome page links to the file system page and also displays other CircuitPython devices
108+
found using MDNS service discovery. This allows web browsers to find other devices from one. (All
109+
devices will respond to `circuitpython.local` so the device redirected to may vary.)
110+
111+
### CORS
112+
The web server will allow requests from `cpy-XXXXXX.local`, `127.0.0.1`, the device's IP and
113+
`code.circuitpython.org`. (`circuitpython.local` requests will be redirected to `cpy-XXXXXX.local`.)
114+
115+
### File REST API
116+
All file system related APIs are protected by HTTP basic authentication. It is *NOT* secure but will
117+
hopefully prevent some griefing in shared settings. The password is sent unencrypted so do not reuse
118+
a password with something important.
119+
120+
The password is taken from `/.env` with the key `CIRCUITPY_WEB_API_PASSWORD`. If this is unset, the
121+
server will respond with `403 Forbidden`. When a password is set, but not provided in a request, it
122+
will respond `401 Unauthorized`.
123+
124+
#### `/fs/`
125+
126+
The `/fs/` page will respond with a directory browsing HTML once authenticated. This page is always
127+
gzipped. If the `Accept: application/json` header is provided, then the JSON representation of the
128+
root will be returned.
129+
130+
##### OPTIONS
131+
When requested with the `OPTIONS` method, the server will respond with CORS related headers. Most
132+
aren't needed for API use. They are there for the web browser.
133+
134+
* `Access-Control-Allow-Methods` - Varies with USB state. `GET, OPTIONS` when USB is active. `GET, OPTIONS, PUT, DELETE` otherwise.
135+
136+
Example:
137+
138+
```sh
139+
curl -v -u :passw0rd -X OPTIONS -L --location-trusted http://circuitpython.local/fs/
140+
```
141+
142+
#### `/fs/<directory path>/`
143+
Directory paths must end with a /. Otherwise, the path is assumed to be a file.
144+
145+
##### GET
146+
Returns a JSON representation of the directory.
147+
148+
* `200 OK` - Directory exists and JSON returned
149+
* `401 Unauthorized` - Incorrect password
150+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
151+
* `404 Not Found` - Missing directory
152+
153+
Returns information about each file in the directory:
154+
155+
* `name` - File name. No trailing `/` on directory names
156+
* `directory` - `true` when a directory. `false` otherwise
157+
* `modified_ns` - File modification time in nanoseconds since January 1st, 1970. May not use full resolution
158+
* `file_size` - File size in bytes. `0` for directories
159+
160+
Example:
161+
162+
```sh
163+
curl -v -u :passw0rd -H "Accept: application/json" -L --location-trusted http://circuitpython.local/fs/lib/hello/
164+
```
165+
166+
```json
167+
[
168+
{
169+
"name": "world.txt",
170+
"directory": false,
171+
"modified_ns": 946934328000000000,
172+
"file_size": 12
173+
}
174+
]
175+
```
176+
177+
##### PUT
178+
Tries to make a directory at the given path. Request body is ignored. The custom `X-Timestamp`
179+
header can provide a timestamp in milliseconds since January 1st, 1970 (to match JavaScript's file
180+
time resolution) used for the directories modification time. The RTC time will used otherwise.
181+
182+
Returns:
183+
184+
* `204 No Content` - Directory exists
185+
* `201 Created` - Directory created
186+
* `401 Unauthorized` - Incorrect password
187+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
188+
* `409 Conflict` - USB is active and preventing file system modification
189+
* `404 Not Found` - Missing parent directory
190+
* `500 Server Error` - Other, unhandled error
191+
192+
Example:
193+
194+
```sh
195+
curl -v -u :passw0rd -X PUT -L --location-trusted http://circuitpython.local/fs/lib/hello/world/
196+
```
197+
198+
##### DELETE
199+
Deletes the directory and all of its contents.
200+
201+
* `204 No Content` - Directory and its contents deleted
202+
* `401 Unauthorized` - Incorrect password
203+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
204+
* `404 Not Found` - No directory
205+
* `409 Conflict` - USB is active and preventing file system modification
206+
207+
Example:
208+
209+
```sh
210+
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world/
211+
```
212+
213+
214+
#### `/fs/<file path>`
215+
216+
##### PUT
217+
Stores the provided content to the file path.
218+
219+
The custom `X-Timestamp` header can provide a timestamp in milliseconds since January 1st, 1970
220+
(to match JavaScript's file time resolution) used for the directories modification time. The RTC
221+
time will used otherwise.
222+
223+
Returns:
224+
225+
* `201 Created` - File created and saved
226+
* `204 No Content` - File existed and overwritten
227+
* `401 Unauthorized` - Incorrect password
228+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
229+
* `404 Not Found` - Missing parent directory
230+
* `409 Conflict` - USB is active and preventing file system modification
231+
* `413 Payload Too Large` - `Expect` header not sent and file is too large
232+
* `417 Expectation Failed` - `Expect` header sent and file is too large
233+
* `500 Server Error` - Other, unhandled error
234+
235+
If the client sends the `Expect` header, the server will reply with `100 Continue` when ok.
236+
237+
Example:
238+
239+
```sh
240+
echo "Hello world" >> test.txt
241+
curl -v -u :passw0rd -T test.txt -L --location-trusted http://circuitpython.local/fs/lib/hello/world.txt
242+
```
243+
244+
##### GET
245+
Returns the raw file contents. `Content-Type` will be set based on extension:
246+
247+
* `text/plain` - `.py`, `.txt`
248+
* `text/javascript` - `.js`
249+
* `text/html` - `.html`
250+
* `application/json` - `.json`
251+
* `application/octet-stream` - Everything else
252+
253+
Will return:
254+
* `200 OK` - File exists and file returned
255+
* `401 Unauthorized` - Incorrect password
256+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
257+
* `404 Not Found` - Missing file
258+
259+
Example:
260+
261+
```sh
262+
curl -v -u :passw0rd -L --location-trusted http://circuitpython.local/fs/lib/hello/world.txt
263+
```
264+
265+
266+
##### DELETE
267+
Deletes the file.
268+
269+
270+
* `204 No Content` - File existed and deleted
271+
* `401 Unauthorized` - Incorrect password
272+
* `403 Forbidden` - No `CIRCUITPY_WEB_API_PASSWORD` set
273+
* `404 Not Found` - File not found
274+
* `409 Conflict` - USB is active and preventing file system modification
275+
276+
Example:
277+
278+
```sh
279+
curl -v -u :passw0rd -X DELETE -L --location-trusted http://circuitpython.local/fs/lib/hello/world.txt
280+
```
281+
282+
### `/cp/`
283+
284+
`/cp/` serves basic info about the CircuitPython device and others discovered through MDNS. It is
285+
not protected by basic auth in case the device is someone elses.
286+
287+
Only `GET` requests are supported and will return `405 Method Not Allowed` otherwise.
288+
289+
#### 97AE `/cp/version.json`
290+
291+
Returns information about the device.
292+
293+
* `web_api_version`: Always `1`. This versions the rest of the API and new versions may not be backwards compatible.
294+
* `version`: CircuitPython build version.
295+
* `build_date`: CircuitPython build date.
296+
* `board_name`: Human readable name of the board.
297+
* `mcu_name`: Human readable name of the microcontroller.
298+
* `board_id`: Board id used in code and on circuitpython.org.
299+
* `creator_id`: Creator ID for the board.
300+
* `creation_id`: Creation ID for the board, set by the creator.
301+
* `hostname`: MDNS hostname.
302+
* `port`: Port of CircuitPython Web Service.
303+
* `ip`: IP address of the device.
304+
305+
Example:
306+
```sh
307+
curl -v -L http://circuitpython.local/cp/version.json
308+
```
309+
310+
```json
311+
{
312+
"web_api_version": 1,
313+
"version": "8.0.0-alpha.1-20-ge1d4518a9-dirty",
314+
"build_date": "2022-06-24",
315+
741A "board_name": "ESP32-S3-USB-OTG-N8",
316+
"mcu_name": "ESP32S3",
317+
"board_id": "espressif_esp32s3_usb_otg_n8",
318+
"creator_id": 12346,
319+
"creation_id": 28683,
320+
"hostname": "cpy-f57ce8",
321+
"port": 80,
322+
"ip": "192.168.1.94"
323+
}
324+
```
325+
326+
#### `/cp/devices.json`
327+
328+
Returns information about other devices found on the network using MDNS.
329+
330+
* `total`: Total MDNS response count. May be more than in `devices` if internal limits were hit.
331+
* `devices`: List of discovered devices.
332+
* `hostname`: MDNS hostname
333+
* `instance_name`: MDNS instance name. Defaults to human readable board name.
334+
* `port`: Port of CircuitPython Web API
335+
* `ip`: IP address
336+
337+
Example:
338+
```sh
339+
curl -v -L http://circuitpython.local/cp/devices.json
340+
```
341+
342+
```json
343+
{
344+
"total": 1,
345+
"devices": [
346+
{
347+
"hostname": "cpy-951032",
348+
"instance_name": "Adafruit Feather ESP32-S2 TFT",
349+
"port": 80,
350+
"ip": "192.168.1.235"
351+
}
352+
]
353+
}
354+
```
355+
356+
### Static files
357+
358+
* `/favicon.ico` - Blinka
359+
* `/directory.js` - JavaScript for `/fs/`
360+
* `/welcome.js` - JavaScript for `/`
361+
362+
### WebSocket
363+
364+
Coming soon!

ports/espressif/background.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,20 @@
4040
#include "common-hal/pulseio/PulseIn.h"
4141
#endif
4242

43+
#if CIRCUITPY_WEB_WORKFLOW
44+
#include "supervisor/shared/web_workflow/web_workflow.h"
45+
#endif
4346

4447
void port_background_task(void) {
4548
// Zero delay in case FreeRTOS wants to switch to something else.
4649
vTaskDelay(0);
4750
#if CIRCUITPY_PULSEIO
4851
pulsein_background();
4952
#endif
53+
54+
#if CIRCUITPY_WEB_WORKFLOW
55+
supervisor_web_workflow_background();
56+
#endif
5057
}
5158

5259
void port_start_background_task(void) {

0 commit comments

Comments
 (0)
0