diff --git a/.gitignore b/.gitignore index 7ecf9b0..7edd53f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -venv/** +venv/ __pycache__ pyrobusta.env build/ runtime/ runtime-test/ -tls/ \ No newline at end of file +tls/ +tmp/ \ No newline at end of file diff --git a/Makefile b/Makefile index 9628d50..47adcb9 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ DEVICE ?= u0 SRC_DIR := src TEST_DIR := tests -EXAMPLE_DIR := example/demo_app +APP_DIR := example/demo_app BUILD_DIR := build DIST_DIR := dist TLS_DIR := tls -ASSETS_DIR := assets +DOCS_DIR := docs PKG := pyrobusta @@ -30,7 +30,7 @@ DEVICE_NAME := # e.g. ESP32-C3, will be used for report generation PT_DIR := tests/system .PHONY: all -all: clean toolchain static-checkers unit-test build test-unix deploy deploy-config tls-cert deploy-cert deploy-example +all: clean toolchain static-checkers unit-test build test-unix deploy deploy-config tls-cert deploy-cert deploy-app # ================================================ # Build @@ -55,10 +55,7 @@ toolchain: .PHONY: build build: $(MPY_TARGETS) $(INIT_TARGETS) @mkdir -p $(BUILD_DIR) - @if [ -d assets ]; then \ - echo "Copying assets/ -> $(BUILD_DIR)"; \ - cp -r assets $(BUILD_DIR)/${PKG}/; \ - fi + $(MAKE) docs # Compile .py -> .mpy $(BUILD_DIR)/%.mpy: $(SRC_DIR)/%.py @@ -126,6 +123,28 @@ redeploy: clean build clean-device deploy # Rules for release # ================================================ +# ----------------------------- +# Pre-process documentation +# ----------------------------- +.PHONY: docs_preprocess +docs_preprocess: + @find docs -type f -name "*.md" -exec \ + sed -E -i.bak \ + 's/(PyRobusta[[:space:]]).+([[:space:]]Web Server)/\1$(PYROBUSTA_VERSION)\2/' {} \; \ + && find docs -name "*.bak" -delete + + +# ----------------------------- +# Build documentation +# ----------------------------- +.PHONY: docs +docs: docs_preprocess + python3 scripts/generate_docs.py \ + $(DOCS_DIR)/application_development \ + $(BUILD_DIR)/$(PKG)/assets/www \ + --css $(DOCS_DIR)/application_development/styles/style.css + + # ----------------------------- # Prepare distribution # ----------------------------- @@ -133,27 +152,28 @@ redeploy: clean build clean-device deploy publish: test -n "$(DIST_DIR)" && rm -rf "$(PWD)/$(DIST_DIR)" mkdir -p "$(PWD)/$(DIST_DIR)" - @sed -E -i.bak 's/(PYROBUSTA_VERSION[[:space:]]*=[[:space:]]*)"[^"]*"/\1"$(PYROBUSTA_VERSION)"/' \ + + # Bump version in Python source + @sed -E -i.bak \ + 's/(PYROBUSTA_VERSION[[:space:]]*=[[:space:]]*)"[^"]*"/\1"$(PYROBUSTA_VERSION)"/' \ $(SRC_DIR)/pyrobusta/utils/config.py \ && rm -f $(SRC_DIR)/pyrobusta/utils/config.py.bak - @sed -E -i.bak 's/(PyRobusta[[:space:]]).+([[:space:]]Web Server)/\1$(PYROBUSTA_VERSION)\2/' \ - $(ASSETS_DIR)/www/*.html \ - && rm -f $(ASSETS_DIR)/www/*.html.bak + $(MAKE) clean - $(MAKE) build BUILD_DIR=$(DIST_DIR) - scripts/update_package.bash $(DIST_DIR) package.json $(PYROBUSTA_VERSION) + $(MAKE) build docs BUILD_DIR=$(DIST_DIR) + scripts/update_package.bash $(DIST_DIR) package.json $(PYROBUSTA_VERSION) # ================================================ # Example apps # ================================================ # ----------------------------- -# Prepare unix example runtime +# Prepare UNIX example runtime # ----------------------------- -.PHONY: stage-example -stage-example: - @echo "Preparing unix runtime in $(RUNTIME_DIR)" +.PHONY: stage-app +stage-app: + @echo "Preparing UNIX runtime in $(RUNTIME_DIR)" @rm -rf $(RUNTIME_DIR) @mkdir -p $(RUNTIME_DIR)/lib @@ -161,9 +181,9 @@ stage-example: @cp -r build/pyrobusta $(RUNTIME_DIR)/lib @cp -r build/pyrobusta/assets/www $(RUNTIME_DIR)/ - @echo "Copying example app" - @cp $(EXAMPLE_DIR)/app.py $(RUNTIME_DIR)/ - @cp $(EXAMPLE_DIR)/boot.py $(RUNTIME_DIR)/ + @echo "Copying app" + @cp $(APP_DIR)/app.py $(RUNTIME_DIR)/ + @cp $(APP_DIR)/boot.py $(RUNTIME_DIR)/ @echo "Copying TLS certificate" @cp $(TLS_DIR)/cert.der $(RUNTIME_DIR)/ @@ -174,27 +194,27 @@ stage-example: @echo "https_port=4443" >> $(RUNTIME_DIR)/pyrobusta.env # ----------------------------- -# Run example locally with unix micropython +# Run app locally with UNIX MicroPython # ----------------------------- .PHONY: run-unix -run-unix: stage-example - @echo "Running example with unix micropython" +run-unix: stage-app + @echo "Running app with UNIX MicroPython" cd $(RUNTIME_DIR) && MICROPYPATH=":.frozen:lib" ../$(MICROPYTHON) app.py # ----------------------------- -# Deploy example app +# Deploy application # ----------------------------- -.PHONY: deploy-example -deploy-example: +.PHONY: deploy-app +deploy-app: @echo "Uploading boot.py, app.py" @mpremote $(DEVICE) soft-reset - mpremote $(DEVICE) cp $(EXAMPLE_DIR)/boot.py :boot.py - mpremote $(DEVICE) cp $(EXAMPLE_DIR)/app.py :app.py + mpremote $(DEVICE) cp $(APP_DIR)/boot.py :boot.py + mpremote $(DEVICE) cp $(APP_DIR)/app.py :app.py @echo "Uploading pyrobusta.env" @if [ -f pyrobusta.env ]; then mpremote $(DEVICE) cp pyrobusta.env :pyrobusta.env; fi @mpremote $(DEVICE) reset - @echo "\e[32m$(EXAMPLE_DIR) example is successfully deployed, \n"\ + @echo "\e[32m$(APP_DIR) app is successfully deployed, \n"\ "run 'make DEVICE=$(DEVICE) run-device' to restart the device and check the output.\e[0m" # ----------------------------- @@ -259,7 +279,7 @@ stage-test: @cp tests/functional/*.py $(TEST_RUNTIME)/ # ----------------------------- -# Run functional tests on unix port +# Run functional tests on UNIX port # ----------------------------- .PHONY: test-unix test-unix: TLS_DIR=$(TEST_RUNTIME) diff --git a/README.md b/README.md index 3e2a4d7..11aefb9 100644 --- a/README.md +++ b/README.md @@ -102,13 +102,17 @@ async def main(): asyncio.run(main()) ``` +Check the [Application Development](./docs/application_development/index.md) guide for +more details on supported features and practical examples. + + # Configuration and Optimization To fine-tune heap usage and optimize performance, see: -- [dimensioning guide](./docs/dimensioning/http_dimensioning.md) -- [configuration settings](./docs/configuration.md) +- [Dimensioning](./docs/dimensioning/http_dimensioning.md) +- [Configuration Settings](./docs/application_development/configuration.md) # Development -Check the provided development guide to create and deploy custom builds -to your device: [development guide](./docs/development.md) +Check the provided [Development](./docs/development.md) guide to create and deploy custom builds +to your device, as well as running tests and static code checkers. diff --git a/assets/www/configuration.html b/assets/www/configuration.html deleted file mode 100644 index f777411..0000000 --- a/assets/www/configuration.html +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - PyRobusta Home - - - - -

Configuration

- - ← Back - -

- This page documents PyRobusta configuration options, - configuration deployment using mpremote, - and runtime access to configuration values through the - configuration API. -

- -
- -

Table of Contents

- - Configuration
- ├── Configuration Format & Deployment
- ├── Parameter Description
- └── Configuration API
- -
- - -

Configuration Format & Deployment

- -

- Configuration overrides can be provided through pyrobusta.env, using standard .env syntax. - pyrobusta.env must be stored in the server root. Inline comments are supported using #. -

- - - -

Perform a soft reset and upload pyrobusta.env using mpremote.

- - - -

Parameter Description

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDescriptionDefault
wifi_ssidName of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in wifi.py module.None
wifi_passwordPassword of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in wifi.py module.None
http_portPort number for HTTP.80
https_portPort number for HTTPS.443
http_multipartEnables or disables multipart request and response processing. Enabling multipart support increases memory usage.False
http_mem_cap - Fraction of available heap memory reserved for stream buffers. Valid range: (0, 1]. - 0.1
http_served_paths - Space-separated list of filesystem paths that may be served over HTTP. - /www /lib/pyrobusta
http_files_api - Enables or disables the file management API endpoint (/files), allowing upload, download, and listing of files. - False
socket_max_conMaximum number of simultaneous socket connections.2
tls - Enables or disables TLS. When enabled, cert.der and key.der must be installed at the server root. - False
log_levelLogging level. Can be one of: warning, info, debug.info
- -

Configuration API

- -

- Configuration values can be accessed through the - pyrobusta.utils.config module. - Values are loaded from pyrobusta.env during server initialization. - Configuration values can be retrieved using - get_config() together with one of the - predefined CONF_* constants. -

- -

- After initialization, configuration values are retrieved from an internal cache. - The cached values are normalized to their expected runtime types to avoid repeated - parsing of environment strings. -

- -

- Configuration values are treated as immutable during runtime. - Changes are applied only when the configuration cache is reloaded. - The configuration cache can be reloaded by calling - read_config(), which re-reads pyrobusta.env - and rebuilds the internal normalized cache. -

- - - - - - diff --git a/assets/www/file_server.html b/assets/www/file_server.html deleted file mode 100644 index e54769b..0000000 --- a/assets/www/file_server.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - PyRobusta Home - - - - -

File Server API

- - ← Back - -

This API provides file management capabilities, allowing clients to upload, - retrieve, and manage files through various HTTP methods. - http_files_api must be set to True in - pyrobusta.env to enable this API. -

- -
- -

Table of Contents

- - Static Content
- ├── Summary
- ├── File Retrieval
- ├── File Upload / Overwrite
- ├── Bulk File Upload
- └── File Delete
- -
- -

Summary

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodPathDescription
GET/files/{path}Lists or retrieves metadata about files.
PUT/files/{file path}Uploads or overwrites a file at the specified path.
POST/filesUploads multiple files in multipart/form-data.
DELETE/files/{file path}Deletes a file at the specified path.
- -
-

File Retrieval / Listing

-

Endpoint: GET /files/{path}

- -

- This method allows general file system interaction, enabling operations - such as listing directory contents, retrieving metadata, and downloading - files. -

- - - -

Example Request

- - -
- -
-

File Upload / Overwrite

-

Endpoint: PUT /files/{file path}

- -

- This method uploads a file or overwrites an existing file at a specific - path. The upload path is restricted to - /www/user_data. -

- - - -

Example Request

- - -
- -
-

Bulk File Upload

-

Endpoint: POST /files

- -

- This method handles general file uploads, designed for uploading multiple - files with per-file chunking supported. Only - multipart/form-data is accepted. -

- -

- Uploads are restricted to /www/user_data. The - Content-Disposition header only needs to specify the file - name; the upload directory is prepended automatically. -

- -

- http_multipart must be set to True in the - configuration to use this method. -

- - - -

Example Request

- - -
- -
-

File Delete

-

Endpoint: DELETE /files/{file path}

- -

- This method deletes a file at a specific path. The path is restricted to - /www/user_data. -

- - - -

Example Request

- - -
- - - - - diff --git a/assets/www/index.html b/assets/www/index.html deleted file mode 100644 index 7abaf0e..0000000 --- a/assets/www/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - PyRobusta Home - - - - -

PyRobusta Home

- -

The server is running correctly and is ready to serve content.

- -
- -

Available Resources

- - - - - - diff --git a/assets/www/request.html b/assets/www/request.html deleted file mode 100644 index 75600d6..0000000 --- a/assets/www/request.html +++ /dev/null @@ -1,243 +0,0 @@ - - - - - - PyRobusta Home - - - - -

Request Processing

- - ← Back - -

This page describes how route handlers receive and process incoming HTTP requests.

- -
- -

Table of Contents

- - Request Processing
- ├── Query Parameters
- ├── Request Headers
- ├── Request Bodies
- ├── Streamed Requests
- └── Multipart Requests
- -
- -

Query Parameters

- - -

Handler functions take an HTTP context and payload as arguments. - While handler functions cannot define additional arguments, extra input can be provided - through query parameters. Query parameters appear after the path component of a URL and - are introduced by a question mark (?), for example: - -

http://192.168.1.101/path/to/resource?param-key=param-value

- - Query parameters use the same encoding rules as the application/x-www-form-urlencoded format. - Multiple query parameters are separated by the ampersand (&). Percent-encoded characters - (for example, %2F representing /) are decoded automatically by the server. - -

- - Query parameters can be retrieved using the get_query_param() function, which accepts two - arguments: the name of the key and an optional default value. The function returns a string - that can be further processed by the application. -

- - - -

Request Headers

- -

Headers received in a request are available in the headers attribute of the HTTP context. - The headers attribute is a dictionary of key-value pairs. Header names and values are exposed as strings. - As a convenience, the Content-Length header is automatically converted to an integer because it is frequently used for - payload size calculations. Headers are normalized to lower case, so the key "Content-Length" is equivalent to the key - "content-length". -

- -

Request headers must contain only a subset of US-ASCII characters:

- -
- -
- - - -

Request Bodies

- -

A request payload is passed as the second positional argument of the - route handler. Unless the request uses streaming or multipart encoding, the payload - is provided as a memoryview to avoid unnecessary memory allocations. Deserialization must be done - by the user application. -

- - - -

Streamed Requests

- -

PyRobusta supports streaming requests by processing individual - chunks of the request body as they are received. To enforce bounded memory usage, - request chunks are processed individually by calling registered route handlers - for each chunk received. As a result, the application must process the request body - incrementally rather than assuming the full payload is available at once. -

- - - -

Multipart Requests

- -

Multipart requests allow clients to send composite payloads with - the option of varying content metadata or multiple resources in a single request. - Similar to streamed requests, multipart requests are processed one part at a time. - The route handler is invoked once for each part. Unlike regular request bodies, multipart - parts are parsed by the server before being passed to the application. Peprocessed parts - consist of headers (dictionary) and the raw part body (bytes), passed as a tuple. -

- -
- Multipart state tracking
- The HTTP context exposes the boolean attributes - mp_is_first and - mp_is_last - to identify the first and final part of a multipart request. This allows stateful processing - of multiple parts belonging to the same request. -

- - - - - - diff --git a/assets/www/response.html b/assets/www/response.html deleted file mode 100644 index f149882..0000000 --- a/assets/www/response.html +++ /dev/null @@ -1,311 +0,0 @@ - - - - - - PyRobusta Home - - - - -

Response Processing

- - ← Back - -

Response processing controls how route handlers construct HTTP responses. - This includes setting status codes, configuring response headers, serializing response bodies, - and generating streamed or multipart responses. -

-
- -

Table of Contents

- - Response Processing
- ├── Status Codes
- ├── Response Headers
- ├── Cache Control
- ├── Content Types & Serialization
- ├── Streamed Responses
- └── Streamed Multipart Responses
- -
- -

Status Codes

- -

Route handlers may optionally set the status code of the HTTP response. - If unspecified, the server defaults to HTTP 200. The status code can be overridden - through the terminate() method of the HTTP context. -

- -

The terminate() method updates the response status code and marks the - request as complete, but does not interrupt execution. Route handlers should still return - an appropriate response body. -

- - - -

Response Headers

- -

Response headers and response bodies can be configured through methods exposed by the HTTP context - (set_response_header(), set_response_body()). - Alternatively, route handlers may return a (content_type, body) tuple. -

- - - - -

Cache Control

- -

- PyRobusta implements a simple caching policy. - Unless overridden by the application, all HTTP responses include the - header Cache-Control: no-store. - - Conditional requests and cache validation mechanisms are not supported. - This includes ETag, Last-Modified, - If-None-Match, and If-Modified-Since. - - This design reduces implementation complexity and avoids additional - filesystem metadata lookups on resource-constrained devices. -

- -

Content Types & Serialization

- -

PyRobusta can automatically serialize a limited set of built-in types and data structures. - Unsupported types must be serialized by the application before being returned as either a string or a bytes-like - object. The following response body types are currently supported: -

- - - -

For non-streamed responses, the entire response body must exist in memory before it is - transmitted. As a result, the maximum response size is limited by the available heap. In the meantime, a - response buffer has a fixed size depending on the configuration. Internally, response bodies are wrapped - in a BytesIO object so that both fixed-size and streamed responses can be written through the same - buffer-oriented interface. Each response body returned by a route handler has a known size, allowing the - content-length header to be filled by the server. -

- -

Streamed Responses

-

- Responses can be streamed in chunks when the application cannot determine the size of the response body in advance. - Such responses must use chunked transfer encoding, indicated by the Transfer-Encoding header. With chunked encoding, - the Content-Length header must be omitted; instead the size of each chunk must be indicated as the chunks are sent. - The server automatically generates the required chunk metadata. -

- -

- When chunked transfer encoding is enabled, the server automatically generates the required encoding format. - The application must assign a generator function to http_ctx.resp_handler. The server then invokes the generator - when data is ready to be transmitted. The generator may be resumed multiple times until the stream is complete. - The following requirements must be fulfilled by the generator: -

- -
-
    -
  1. the generator function must accept a single response buffer argument (tx) used to write response data
  2. -
  3. the generator yields False after producing a chunk while additional data remains
  4. -
  5. the generator yields True exactly once to indicate that the stream is complete
  6. -
  7. the generator verifies the writable capacity of the buffer and - writes at most that much data to the buffer before yielding
  8. -
-
- - - - - -

Streamed Multipart Responses

-

- Multipart responses allow a single HTTP response to contain multiple independently typed payloads, - each with its own headers and body. Similar to streamed responses, PyRobusta uses response producer - functions to generate multipart response parts on demand. Because the total response size is not known - in advance, the server automatically uses chunked transfer encoding for multipart responses. It is - explicitly enabled in the example below for completeness. -

- -

- Routes producing streamed multipart responses must satisfy the following requirements: -

- -
-
    -
  1. the route must return a tuple containing the multipart content type and a callable response producer
  2. -
  3. the response producer must return a tuple containing the content type and payload of each part
  4. -
  5. after producing the final part, the response producer must return None
  6. -
-
- - - -

- Try the X-Part-Count and X-Part-Size headers to arbitrarily configure the response size. -

- - - - - - diff --git a/assets/www/routing.html b/assets/www/routing.html deleted file mode 100644 index 10c14c7..0000000 --- a/assets/www/routing.html +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - PyRobusta Home - - - - -

Routing

- - ← Back - -

Routing maps incoming HTTP requests to application-defined route handlers. - This page describes how routes are defined, how route handlers receive requests, and how wildcard routes - can be used to match dynamic URL paths. -

- -
- -

Table of Contents

- - Routing
- ├── Route Definitions
- ├── Route Handlers
- ├── Wildcard Routes
- └── Route Registration & Deregistration
- -
- -

Route Definitions

- -

Routes map HTTP requests to server-side handler functions that - process requests and manage resources. Similar to common web frameworks, PyRobusta - utilizes function decorators to map handler functions to URL paths. A route handler - can only be mapped to a single URL path and HTTP method. The same URL path may be - associated with multiple route handlers provided that each handler uses a different - HTTP method. -

- - - -

Route Handlers

- -

In PyRobusta, a route handler is a synchronous function registered to a - specific URL path and HTTP method. PyRobusta invokes a route handler whenever it receives a - request whose URL path and HTTP method match the registered route. Route handlers must accept - exactly two positional arguments: -

- - -

HTTP Context

- -

The HTTP context is an instance of the HttpEngine class that exposes the - public API used to inspect requests and construct responses. The HTTP context provides public - methods and attributes that enable user applications to process headers and structure responses. - By convention, non-public attributes and methods are prefixed with an underscore. - -

- - Apart from the public API, the HTTP context also encapsulates the state associated with the current - request and response exchange. Internally, the HTTP context ensures protocol correctness and assists - with request routing. -

- -

Request Body

- -

Depending on the request type, the body argument may contain either the complete - request body or a partial payload chunk. Partial request bodies are passed to route handlers - when the request uses multipart encoding or chunked transfer encoding, with each chunk of the payload - fed incrementally to the route handler. Such request processing is documented in the - Request Processing guide. -

- -

Wildcard Routes

- -

Wildcard routes use placeholders in one or more segments of a URL path. - These placeholders match varying path values, allowing multiple URL paths to be mapped to - a single handler function. A placeholder matches a single path segment by default. Alternatively, - a placeholder can match multiple segments by using the :path suffix. For example: - -

/path/to/{resource:path}

- - Placeholders that match multiple path segments are only allowed at the end of a route. For example, - /path/{to:path}/resource is disallowed because it would result in ambiguous route resolution. -

- - - -

Route Registration & Deregistration

- -

By convention, user applications should use decorators - to register route handlers in most cases. This approach ensures that routes are registered - during application initialization, and routes remain available for the lifetime of the application. - The public API exposed by HttpEngine allows route registration and deregistration - during an application's lifecycle, enabling applications to dynamically expose or - remove functionality at runtime. -

- - - - - - diff --git a/assets/www/static_content.html b/assets/www/static_content.html deleted file mode 100644 index 344b32c..0000000 --- a/assets/www/static_content.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - PyRobusta Home - - - - -

Static Content

- - ← Back - -

- PyRobusta serves static content directly from the filesystem. This page - describes the directory structure, file resolution rules, and MIME type - handling used by the server. -

- -
- -

Table of Contents

- - Static Content
- ├── Static File Serving
- ├── Directory Structure
- └── MIME Type Handling
- -
- -

Static File Serving

- -

Files stored under /www are served as static content. - Requests to the server root (/) return the default landing page (index.html). - For static content requests, the server automatically prepends /www to the requested path before - resolving the corresponding file on the filesystem. -

- -

When the file management API is enabled (http_files_api=True), - additional filesystem locations can be exposed through the http_served_paths - configuration option. See the Server Configuration - and File Server API guide for additional details. -

- -

Directory Structure

- -
-root/
-├── www                     document root for static content
-│   ├── index.html
-│   ├── introduction.html
-│   ├── ...
-│   └── user_data           root for user uploads
-│       └── ...
-├── lib                     root for installed MIP packages
-│   ├── pyrobusta
-│   │   ├── bindings
-│   │   ├── connectivity
-│   │   └── ...
-│   └── <other packages>
-├── cert.der                TLS certificate
-└── key.der                 TLS key
-    
- -

MIME Type Handling

- -

PyRobusta automatically determines the Content-Type header for - static files based on their filename extension. The selected content type depends on the file extension. - Unknown extensions are mapped to application/octet-stream. - The following mapping between extensions and content types is maintained by the server: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ExtensionContent-Type header
.htmltext/html
.csstext/css
.jsapplication/javascript
.jsonapplication/json
.icoimage/x-icon
.jpegimage/jpeg
.jpgimage/jpeg
.pngimage/png
.txttext/plain
.gifimage/gif
.raw, unknown extensionsapplication/octet-stream
- - - - diff --git a/assets/www/styles.css b/assets/www/styles.css deleted file mode 100644 index 9076f4a..0000000 --- a/assets/www/styles.css +++ /dev/null @@ -1,50 +0,0 @@ -body { - font-family: serif; - margin: 40px; - background: white; - color: black; -} - -h1 { - border-bottom: 1px solid #999; - padding-bottom: 10px; -} - -hr { - margin: 20px 0; -} - -a { - color: blue; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -.description { - width: clamp(300px, 70vw, 800px); -} - -table, th, td { - border: 1px solid black; - border-collapse: collapse; -} - -textarea { - resize: none; - background-color: rgb(37, 37, 37); - color: white; - box-sizing: border-box; - width: 100%; - padding-left: 10px; - field-sizing: content; - white-space: pre; -} - -.footer { - font-size: 0.9em; - color: #555; - margin-top: 30px; -} diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 7f0f28f..0000000 --- a/docs/api.md +++ /dev/null @@ -1,98 +0,0 @@ -*** - -# File Management API - -This API provides file management capabilities, allowing clients to upload, retrieve, and manage files through various HTTP methods. `http_files_api` must be set to `True` in pyrobusta.env to enable this API. - -## Summary - -| Method | Path | Description | -| :------- | :------------------- | :---------- | -| `GET` | `/files/{path}` | Lists or retrieves metadata about files. | -| `PUT` | `/files/{file path}` | Uploads or overwrites a file at the specified path. | -| `POST` | `/files` | Uploads multiple files in multipart/form-data. | -| `DELETE` | `/files/{file path}` | Delete a file at the specified path. | - ---- - -## Endpoint Details - -### 1. File Retrieval/Listing (`GET /files/{path}`) - -This method allows general file system interaction, enabling operations such as listing directory contents and retrieving metadata as well as downloading files. - -* **Method:** `GET` -* **Path:** `/files/{path}` -* **Success Response:** 200 OK. - -#### Example request - -```bash -$ curl 192.168.1.100/files/www -[ - {"path": "/www/examples.html", "created": "90", "size": "4507"}, - {"path": "/www/index.html", "created": "91", "size": "1198"} -] -``` - -### 2. File Upload / Overwrite (`PUT /files/{file path}`) - -This method is used to upload a file or overwrite an existing file at a specific path. -The upload path is restricted to /www/user_data. - -* **Method:** `PUT` -* **Path:** `/files/{file path}` -* **Body:** Raw file content (e.g., binary data). -* **Success Response:** 201 Created. -* **Notes:** `transfer-encoding: chunked` is supported. - -#### Example request - -```bash -$ curl -X PUT --data 'This is a test.' http://192.168.1.100/files/www/user_data/test.txt -OK - -$ curl 192.168.1.100/files/www/user_data/test.txt -This is a test. -``` - -### 3. File Upload (`POST /files`) - -This method handles general file uploads, designed for uploading multiple files with per-file chunking supported. Only multipart/form-data is accepted as a content type. - -The upload path is restricted to /www/user_data, however, content-disposition headers only have to specify the file name, /www/user_data is prepended by default. - -`http_multipart` must be set to `True` in the configuration to use this method. - -* **Method:** `POST` -* **Path:** `/files` -* **Body:** File content encapsulated in multipart/form-data. -* **Success Response:** 201 Created. - -#### Example request - -```bash -$ echo "File 1 content" > /tmp/upload-1.txt -$ echo "File 2 content" > /tmp/upload-2.txt -$ curl -X POST --form file1='@/tmp/upload-1.txt' --form file2='@/tmp/upload-2.txt' http://192.168.1.100/files -$ curl 192.168.1.100/files/www/user_data -[ - {"path": "/www/user_data/upload-1.txt", "created": "418", "size": "15"}, - {"path": "/www/user_data/upload-2.txt", "created": "418", "size": "15"} -] -``` - -### 4. File Delete (`DELETE /files/{file path}`) - -This method is used to delete a file at a specific path. -The path is restricted to /www/user_data. - -* **Method:** `PUT` -* **Path:** `/files/{file path}` -* **Success Response:** 204 No Content. - -#### Example request - -```bash -$ curl -X DELETE 192.168.1.100/files/www/user_data/test.txt -``` diff --git a/docs/application_development/configuration.md b/docs/application_development/configuration.md new file mode 100644 index 0000000..46c2d5c --- /dev/null +++ b/docs/application_development/configuration.md @@ -0,0 +1,88 @@ +# Configuration + +[← Back](index.md) + +This page documents PyRobusta configuration options, +configuration deployment using `mpremote`, +and runtime access to configuration values through the +configuration API. + +--- + +## Table of Contents + +* [Configuration](#configuration) + + [Configuration Format & Deployment](#configuration-format-deployment) + + [Parameter Description](#parameter-description) + + [Configuration API](#configuration-api) + +--- + +## Configuration Format & Deployment + +Configuration overrides can be provided through `pyrobusta.env`, using standard `.env` syntax. +`pyrobusta.env` must be stored in the server root. Inline comments are supported using `#`. + +``` +# /pyrobusta.env - Example configuration + +socket_max_con=2 # allow two simultaneous socket connections +http_multipart=False # turn off multipart parser to lower heap usage +http_mem_cap=0.05 # limit heap usage of stream buffers to 5% of the total heap +tls=False # turn off TLS +``` + +Perform a soft reset and upload `pyrobusta.env` using mpremote. + +``` +$ mpremote a0 soft-reset +$ mpremote a0 cp pyrobusta.env :/pyrobusta.env +``` + +## Parameter Description + +| Name | Description | Default | +| --- | --- | --- | +| `wifi_ssid` | Name of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in `wifi.py` module. | None | +| `wifi_password` | Password of the Wi-Fi network. When empty, Wi-Fi is not initialized by the built-in `wifi.py` module. | None | +| `http_port` | Port number for HTTP. | 80 | +| `https_port` | Port number for HTTPS. | 443 | +| `http_multipart` | Enables or disables multipart request and response processing. Enabling multipart support increases memory usage. | False | +| `http_mem_cap` | Fraction of available heap memory reserved for stream buffers. Valid range: (0, 1]. | 0.1 | +| `http_served_paths` | Space-separated list of filesystem paths that may be served over HTTP. | `/www /lib/pyrobusta` | +| `http_files_api` | Enables or disables the file management API endpoint (`/files`), allowing upload, download, and listing of files. | False | +| `socket_max_con` | Maximum number of simultaneous socket connections. | 2 | +| `tls` | Enables or disables TLS. When enabled, `cert.der` and `key.der` must be installed at the server root. | False | +| `log_level` | Logging level. Can be one of: `warning`, `info`, `debug`. | info | + +## Configuration API + +Configuration values can be accessed through the +`pyrobusta.utils.config` module. +Values are loaded from `pyrobusta.env` during server initialization. +Configuration values can be retrieved using +`get_config()` together with one of the +predefined `CONF_*` constants. + +After initialization, configuration values are retrieved from an internal cache. +The cached values are normalized to their expected runtime types to avoid repeated +parsing of environment strings. + +Configuration values are treated as immutable during runtime. +Changes are applied only when the configuration cache is reloaded. +The configuration cache can be reloaded by calling +`read_config()`, which re-reads `pyrobusta.env` +and rebuilds the internal normalized cache. + +``` +from pyrobusta.utils.config import get_config, CONF_TLS + +@HttpEngine.route("/tls", "GET") +def tls_status(http_ctx, _): + enabled = get_config(CONF_TLS) + return "text/plain", f"TLS enabled: {enabled}" +``` + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/error_handling.md b/docs/application_development/error_handling.md new file mode 100644 index 0000000..0c5687d --- /dev/null +++ b/docs/application_development/error_handling.md @@ -0,0 +1,27 @@ +# Error Handling + +[← Back](index.md) + +--- + +## Table of Contents + +* [Error Handling](#error-handling) + + [Client Errors (4xx)](#client-errors-4xx) + + [Server Errors (5xx)](#server-errors-5xx) + + [Exception Handling](#exception-handling) + + [Custom Error Responses](#custom-error-responses) + +--- + +## Client Errors (4xx) + +## Server Errors (5xx) + +## Exception Handling + +## Custom Error Responses + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/file_server.md b/docs/application_development/file_server.md new file mode 100644 index 0000000..a2be14c --- /dev/null +++ b/docs/application_development/file_server.md @@ -0,0 +1,139 @@ +# File Server API + +[← Back](index.md) + +This API provides file management capabilities, allowing clients to upload, +retrieve, and manage files through various HTTP methods. +`http_files_api` must be set to `True` in +`pyrobusta.env` to enable this API. + +--- + +## Table of Contents + +* [File Server API](#file-server-api) + + [Summary](#summary) + + [File Retrieval](#file-retrieval-listing) + + [File Upload / Overwrite](#file-upload-overwrite) + + [Bulk File Upload](#bulk-file-upload) + + [File Delete](#file-delete) + +--- + +## Summary + +| Method | Path | Description | +| --- | --- | --- | +| `GET` | `/files/{path}` | Lists or retrieves metadata about files. | +| `PUT` | `/files/{file path}` | Uploads or overwrites a file at the specified path. | +| `POST` | `/files` | Uploads multiple files in multipart/form-data. | +| `DELETE` | `/files/{file path}` | Deletes a file at the specified path. | + +## File Retrieval / Listing + +**Endpoint:** `GET /files/{path}` + +This method allows general file system interaction, enabling operations +such as listing directory contents, retrieving metadata, and downloading +files. + +* **Method:** `GET` +* **Path:** `/files/{path}` +* **Success Response:** `200 OK` + +### Example Request + +``` +$ curl 192.168.1.100/files/www +[ + {"path": "/www/examples.html", "created": "90", "size": "4507"}, + {"path": "/www/index.html", "created": "91", "size": "1198"} +] +``` + +## File Upload / Overwrite + +**Endpoint:** `PUT /files/{file path}` + +This method uploads a file or overwrites an existing file at a specific +path. The upload path is restricted to +`/www/user_data`. + +* **Method:** `PUT` +* **Path:** `/files/{file path}` +* **Body:** Raw file content (binary or text). +* **Success Response:** `201 Created` +* **Notes:** + `transfer-encoding: chunked` is supported. + +### Example Request + +``` +$ curl -X PUT --data 'This is a test.' \ +http://192.168.1.100/files/www/user_data/test.txt +OK + +$ curl 192.168.1.100/files/www/user_data/test.txt +This is a test. +``` + +## Bulk File Upload + +**Endpoint:** `POST /files` + +This method handles general file uploads, designed for uploading multiple +files with per-file chunking supported. Only +`multipart/form-data` is accepted. + +Uploads are restricted to `/www/user_data`. The +`Content-Disposition` header only needs to specify the file +name; the upload directory is prepended automatically. + +`http_multipart` must be set to `True` in the +configuration to use this method. + +* **Method:** `POST` +* **Path:** `/files` +* **Body:** + File content encapsulated in multipart/form-data. +* **Success Response:** `201 Created` + +### Example Request + +``` +$ echo "File 1 content" > /tmp/upload-1.txt +$ echo "File 2 content" > /tmp/upload-2.txt + +$ curl -X POST \ + --form file1='@/tmp/upload-1.txt' \ + --form file2='@/tmp/upload-2.txt' \ + http://192.168.1.100/files + +$ curl 192.168.1.100/files/www/user_data +[ + {"path": "/www/user_data/upload-1.txt", "created": "418", "size": "15"}, + {"path": "/www/user_data/upload-2.txt", "created": "418", "size": "15"} +] +``` + +## File Delete + +**Endpoint:** `DELETE /files/{file path}` + +This method deletes a file at a specific path. The path is restricted to +`/www/user_data`. + +* **Method:** `DELETE` +* **Path:** `/files/{file path}` +* **Success Response:** `204 No Content` + +### Example Request + +``` +$ curl -X DELETE \ +192.168.1.100/files/www/user_data/test.txt +``` + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/index.md b/docs/application_development/index.md new file mode 100644 index 0000000..743d12d --- /dev/null +++ b/docs/application_development/index.md @@ -0,0 +1,23 @@ + + + +This page collects user guides for configuring the server and developing applications with it. +If you're new to the project, start with the Introduction, which provides a hands-on application example. +Then continue with the remaining guides in the order listed below. + +--- + +## Available Resources + +* [Introduction](introduction.md) +* [Configuration](configuration.md) +* [Routing](routing.md) +* [Request Processing](request.md) +* [Response Processing](response.md) +* [Static Content](static_content.md) +* [File Server API](file_server.md) +* [Source Code](https://github.com/szeka9/PyRobusta) + +--- + +PyRobusta v0.7.0 Web Server diff --git a/assets/www/introduction.html b/docs/application_development/introduction.md similarity index 62% rename from assets/www/introduction.html rename to docs/application_development/introduction.md index baa1d8c..bb07a6e 100644 --- a/assets/www/introduction.html +++ b/docs/application_development/introduction.md @@ -1,41 +1,29 @@ - - - - - - PyRobusta Home - - - - -

Introduction

- - ← Back - -

This page provides practical examples for using your server.

- -
- -

Table of Contents

- - Introduction
- ├── Demo Application
- └── Deployment with mpremote
- -
- -

Demo Application

-
-

The following application demonstrates common use cases for handling headers, status codes, query parameters, and wildcard routes.

-
    -
  1. /version returns the version of the application. -
    The server version can optionally be included in the response by setting the detailed query parameter to true. -
  2. -
  3. /app/version or /server/version returns the designated version string, handled by a single route handler with a wildcard URL. -
  4. -
-
- - +``` + +## Deployment with mpremote -

Deployment with mpremote

+Perform a soft reset and upload app.py and boot.py using mpremote. -

Perform a soft reset and upload app.py and boot.py using mpremote.

- +``` -

Perform a hard reset to start the application and connect to the REPL.

- +``` + +Use curl to test the application. -

Use curl to test the application.

- - - - - +``` + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/performance.md b/docs/application_development/performance.md new file mode 100644 index 0000000..55d4115 --- /dev/null +++ b/docs/application_development/performance.md @@ -0,0 +1,30 @@ +# Performance & Memory + +[← Back](index.md) + +--- + +## Table of Contents + +* [Performance & Memory](#performance-memory) + + [Memory Configuration](#memory-configuration) + + [Connection Limits](#connection-limits) + + [Buffer Sizing](#buffer-sizing) + + [Streaming vs Buffered Responses](#streaming-vs-buffered-responses) + + [Multipart Memory Usage](#multipart-memory-usage) + +--- + +## Memory Configuration + +## Connection Limits + +## Buffer Sizing + +## Streaming vs Buffered Responses + +## Multipart Memory Usage + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/reference.md b/docs/application_development/reference.md new file mode 100644 index 0000000..c164cbf --- /dev/null +++ b/docs/application_development/reference.md @@ -0,0 +1,33 @@ +# Reference + +[← Back](index.md) + +--- + +## Table of Contents + +* [Reference](#reference) + + [HttpServer](#httpserver) + + [HttpEngine](#httpengine) + + [HttpContext](#httpcontext) + + [Route Decorators](#route-decorators) + + [Configuration Options](#configuration-options) + + [Environment Variables](#environment-variables) + +--- + +## HttpServer + +## HttpEngine + +## HttpContext + +## Route Decorators + +## Configuration Options + +## Environment Variables + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/request.md b/docs/application_development/request.md new file mode 100644 index 0000000..3350f63 --- /dev/null +++ b/docs/application_development/request.md @@ -0,0 +1,211 @@ +# Request Processing + +[← Back](index.md) + +This page describes how route handlers receive and process incoming HTTP requests. + +--- + +## Table of Contents + +* [Request Processing](#request-processing) + + [Query Parameters](#query-parameters) + + [Request Headers](#request-headers) + + [Request Bodies](#request-bodies) + + [Streamed Requests](#streamed-requests) + + [Multipart Requests](#multipart-requests) + +--- + +## Query Parameters + +Handler functions take an HTTP context and payload as arguments. +While handler functions cannot define additional arguments, extra input can be provided +through query parameters. Query parameters appear after the path component of a URL and +are introduced by a question mark (?), for example: + +`http://192.168.1.101/path/to/resource?param-key=param-value` + +Query parameters use the same encoding rules as the `application/x-www-form-urlencoded` format. +Multiple query parameters are separated by the ampersand (&). Percent-encoded characters +(for example, %2F representing /) are decoded automatically by the server. + +Query parameters can be retrieved using the `get_query_param()` function, which accepts two +arguments: the name of the key and an optional default value. The function returns a string +that can be further processed by the application. + +``` +from pyrobusta.protocol.http import HttpEngine + +@HttpEngine.route("/app/resource", "GET") +def query_param_handler(http_ctx, _): + # Handler for /app/resource?detailed=true/false + + is_detailed = False + + if http_ctx.query: + is_detailed = http_ctx.get_query_param( + "detailed", default="false" + ).lower() + + if is_detailed not in ("true", "false"): + http_ctx.terminate(400) + return "text/plain", "Invalid query" + + resource = "resource content\n" + if is_detailed: + resource = "detailed " + resource + + return "text/plain", resource +``` + +## Request Headers + +Headers received in a request are available in the `headers` attribute of the HTTP context. +The `headers` attribute is a dictionary of key-value pairs. Header names and values are exposed as strings. +As a convenience, the `Content-Length` header is automatically converted to an integer because it is frequently used for +payload size calculations. Headers are normalized to lower case, so the key `"Content-Length"` is equivalent to the key +`"content-length"`. + +Request headers must contain only a subset of US-ASCII characters: + +* header names are restricted to letters, digits, hyphens, and underscores +* header values are limited to US-ASCII characters, excluding control characters + +``` +from pyrobusta.protocol.http import HttpEngine + +@HttpEngine.route("/app", "GET") +def app(http_ctx, _): + if http_ctx.headers.get("accept", "*/*") == "text/plain": + return "text/plain", "App response\n" + elif http_ctx.headers["accept"] == "application/json": + return "application/json", {"response": "App response"} + raise ValueError("Unsupported accept header") +``` + +## Request Bodies + +A request payload is passed as the second positional argument of the +route handler. Unless the request uses streaming or multipart encoding, the payload +is provided as a memoryview to avoid unnecessary memory allocations. Deserialization must be done +by the user application. + +``` +import json + +from pyrobusta.protocol.http import HttpEngine + +@HttpEngine.route("/app", "GET") +def app(http_ctx, _): + return "text/plain", "GET request without payload" + +@HttpEngine.route("/app", "POST") +def app(http_ctx, payload): + data = json.loads(bytes(payload)) + return "text/plain", "POST request with payload" +``` + +## Streamed Requests + +PyRobusta supports streaming requests by processing individual +chunks of the request body as they are received. To enforce bounded memory usage, +request chunks are processed individually by calling registered route handlers +for each chunk received. As a result, the application must process the request body +incrementally rather than assuming the full payload is available at once. + +``` +import pyrobusta.server.http_server as http_server +from pyrobusta.protocol.http import HttpEngine +from pyrobusta.utils.helpers import normalize_path + +@HttpEngine.route("/app/chunks", "POST") +def upload_chunks(http_ctx, payload: bytes): + """ + Route handler for demonstrating chunked transfer encoding. + """ + if not http_ctx.is_chunked(): + http_ctx.terminate(400) + return "text/plain", "Bad request" + + if payload: + # Wait for more chunks before setting response status + with open(normalize_path("/tmp/chunks.txt"), "ab") as f: + f.write(payload) + return + + # Last (empty) chunk received + http_ctx.terminate(201) + return "text/plain", "OK" + +async def main(): + server = http_server.HttpServer() + asyncio.create_task(server.start_socket_server()) + while True: + await asyncio.sleep(1) +``` + +## Multipart Requests + +Multipart requests allow clients to send composite payloads with +the option of varying content metadata or multiple resources in a single request. +Similar to streamed requests, multipart requests are processed one part at a time. +The route handler is invoked once for each part. Unlike regular request bodies, multipart +parts are parsed by the server before being passed to the application. Peprocessed parts +consist of headers (dictionary) and the raw part body (bytes), passed as a tuple. + +**Multipart state tracking** +The HTTP context exposes the boolean attributes +`mp_is_first` and +`mp_is_last` +to identify the first and final part of a multipart request. This allows stateful processing +of multiple parts belonging to the same request. + +``` +from os import listdir, remove, rename, mkdir + +import pyrobusta.server.http_server as http_server +from pyrobusta.protocol.http import HttpEngine +from pyrobusta.utils.helpers import normalize_path + +@HttpEngine.route("/app/parts", "POST") +def handle_parts(http_ctx, payload: tuple): + """ + Route handler for demonstrating multipart processing. + """ + if not http_ctx.is_multipart(): + http_ctx.terminate(400) + return "text/plain", "Bad request" + + part_headers, part_body = payload + + tmp_dir = normalize_path("/tmp") + tmp_path = normalize_path("/tmp/parts.txt") + target_path = normalize_path("/www/user_data/parts.txt") + + # Clean stale partial uploads + if http_ctx.mp_is_first: + if not "tmp" in listdir(normalize_path("/")): + mkdir(tmp_dir) + if "parts.txt" in listdir(tmp_dir): + remove(tmp_path) + + with open(tmp_path, "ab") as f: + f.write(part_body) + + # Finalize uploads + if http_ctx.mp_is_last: + rename(tmp_path, target_path) + http_ctx.terminate(201) + return "text/plain", "OK" + +async def main(): + server = http_server.HttpServer() + asyncio.create_task(server.start_socket_server()) + while True: + await asyncio.sleep(1) +``` + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/response.md b/docs/application_development/response.md new file mode 100644 index 0000000..39cbe67 --- /dev/null +++ b/docs/application_development/response.md @@ -0,0 +1,261 @@ +# Response Processing + +[← Back](index.md) + +Response processing controls how route handlers construct HTTP responses. +This includes setting status codes, configuring response headers, serializing response bodies, +and generating streamed or multipart responses. + +--- + +## Table of Contents + +* [Response Processing](#response-processing) + + [Status Codes](#status-codes) + + [Response Headers](#response-headers) + + [Cache Control](#cache-control) + + [Content Types & Serialization](#content-types-serialization) + + [Streamed Responses](#streamed-responses) + + [Streamed Multipart Responses](#streamed-multipart-responses) + +--- + +## Status Codes + +Route handlers may optionally set the status code of the HTTP response. +If unspecified, the server defaults to HTTP 200. The status code can be overridden +through the `terminate()` method of the HTTP context. + +The `terminate()` method updates the response status code and marks the +request as complete, but does not interrupt execution. Route handlers should still return +an appropriate response body. + +``` +from pyrobusta.protocol.http import HttpEngine + +@HttpEngine.route("/app/{resource}", "GET") +def inventory_manager(http_ctx, _): + resource = http_ctx.path_segment(1) + + if resource == "items": + # Default HTTP 200 + return "application/json", ["item-1", "item-2", "item-3"] + + elif resource == "version": + # Default HTTP 200 + return "text/plain", "v0.1.0" + + else: + # Set 404 status code explicitly + http_ctx.terminate(404) + return "text/plain", "Not found" +``` + +## Response Headers + +Response headers and response bodies can be configured through methods exposed by the HTTP context +(`set_response_header()`, `set_response_body()`). +Alternatively, route handlers may return a `(content_type, body)` tuple. + +``` +import json + +from pyrobusta.protocol.http import HttpEngine + +config = {"max-items": 5} +items = set(["apple", "orange", "grapes"]) + +@HttpEngine.route("/inventory/{resource}", "POST") +def inventory_manager(http_ctx, payload): + resource = http_ctx.path_segment(1) + + if resource == "items": + if http_ctx.headers.get("content-type") != "text/plain": + http_ctx.set_response_header(b"accept-post", b"text/plain") + http_ctx.terminate(415) + return "text/plain", "Unsupported type" + if len(items) >= config["max-items"]: + http_ctx.terminate(400) + return "text/plain", "Inventory full" + items.add(payload.decode()) + return "text/plain", ", ".join(items) + + elif resource == "config": + if http_ctx.headers.get("content-type") != "application/json": + http_ctx.set_response_header(b"accept-post", b"application/json") + http_ctx.terminate(415) + return "text/plain", "Unsupported type" + payload_json = json.loads(payload) + if any([key not in config for key in payload_json]) or \ + any([type(value) != type(config[key]) for key, value in payload_json.items()]): + http_ctx.terminate(400) + return "text/plain", "Invalid config" + config.update(payload_json) + return "application/json", config + + else: + http_ctx.terminate(404) + return "text/plain", "Not found" +``` + +## Cache Control + +PyRobusta implements a simple caching policy. +Unless overridden by the application, all HTTP responses include the +header `Cache-Control: no-store`. +Conditional requests and cache validation mechanisms are not supported. +This includes `ETag`, `Last-Modified`, +`If-None-Match`, and `If-Modified-Since`. +This design reduces implementation complexity and avoids additional +filesystem metadata lookups on resource-constrained devices. + +## Content Types & Serialization + +PyRobusta can automatically serialize a limited set of built-in types and data structures. +Unsupported types must be serialized by the application before being returned as either a string or a bytes-like +object. The following response body types are currently supported: + +* `str` +* bytes-like: `bytes`, `bytearray`, `memoryview` +* data structures: `dict`, `tuple`, `list` + +For non-streamed responses, the entire response body must exist in memory before it is +transmitted. As a result, the maximum response size is limited by the available heap. In the meantime, a +response buffer has a fixed size depending on the configuration. Internally, response bodies are wrapped +in a `BytesIO` object so that both fixed-size and streamed responses can be written through the same +buffer-oriented interface. Each response body returned by a route handler has a known size, allowing the +content-length header to be filled by the server. + +## Streamed Responses + +Responses can be streamed in chunks when the application cannot determine the size of the response body in advance. +Such responses must use chunked transfer encoding, indicated by the `Transfer-Encoding` header. With chunked encoding, +the `Content-Length` header must be omitted; instead the size of each chunk must be indicated as the chunks are sent. +The server automatically generates the required chunk metadata. + +When chunked transfer encoding is enabled, the server automatically generates the required encoding format. +The application must assign a generator function to `http_ctx.resp_handler`. The server then invokes the generator +when data is ready to be transmitted. The generator may be resumed multiple times until the stream is complete. +The following requirements must be fulfilled by the generator: + +1. the generator function must accept a single response buffer argument (`tx`) used to write response data +2. the generator yields `False` after producing a chunk while additional data remains +3. the generator yields `True` exactly once to indicate that the stream is complete +4. the generator verifies the writable capacity of the buffer and + writes at most that much data to the buffer before yielding + +``` +# /app.py +import asyncio + +from pyrobusta.server import http_server +from pyrobusta.protocol.http import HttpEngine + +@HttpEngine.route("/stream", "GET") +def stream(http_ctx, _): + + def generate_chunks(tx): + for i in range(10): + data = b"data: chunk %d\n\n" % i + written = 0 + while written < len(data): + to_write = tx.capacity - tx.size() + # Defensive check; buffer should never be full here + if not to_write: + raise BufferError() + tx.write(data[written : written + to_write]) + written += to_write + yield False + yield True + + http_ctx.set_response_header(b"transfer-encoding", b"chunked") + http_ctx.set_response_header(b"content-type", b"text/event-stream") + http_ctx.resp_handler = generate_chunks + +async def main(): + server = http_server.HttpServer() + asyncio.create_task(server.start_socket_server()) + while True: + await asyncio.sleep(1) +``` + +``` +$ curl 192.168.1.101/stream +data: chunk 0 + +data: chunk 1 + +data: chunk 2 + +... + +data: chunk 9 +``` + +## Streamed Multipart Responses + +Multipart responses allow a single HTTP response to contain multiple independently typed payloads, +each with its own headers and body. Similar to streamed responses, PyRobusta uses response producer +functions to generate multipart response parts on demand. Because the total response size is not known +in advance, the server automatically uses chunked transfer encoding for multipart responses. It is +explicitly enabled in the example below for completeness. + +Routes producing streamed multipart responses must satisfy the following requirements: + +1. the route must return a tuple containing the multipart content type and a callable response producer +2. the response producer must return a tuple containing the content type and payload of each part +3. after producing the final part, the response producer must return None + +``` +# /app.py +import asyncio + +from pyrobusta.server import http_server +from pyrobusta.protocol.http import HttpEngine + +def multipart_response(num_responses, part_size): + i = 0 + def response_producer(): + nonlocal i + i += 1 + if i > num_responses: + return None + return "text/plain", b"X" * part_size + return response_producer + +@HttpEngine.route("/multipart", "GET") +def multipart_handler(http_ctx, _): + http_ctx.set_response_header(b"transfer-encoding", b"chunked") + part_count = int(http_ctx.headers.get("x-part-count", 1)) + part_size = int(http_ctx.headers.get("x-part-size", 1024)) + return "multipart/form-data", multipart_response(part_count, part_size) + +async def main(): + server = http_server.HttpServer() + asyncio.create_task(server.start_socket_server()) + while True: + await asyncio.sleep(1) +``` + +Try the `X-Part-Count` and `X-Part-Size` headers to arbitrarily configure the response size. + +``` +$ curl -H "X-Part-Count: 3" -H "X-Part-Size: 10" 192.168.1.101/multipart +--pyrobusta-boundary +content-type:text/plain + +XXXXXXXXXX +--pyrobusta-boundary +content-type:text/plain + +XXXXXXXXXX +--pyrobusta-boundary +content-type:text/plain + +XXXXXXXXXX +--pyrobusta-boundary-- +``` + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/routing.md b/docs/application_development/routing.md new file mode 100644 index 0000000..1ca4bbe --- /dev/null +++ b/docs/application_development/routing.md @@ -0,0 +1,169 @@ +# Routing + +[← Back](index.md) + +Routing maps incoming HTTP requests to application-defined route handlers. +This page describes how routes are defined, how route handlers receive requests, and how wildcard routes +can be used to match dynamic URL paths. + +--- + +## Table of Contents + +* [Routing](#routing) + + [Route Definitions](#route-definitions) + + [Route Handlers](#route-handlers) + + [Wildcard Routes](#wildcard-routes) + + [Route Registration & Deregistration](#route-registration-deregistration) + +--- + +## Route Definitions + +Routes map HTTP requests to server-side handler functions that +process requests and manage resources. Similar to common web frameworks, PyRobusta +utilizes function decorators to map handler functions to URL paths. A route handler +can only be mapped to a single URL path and HTTP method. The same URL path may be +associated with multiple route handlers provided that each handler uses a different +HTTP method. + +``` +from pyrobusta.protocol.http import HttpEngine + +@HttpEngine.route("/app/resource", "GET") +def get_handler(http_ctx, _): + return "text/plain", "resource content\n" + +@HttpEngine.route("/app/resource", "POST") +def post_handler(http_ctx, payload): + return "text/plain", "payload processed\n" +``` + +## Route Handlers + +In PyRobusta, a route handler is a synchronous function registered to a +specific URL path and HTTP method. PyRobusta invokes a route handler whenever it receives a +request whose URL path and HTTP method match the registered route. Route handlers must accept +exactly two positional arguments: + +* HTTP context (`HttpEngine` class) +* Request body (`memoryview`) + +### HTTP Context + +The HTTP context is an instance of the `HttpEngine` class that exposes the +public API used to inspect requests and construct responses. The HTTP context provides public +methods and attributes that enable user applications to process headers and structure responses. +By convention, non-public attributes and methods are prefixed with an underscore. + +Apart from the public API, the HTTP context also encapsulates the state associated with the current +request and response exchange. Internally, the HTTP context ensures protocol correctness and assists +with request routing. + +### Request Body + +Depending on the request type, the body argument may contain either the complete +request body or a partial payload chunk. Partial request bodies are passed to route handlers +when the request uses multipart encoding or chunked transfer encoding, with each chunk of the payload +fed incrementally to the route handler. Such request processing is documented in the +[Request Processing](./request.md#streamed-requests) guide. + +## Wildcard Routes + +Wildcard routes use placeholders in one or more segments of a URL path. +These placeholders match varying path values, allowing multiple URL paths to be mapped to +a single handler function. A placeholder matches a single path segment by default. Alternatively, +a placeholder can match multiple segments by using the `:path` suffix. For example: + +`/path/to/{resource:path}` + +Placeholders that match multiple path segments are only allowed at the end of a route. For example, +`/path/{to:path}/resource` is disallowed because it would result in ambiguous route resolution. + +``` +from pyrobusta.protocol.http import HttpEngine + +sensor_values = { + "dht22": { + "temperature_c": 22, + "rel_hum": 45 + } +} + +@HttpEngine.route("/sensors/{name}/values/{value_name}", "GET") +def wildcard_url_handler(http_ctx, _): + sensor_name = http_ctx.path_segment(1) + sensor_value = http_ctx.path_segment(3) + + if sensor_name not in sensor_values: + http_ctx.terminate(404) + return "text/plain", "Not found" + + values = sensor_values[sensor_name] + + if sensor_value not in values: + http_ctx.terminate(404) + return "text/plain", "Not found" + + return "text/plain", str(values[sensor_value]) +``` + +## Route Registration & Deregistration + +By convention, user applications should use decorators +to register route handlers in most cases. This approach ensures that routes are registered +during application initialization, and routes remain available for the lifetime of the application. +The public API exposed by `HttpEngine` allows route registration and deregistration +during an application's lifecycle, enabling applications to dynamically expose or +remove functionality at runtime. + +``` +from pyrobusta.protocol.http import HttpEngine + +sensor_values = { + "dht22": { + "temperature_c": 22, + "rel_hum": 45 + }, + "ldr": { + "illuminance_lx": 50 + } +} + +def dht22_handler(http_ctx, _): + return "application/json", sensor_values["dht22"] + +def ldr_handler(http_ctx, _): + return "application/json", sensor_values["ldr"] + +@HttpEngine.route("/sensor/{name}/enabled", "PUT") +def enable_sensor(http_ctx, enabled): + sensor_name = http_ctx.path_segment(1) + + if sensor_name not in sensor_values: + http_ctx.terminate(404) + return "text/plain", "Not found" + + if enabled.decode().lower() not in ("true", "false"): + http_ctx.terminate(400) + return "text/plain", "Invalid value" + + is_enabled = enabled.decode().lower() == "true" + + if sensor_name == "dht22": + route_handler = dht22_handler + + elif sensor_name == "ldr": + route_handler = ldr_handler + + if is_enabled: + HttpEngine.register(f"/sensor/{sensor_name}/value", route_handler, "GET") + else: + HttpEngine.deregister(f"/sensor/{sensor_name}/value", "GET") + + return "text/plain", "OK" +``` + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/security.md b/docs/application_development/security.md new file mode 100644 index 0000000..e935a2b --- /dev/null +++ b/docs/application_development/security.md @@ -0,0 +1,27 @@ +# Authentication & Security + +[← Back](index.md) + +--- + +## Table of Contents + +* [Authentication & Security](#authentication-security) + + [API Key Authentication](#api-key-authentication) + + [Basic Authentication](#basic-authentication) + + [HTTPS / TLS](#https-tls) + + [Certificate Installation](#certificate-installation) + +--- + +## API Key Authentication + +## Basic Authentication + +## HTTPS / TLS + +## Certificate Installation + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/server_lifecycle.md b/docs/application_development/server_lifecycle.md new file mode 100644 index 0000000..abca0c0 --- /dev/null +++ b/docs/application_development/server_lifecycle.md @@ -0,0 +1,30 @@ +# Server Lifecycle + +[← Back](index.md) + +--- + +## Table of Contents + +* [Server Lifecycle](#server-lifecycle) + + [Server Startup](#server-startup) + + [Application Initialization](#application-initialization) + + [Wi-Fi Connectivity](#wi-fi-connectivity) + + [Keep-alive Connections](#keep-alive-connections) + + [Deep Sleep Considerations](#deep-sleep-considerations) + +--- + +## Server Startup + +## Application Initialization + +## Wi-Fi Connectivity + +## Keep-alive Connections + +## Deep Sleep Considerations + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/static_content.md b/docs/application_development/static_content.md new file mode 100644 index 0000000..27238ea --- /dev/null +++ b/docs/application_development/static_content.md @@ -0,0 +1,75 @@ +# Static Content + +[← Back](index.md) + +PyRobusta serves static content directly from the filesystem. This page +describes the directory structure, file resolution rules, and MIME type +handling used by the server. + +--- + +## Table of Contents + +* [Static Content](#static-content) + + [Static File Serving](#static-file-serving) + + [Directory Structure](#directory-structure) + + [MIME Type Handling](#mime-type-handling) + +--- + +## Static File Serving + +Files stored under `/www` are served as static content. +Requests to the server root (`/`) return the default landing page (`index.html`). +For static content requests, the server automatically prepends `/www` to the requested path before +resolving the corresponding file on the filesystem. + +When the file management API is enabled (`http_files_api=True`), +additional filesystem locations can be exposed through the `http_served_paths` +configuration option. See the [Server Configuration](./configuration.md) +and [File Server API](./file_server.md) guide for additional details. + +## Directory Structure + +``` +root/ +├── www document root for static content +│ ├── index.html +│ ├── introduction.html +│ ├── ... +│ └── user_data root for user uploads +│ └── ... +├── lib root for installed MIP packages +│ ├── pyrobusta +│ │ ├── bindings +│ │ ├── connectivity +│ │ └── ... +│ └── +├── cert.der TLS certificate +└── key.der TLS key +``` + +## MIME Type Handling + +PyRobusta automatically determines the `Content-Type` header for +static files based on their filename extension. The selected content type depends on the file extension. +Unknown extensions are mapped to `application/octet-stream`. +The following mapping between extensions and content types is maintained by the server: + +| Extension | Content-Type header | +| --- | --- | +| .html | text/html | +| .css | text/css | +| .js | application/javascript | +| .json | application/json | +| .ico | image/x-icon | +| .jpeg | image/jpeg | +| .jpg | image/jpeg | +| .png | image/png | +| .txt | text/plain | +| .gif | image/gif | +| .raw, unknown extensions | application/octet-stream | + +--- + +PyRobusta v0.7.0 Web Server diff --git a/docs/application_development/styles/style.css b/docs/application_development/styles/style.css new file mode 100644 index 0000000..548db71 --- /dev/null +++ b/docs/application_development/styles/style.css @@ -0,0 +1,94 @@ +:root { + --bg: #ffffff; + --fg: #1a1a1a; + --muted: #666; + --link: #0b5fff; + --code-bg: #f5f5f5; + --border: #e5e5e5; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0f1115; + --fg: #e6e6e6; + --muted: #9aa0a6; + --link: #7aa2ff; + --code-bg: #1a1d23; + --border: #2a2f3a; + } +} + +body { + font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + max-width: 850px; + margin: 40px auto; + padding: 0 16px; + background: var(--bg); + color: var(--fg); + line-height: 1.6; +} + +h1, h2, h3 { + line-height: 1.25; + margin-top: 1.4em; +} + +a { + color: var(--link); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +code { + background: var(--code-bg); + padding: 2px 5px; + border-radius: 4px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.9em; +} + +pre { + background: var(--code-bg); + padding: 12px; + overflow-x: auto; + border-radius: 6px; + border: 1px solid var(--border); +} + +table { + border-collapse: collapse; + width: 100%; +} + +th, td { + border: 1px solid var(--border); + padding: 6px 10px; + text-align: left; +} + +blockquote { + border-left: 3px solid var(--border); + padding-left: 12px; + color: var(--muted); + margin-left: 0; +} + +.build-note { + padding: 10px; + margin: 12px 0; + border-left: 3px solid #888; + background: #f6f6f6; + font-size: 0.9em; + color: #444; +} + +@media (prefers-color-scheme: dark) { + .build-note { + background: #1a1d23; + border-left-color: #777; + color: #bbb; + } +} \ No newline at end of file diff --git a/docs/architecture/state_machine.md b/docs/architecture/state_machine.md index fb336bf..1b30da4 100644 --- a/docs/architecture/state_machine.md +++ b/docs/architecture/state_machine.md @@ -1,18 +1,23 @@ # HTTP state machine parser -[http.py](../../src/pyrobusta/protocol/http.py) implements a continuation passing parser using a -finite state machine (FSM). Each state consumes available sufficient data to make progress or explicitly -suspend until more data arrives. +[http.py](../../src/pyrobusta/protocol/http.py) implements a finite state machine (FSM) whose states +are represented by handler functions. Each state consumes as much available data as necessary +to make progress, or returns control to the event loop until additional input becomes available. In general, states are not required to transition to a terminal state if a request is incomplete. Instead, states return control to the asyncio event loop, which drives subsequent invocations of the state machine based on socket readiness. The state machine may be terminated by the surrounding coroutine in -the case of a session timeout or transport error. This is a deliberate architectural decision to separate HTTP -protocol semantics from transport-level I/O scheduling concerns. +the case of a connection timeout or transport error, separating HTTP protocol semantics from transport-level +I/O scheduling concerns. -The state machine can be decomposed to four sub-FSMs, depicted by the below diagrams. The state machine applies -to a single HTTP session with a dedicated request and response stream buffer. +The state machine can be decomposed into four sub-FSMs with a common terminal state. Each sub-FSM eventually +transitions to `terminal_st`, which serves as a finalization state responsible for emitting HTTP headers required +for interoperability, such as connection persistence and cache-control directives. The terminal state can only be reached +by calling `HttpEngine.terminate()`, which requires a valid HTTP status code. The method may be invoked by the user application, the coroutine responsible for socket handling, or the state machine itself. +The state machine is associated with a single HTTP connection and maintains dedicated request and response stream buffers. +For persistent connections, the state machine instance is reset and reused for each request received on the connection. +The `HttpConnection` class is responsible for advancing the state machine, scheduling socket I/O through asyncio's `StreamReader` and `StreamWriter` interfaces, and reusing the state machine across persistent connections. ## HTTP Request Line and Header Parsing ```mermaid @@ -25,11 +30,11 @@ stateDiagram-v2 parse_request_line_st --> parse_headers_st: valid request line parsed parse_request_line_st --> parse_request_line_st: incomplete line - parse_request_line_st --> [*]: 405/505 terminate + parse_request_line_st --> terminal_st: 405/505 terminate parse_headers_st --> route_request_st: headers complete parse_headers_st --> parse_headers_st: waiting for \r\n\r\n - parse_headers_st --> [*]: invalid headers (host missing etc.) + parse_headers_st --> terminal_st: invalid headers (host missing etc.) ``` ## Routing and Body Strategy Selection @@ -44,9 +49,9 @@ stateDiagram-v2 route_request_st --> fs_retrieve_st: GET/HEAD fallback file server - route_request_st --> [*]: 404 no route - route_request_st --> [*]: 405 method not allowed - route_request_st --> [*]: 204 OPTIONS + route_request_st --> terminal_st: 404 no route + route_request_st --> terminal_st: 405 method not allowed + route_request_st --> terminal_st: 204 OPTIONS recv_payload_st --> handle_route_st: full body received recv_payload_st --> recv_payload_st: waiting for content-length @@ -62,19 +67,17 @@ stateDiagram-v2 ```mermaid stateDiagram-v2 - handle_route_st --> handle_route_st: execute handler / process request + handle_route_st --> handle_route_st: application processing handle_route_st --> recv_chunk_size_st: more chunked data expected - handle_route_st --> generate_multipart_response_st: multipart response + handle_route_st --> terminal_st: default termination (200 OK) - handle_route_st --> [*]: 200 OK (default completion) + handle_route_st --> terminal_st: 2XX/4XX/5XX (terminated by application) - fs_retrieve_st --> [*]: 200 file served - fs_retrieve_st --> [*]: 403 forbidden - fs_retrieve_st --> [*]: 404 file missing - - generate_multipart_response_st --> [*]: 200 headers set + stream ready + fs_retrieve_st --> terminal_st: 200 file served + fs_retrieve_st --> terminal_st: 403 forbidden + fs_retrieve_st --> terminal_st: 404 file missing ``` ## Multipart Request Processing @@ -87,5 +90,26 @@ stateDiagram-v2 parse_boundary_st --> parse_boundary_st: waiting for boundary parse_complete_part_st --> parse_boundary_st: more parts remain - parse_complete_part_st --> [*]: final part processed (200) + parse_complete_part_st --> terminal_st: 200 OK (final part processed - default) + parse_complete_part_st --> terminal_st: 2XX/4XX/5XX (terminated by application) +``` + +## State Machine Termination +```mermaid +stateDiagram-v2 + + terminal_st --> [*]: finalize headers (keep-alive connection, cache policy) +``` + +## Connection Lifecycle +```mermaid +flowchart LR + + HttpConnection --> id1[run parser] + id1[run parser] --> id2[terminate state machine] + id2[terminate state machine] --> id3[Connection: close] + id2[terminate state machine] --> id4[Connection: keep-alive] + id3[Connection: close] --> id5[Destroy Parser] + id4[Connection: keep-alive] --> id6[Reset Parser] + id6[Reset Parser] --> id1[run parser] ``` \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index b74211e..0000000 --- a/docs/configuration.md +++ /dev/null @@ -1,18 +0,0 @@ -# Configuration parameters - -Configuration can be overridden in pyrobusta.env, in .env format. Create pyrobusta.env in the project root, and run ```make deploy-config``` -to upload it to the root directory of the target device. - -| Name | Description | Default | -| :---------------- | :---------- | :------ | -| wifi_ssid | Name of the Wi-Fi network. When empty, Wi-Fi is not initalized by the built-in wifi.py module. | None | -| wifi_password | Password of the Wi-Fi network. When empty, Wi-Fi is not initalized by the built-in wifi.py module. | None | -| http_port | Port number for HTTP. | 80 | -| https_port | Port number for HTTPS. | 443 | -| http_multipart | Enables or disables multipart request and response processing. Enabling multipart support increases memory usage. | False | -| http_mem_cap | Fraction of available heap memory reserved for stream buffers; (0;1] | 0.1 | -| http_served_paths | Space-separated list of filesystem paths that may be served over HTTP. | "/www /lib/pyrobusta" | -| http_files_api | Enables or disables the [file management API](./api.md#file-management-api) endpoint (/files), allowing to upload, download, and list files. | False | -| socket_max_con | Maximum number of simultaneous socket connections. | 2 | -| tls | Enables or disables TLS. When enabled, cert.der and key.der must be installed at the server root. | False | -| log_level | Logging level. Can be one of: warning, info, debug. | "info" | diff --git a/docs/development.md b/docs/development.md index 2c23e7a..b41dadb 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,14 +1,18 @@ -# Prerequisites +# Development Guide -## Setup virtual environment +This guide describes the development workflow for PyRobusta itself, including building the framework, deploying examples, and running the test suite. + +## Prerequisites + +### Setup Virtual Environment ```bash python3 -m venv venv source venv/bin/activate python3 -m pip install -r requirements.txt ``` -## Create pyrobusta.env in the project root +### Create ```pyrobusta.env``` ```bash # pyrobusta.env @@ -19,58 +23,78 @@ socket_max_con=2 http_mem_cap=0.05 ... ``` -pyrobusta.env contains runtime configuration, deployed to the device. This allows the user to override the\ -default behavior and configure optional settings. Configuration settings are optional, however, you need to define\ - ̇```wifi_ssid``` and ̇```wifi_password``` if your application does not already handle network connectivity. +```pyrobusta.env``` contains runtime configuration, deployed to the device. This allows you to override the +default behavior and configure optional settings. All configuration settings are optional. However, ```wifi_ssid``` +and ```wifi_password``` must be defined unless the application initializes network connectivity itself. -- rules such as ```make run-unix``` or ```make run-device``` also rely on pyrobusta.env, allowing the user to experiment with different settings -- pyrobusta.env is ignored when running functional tests (```make test-unix```, ```make test-device```) +- Running applications with ```make run-unix``` or ```make run-device``` uses ```pyrobusta.env``` +- Functional tests (```make test-unix```, ```make test-device```) ignore ```pyrobusta.env```. -Check [configuration.md](./configuration.md) for all configuration options. +Check the [Configuration](./application_development/configuration.md) guide for all configuration options. -# Build and run example application +## Build and Run Example Application -## Run on unix port +### Run on UNIX Port ```bash make toolchain # Setup mpy-cross and micropython make build # Cross-compile, create build artifacts -make stage-example # Create runtime directory for unix port -make run-unix # Run example application on the unix port of micropython +make stage-app # Create runtime directory for UNIX port +make run-unix # Run example application on the UNIX port of MicroPython ``` -## Deploy to a device +### Deploy to a Device ```bash make toolchain # Setup mpy-cross and micropython make build # Cross-compile, create build artifacts -make deploy # Upload build artifacts to device using mpremote +make deploy # Upload build artifacts to the device using mpremote make tls-cert # Optional: generate self-signed certificate for the device make deploy-cert # Optional: upload generated certificate to the device -make deploy-example # Deploy the selected example app using mpremote -make run-device # Optional: Reset the device and connect through REPL +make deploy-app # Deploy the selected example application using mpremote +make run-device # Reset the device and connect through REPL +``` + +Deploy a specific application by overriding the ```APP_DIR``` variable. For example: + +```bash +make APP_DIR=example/demo_app deploy-app +``` + +Make targets that communicate with a device (```deploy```, ```deploy-cert```, ```deploy-app```, ```run-device```) use the ```DEVICE`` +variable, which defaults to ```u0``` (/dev/ttyUSB0). + +Override ```DEVICE``` to select a different serial device, for example: + +```bash +make DEVICE=a0 run-device ``` -```deploy-example``` and ```run-device``` uses the DEVICE argument -set to ```u0``` (/dev/ttyUSB0) by default, passed to mpremote. -Override the DEVICE argument to select a different device, e.g. -```make DEVICE=a0 run-device``` for /dev/ttyACM0. Check mpremote --help -for additional shortcuts. +which corresponds to ```/dev/ttyACM0```. Refer to ```mpremote --help``` for additional device shortcuts. -## Redeploy +### Redeploy -When changing the source code, run the below rule for redeploying to the device. +After modifying the source code, run the following rule to redeploy the application to the device. ```bash make redeploy # Will run the following rules: clean build clean-device deploy ``` -## Unit tests, pylint, functional tests +### Code Quality and Testing ```bash -make static-checkers # Run static checkers (Pylint, black formatter) +make static-checkers # Run static checkers (Pylint, Black formatter) make unit-test # Run unit tests -make test-unix # Run functional tests on the unix port +make test-unix # Run functional tests on the UNIX port make test-device # Run functional tests on a device ``` + +#### Performance testing + +Performance tests must be run on a physical device. Results are exported to a directory named after the device. + +```bash +# Run performance tests and export results to docs/dimensioning/esp32_c3 +make DEVICE=a1 DEVICE_IP=192.168.0.100 DEVICE_NAME=ESP32-C3 perf-test-device +``` diff --git a/docs/img/home_page.png b/docs/img/home_page.png index 95b02ba..929d8e2 100644 Binary files a/docs/img/home_page.png and b/docs/img/home_page.png differ diff --git a/requirements.txt b/requirements.txt index 361a557..3823697 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,8 @@ gevent==24.2.1 greenlet==3.0.3 requests==2.32.3 urllib3==2.2.2 -pylint -black -requests -matplotlib +pylint==4.0.5 +black==26.3.1 +requests==2.32.3 +matplotlib==3.10.8 +markdown==3.10.2 diff --git a/scripts/generate_docs.py b/scripts/generate_docs.py new file mode 100644 index 0000000..3581dd0 --- /dev/null +++ b/scripts/generate_docs.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 + +import argparse +from pathlib import Path +import markdown +import re +import shutil + +MD_LINK_RE = re.compile(r"\[([^\]]+)\]\(([^)]+\.md(?:#[^)]+)?)\)") +BUILD_NOTE_RE = re.compile(r"") +BUILD_IGNORE_RE = re.compile(r"") + + +def clean_output_dir(dst_root: Path): + """ + Fully remove and recreate the output directory. + """ + if not dst_root.is_relative_to(Path.cwd()): + raise ValueError("The output must be in the current working directory.") + if dst_root == Path("/"): + raise ValueError("Invalid path.") + + if dst_root.exists(): + shutil.rmtree(dst_root) + dst_root.mkdir(parents=True, exist_ok=True) + + +def rewrite_md_links(text: str) -> str: + """ + Convert relative .md links to .html while preserving anchors. + """ + + def repl(match): + label = match.group(1) + target = match.group(2) + + if "#" in target: + path, anchor = target.split("#", 1) + return f"[{label}]({Path(path).with_suffix('.html')}#{anchor})" + else: + return f"[{label}]({Path(target).with_suffix('.html')})" + + return MD_LINK_RE.sub(repl, text) + + +def build_html(md_text: str, stylesheet_name: str | None) -> str: + md = markdown.Markdown(extensions=["fenced_code", "tables", "toc"]) + + body = md.convert(md_text) + + css_link = "" + if stylesheet_name: + css_link = f'' + + return f""" + + + +{css_link} + + + +{body} + + + +""" + + +def process_build_comments(md_text: str) -> tuple[str, list[str]]: + """ + Processes build-only comments in markdowns. + Returns: + - cleaned markdown + """ + + # Handle build:note → convert to HTML block + def note_repl(match): + content = match.group(1).strip() + return content + + md_text = BUILD_NOTE_RE.sub(note_repl, md_text) + + # Remove build:ignore comments + md_text = BUILD_IGNORE_RE.sub("", md_text) + + return md_text + + +def copy_stylesheet(css_src: Path, dst_root: Path): + dst_file = dst_root / css_src.name + dst_file.parent.mkdir(parents=True, exist_ok=True) + dst_file.write_text(css_src.read_text(encoding="utf-8"), encoding="utf-8") + + +def convert_file(src_path, src_root, dst_root, stylesheet_name): + rel_path = src_path.relative_to(src_root) + out_path = (dst_root / rel_path).with_suffix(".html") + out_path.parent.mkdir(parents=True, exist_ok=True) + + md_text = src_path.read_text(encoding="utf-8") + + # 1. Process build comments + md_text = process_build_comments(md_text) + + # 2. Rewrite links + md_text = rewrite_md_links(md_text) + + # 3. Render HTML + html = build_html(md_text, stylesheet_name) + + out_path.write_text(html, encoding="utf-8") + + +def main(): + parser = argparse.ArgumentParser(description="Convert Markdown to static HTML") + parser.add_argument("input_dir", type=Path) + parser.add_argument("output_dir", type=Path) + parser.add_argument("--css", type=str, default=None) + + args = parser.parse_args() + + src_root = args.input_dir.resolve() + dst_root = args.output_dir.resolve() + + clean_output_dir(dst_root) + + css_name = None + if args.css: + css_path = Path(args.css) + copy_stylesheet(css_path, dst_root) + css_name = css_path.name # referenced in HTML + + for md_file in src_root.rglob("*.md"): + convert_file(md_file, src_root, dst_root, css_name) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index 7214a41..a17ed72 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -19,7 +19,7 @@ def setUp(self): def test_path_normalization_virtual_root(self): """ - Test lexical path normalization in a Unix-port environment + Test lexical path normalization in a UNIX-port environment with a virtual root. Simulates the situation where the process working directory acts as a virtual filesystem root. """