├── .gitignore ├── picoserve ├── src │ ├── sync.rs │ ├── doctests_utils.rs │ ├── json.rs │ ├── sync │ │ └── oneshot_broadcast.rs │ ├── futures.rs │ ├── response │ │ ├── response_stream.rs │ │ ├── with_state.rs │ │ ├── chunked.rs │ │ ├── custom.rs │ │ └── status.rs │ ├── logging.rs │ ├── time.rs │ └── routing │ │ └── layer.rs └── Cargo.toml ├── examples ├── embassy │ ├── .gitignore │ ├── hello_world_defmt │ │ ├── .gitignore │ │ ├── rust-toolchain.toml │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── memory.x │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── rust-toolchain.toml │ ├── cyw43-firmware │ │ ├── 43439A0.bin │ │ ├── 43439A0_clm.bin │ │ ├── README.md │ │ └── LICENSE-permissive-binary-license-1.0.txt │ ├── set_pico_w_led │ │ ├── src │ │ │ ├── index.js │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ └── main.rs │ │ └── Cargo.toml │ ├── example_secrets │ │ ├── src │ │ │ └── lib.rs │ │ └── Cargo.toml │ ├── web_sockets │ │ ├── src │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── main.rs │ │ └── Cargo.toml │ ├── memory.x │ ├── hello_world │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── app_with_props │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── various_states │ │ └── Cargo.toml │ ├── graceful_shutdown_using_tasks │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── graceful_shutdown_using_future_array │ │ └── Cargo.toml │ ├── .cargo │ │ └── config.toml │ └── Cargo.toml ├── static_content │ ├── src │ │ ├── index.css │ │ ├── index.html │ │ └── main.rs │ └── Cargo.toml ├── web_sockets │ ├── src │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── main.rs │ └── Cargo.toml ├── server_sent_events │ ├── src │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── main.rs │ └── Cargo.toml ├── graceful_shutdown_web_sockets │ ├── src │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ └── Cargo.toml ├── graceful_shutdown_server_sent_events │ ├── src │ │ ├── index.css │ │ ├── index.js │ │ ├── index.html │ │ └── main.rs │ └── Cargo.toml ├── layers │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── custom_extractor │ ├── src │ │ ├── index.js │ │ ├── index.html │ │ └── main.rs │ └── Cargo.toml ├── hello_world │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── huge_requests │ ├── Cargo.toml │ └── src │ │ ├── index.html │ │ └── main.rs ├── request_info │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── chunked_response │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── nested_router │ ├── Cargo.toml │ └── src │ │ ├── index.html │ │ └── main.rs ├── routing_fallback │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── conditional_routing │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── response_using_state │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── hello_world_single_thread │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── state │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── path_parameters │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── state_local │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── state_multiple │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── form │ ├── Cargo.toml │ └── src │ │ ├── index.html │ │ └── main.rs ├── query │ ├── Cargo.toml │ └── src │ │ ├── index.html │ │ └── main.rs ├── graceful_shutdown │ ├── Cargo.toml │ └── src │ │ └── main.rs └── README.md ├── rust-toolchain.toml ├── picoserve_derive ├── src │ └── internal.rs └── Cargo.toml ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /picoserve/src/sync.rs: -------------------------------------------------------------------------------- 1 | pub mod oneshot_broadcast; 2 | -------------------------------------------------------------------------------- /examples/embassy/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85" 3 | -------------------------------------------------------------------------------- /picoserve_derive/src/internal.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod router; 2 | -------------------------------------------------------------------------------- /examples/static_content/src/index.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: blue; 3 | } -------------------------------------------------------------------------------- /examples/embassy/hello_world_defmt/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /examples/embassy/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | targets = ["thumbv6m-none-eabi"] 3 | channel = "nightly-2025-09-26" 4 | -------------------------------------------------------------------------------- /examples/embassy/cyw43-firmware/43439A0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sammhicks/picoserve/HEAD/examples/embassy/cyw43-firmware/43439A0.bin -------------------------------------------------------------------------------- /examples/embassy/hello_world_defmt/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | targets = ["thumbv6m-none-eabi"] 3 | channel = "nightly-2025-09-26" 4 | -------------------------------------------------------------------------------- /examples/embassy/cyw43-firmware/43439A0_clm.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sammhicks/picoserve/HEAD/examples/embassy/cyw43-firmware/43439A0_clm.bin -------------------------------------------------------------------------------- /examples/embassy/set_pico_w_led/src/index.js: -------------------------------------------------------------------------------- 1 | function on() { 2 | fetch("/set_led/true") 3 | } 4 | 5 | function off() { 6 | fetch("/set_led/false") 7 | } -------------------------------------------------------------------------------- /examples/embassy/example_secrets/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub const WIFI_SSID: &str = "Pico W WiFi"; 4 | pub const WIFI_PASSWORD: &str = "MyVerySecurePassword"; 5 | -------------------------------------------------------------------------------- /examples/web_sockets/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-flow: column nowrap; 4 | align-items: center; 5 | } 6 | 7 | body>* { 8 | margin: 1em; 9 | } -------------------------------------------------------------------------------- /examples/embassy/web_sockets/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-flow: column nowrap; 4 | align-items: center; 5 | } 6 | 7 | body>* { 8 | margin: 1em; 9 | } -------------------------------------------------------------------------------- /examples/server_sent_events/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-flow: column nowrap; 4 | align-items: center; 5 | } 6 | 7 | body>* { 8 | margin: 1em; 9 | } 10 | -------------------------------------------------------------------------------- /picoserve/src/doctests_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::routing::{PathRouter, Router}; 2 | 3 | pub fn router(_: Router) {} 4 | 5 | pub fn router_with_state(_: Router, State>) {} 6 | -------------------------------------------------------------------------------- /examples/embassy/set_pico_w_led/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-flow: column nowrap; 4 | align-items: center; 5 | } 6 | 7 | button { 8 | margin: 1em 0; 9 | padding: 1em; 10 | } -------------------------------------------------------------------------------- /examples/graceful_shutdown_web_sockets/src/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | main, 3 | fieldset { 4 | display: flex; 5 | flex-flow: column nowrap; 6 | } 7 | 8 | body { 9 | align-items: center; 10 | } 11 | 12 | main { 13 | gap: 1rem; 14 | } -------------------------------------------------------------------------------- /examples/graceful_shutdown_server_sent_events/src/index.css: -------------------------------------------------------------------------------- 1 | body, 2 | main, 3 | fieldset { 4 | display: flex; 5 | flex-flow: column nowrap; 6 | } 7 | 8 | body { 9 | align-items: center; 10 | } 11 | 12 | main { 13 | gap: 1rem; 14 | } -------------------------------------------------------------------------------- /examples/embassy/example_secrets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example_secrets" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /examples/embassy/hello_world_defmt/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Target specific options 2 | [target.thumbv6m-none-eabi] 3 | rustflags = [ 4 | "-C", "link-arg=-Tdefmt.x", 5 | ] 6 | 7 | # This runner will transfer and run the binary using the connected debug probe 8 | runner = "probe-rs run --chip RP2040" 9 | -------------------------------------------------------------------------------- /examples/layers/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "layers" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/custom_extractor/src/index.js: -------------------------------------------------------------------------------- 1 | async function send() { 2 | let response = await fetch("/number", { 3 | method: "POST", 4 | headers: { 5 | "content-type": "text/plain" 6 | }, 7 | body: document.getElementById("data").value 8 | }); 9 | 10 | document.getElementById("response").innerText = await response.text(); 11 | } -------------------------------------------------------------------------------- /examples/hello_world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/huge_requests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "huge_requests" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/request_info/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "request_info" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/chunked_response/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chunked_response" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/nested_router/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nested_router" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio", "json"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/routing_fallback/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "routing_fallback" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/conditional_routing/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "conditional_routing" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/response_using_state/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "response_using_state" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/hello_world_single_thread/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world_single_thread" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | tokio = { workspace = true } 12 | -------------------------------------------------------------------------------- /examples/state/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "state" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio", "json"] } 11 | serde = { workspace = true } 12 | tokio = { workspace = true } 13 | -------------------------------------------------------------------------------- /examples/embassy/cyw43-firmware/README.md: -------------------------------------------------------------------------------- 1 | # WiFi firmware 2 | 3 | Firmware obtained from https://github.com/Infineon/wifi-host-driver/tree/master/WiFi_Host_Driver/resources/firmware/COMPONENT_43439 4 | 5 | Licensed under the [Infineon Permissive Binary License](./LICENSE-permissive-binary-license-1.0.txt) 6 | 7 | ## Changelog 8 | 9 | * 2023-07-28: synced with `ad3bad0` - Update 43439 fw from 7.95.55 ot 7.95.62 10 | -------------------------------------------------------------------------------- /examples/path_parameters/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "path_parameters" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | serde = { workspace = true } 12 | tokio = { workspace = true } 13 | -------------------------------------------------------------------------------- /examples/state_local/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "state_local" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio", "json"] } 11 | serde = { workspace = true } 12 | tokio = { workspace = true } 13 | -------------------------------------------------------------------------------- /examples/embassy/hello_world_defmt/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | RAM : ORIGIN = 0x20000000, LENGTH = 256K 5 | } 6 | 7 | EXTERN(BOOT2_FIRMWARE) 8 | 9 | SECTIONS { 10 | /* ### Boot loader */ 11 | .boot2 ORIGIN(BOOT2) : 12 | { 13 | KEEP(*(.boot2)); 14 | } > BOOT2 15 | } INSERT BEFORE .text; 16 | -------------------------------------------------------------------------------- /examples/state_multiple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "state_multiple" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio", "json"] } 11 | serde = { workspace = true } 12 | tokio = { workspace = true } 13 | -------------------------------------------------------------------------------- /picoserve/src/json.rs: -------------------------------------------------------------------------------- 1 | /// A JSON encoded value. 2 | /// When serializing, the value might be serialized several times during sending, 3 | /// so the value must be serialized in the same way each time. 4 | /// When deserializing, only short strings can be unescaped. 5 | /// If you want to handle longed escaped strings, use [`JsonWithUnescapeBufferSize`](crate::extract::JsonWithUnescapeBufferSize). 6 | pub struct Json(pub T); 7 | -------------------------------------------------------------------------------- /examples/form/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "form" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | heapless = { workspace = true } 11 | picoserve = { path = "../../picoserve", features = ["tokio"] } 12 | serde = { workspace = true } 13 | tokio = { workspace = true } 14 | -------------------------------------------------------------------------------- /examples/query/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "query" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | heapless = { workspace = true } 11 | picoserve = { path = "../../picoserve", features = ["tokio"] } 12 | serde = { workspace = true } 13 | tokio = { workspace = true } 14 | -------------------------------------------------------------------------------- /examples/web_sockets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web_sockets" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | futures-util = { workspace = true } 11 | picoserve = { path = "../../picoserve", features = ["tokio", "ws"] } 12 | tokio = { workspace = true, features = ["sync"] } 13 | -------------------------------------------------------------------------------- /examples/graceful_shutdown/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graceful_shutdown" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | futures-util = { workspace = true } 11 | picoserve = { path = "../../picoserve", features = ["tokio"] } 12 | tokio = { workspace = true, features = ["sync"] } 13 | -------------------------------------------------------------------------------- /examples/huge_requests/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Huge Requests 8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/server_sent_events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server_sent_events" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | thiserror = { workspace = true } 12 | tokio = { workspace = true, features = ["sync"] } 13 | -------------------------------------------------------------------------------- /examples/static_content/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static_content" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | heapless = { workspace = true } 11 | picoserve = { path = "../../picoserve", features = ["tokio"] } 12 | serde = { workspace = true } 13 | tokio = { workspace = true } 14 | -------------------------------------------------------------------------------- /examples/static_content/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Picoserve Static Content 8 | 9 | 10 | 11 | 12 |

Picoserve Static Content!

13 | 14 |

This text should be blue

15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/custom_extractor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "custom_extractor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | picoserve = { path = "../../picoserve", features = ["tokio"] } 11 | serde = { workspace = true } 12 | thiserror = { workspace = true } 13 | tokio = { workspace = true } 14 | -------------------------------------------------------------------------------- /examples/custom_extractor/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Custom Extractor Demo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/graceful_shutdown_server_sent_events/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graceful_shutdown_server_sent_events" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | futures-util = { workspace = true } 11 | picoserve = { path = "../../picoserve", features = ["tokio"] } 12 | tokio = { workspace = true, features = ["sync"] } 13 | -------------------------------------------------------------------------------- /examples/graceful_shutdown_web_sockets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graceful_shutdown_web_sockets" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = { workspace = true } 10 | futures-util = { workspace = true } 11 | picoserve = { path = "../../picoserve", features = ["tokio", "ws", "json"] } 12 | serde = { workspace = true } 13 | tokio = { workspace = true, features = ["sync"] } 14 | -------------------------------------------------------------------------------- /examples/embassy/set_pico_w_led/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Set Pico W LED 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/form/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Form Demo 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/query/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Form Demo 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/embassy/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | RAM : ORIGIN = 0x20000000, LENGTH = 200K 5 | PANDUMP : ORIGIN = 0x20000000 + 200K, LENGTH = 1K 6 | } 7 | 8 | _panic_dump_start = ORIGIN(PANDUMP); 9 | _panic_dump_end = ORIGIN(PANDUMP) + LENGTH(PANDUMP); 10 | 11 | EXTERN(BOOT2_FIRMWARE) 12 | 13 | SECTIONS { 14 | /* ### Boot loader */ 15 | .boot2 ORIGIN(BOOT2) : 16 | { 17 | KEEP(*(.boot2)); 18 | } > BOOT2 19 | } INSERT BEFORE .text; 20 | -------------------------------------------------------------------------------- /examples/server_sent_events/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Server-Sent Events 8 | 9 | 10 | 11 | 12 |
13 | Messages sent will be send to all connections 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/embassy/web_sockets/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Websockets 8 | 9 | 10 | 11 | 12 | 13 |
14 | Messages sent will be echoed back to the client 15 |
16 | 17 | 18 |
    19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/web_sockets/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Websockets 8 | 9 | 10 | 11 | 12 | 13 |
    14 | Messages sent will be send to all connections 15 |
    16 | 17 | 18 |
      19 | 20 | 21 | -------------------------------------------------------------------------------- /picoserve_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "picoserve_derive" 3 | version = "0.1.4" 4 | authors = ["Samuel Hicks"] 5 | edition = "2021" 6 | rust-version = "1.80" 7 | description = "Macros for picoserve" 8 | repository = "https://github.com/sammhicks/picoserve" 9 | license = "MIT" 10 | keywords = ["no_std", "http", "web", "framework"] 11 | categories = ["asynchronous", "network-programming", "web-programming::http-server"] 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | proc-macro2 = "1.0.92" 18 | quote = "1.0.38" 19 | syn = { version = "2.0.92", features = ["full"] } 20 | 21 | [dev-dependencies] 22 | thiserror = { version = "2.0.9", default-features = false } 23 | -------------------------------------------------------------------------------- /examples/server_sent_events/src/index.js: -------------------------------------------------------------------------------- 1 | let input = document.getElementsByTagName("input")[0]; 2 | let output = document.getElementsByTagName("output")[0]; 3 | let button = document.getElementsByTagName("button")[0]; 4 | 5 | input.addEventListener("input", function () { 6 | button.disabled = !input.value; 7 | }); 8 | 9 | button.addEventListener("click", function () { 10 | fetch("set_message", { 11 | method: "POST", 12 | headers: { 13 | "Content-Type": "text/plain", 14 | }, 15 | body: input.value, 16 | }); 17 | 18 | input.value = ""; 19 | }); 20 | 21 | let events = new EventSource("events"); 22 | 23 | events.addEventListener("error", function (ev) { 24 | events.close(); 25 | output.innerText = "Events Closed"; 26 | }); 27 | 28 | events.addEventListener("message_changed", function (ev) { 29 | output.innerText = ev.data; 30 | }) 31 | -------------------------------------------------------------------------------- /examples/graceful_shutdown_server_sent_events/src/index.js: -------------------------------------------------------------------------------- 1 | const counterOutput = document.getElementById("counterOutput"); 2 | const disconnectionMessage = document.getElementById("disconnectionMessage"); 3 | const shutdownButton = document.getElementById("shutdownButton"); 4 | const shutdownMessage = document.getElementById("shutdownMessage"); 5 | 6 | const events = new EventSource("counter"); 7 | 8 | events.addEventListener("tick", (ev) => { 9 | counterOutput.innerText = ev.data; 10 | }); 11 | 12 | events.addEventListener("error", (ev) => { 13 | disconnectionMessage.innerText = "Connection Closed"; 14 | events.close(); 15 | }); 16 | 17 | 18 | shutdownButton.addEventListener("click", async () => { 19 | const message = await (await fetch("shutdown", { 20 | method: "POST", 21 | })).text(); 22 | 23 | shutdownMessage.innerText = message; 24 | }); -------------------------------------------------------------------------------- /examples/graceful_shutdown_server_sent_events/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Graceful shutdown demo 8 | 9 | 10 | 11 | 12 |
      13 |
      14 | Counter produced by Server-Sent Events 15 | 16 | 17 |
      18 | 19 |
      20 | Shutdown 21 | 22 | 23 |
      24 |
      25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/embassy/hello_world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = { workspace = true } 10 | cyw43 = { workspace = true } 11 | cyw43-pio = { workspace = true } 12 | embassy-executor = { workspace = true } 13 | embassy-futures = { workspace = true } 14 | embassy-net = { workspace = true } 15 | embassy-rp = { workspace = true } 16 | embassy-sync = { workspace = true } 17 | embassy-time = { workspace = true } 18 | embassy-usb-logger = { workspace = true } 19 | embedded-io-async = { workspace = true } 20 | log = { workspace = true } 21 | panic-persist = { workspace = true } 22 | picoserve = { workspace = true } 23 | portable-atomic = { workspace = true } 24 | rand = { workspace = true } 25 | static_cell = { workspace = true } 26 | example_secrets = { path = "../example_secrets" } -------------------------------------------------------------------------------- /examples/embassy/set_pico_w_led/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "set_led" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = { workspace = true } 10 | cyw43 = { workspace = true } 11 | cyw43-pio = { workspace = true } 12 | embassy-executor = { workspace = true } 13 | embassy-futures = { workspace = true } 14 | embassy-net = { workspace = true } 15 | embassy-rp = { workspace = true } 16 | embassy-sync = { workspace = true } 17 | embassy-time = { workspace = true } 18 | embassy-usb-logger = { workspace = true } 19 | embedded-io-async = { workspace = true } 20 | log = { workspace = true } 21 | panic-persist = { workspace = true } 22 | picoserve = { workspace = true } 23 | portable-atomic = { workspace = true } 24 | rand = { workspace = true } 25 | static_cell = { workspace = true } 26 | example_secrets = { path = "../example_secrets" } -------------------------------------------------------------------------------- /examples/embassy/app_with_props/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app_with_props" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = { workspace = true } 10 | cyw43 = { workspace = true } 11 | cyw43-pio = { workspace = true } 12 | embassy-executor = { workspace = true } 13 | embassy-futures = { workspace = true } 14 | embassy-net = { workspace = true } 15 | embassy-rp = { workspace = true } 16 | embassy-sync = { workspace = true } 17 | embassy-time = { workspace = true } 18 | embassy-usb-logger = { workspace = true } 19 | embedded-io-async = { workspace = true } 20 | log = { workspace = true } 21 | panic-persist = { workspace = true } 22 | picoserve = { workspace = true } 23 | portable-atomic = { workspace = true } 24 | rand = { workspace = true } 25 | static_cell = { workspace = true } 26 | example_secrets = { path = "../example_secrets" } -------------------------------------------------------------------------------- /examples/embassy/various_states/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "various_states" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = { workspace = true } 10 | cyw43 = { workspace = true } 11 | cyw43-pio = { workspace = true } 12 | embassy-executor = { workspace = true } 13 | embassy-futures = { workspace = true } 14 | embassy-net = { workspace = true } 15 | embassy-rp = { workspace = true } 16 | embassy-sync = { workspace = true } 17 | embassy-time = { workspace = true } 18 | embassy-usb-logger = { workspace = true } 19 | embedded-io-async = { workspace = true } 20 | log = { workspace = true } 21 | panic-persist = { workspace = true } 22 | picoserve = { workspace = true } 23 | portable-atomic = { workspace = true } 24 | rand = { workspace = true } 25 | static_cell = { workspace = true } 26 | example_secrets = { path = "../example_secrets" } -------------------------------------------------------------------------------- /examples/embassy/web_sockets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web_sockets" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = { workspace = true } 10 | cyw43 = { workspace = true } 11 | cyw43-pio = { workspace = true } 12 | embassy-executor = { workspace = true } 13 | embassy-futures = { workspace = true } 14 | embassy-net = { workspace = true } 15 | embassy-rp = { workspace = true } 16 | embassy-sync = { workspace = true } 17 | embassy-time = { workspace = true } 18 | embassy-usb-logger = { workspace = true } 19 | embedded-io-async = { workspace = true } 20 | log = { workspace = true } 21 | panic-persist = { workspace = true } 22 | picoserve = { workspace = true, features = ["ws"] } 23 | portable-atomic = { workspace = true } 24 | rand = { workspace = true } 25 | static_cell = { workspace = true } 26 | example_secrets = { path = "../example_secrets" } -------------------------------------------------------------------------------- /examples/embassy/graceful_shutdown_using_tasks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graceful_shutdown_using_tasks" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = { workspace = true } 10 | cyw43 = { workspace = true } 11 | cyw43-pio = { workspace = true } 12 | embassy-executor = { workspace = true } 13 | embassy-futures = { workspace = true } 14 | embassy-net = { workspace = true } 15 | embassy-rp = { workspace = true } 16 | embassy-sync = { workspace = true } 17 | embassy-time = { workspace = true } 18 | embassy-usb-logger = { workspace = true } 19 | embedded-io-async = { workspace = true } 20 | log = { workspace = true } 21 | panic-persist = { workspace = true } 22 | picoserve = { workspace = true } 23 | portable-atomic = { workspace = true } 24 | rand = { workspace = true } 25 | static_cell = { workspace = true } 26 | example_secrets = { path = "../example_secrets" } -------------------------------------------------------------------------------- /examples/embassy/graceful_shutdown_using_future_array/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graceful_shutdown_using_future_array" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = { workspace = true } 10 | cyw43 = { workspace = true } 11 | cyw43-pio = { workspace = true } 12 | embassy-executor = { workspace = true } 13 | embassy-futures = { workspace = true } 14 | embassy-net = { workspace = true } 15 | embassy-rp = { workspace = true } 16 | embassy-sync = { workspace = true } 17 | embassy-time = { workspace = true } 18 | embassy-usb-logger = { workspace = true } 19 | embedded-io-async = { workspace = true } 20 | log = { workspace = true } 21 | panic-persist = { workspace = true } 22 | picoserve = { workspace = true } 23 | portable-atomic = { workspace = true } 24 | rand = { workspace = true } 25 | static_cell = { workspace = true } 26 | example_secrets = { path = "../example_secrets" } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Samuel Hicks 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /examples/graceful_shutdown_web_sockets/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Graceful shutdown demo 8 | 9 | 10 | 11 | 12 |
      13 |
      14 | Counter produced by Websockets 15 | 16 |
      17 | 18 |
      19 | Text echo using Websockets 20 | 21 | 22 |
      23 | 24 |
      25 | Shutdown 26 | 27 | 28 | 29 |
      30 |
      31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/embassy/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Set the default target to match the Cortex-M0+ in the RP2040 3 | target = "thumbv6m-none-eabi" 4 | 5 | # Target specific options 6 | [target.thumbv6m-none-eabi] 7 | # Pass some extra options to rustc, some of which get passed on to the linker. 8 | # 9 | # * linker argument --nmagic turns off page alignment of sections (which saves 10 | # flash space) 11 | # * linker argument -Tlink.x tells the linker to use link.x as the linker 12 | # script. This is usually provided by the cortex-m-rt crate, and by default 13 | # the version in that crate will include a file called `memory.x` which 14 | # describes the particular memory layout for your specific chip. 15 | # * inline-threshold=5 makes the compiler more aggressive and inlining functions 16 | # * no-vectorize-loops turns off the loop vectorizer (seeing as the M0+ doesn't 17 | # have SIMD) 18 | rustflags = [ 19 | "-C", "link-arg=--nmagic", 20 | "-C", "link-arg=-Tlink.x", 21 | "-Cllvm-args=--inline-threshold=5", 22 | "-C", "no-vectorize-loops", 23 | ] 24 | 25 | # This runner will make a UF2 file and then copy it to a mounted RP2040 in USB 26 | # Bootloader mode: 27 | runner = "elf2uf2-rs -sd" 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ "main", "development" ] 6 | pull_request: 7 | branches: [ "main", "development" ] 8 | workflow_dispatch: 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | build_and_test: 15 | name: Build and Test picoserve 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Build 21 | run: cargo build --verbose --all 22 | - name: Run tests 23 | run: cargo test --verbose 24 | 25 | build_embassy_examples: 26 | name: Build Embassy Examples 27 | runs-on: ubuntu-latest 28 | needs: build_and_test 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Build embassy examples 33 | working-directory: ./examples/embassy 34 | run: cargo build --verbose 35 | 36 | build_embassy_defmt_example: 37 | name: Build Embassy defmt Example 38 | runs-on: ubuntu-latest 39 | needs: build_and_test 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Build embassy defmt example 44 | working-directory: ./examples/embassy/hello_world_defmt 45 | run: cargo build --verbose 46 | -------------------------------------------------------------------------------- /examples/web_sockets/src/index.js: -------------------------------------------------------------------------------- 1 | let input = document.getElementsByTagName("input")[0]; 2 | let output = document.getElementById("output"); 3 | let button = document.getElementsByTagName("button")[0]; 4 | 5 | input.addEventListener("input", function () { 6 | button.disabled = !input.value; 7 | }); 8 | 9 | const currentPath = window.location.pathname; 10 | 11 | let websocketUri = (window.location.protocol === "https:") ? "wss:" : "ws:"; 12 | websocketUri += "//" + window.location.host; 13 | websocketUri += currentPath.slice(0, currentPath.lastIndexOf("/") + 1) + "ws"; 14 | 15 | let ws = new WebSocket(websocketUri, ["messages", "ignored_protocol"]); 16 | 17 | ws.addEventListener("close", function (ev) { 18 | ws.close(); 19 | output.innerText = `Events Closed: ${ev.reason} (${ev.code})`; 20 | }) 21 | 22 | ws.addEventListener("error", function (ev) { 23 | ws.close(); 24 | console.error(ev); 25 | output.innerText = "Events Error"; 26 | }); 27 | 28 | ws.addEventListener("message", function (ev) { 29 | let message = document.createElement("li"); 30 | message.innerText = ev.data; 31 | output.appendChild(message); 32 | }); 33 | 34 | button.addEventListener("click", function () { 35 | ws.send(input.value); 36 | 37 | input.value = ""; 38 | }); -------------------------------------------------------------------------------- /examples/embassy/web_sockets/src/index.js: -------------------------------------------------------------------------------- 1 | let input = document.getElementsByTagName("input")[0]; 2 | let output = document.getElementById("output"); 3 | let button = document.getElementsByTagName("button")[0]; 4 | 5 | input.addEventListener("input", function () { 6 | button.disabled = !input.value; 7 | }); 8 | 9 | const currentPath = window.location.pathname; 10 | 11 | let websocketUri = (window.location.protocol === "https:") ? "wss:" : "ws:"; 12 | websocketUri += "//" + window.location.host; 13 | websocketUri += currentPath.slice(0, currentPath.lastIndexOf("/") + 1) + "ws"; 14 | 15 | let ws = new WebSocket(websocketUri, ["echo", "ignored_protocol"]); 16 | 17 | ws.addEventListener("close", function (ev) { 18 | ws.close(); 19 | output.innerText = `Websockets Closed: ${ev.reason} (${ev.code})`; 20 | }) 21 | 22 | ws.addEventListener("error", function (ev) { 23 | ws.close(); 24 | console.error(ev); 25 | output.innerText = "Websockets Error"; 26 | }); 27 | 28 | ws.addEventListener("message", function (ev) { 29 | let message = document.createElement("li"); 30 | message.innerText = ev.data; 31 | output.appendChild(message); 32 | }); 33 | 34 | button.addEventListener("click", function () { 35 | ws.send(input.value); 36 | 37 | input.value = ""; 38 | }); -------------------------------------------------------------------------------- /examples/embassy/hello_world_defmt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello_world_defmt" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | cortex-m-rt = "0.7.3" 10 | cyw43 = { version = "0.5.0", features = ["firmware-logs"] } 11 | cyw43-pio = "0.8.0" 12 | defmt-rtt = "1.0.0" 13 | embassy-executor = { version = "0.9.1", features = ["arch-cortex-m", "executor-thread", "nightly"] } 14 | embassy-futures = "0.1.2" 15 | embassy-net = { version = "0.7.1", features = ["tcp", "proto-ipv4", "medium-ethernet"] } 16 | embassy-rp = { version = "0.8.0", features = ["rp2040", "critical-section-impl", "time-driver"] } 17 | embassy-sync = "0.7.2" 18 | embassy-time = { version = "0.5.0", features = ["defmt-timestamp-uptime"] } 19 | embedded-io-async = "0.6.0" 20 | example_secrets = { path = "../example_secrets" } 21 | log = { version = "0.4.20", default-features = false } 22 | panic-probe = { version = "1.0.0", features = ["print-defmt"] } 23 | picoserve = { path = "../../../picoserve", features = ["embassy", "defmt"] } 24 | portable-atomic = { version = "1.6.0", features = ["critical-section"], default-features = false } 25 | rand = { version = "0.8.5", default-features = false } 26 | static_cell = { version = "2.0.0", features = ["nightly"] } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "picoserve", 5 | "picoserve_derive", 6 | "examples/chunked_response", 7 | "examples/conditional_routing", 8 | "examples/custom_extractor", 9 | "examples/form", 10 | "examples/query", 11 | "examples/graceful_shutdown", 12 | "examples/graceful_shutdown_server_sent_events", 13 | "examples/graceful_shutdown_web_sockets", 14 | "examples/hello_world", 15 | "examples/hello_world_single_thread", 16 | "examples/huge_requests", 17 | "examples/layers", 18 | "examples/nested_router", 19 | "examples/path_parameters", 20 | "examples/request_info", 21 | "examples/response_using_state", 22 | "examples/routing_fallback", 23 | "examples/server_sent_events", 24 | "examples/state", 25 | "examples/state_local", 26 | "examples/state_multiple", 27 | "examples/static_content", 28 | "examples/web_sockets", 29 | ] 30 | exclude = [ 31 | "examples/embassy", 32 | ] 33 | 34 | [workspace.dependencies] 35 | anyhow = "1.0.86" 36 | futures-util = "0.3.31" 37 | heapless = { version = "0.8.0", features = ["serde"] } 38 | serde = { version = "1.0.204", features = ["derive"] } 39 | thiserror = { version = "2.0.9", default-features = false } 40 | tokio = { version = "1.38.1", features = ["rt", "io-util", "net", "time", "macros"] } 41 | -------------------------------------------------------------------------------- /examples/embassy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "app_with_props", 5 | "example_secrets", 6 | "graceful_shutdown_using_future_array", 7 | "graceful_shutdown_using_tasks", 8 | "hello_world", 9 | "set_pico_w_led", 10 | "various_states", 11 | "web_sockets" 12 | ] 13 | exclude = [ 14 | "hello_world_defmt", 15 | ] 16 | 17 | [workspace.dependencies] 18 | cortex-m-rt = "0.7.3" 19 | cyw43 = { version = "0.5.0", features = ["firmware-logs"] } 20 | cyw43-pio = "0.8.0" 21 | embassy-executor = { version = "0.9.1", features = ["arch-cortex-m", "executor-thread", "nightly"] } 22 | embassy-futures = "0.1.2" 23 | embassy-net = { version = "0.7.1", features = ["tcp", "proto-ipv4", "medium-ethernet"] } 24 | embassy-rp = { version = "0.8.0", features = ["rp2040", "critical-section-impl", "time-driver"] } 25 | embassy-sync = "0.7.2" 26 | embassy-time = "0.5.0" 27 | embassy-usb-logger = "0.5.1" 28 | embedded-io-async = "0.6.1" 29 | log = { version = "0.4.22", default-features = false } 30 | panic-persist = { version = "0.3.0", features = ["utf8"] } 31 | picoserve = { path = "../../picoserve", features = ["embassy", "log"] } 32 | portable-atomic = { version = "1.7.0", features = ["critical-section"], default-features = false } 33 | rand = { version = "0.8.5", default-features = false } 34 | static_cell = "2.1.0" 35 | -------------------------------------------------------------------------------- /examples/nested_router/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Picoserve 8 | 27 | 28 | 29 | 30 |
      31 | Current Value 32 | 33 | 34 | 35 | 36 |
      37 | 38 |
      39 | Set Value 40 | 41 | 42 | 43 | 44 |
      45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/hello_world_single_thread/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::routing::get; 4 | 5 | #[tokio::main(flavor = "current_thread")] 6 | async fn main() -> anyhow::Result<()> { 7 | let port = 8000; 8 | 9 | let app = picoserve::Router::new().route("/", get(|| async { "Hello World" })); 10 | 11 | let config = picoserve::Config::new(picoserve::Timeouts { 12 | start_read_request: Some(Duration::from_secs(5)), 13 | persistent_start_read_request: Some(Duration::from_secs(1)), 14 | read_request: Some(Duration::from_secs(1)), 15 | write: Some(Duration::from_secs(1)), 16 | }); 17 | 18 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 19 | 20 | println!("http://localhost:{port}/"); 21 | 22 | loop { 23 | let (stream, remote_address) = socket.accept().await?; 24 | 25 | println!("Connection from {remote_address}"); 26 | 27 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 28 | .serve(stream) 29 | .await 30 | { 31 | Ok(picoserve::DisconnectionInfo { 32 | handled_requests_count, 33 | .. 34 | }) => { 35 | println!("{handled_requests_count} requests handled from {remote_address}") 36 | } 37 | Err(err) => println!("{err:?}"), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/graceful_shutdown_web_sockets/src/index.js: -------------------------------------------------------------------------------- 1 | const counterOutput = document.getElementById("counterOutput"); 2 | 3 | const echoInput = document.getElementById("echoInput"); 4 | const echoOutput = document.getElementById("echoOutput"); 5 | 6 | const shutdownButton = document.getElementById("shutdownButton"); 7 | const shutdown = document.getElementById("shutdown"); 8 | const shutdownMessage = document.getElementById("shutdownMessage"); 9 | const disconnectionMessage = document.getElementById("disconnectionMessage"); 10 | 11 | shutdownButton.addEventListener("click", async () => { 12 | const message = await (await fetch("shutdown", { 13 | method: "POST", 14 | })).text(); 15 | 16 | shutdownMessage.innerText = message; 17 | }); 18 | 19 | const currentPath = window.location.pathname; 20 | 21 | let websocketUri = (window.location.protocol === "https:") ? "wss:" : "ws:"; 22 | websocketUri += "//" + window.location.host; 23 | websocketUri += currentPath.slice(0, currentPath.lastIndexOf("/") + 1) + "ws"; 24 | 25 | const ws = new WebSocket(websocketUri); 26 | 27 | let wsIsConnected = false; 28 | 29 | ws.addEventListener("open", () => { 30 | wsIsConnected = true; 31 | }) 32 | 33 | ws.addEventListener("close", (ev) => { 34 | ws.close(); 35 | 36 | disconnectionMessage.innerText = `Disconnected: ${ev.reason} (${ev.code})`; 37 | wsIsConnected = false; 38 | 39 | echoInput.remove(); 40 | }); 41 | 42 | ws.addEventListener("message", (ev) => { 43 | const message = JSON.parse(ev.data); 44 | 45 | switch (message.type) { 46 | case "Count": 47 | counterOutput.innerText = message.value; 48 | 49 | break; 50 | case "Echo": 51 | echoOutput.innerText = message.payload; 52 | 53 | break; 54 | default: 55 | console.error("Unknown message: ", message); 56 | } 57 | }); 58 | 59 | echoInput.addEventListener("input", () => { 60 | if (wsIsConnected) { 61 | ws.send(echoInput.value); 62 | } 63 | }) -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Tokio 4 | 5 | | Example | Description | 6 | | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | 7 | | [`hello_world`](../examples/hello_world/src/main.rs) | A minimal example showing how to set up a Router | 8 | | [`form`](../examples/form/src/main.rs) | GET and POST Methods, and serving File | 9 | | [`path_parameters`](../examples/path_parameters/src/main.rs) | Extracing data from path segments | 10 | | [`state`](../examples/state/src/main.rs) | Stateful Applications | 11 | | [`server_sent_events`](../examples/server_sent_events/src/main.rs) | A long-lived connection generating Server-Sent Events with Keep-Alive messages | 12 | | [`web_sockets`](../examples/web_sockets/src/main.rs) | A long-lived connection both sending and receiving WebSocket messages | 13 | | [`layers`](../examples/layers/src/main.rs) | Middleware example which logs how long a request took to be handled | 14 | 15 | 16 | ## Embassy on Pico W 17 | 18 | | Example | Description | 19 | | -------------------------------------------------------------------- | --------------------------------------------------------------------- | 20 | | [`hello_world_embassy`](../examples/embassy/hello_world/src/main.rs) | A minimal example showing how to set up a Router | 21 | | [`web_sockets_embassy`](../examples/embassy/web_sockets/src/main.rs) | A long-lived connection both sending and receiving WebSocket messages | 22 | 23 | -------------------------------------------------------------------------------- /examples/hello_world/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::routing::get; 4 | 5 | #[tokio::main(flavor = "current_thread")] 6 | async fn main() -> anyhow::Result<()> { 7 | let port = 8000; 8 | 9 | let app = std::rc::Rc::new( 10 | picoserve::Router::new().route( 11 | "/", 12 | get(|| async { "Hello World" }) 13 | .post(|| async { "Hello post" }) 14 | .put(|| async { "Hello put" }) 15 | .delete(|| async { "Hello delete" }), 16 | ), 17 | ); 18 | 19 | let config = picoserve::Config::new(picoserve::Timeouts { 20 | start_read_request: Some(Duration::from_secs(5)), 21 | persistent_start_read_request: Some(Duration::from_secs(1)), 22 | read_request: Some(Duration::from_secs(1)), 23 | write: Some(Duration::from_secs(1)), 24 | }) 25 | .keep_connection_alive(); 26 | 27 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 28 | 29 | println!("http://localhost:{port}/"); 30 | 31 | tokio::task::LocalSet::new() 32 | .run_until(async { 33 | loop { 34 | let (stream, remote_address) = socket.accept().await?; 35 | 36 | println!("Connection from {remote_address}"); 37 | 38 | let app = app.clone(); 39 | let config = config.clone(); 40 | 41 | tokio::task::spawn_local(async move { 42 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 43 | .serve(stream) 44 | .await 45 | { 46 | Ok(picoserve::DisconnectionInfo { 47 | handled_requests_count, 48 | .. 49 | }) => { 50 | println!( 51 | "{handled_requests_count} requests handled from {remote_address}" 52 | ) 53 | } 54 | Err(err) => println!("{err:?}"), 55 | } 56 | }); 57 | } 58 | }) 59 | .await 60 | } 61 | -------------------------------------------------------------------------------- /picoserve/src/sync/oneshot_broadcast.rs: -------------------------------------------------------------------------------- 1 | use core::{cell::Cell, task::Waker}; 2 | 3 | pub struct SignalCore { 4 | value: Cell>, 5 | waker: Cell>, 6 | } 7 | 8 | impl SignalCore { 9 | // Take &mut self to avoid multiple calls to make_signal for a SignalCore. 10 | pub fn make_signal(&mut self) -> Signal<'_, T> { 11 | Signal { channel: self } 12 | } 13 | } 14 | 15 | pub struct Signal<'a, T: Copy> { 16 | channel: &'a SignalCore, 17 | } 18 | 19 | impl<'a, T: Copy> Signal<'a, T> { 20 | pub fn core() -> SignalCore { 21 | SignalCore { 22 | value: None.into(), 23 | waker: None.into(), 24 | } 25 | } 26 | 27 | pub fn notify(self, value: T) { 28 | self.channel.value.set(Some(value)); 29 | 30 | if let Some(waker) = self.channel.waker.take() { 31 | waker.wake(); 32 | } 33 | } 34 | 35 | pub fn listen(&self) -> Listener<'a, T> { 36 | Listener { 37 | channel: Some(self.channel), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Clone)] 43 | pub struct Listener<'a, T: Copy> { 44 | channel: Option<&'a SignalCore>, 45 | } 46 | 47 | impl Listener<'_, T> { 48 | pub fn never() -> Self { 49 | Self { channel: None } 50 | } 51 | } 52 | 53 | impl core::future::Future for Listener<'_, T> { 54 | type Output = T; 55 | 56 | fn poll( 57 | mut self: core::pin::Pin<&mut Self>, 58 | cx: &mut core::task::Context<'_>, 59 | ) -> core::task::Poll { 60 | let Some(mux) = self.channel else { 61 | return core::task::Poll::Pending; 62 | }; 63 | 64 | if let Some(value) = mux.value.get() { 65 | self.channel = None; 66 | 67 | return core::task::Poll::Ready(value); 68 | } 69 | 70 | let new_waker = if let Some(current_waker) = mux.waker.take() { 71 | if current_waker.will_wake(cx.waker()) { 72 | current_waker 73 | } else { 74 | current_waker.wake(); 75 | 76 | cx.waker().clone() 77 | } 78 | } else { 79 | cx.waker().clone() 80 | }; 81 | 82 | mux.waker.set(Some(new_waker)); 83 | 84 | core::task::Poll::Pending 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/form/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::routing::get_service; 4 | 5 | #[derive(serde::Deserialize)] 6 | struct FormValue { 7 | a: i32, 8 | b: heapless::String<32>, 9 | } 10 | 11 | #[tokio::main(flavor = "current_thread")] 12 | async fn main() -> anyhow::Result<()> { 13 | let port = 8000; 14 | 15 | let app = std::rc::Rc::new(picoserve::Router::new().route( 16 | "/", 17 | get_service(picoserve::response::File::html(include_str!("index.html"))).post( 18 | async |picoserve::extract::Form(FormValue { a, b })| { 19 | picoserve::response::DebugValue((("a", a), ("b", b))) 20 | }, 21 | ), 22 | )); 23 | 24 | let config = picoserve::Config::new(picoserve::Timeouts { 25 | start_read_request: Some(Duration::from_secs(5)), 26 | persistent_start_read_request: Some(Duration::from_secs(1)), 27 | read_request: Some(Duration::from_secs(1)), 28 | write: Some(Duration::from_secs(1)), 29 | }) 30 | .keep_connection_alive(); 31 | 32 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 33 | 34 | println!("http://localhost:{port}/"); 35 | 36 | tokio::task::LocalSet::new() 37 | .run_until(async { 38 | loop { 39 | let (stream, remote_address) = socket.accept().await?; 40 | 41 | println!("Connection from {remote_address}"); 42 | 43 | let app = app.clone(); 44 | let config = config.clone(); 45 | 46 | tokio::task::spawn_local(async move { 47 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 48 | .serve(stream) 49 | .await 50 | { 51 | Ok(picoserve::DisconnectionInfo { 52 | handled_requests_count, 53 | .. 54 | }) => { 55 | println!( 56 | "{handled_requests_count} requests handled from {remote_address}" 57 | ) 58 | } 59 | Err(err) => println!("{err:?}"), 60 | } 61 | }); 62 | } 63 | }) 64 | .await 65 | } 66 | -------------------------------------------------------------------------------- /examples/conditional_routing/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::routing::get; 4 | 5 | #[tokio::main(flavor = "current_thread")] 6 | async fn main() -> anyhow::Result<()> { 7 | let port = 8000; 8 | 9 | let common_routes = picoserve::Router::new().route("/", get(|| async { "Hello World" })); 10 | 11 | // If you change this to false, `http://localhost:8000/other` will return a 404 instead of "Other Route!". 12 | let include_other_route = true; 13 | 14 | let app = std::rc::Rc::new(if include_other_route { 15 | common_routes 16 | .route("/other", get(|| async { "Other Route!" })) 17 | .either_left_route() 18 | } else { 19 | common_routes.either_right_route() 20 | }); 21 | 22 | let config = picoserve::Config::new(picoserve::Timeouts { 23 | start_read_request: Some(Duration::from_secs(5)), 24 | persistent_start_read_request: Some(Duration::from_secs(1)), 25 | read_request: Some(Duration::from_secs(1)), 26 | write: Some(Duration::from_secs(1)), 27 | }) 28 | .keep_connection_alive(); 29 | 30 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 31 | 32 | println!("http://localhost:{port}/"); 33 | 34 | tokio::task::LocalSet::new() 35 | .run_until(async { 36 | loop { 37 | let (stream, remote_address) = socket.accept().await?; 38 | 39 | println!("Connection from {remote_address}"); 40 | 41 | let app = app.clone(); 42 | let config = config.clone(); 43 | 44 | tokio::task::spawn_local(async move { 45 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 46 | .serve(stream) 47 | .await 48 | { 49 | Ok(picoserve::DisconnectionInfo { 50 | handled_requests_count, 51 | .. 52 | }) => { 53 | println!( 54 | "{handled_requests_count} requests handled from {remote_address}" 55 | ) 56 | } 57 | Err(err) => println!("{err:?}"), 58 | } 59 | }); 60 | } 61 | }) 62 | .await 63 | } 64 | -------------------------------------------------------------------------------- /examples/static_content/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{ 4 | response::{Directory, File}, 5 | routing::get_service, 6 | }; 7 | 8 | #[tokio::main(flavor = "current_thread")] 9 | async fn main() -> anyhow::Result<()> { 10 | let port = 8000; 11 | 12 | let app = std::rc::Rc::new( 13 | picoserve::Router::new() 14 | .route("/", get_service(File::html(include_str!("index.html")))) 15 | .nest_service( 16 | "/static", 17 | const { 18 | Directory { 19 | files: &[("index.css", File::css(include_str!("index.css")))], 20 | ..Directory::DEFAULT 21 | } 22 | }, 23 | ), 24 | ); 25 | 26 | let config = picoserve::Config::new(picoserve::Timeouts { 27 | start_read_request: Some(Duration::from_secs(5)), 28 | persistent_start_read_request: Some(Duration::from_secs(1)), 29 | read_request: Some(Duration::from_secs(1)), 30 | write: Some(Duration::from_secs(1)), 31 | }) 32 | .keep_connection_alive(); 33 | 34 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 35 | 36 | println!("http://localhost:{port}/"); 37 | 38 | tokio::task::LocalSet::new() 39 | .run_until(async { 40 | loop { 41 | let (stream, remote_address) = socket.accept().await?; 42 | 43 | println!("Connection from {remote_address}"); 44 | 45 | let app = app.clone(); 46 | let config = config.clone(); 47 | 48 | tokio::task::spawn_local(async move { 49 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 50 | .serve(stream) 51 | .await 52 | { 53 | Ok(picoserve::DisconnectionInfo { 54 | handled_requests_count, 55 | .. 56 | }) => { 57 | println!( 58 | "{handled_requests_count} requests handled from {remote_address}" 59 | ) 60 | } 61 | Err(err) => println!("{err:?}"), 62 | } 63 | }); 64 | } 65 | }) 66 | .await 67 | } 68 | -------------------------------------------------------------------------------- /picoserve/src/futures.rs: -------------------------------------------------------------------------------- 1 | use core::future::Future; 2 | 3 | pub enum Either { 4 | First(A), 5 | Second(B), 6 | } 7 | 8 | impl Either { 9 | pub fn ignore_never_b(self) -> A { 10 | match self { 11 | Self::First(a) => a, 12 | Self::Second(b) => match b {}, 13 | } 14 | } 15 | } 16 | 17 | impl Either { 18 | pub fn ignore_never_a(self) -> B { 19 | match self { 20 | Self::First(a) => match a {}, 21 | Self::Second(b) => b, 22 | } 23 | } 24 | } 25 | 26 | pub(crate) async fn select_either( 27 | a: A, 28 | b: B, 29 | ) -> Either { 30 | let mut a = core::pin::pin!(a); 31 | let mut b = core::pin::pin!(b); 32 | 33 | core::future::poll_fn(|cx| { 34 | use core::task::Poll; 35 | 36 | match a.as_mut().poll(cx) { 37 | Poll::Ready(output) => return Poll::Ready(Either::First(output)), 38 | Poll::Pending => (), 39 | } 40 | 41 | match b.as_mut().poll(cx) { 42 | Poll::Ready(output) => return Poll::Ready(Either::Second(output)), 43 | Poll::Pending => (), 44 | } 45 | 46 | Poll::Pending 47 | }) 48 | .await 49 | } 50 | 51 | pub(crate) async fn select>(a: A, b: B) -> A::Output { 52 | match select_either(a, b).await { 53 | Either::First(output) | Either::Second(output) => output, 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | use core::future::pending; 60 | 61 | use futures_util::FutureExt; 62 | 63 | use super::select; 64 | 65 | struct Success; 66 | 67 | #[test] 68 | #[ntest::timeout(1000)] 69 | fn select_first() { 70 | let Success = select(async { Success }, pending()) 71 | .now_or_never() 72 | .expect("Future must resolve"); 73 | } 74 | 75 | #[test] 76 | #[ntest::timeout(1000)] 77 | fn select_second() { 78 | let Success = select(pending(), async { Success }) 79 | .now_or_never() 80 | .expect("Future must resolve"); 81 | } 82 | 83 | #[test] 84 | #[ntest::timeout(1000)] 85 | fn select_neither() { 86 | enum Never {} 87 | 88 | assert!(select(pending::(), pending::()) 89 | .now_or_never() 90 | .is_none()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /examples/query/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::routing::{get, get_service}; 4 | 5 | #[derive(serde::Deserialize)] 6 | struct QueryParams { 7 | a: i32, 8 | b: heapless::String<32>, 9 | } 10 | 11 | #[tokio::main(flavor = "current_thread")] 12 | async fn main() -> anyhow::Result<()> { 13 | let port = 8000; 14 | 15 | let app = std::rc::Rc::new( 16 | picoserve::Router::new() 17 | .route( 18 | "/", 19 | get_service(picoserve::response::File::html(include_str!("index.html"))), 20 | ) 21 | .route( 22 | "/get-thing", 23 | get(async |picoserve::extract::Query(QueryParams { a, b })| { 24 | picoserve::response::DebugValue((("a", a), ("b", b))) 25 | }), 26 | ), 27 | ); 28 | 29 | let config = picoserve::Config::new(picoserve::Timeouts { 30 | start_read_request: Some(Duration::from_secs(5)), 31 | persistent_start_read_request: Some(Duration::from_secs(1)), 32 | read_request: Some(Duration::from_secs(1)), 33 | write: Some(Duration::from_secs(1)), 34 | }) 35 | .keep_connection_alive(); 36 | 37 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 38 | 39 | println!("http://localhost:{port}/"); 40 | 41 | tokio::task::LocalSet::new() 42 | .run_until(async { 43 | loop { 44 | let (stream, remote_address) = socket.accept().await?; 45 | 46 | println!("Connection from {remote_address}"); 47 | 48 | let app = app.clone(); 49 | let config = config.clone(); 50 | 51 | tokio::task::spawn_local(async move { 52 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 53 | .serve(stream) 54 | .await 55 | { 56 | Ok(picoserve::DisconnectionInfo { 57 | handled_requests_count, 58 | .. 59 | }) => { 60 | println!( 61 | "{handled_requests_count} requests handled from {remote_address}" 62 | ) 63 | } 64 | Err(err) => println!("{err:?}"), 65 | } 66 | }); 67 | } 68 | }) 69 | .await 70 | } 71 | -------------------------------------------------------------------------------- /examples/embassy/cyw43-firmware/LICENSE-permissive-binary-license-1.0.txt: -------------------------------------------------------------------------------- 1 | Permissive Binary License 2 | 3 | Version 1.0, July 2019 4 | 5 | Redistribution. Redistribution and use in binary form, without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | 1) Redistributions must reproduce the above copyright notice and the 10 | following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | 2) Unless to the extent explicitly permitted by law, no reverse 14 | engineering, decompilation, or disassembly of this software is 15 | permitted. 16 | 17 | 3) Redistribution as part of a software development kit must include the 18 | accompanying file named �DEPENDENCIES� and any dependencies listed in 19 | that file. 20 | 21 | 4) Neither the name of the copyright holder nor the names of its 22 | contributors may be used to endorse or promote products derived from 23 | this software without specific prior written permission. 24 | 25 | Limited patent license. The copyright holders (and contributors) grant a 26 | worldwide, non-exclusive, no-charge, royalty-free patent license to 27 | make, have made, use, offer to sell, sell, import, and otherwise 28 | transfer this software, where such license applies only to those patent 29 | claims licensable by the copyright holders (and contributors) that are 30 | necessarily infringed by this software. This patent license shall not 31 | apply to any combinations that include this software. No hardware is 32 | licensed hereunder. 33 | 34 | If you institute patent litigation against any entity (including a 35 | cross-claim or counterclaim in a lawsuit) alleging that the software 36 | itself infringes your patent(s), then your rights granted under this 37 | license shall terminate as of the date such litigation is filed. 38 | 39 | DISCLAIMER. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 40 | CONTRIBUTORS "AS IS." ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 41 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 42 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 43 | HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 44 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 45 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 46 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 47 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 48 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 49 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /picoserve/src/response/response_stream.rs: -------------------------------------------------------------------------------- 1 | use crate::io::{Read, Write}; 2 | 3 | pub(crate) struct ResponseSentCore(()); 4 | 5 | /// A marker showing that the response has been sent. 6 | pub struct ResponseSent(pub(crate) ResponseSentCore); 7 | 8 | pub(crate) struct ResponseStream { 9 | writer: W, 10 | connection_header: super::KeepAlive, 11 | } 12 | 13 | impl ResponseStream { 14 | pub(crate) fn new(writer: W, connection_header: super::KeepAlive) -> Self { 15 | Self { 16 | writer, 17 | connection_header, 18 | } 19 | } 20 | } 21 | 22 | impl super::ResponseWriter for ResponseStream { 23 | type Error = W::Error; 24 | 25 | async fn write_response, H: super::HeadersIter, B: super::Body>( 26 | mut self, 27 | connection: super::Connection<'_, R>, 28 | super::Response { 29 | status_code, 30 | headers, 31 | body, 32 | }: super::Response, 33 | ) -> Result { 34 | struct HeadersWriter { 35 | writer: WW, 36 | connection_header: Option, 37 | } 38 | 39 | impl super::ForEachHeader for HeadersWriter { 40 | type Output = (); 41 | type Error = WW::Error; 42 | 43 | async fn call( 44 | &mut self, 45 | name: &str, 46 | value: Value, 47 | ) -> Result<(), Self::Error> { 48 | if name.eq_ignore_ascii_case("connection") { 49 | self.connection_header = None; 50 | } 51 | write!(self.writer, "{name}: {value}\r\n").await 52 | } 53 | 54 | async fn finalize(mut self) -> Result<(), Self::Error> { 55 | if let Some(connection_header) = self.connection_header { 56 | self.call("Connection", connection_header).await?; 57 | } 58 | 59 | Ok(()) 60 | } 61 | } 62 | 63 | use crate::io::WriteExt; 64 | write!(self.writer, "HTTP/1.1 {status_code} \r\n").await?; 65 | 66 | headers 67 | .for_each_header(HeadersWriter { 68 | writer: &mut self.writer, 69 | connection_header: Some(self.connection_header), 70 | }) 71 | .await?; 72 | 73 | self.writer.write_all(b"\r\n").await?; 74 | self.writer.flush().await?; 75 | 76 | body.write_response_body(connection, &mut self.writer) 77 | .await 78 | .map(|()| super::ResponseSent(ResponseSentCore(()))) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/routing_fallback/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{response::IntoResponse, routing::get}; 4 | 5 | struct CustomNotFound; 6 | 7 | impl picoserve::routing::PathRouterService<()> for CustomNotFound { 8 | async fn call_path_router_service< 9 | R: picoserve::io::Read, 10 | W: picoserve::response::ResponseWriter, 11 | >( 12 | &self, 13 | _state: &(), 14 | _path_parameters: (), 15 | path: picoserve::request::Path<'_>, 16 | request: picoserve::request::Request<'_, R>, 17 | response_writer: W, 18 | ) -> Result { 19 | ( 20 | picoserve::response::StatusCode::NOT_FOUND, 21 | format_args!("{:?} not found\n", path.encoded()), 22 | ) 23 | .write_to(request.body_connection.finalize().await?, response_writer) 24 | .await 25 | } 26 | } 27 | 28 | #[tokio::main(flavor = "current_thread")] 29 | async fn main() -> anyhow::Result<()> { 30 | let port = 8000; 31 | 32 | let app = std::rc::Rc::new( 33 | picoserve::Router::from_service(CustomNotFound).route("/", get(|| async { "Hello World" })), 34 | ); 35 | 36 | let config = picoserve::Config::new(picoserve::Timeouts { 37 | start_read_request: Some(Duration::from_secs(5)), 38 | persistent_start_read_request: Some(Duration::from_secs(1)), 39 | read_request: Some(Duration::from_secs(1)), 40 | write: Some(Duration::from_secs(1)), 41 | }) 42 | .keep_connection_alive(); 43 | 44 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 45 | 46 | println!("http://localhost:{port}/"); 47 | 48 | tokio::task::LocalSet::new() 49 | .run_until(async { 50 | loop { 51 | let (stream, remote_address) = socket.accept().await?; 52 | 53 | println!("Connection from {remote_address}"); 54 | 55 | let app = app.clone(); 56 | let config = config.clone(); 57 | 58 | tokio::task::spawn_local(async move { 59 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 60 | .serve(stream) 61 | .await 62 | { 63 | Ok(picoserve::DisconnectionInfo { 64 | handled_requests_count, 65 | .. 66 | }) => { 67 | println!( 68 | "{handled_requests_count} requests handled from {remote_address}" 69 | ) 70 | } 71 | Err(err) => println!("{err:?}"), 72 | } 73 | }); 74 | } 75 | }) 76 | .await 77 | } 78 | -------------------------------------------------------------------------------- /picoserve/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "picoserve" 3 | version = "0.17.1" 4 | authors = ["Samuel Hicks"] 5 | edition = "2021" 6 | rust-version = "1.85" 7 | description = "An async no_std HTTP server suitable for bare-metal environments" 8 | readme = "../README.md" 9 | repository = "https://github.com/sammhicks/picoserve" 10 | license = "MIT" 11 | keywords = ["no_std", "http", "web", "framework"] 12 | categories = ["asynchronous", "network-programming", "web-programming::http-server"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [package.metadata.docs.rs] 17 | 18 | features = ["tokio", "embassy", "json", "ws"] 19 | 20 | [dependencies] 21 | const-sha1 = { version = "0.3.0", default-features = false } 22 | data-encoding = { version = "2.4.0", default-features = false, optional = true } 23 | defmt = { version = "1.0.1", optional = true } 24 | embassy-net = { version = ">=0.6.0", optional = true, features = ["tcp", "proto-ipv4", "medium-ethernet"] } 25 | embassy-time = { version = ">=0.4.0", optional = true } 26 | embedded-io-async = "0.6.0" 27 | heapless = { version = "0.8.0", features = ["serde"] } 28 | lhash = { version = "1.1.0", features = ["sha1"], optional = true } 29 | log = { version = "0.4.19", optional = true, default-features = false } 30 | picoserve_derive = { version = "0.1.4", path = "../picoserve_derive" } 31 | ryu = { version = "1.0.14", optional = true } 32 | serde = { version = "1.0.171", default-features = false, features = ["derive"] } 33 | serde-json-core = { version = "0.6.0", optional = true } 34 | thiserror = { version = "2.0.9", default-features = false } 35 | tokio = { version = "1.32.0", optional = true, features = ["io-util", "net", "time"] } 36 | 37 | [features] 38 | std = ["alloc"] # Use the standard library. Used by examples. 39 | alloc = [] # Enable `FromRequest` and `Content` for some alloc types. 40 | 41 | tokio = ["dep:tokio", "std", "serde/std"] # Use the `tokio` runtime. Used by examples. 42 | embassy = ["dep:embassy-time", "dep:embassy-net"] # Use the `embassy` runtime and `embassy-net` sockets. 43 | defmt = ["dep:defmt", "embassy-net?/defmt", "serde-json-core/defmt"] # Emit log messages using the `defmt` crate. 44 | log = ["dep:log"] # Emit log messages using the `log` crate. 45 | 46 | json = ["dep:ryu", "dep:serde-json-core"] # Enable JSON support 47 | ws = ["dep:data-encoding", "dep:lhash"] # Enable WebSocket support 48 | 49 | [dev-dependencies] 50 | embedded-io-async = { version = "0.6.0", features = ["std"] } 51 | futures-util = { version = "0.3.28", default-features = false } 52 | http-body-util = "0.1.0" 53 | hyper = { version = "1.1.0", features = ["client", "http1"] } 54 | ntest = "0.9.3" 55 | tokio = { version = "1.0.0", features = ["rt", "io-util", "net", "time", "macros", "sync"] } -------------------------------------------------------------------------------- /examples/chunked_response/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{ 4 | response::chunked::{ChunkWriter, ChunkedResponse, Chunks, ChunksWritten}, 5 | routing::get, 6 | }; 7 | 8 | struct TextChunks { 9 | text: &'static str, 10 | } 11 | 12 | impl Chunks for TextChunks { 13 | fn content_type(&self) -> &'static str { 14 | "text/plain" 15 | } 16 | 17 | async fn write_chunks( 18 | self, 19 | mut chunk_writer: ChunkWriter, 20 | ) -> Result { 21 | for word in self.text.split_inclusive(char::is_whitespace) { 22 | chunk_writer.write_chunk(word.as_bytes()).await?; 23 | 24 | tokio::time::sleep(std::time::Duration::from_millis(100)).await; 25 | } 26 | 27 | chunk_writer.finalize().await 28 | } 29 | } 30 | 31 | #[tokio::main(flavor = "current_thread")] 32 | async fn main() -> anyhow::Result<()> { 33 | let port = 8000; 34 | 35 | let app = std::rc::Rc::new(picoserve::Router::new().route( 36 | "/", 37 | get(|| async move { 38 | ChunkedResponse::new(TextChunks { 39 | text: "This is a chunked response\r\n", 40 | }) 41 | .into_response() 42 | .with_header("x-header", "x-value") 43 | }), 44 | )); 45 | 46 | let config = picoserve::Config::new(picoserve::Timeouts { 47 | start_read_request: Some(Duration::from_secs(5)), 48 | persistent_start_read_request: Some(Duration::from_secs(1)), 49 | read_request: Some(Duration::from_secs(1)), 50 | write: Some(Duration::from_secs(1)), 51 | }) 52 | .keep_connection_alive(); 53 | 54 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 55 | 56 | println!("http://localhost:{port}/"); 57 | 58 | tokio::task::LocalSet::new() 59 | .run_until(async { 60 | loop { 61 | let (stream, remote_address) = socket.accept().await?; 62 | 63 | println!("Connection from {remote_address}"); 64 | 65 | let app = app.clone(); 66 | let config = config.clone(); 67 | 68 | tokio::task::spawn_local(async move { 69 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 70 | .serve(stream) 71 | .await 72 | { 73 | Ok(picoserve::DisconnectionInfo { 74 | handled_requests_count, 75 | .. 76 | }) => { 77 | println!( 78 | "{handled_requests_count} requests handled from {remote_address}" 79 | ) 80 | } 81 | Err(err) => println!("{err:?}"), 82 | } 83 | }); 84 | } 85 | }) 86 | .await 87 | } 88 | -------------------------------------------------------------------------------- /picoserve/src/logging.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_macros)] 2 | 3 | macro_rules! log_error { 4 | ($f:literal $(,$arg:expr)* $(,)?) => { 5 | { 6 | #[cfg(feature = "log")] 7 | log::error!($f $(,$arg)*); 8 | 9 | #[cfg(feature = "defmt")] 10 | defmt::error!($f $(,$arg)*); 11 | 12 | $( 13 | let _ = &$arg; 14 | )* 15 | } 16 | }; 17 | } 18 | 19 | macro_rules! log_warn { 20 | ($f:literal $(,$arg:expr)* $(,)?) => { 21 | { 22 | #[cfg(feature = "log")] 23 | log::warn!($f $(,$arg)*); 24 | 25 | #[cfg(feature = "defmt")] 26 | defmt::warn!($f $(,$arg)*); 27 | 28 | $( 29 | let _ = &$arg; 30 | )* 31 | } 32 | }; 33 | } 34 | 35 | macro_rules! log_info { 36 | ($f:literal $(,$arg:expr)* $(,)?) => { 37 | { 38 | #[cfg(feature = "log")] 39 | log::info!($f $(,$arg)*); 40 | 41 | #[cfg(feature = "defmt")] 42 | defmt::info!($f $(,$arg)*); 43 | 44 | $( 45 | let _ = &$arg; 46 | )* 47 | } 48 | }; 49 | } 50 | 51 | #[cfg(feature = "defmt")] 52 | pub use defmt::Debug2Format; 53 | 54 | #[cfg(not(feature = "defmt"))] 55 | pub struct Debug2Format<'a, T: core::fmt::Debug + ?Sized>(pub &'a T); 56 | 57 | #[cfg(not(feature = "defmt"))] 58 | impl core::fmt::Debug for Debug2Format<'_, T> { 59 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 60 | core::fmt::Debug::fmt(self.0, f) 61 | } 62 | } 63 | 64 | #[cfg(not(feature = "defmt"))] 65 | impl core::fmt::Display for Debug2Format<'_, T> { 66 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 67 | core::fmt::Debug::fmt(self.0, f) 68 | } 69 | } 70 | 71 | #[cfg(feature = "defmt")] 72 | pub use defmt::Display2Format; 73 | 74 | #[cfg(not(feature = "defmt"))] 75 | pub struct Display2Format<'a, T: core::fmt::Display + ?Sized>(pub &'a T); 76 | 77 | #[cfg(not(feature = "defmt"))] 78 | impl core::fmt::Debug for Display2Format<'_, T> { 79 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 80 | core::fmt::Display::fmt(self.0, f) 81 | } 82 | } 83 | 84 | #[cfg(not(feature = "defmt"))] 85 | impl core::fmt::Display for Display2Format<'_, T> { 86 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 87 | core::fmt::Display::fmt(self.0, f) 88 | } 89 | } 90 | 91 | #[cfg(feature = "defmt")] 92 | pub trait LogDisplay: core::fmt::Display + defmt::Format {} 93 | 94 | #[cfg(feature = "defmt")] 95 | impl LogDisplay for T {} 96 | 97 | #[cfg(not(feature = "defmt"))] 98 | pub trait LogDisplay: core::fmt::Display {} 99 | 100 | #[cfg(not(feature = "defmt"))] 101 | impl LogDisplay for T {} 102 | -------------------------------------------------------------------------------- /examples/huge_requests/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{io::Read, response::IntoResponse, routing::get_service}; 4 | 5 | struct MeasureBody; 6 | 7 | impl picoserve::routing::RequestHandlerService<()> for MeasureBody { 8 | async fn call_request_handler_service< 9 | R: Read, 10 | W: picoserve::response::ResponseWriter, 11 | >( 12 | &self, 13 | (): &(), 14 | (): (), 15 | mut request: picoserve::request::Request<'_, R>, 16 | response_writer: W, 17 | ) -> Result { 18 | let mut reader = request.body_connection.body().reader(); 19 | 20 | let mut buffer = [0; 1024]; 21 | 22 | let mut total_size = 0; 23 | 24 | loop { 25 | let read_size = reader.read(&mut buffer).await?; 26 | if read_size == 0 { 27 | break; 28 | } 29 | 30 | total_size += read_size; 31 | } 32 | 33 | format!("Total Size: {total_size}\r\n") 34 | .write_to(request.body_connection.finalize().await?, response_writer) 35 | .await 36 | } 37 | } 38 | 39 | #[tokio::main(flavor = "current_thread")] 40 | async fn main() -> anyhow::Result<()> { 41 | let port = 8000; 42 | 43 | let app = std::rc::Rc::new( 44 | picoserve::Router::new().route( 45 | "/", 46 | get_service(picoserve::response::File::html(include_str!("index.html"))) 47 | .post_service(MeasureBody), 48 | ), 49 | ); 50 | 51 | let config = picoserve::Config::new(picoserve::Timeouts { 52 | start_read_request: Some(Duration::from_secs(5)), 53 | persistent_start_read_request: Some(Duration::from_secs(1)), 54 | read_request: Some(Duration::from_secs(1)), 55 | write: Some(Duration::from_secs(1)), 56 | }) 57 | .keep_connection_alive(); 58 | 59 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 60 | 61 | println!("http://localhost:{port}/"); 62 | 63 | tokio::task::LocalSet::new() 64 | .run_until(async { 65 | loop { 66 | let (stream, remote_address) = socket.accept().await?; 67 | 68 | println!("Connection from {remote_address}"); 69 | 70 | let app = app.clone(); 71 | let config = config.clone(); 72 | 73 | tokio::task::spawn_local(async move { 74 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 75 | .serve(stream) 76 | .await 77 | { 78 | Ok(picoserve::DisconnectionInfo { 79 | handled_requests_count, 80 | .. 81 | }) => { 82 | println!( 83 | "{handled_requests_count} requests handled from {remote_address}" 84 | ) 85 | } 86 | Err(err) => println!("{err:?}"), 87 | } 88 | }); 89 | } 90 | }) 91 | .await 92 | } 93 | -------------------------------------------------------------------------------- /examples/state/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, time::Duration}; 2 | 3 | use picoserve::{ 4 | extract::State, 5 | response::{with_state::WithStateUpdate, IntoResponse, IntoResponseWithState, Redirect}, 6 | routing::{get, parse_path_segment}, 7 | }; 8 | 9 | struct AppState { 10 | value: RefCell, 11 | } 12 | 13 | #[derive(serde::Serialize)] 14 | struct AppStateValue { 15 | value: i32, 16 | } 17 | 18 | impl picoserve::extract::FromRef for AppStateValue { 19 | fn from_ref(AppState { value, .. }: &AppState) -> Self { 20 | Self { 21 | value: *value.borrow(), 22 | } 23 | } 24 | } 25 | 26 | async fn get_value(State(value): State) -> impl IntoResponse { 27 | picoserve::response::Json(value) 28 | } 29 | 30 | async fn increment_value() -> impl IntoResponseWithState { 31 | Redirect::to(".").with_state_update(async |state: &AppState| { 32 | *state.value.borrow_mut() += 1; 33 | }) 34 | } 35 | 36 | async fn set_value(value: i32) -> impl IntoResponseWithState { 37 | Redirect::to("..").with_state_update(async move |state: &AppState| { 38 | *state.value.borrow_mut() = value; 39 | }) 40 | } 41 | 42 | #[tokio::main(flavor = "current_thread")] 43 | async fn main() -> anyhow::Result<()> { 44 | let port = 8000; 45 | 46 | let state = AppState { value: 0.into() }; 47 | 48 | let app = std::rc::Rc::new( 49 | picoserve::Router::new() 50 | .route("/", get(get_value)) 51 | .route("/increment", get(increment_value)) 52 | .route(("/set", parse_path_segment()), get(set_value)) 53 | .with_state(state), 54 | ); 55 | 56 | let config = picoserve::Config::new(picoserve::Timeouts { 57 | start_read_request: Some(Duration::from_secs(5)), 58 | persistent_start_read_request: Some(Duration::from_secs(1)), 59 | read_request: Some(Duration::from_secs(1)), 60 | write: Some(Duration::from_secs(1)), 61 | }) 62 | .keep_connection_alive(); 63 | 64 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 65 | 66 | println!("http://localhost:{port}/"); 67 | 68 | tokio::task::LocalSet::new() 69 | .run_until(async { 70 | loop { 71 | let (stream, remote_address) = socket.accept().await?; 72 | 73 | println!("Connection from {remote_address}"); 74 | 75 | let app = app.clone(); 76 | let config = config.clone(); 77 | 78 | tokio::task::spawn_local(async move { 79 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 80 | .serve(stream) 81 | .await 82 | { 83 | Ok(picoserve::DisconnectionInfo { 84 | handled_requests_count, 85 | .. 86 | }) => { 87 | println!( 88 | "{handled_requests_count} requests handled from {remote_address}" 89 | ) 90 | } 91 | Err(err) => println!("{err:?}"), 92 | } 93 | }); 94 | } 95 | }) 96 | .await 97 | } 98 | -------------------------------------------------------------------------------- /examples/request_info/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::response::IntoResponse; 4 | 5 | struct ShowRequestInfo; 6 | 7 | impl picoserve::routing::MethodHandlerService for ShowRequestInfo { 8 | async fn call_method_handler_service< 9 | R: picoserve::io::Read, 10 | W: picoserve::response::ResponseWriter, 11 | >( 12 | &self, 13 | (): &(), 14 | (): (), 15 | method: &str, 16 | mut request: picoserve::request::Request<'_, R>, 17 | response_writer: W, 18 | ) -> Result { 19 | use picoserve::io::Read; 20 | 21 | let headers = request.parts.headers(); 22 | 23 | let mut body = request.body_connection.body().reader(); 24 | 25 | let mut body_byte_histogram = [0_u16; 256]; 26 | 27 | loop { 28 | let mut buffer = [0; 32]; 29 | 30 | let read_size = body.read(&mut buffer).await?; 31 | 32 | if read_size == 0 { 33 | break; 34 | } 35 | 36 | for &b in &buffer[..read_size] { 37 | body_byte_histogram[usize::from(b)] += 1; 38 | } 39 | } 40 | 41 | format_args!("Method: {method}\r\nHeaders: {headers:?}\r\nRequest Body Byte Histogram: {body_byte_histogram:?}\r\n") 42 | .write_to(request.body_connection.finalize().await?, response_writer) 43 | .await 44 | } 45 | } 46 | 47 | #[tokio::main(flavor = "current_thread")] 48 | async fn main() -> anyhow::Result<()> { 49 | let port = 8000; 50 | 51 | let app = std::rc::Rc::new(picoserve::Router::new().route_service("/", ShowRequestInfo)); 52 | 53 | let config = picoserve::Config::new(picoserve::Timeouts { 54 | start_read_request: Some(Duration::from_secs(5)), 55 | persistent_start_read_request: Some(Duration::from_secs(1)), 56 | read_request: Some(Duration::from_secs(1)), 57 | write: Some(Duration::from_secs(1)), 58 | }) 59 | .keep_connection_alive(); 60 | 61 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 62 | 63 | println!("http://localhost:{port}/"); 64 | 65 | tokio::task::LocalSet::new() 66 | .run_until(async { 67 | loop { 68 | let (stream, remote_address) = socket.accept().await?; 69 | 70 | println!("Connection from {remote_address}"); 71 | 72 | let app = app.clone(); 73 | let config = config.clone(); 74 | 75 | tokio::task::spawn_local(async move { 76 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 77 | .serve(stream) 78 | .await 79 | { 80 | Ok(picoserve::DisconnectionInfo { 81 | handled_requests_count, 82 | .. 83 | }) => { 84 | println!( 85 | "{handled_requests_count} requests handled from {remote_address}" 86 | ) 87 | } 88 | Err(err) => println!("{err:?}"), 89 | } 90 | }); 91 | } 92 | }) 93 | .await 94 | } 95 | -------------------------------------------------------------------------------- /examples/nested_router/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, time::Duration}; 2 | 3 | use picoserve::{ 4 | response::with_state::WithStateUpdate, 5 | routing::{get, get_service}, 6 | }; 7 | 8 | struct AppState { 9 | value: RefCell, 10 | } 11 | 12 | fn api_router() -> picoserve::Router, AppState> { 13 | picoserve::Router::new().route( 14 | "/value", 15 | get({ 16 | struct GetValue(i32); 17 | 18 | impl picoserve::extract::FromRef for GetValue { 19 | fn from_ref(input: &AppState) -> Self { 20 | Self(*input.value.borrow()) 21 | } 22 | } 23 | 24 | async |picoserve::extract::State(GetValue(value))| picoserve::response::Json(value) 25 | }) 26 | .post(|picoserve::extract::Json(new_value)| async move { 27 | ().with_state_update(async move |state: &AppState| { 28 | *state.value.borrow_mut() = new_value 29 | }) 30 | }), 31 | ) 32 | } 33 | 34 | #[tokio::main(flavor = "current_thread")] 35 | async fn main() -> anyhow::Result<()> { 36 | let port = 8000; 37 | 38 | let app_state = AppState { 39 | value: RefCell::new(0), 40 | }; 41 | 42 | let app = std::rc::Rc::new( 43 | picoserve::Router::new() 44 | .nest("/api", api_router()) 45 | .route( 46 | "/", 47 | get_service(picoserve::response::File::html(include_str!("index.html"))), 48 | ) 49 | .with_state(app_state), 50 | ); 51 | 52 | let config = picoserve::Config::new(picoserve::Timeouts { 53 | start_read_request: Some(Duration::from_secs(5)), 54 | persistent_start_read_request: Some(Duration::from_secs(1)), 55 | read_request: Some(Duration::from_secs(1)), 56 | write: Some(Duration::from_secs(1)), 57 | }) 58 | .keep_connection_alive(); 59 | 60 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 61 | 62 | println!("http://localhost:{port}/"); 63 | 64 | tokio::task::LocalSet::new() 65 | .run_until(async { 66 | loop { 67 | let (stream, remote_address) = socket.accept().await?; 68 | 69 | println!("Connection from {remote_address}"); 70 | 71 | let app = app.clone(); 72 | let config = config.clone(); 73 | 74 | tokio::task::spawn_local(async move { 75 | tokio::task::spawn_local(async move { 76 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 77 | .serve(stream) 78 | .await 79 | { 80 | Ok(picoserve::DisconnectionInfo { 81 | handled_requests_count, 82 | .. 83 | }) => { 84 | println!( 85 | "{handled_requests_count} requests handled from {remote_address}" 86 | ) 87 | } 88 | Err(err) => println!("{err:?}"), 89 | } 90 | }); 91 | }); 92 | } 93 | }) 94 | .await 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # picoserve 2 | 3 | An async `no_std` HTTP server suitable for bare-metal environments, heavily inspired by [axum](https://github.com/tokio-rs/axum). 4 | It was designed with [embassy](https://embassy.dev/) on the Raspberry Pi Pico W in mind, but should work with other embedded runtimes and hardware. 5 | 6 | Features: 7 | + No heap usage 8 | + Handler functions are just async functions that accept zero or more extractors as arguments and returns something that implements IntoResponse 9 | + Query and Form parsing using serde 10 | + JSON responses 11 | + Server-Sent Events 12 | + Web Sockets 13 | + HEAD method is automatically handled 14 | 15 | Shortcomings: 16 | + While in version `0.*.*`, there may be breaking API changes 17 | + URL-Encoded strings, for example in Query and Form parsing, have a maximum length of 1024. 18 | + This has relatively little stress-testing so I advise not to expose it directly to the internet, but place it behind a proxy such as nginx, which will act as a security layer. 19 | + Certain serialization methods, such as the DebugValue response and JSON serialisation might be called several times if the response payload is large. The caller MUST ensure that the output of serialisation is the same during repeated calls with the same value. 20 | + The framework does not verify that the specified length of a reponse body, i.e. the value stored in the "Content-Length" header is actually the length of the body. 21 | 22 | ## Usage examples 23 | 24 | ### tokio (for testing purposes) 25 | 26 | ```rust 27 | use std::time::Duration; 28 | 29 | use picoserve::routing::get; 30 | 31 | #[tokio::main(flavor = "current_thread")] 32 | async fn main() -> anyhow::Result<()> { 33 | let port = 8000; 34 | 35 | let app = 36 | std::rc::Rc::new(picoserve::Router::new().route("/", get(|| async { "Hello World" }))); 37 | 38 | let config = picoserve::Config::new(picoserve::Timeouts { 39 | start_read_request: Some(Duration::from_secs(5)), 40 | persistent_start_read_request: Some(Duration::from_secs(1)), 41 | read_request: Some(Duration::from_secs(1)), 42 | write: Some(Duration::from_secs(1)), 43 | }) 44 | .keep_connection_alive(); 45 | 46 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 47 | 48 | println!("http://localhost:{port}/"); 49 | 50 | tokio::task::LocalSet::new() 51 | .run_until(async { 52 | loop { 53 | let (stream, remote_address) = socket.accept().await?; 54 | 55 | println!("Connection from {remote_address}"); 56 | 57 | let app = app.clone(); 58 | let config = config.clone(); 59 | 60 | tokio::task::spawn_local(async move { 61 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 62 | .serve(stream) 63 | .await 64 | { 65 | Ok(picoserve::DisconnectionInfo { 66 | handled_requests_count, 67 | .. 68 | }) => { 69 | println!( 70 | "{handled_requests_count} requests handled from {remote_address}" 71 | ) 72 | } 73 | Err(err) => println!("{err:?}"), 74 | } 75 | }); 76 | } 77 | }) 78 | .await 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /picoserve/src/time.rs: -------------------------------------------------------------------------------- 1 | //! [Timer] for creating timeouts during request parsing and request handling. 2 | 3 | /// A timer which can be used to abort futures if they take to long to resolve. 4 | pub trait Timer { 5 | /// The measure of time duration for this timer. 6 | type Duration: Clone; 7 | /// The error returned if a future fails to resolve in time. 8 | type TimeoutError; 9 | 10 | /// Drive the future, failing if it takes to long to resolve. 11 | async fn run_with_timeout( 12 | &self, 13 | duration: Self::Duration, 14 | future: F, 15 | ) -> Result; 16 | } 17 | 18 | pub(crate) trait TimerExt: Timer { 19 | async fn run_with_maybe_timeout( 20 | &self, 21 | duration: Option, 22 | future: F, 23 | ) -> Result { 24 | if let Some(duration) = duration { 25 | self.run_with_timeout(duration, future).await 26 | } else { 27 | Ok(future.await) 28 | } 29 | } 30 | } 31 | 32 | impl> TimerExt for T {} 33 | 34 | #[cfg(any(feature = "tokio", test))] 35 | #[doc(hidden)] 36 | pub struct TokioTimer; 37 | 38 | #[cfg(any(feature = "tokio", test))] 39 | impl Timer for TokioTimer { 40 | type Duration = std::time::Duration; 41 | type TimeoutError = tokio::time::error::Elapsed; 42 | 43 | async fn run_with_timeout( 44 | &self, 45 | duration: Self::Duration, 46 | future: F, 47 | ) -> Result { 48 | tokio::time::timeout(duration, future).await 49 | } 50 | } 51 | 52 | #[cfg(feature = "embassy")] 53 | #[doc(hidden)] 54 | pub struct EmbassyTimer; 55 | 56 | #[cfg(feature = "embassy")] 57 | impl Timer for EmbassyTimer { 58 | type Duration = embassy_time::Duration; 59 | type TimeoutError = embassy_time::TimeoutError; 60 | 61 | async fn run_with_timeout( 62 | &self, 63 | duration: Self::Duration, 64 | future: F, 65 | ) -> Result { 66 | embassy_time::with_timeout(duration, future).await 67 | } 68 | } 69 | 70 | pub(crate) struct WriteWithTimeout<'t, Runtime, W: crate::io::Write, T: Timer> { 71 | pub inner: W, 72 | pub timer: &'t T, 73 | pub timeout_duration: Option, 74 | pub _runtime: core::marker::PhantomData, 75 | } 76 | 77 | impl> crate::io::ErrorType 78 | for WriteWithTimeout<'_, Runtime, W, T> 79 | { 80 | type Error = super::Error; 81 | } 82 | 83 | impl> crate::io::Write 84 | for WriteWithTimeout<'_, Runtime, W, T> 85 | { 86 | async fn write(&mut self, buf: &[u8]) -> Result { 87 | self.timer 88 | .run_with_maybe_timeout(self.timeout_duration.clone(), self.inner.write(buf)) 89 | .await 90 | .map_err(|_| super::Error::WriteTimeout)? 91 | .map_err(super::Error::Write) 92 | } 93 | 94 | async fn flush(&mut self) -> Result<(), Self::Error> { 95 | self.timer 96 | .run_with_maybe_timeout(self.timeout_duration.clone(), self.inner.flush()) 97 | .await 98 | .map_err(|_| super::Error::WriteTimeout)? 99 | .map_err(super::Error::Write) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/state_local/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc, time::Duration}; 2 | 3 | use picoserve::{ 4 | extract::State, 5 | response::{IntoResponse, Redirect}, 6 | routing::{get, parse_path_segment}, 7 | }; 8 | 9 | #[derive(Clone, serde::Serialize)] 10 | struct Counter { 11 | counter: i32, 12 | } 13 | 14 | type SharedCounter = Rc>; 15 | 16 | #[derive(Clone)] 17 | struct AppState { 18 | connection_id: usize, 19 | counter: SharedCounter, 20 | } 21 | 22 | async fn get_counter(State(state): State) -> impl IntoResponse { 23 | picoserve::response::Json(state.counter.borrow().clone()) 24 | .into_response() 25 | .with_header("X-Connection-ID", state.connection_id) 26 | } 27 | 28 | async fn increment_counter(State(state): State) -> impl IntoResponse { 29 | state.counter.borrow_mut().counter += 1; 30 | Redirect::to(".") 31 | } 32 | 33 | async fn set_counter(value: i32, State(state): State) -> impl IntoResponse { 34 | state.counter.borrow_mut().counter = value; 35 | Redirect::to(".") 36 | } 37 | 38 | #[tokio::main(flavor = "current_thread")] 39 | async fn main() -> anyhow::Result<()> { 40 | let port = 8000; 41 | 42 | let counter = Rc::new(RefCell::new(Counter { counter: 0 })); 43 | 44 | let app = std::rc::Rc::new( 45 | picoserve::Router::<_, AppState>::new() 46 | .route("/", get(get_counter)) 47 | .route("/increment", get(increment_counter)) 48 | .route(("/set", parse_path_segment()), get(set_counter)), 49 | ); 50 | 51 | let config = picoserve::Config::new(picoserve::Timeouts { 52 | start_read_request: Some(Duration::from_secs(5)), 53 | persistent_start_read_request: Some(Duration::from_secs(1)), 54 | read_request: Some(Duration::from_secs(1)), 55 | write: Some(Duration::from_secs(1)), 56 | }) 57 | .keep_connection_alive(); 58 | 59 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 60 | 61 | println!("http://localhost:{port}/"); 62 | 63 | tokio::task::LocalSet::new() 64 | .run_until(async { 65 | for connection_id in 0.. { 66 | let (stream, remote_address) = socket.accept().await?; 67 | 68 | println!("Connection from {remote_address}"); 69 | 70 | let counter = counter.clone(); 71 | let app = app.clone(); 72 | let config = config.clone(); 73 | 74 | tokio::task::spawn_local(async move { 75 | match picoserve::Server::new( 76 | &app.shared().with_state(AppState { 77 | connection_id, 78 | counter, 79 | }), 80 | &config, 81 | &mut [0; 2048], 82 | ) 83 | .serve(stream) 84 | .await 85 | { 86 | Ok(picoserve::DisconnectionInfo { 87 | handled_requests_count, 88 | .. 89 | }) => { 90 | println!( 91 | "{handled_requests_count} requests handled from {remote_address}" 92 | ) 93 | } 94 | Err(err) => println!("{err:?}"), 95 | } 96 | }); 97 | } 98 | 99 | Ok(()) 100 | }) 101 | .await 102 | } 103 | -------------------------------------------------------------------------------- /examples/custom_extractor/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Test with `curl -d 42 http://localhost:8000/number` 2 | 3 | use std::time::Duration; 4 | 5 | use picoserve::{ 6 | extract::FromRequest, 7 | response::{ErrorWithStatusCode, IntoResponse}, 8 | routing::{get_service, post}, 9 | }; 10 | 11 | struct Number { 12 | value: f32, 13 | } 14 | 15 | #[derive(Debug, thiserror::Error, ErrorWithStatusCode)] 16 | #[status_code(BAD_REQUEST)] 17 | enum BadRequest { 18 | #[error("Read Error")] 19 | #[status_code(INTERNAL_SERVER_ERROR)] 20 | ReadError, 21 | #[error("Request Body is not UTF-8: {0}")] 22 | NotUtf8(core::str::Utf8Error), 23 | #[error("Request Body is not a valid integer: {0}")] 24 | BadNumber(core::num::ParseFloatError), 25 | } 26 | 27 | impl<'r, State> FromRequest<'r, State> for Number { 28 | type Rejection = BadRequest; 29 | 30 | async fn from_request( 31 | _state: &'r State, 32 | _request_parts: picoserve::request::RequestParts<'r>, 33 | request_body: picoserve::request::RequestBody<'r, R>, 34 | ) -> Result { 35 | Ok(Number { 36 | value: core::str::from_utf8( 37 | request_body 38 | .read_all() 39 | .await 40 | .map_err(|_err| BadRequest::ReadError)?, 41 | ) 42 | .map_err(BadRequest::NotUtf8)? 43 | .parse() 44 | .map_err(BadRequest::BadNumber)?, 45 | }) 46 | } 47 | } 48 | 49 | async fn handler_with_extractor(Number { value }: Number) -> impl IntoResponse { 50 | picoserve::response::DebugValue(value) 51 | } 52 | 53 | #[tokio::main(flavor = "current_thread")] 54 | async fn main() -> anyhow::Result<()> { 55 | let port = 8000; 56 | 57 | let app = std::rc::Rc::new( 58 | picoserve::Router::new() 59 | .route( 60 | "/", 61 | get_service(picoserve::response::File::html(include_str!("index.html"))), 62 | ) 63 | .route( 64 | "/index.js", 65 | get_service(picoserve::response::File::html(include_str!("index.js"))), 66 | ) 67 | .route("/number", post(handler_with_extractor)), 68 | ); 69 | 70 | let config = picoserve::Config::new(picoserve::Timeouts { 71 | start_read_request: Some(Duration::from_secs(5)), 72 | persistent_start_read_request: Some(Duration::from_secs(1)), 73 | read_request: Some(Duration::from_secs(1)), 74 | write: Some(Duration::from_secs(1)), 75 | }) 76 | .keep_connection_alive(); 77 | 78 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 79 | 80 | println!("http://localhost:{port}/"); 81 | 82 | tokio::task::LocalSet::new() 83 | .run_until(async { 84 | loop { 85 | let (stream, remote_address) = socket.accept().await?; 86 | 87 | println!("Connection from {remote_address}"); 88 | 89 | let app = app.clone(); 90 | let config = config.clone(); 91 | 92 | tokio::task::spawn_local(async move { 93 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 94 | .serve(stream) 95 | .await 96 | { 97 | Ok(picoserve::DisconnectionInfo { 98 | handled_requests_count, 99 | .. 100 | }) => { 101 | println!( 102 | "{handled_requests_count} requests handled from {remote_address}" 103 | ) 104 | } 105 | Err(err) => println!("{err:?}"), 106 | } 107 | }); 108 | } 109 | }) 110 | .await 111 | } 112 | -------------------------------------------------------------------------------- /examples/state_multiple/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc, time::Duration}; 2 | 3 | use picoserve::{ 4 | extract::State, 5 | response::{IntoResponse, Redirect}, 6 | routing::{get, parse_path_segment}, 7 | }; 8 | 9 | #[derive(Clone, serde::Serialize)] 10 | struct Counter { 11 | counter: i32, 12 | } 13 | 14 | type SharedCounter = Rc>; 15 | 16 | async fn get_counters( 17 | State((a_counter, b_counter)): State<(SharedCounter, SharedCounter)>, 18 | ) -> impl IntoResponse { 19 | #[derive(serde::Serialize)] 20 | struct Response { 21 | a: Counter, 22 | b: Counter, 23 | } 24 | 25 | picoserve::response::Json(Response { 26 | a: a_counter.borrow().clone(), 27 | b: b_counter.borrow().clone(), 28 | }) 29 | } 30 | 31 | async fn get_counter(State(state): State) -> impl IntoResponse { 32 | picoserve::response::Json(state.borrow().clone()) 33 | } 34 | 35 | async fn increment_counter(State(state): State) -> impl IntoResponse { 36 | state.borrow_mut().counter += 1; 37 | Redirect::to(".") 38 | } 39 | 40 | async fn set_counter(value: i32, State(state): State) -> impl IntoResponse { 41 | state.borrow_mut().counter = value; 42 | Redirect::to(".") 43 | } 44 | 45 | fn make_app( 46 | counter: SharedCounter, 47 | ) -> picoserve::Router, State> { 48 | picoserve::Router::new() 49 | .route("/", get(get_counter)) 50 | .route("/increment", get(increment_counter)) 51 | .route(("/set", parse_path_segment()), get(set_counter)) 52 | .with_state(counter) 53 | } 54 | 55 | #[tokio::main(flavor = "current_thread")] 56 | async fn main() -> anyhow::Result<()> { 57 | let port = 8000; 58 | 59 | let a_counter = Rc::new(RefCell::new(Counter { counter: 0 })); 60 | let b_counter = Rc::new(RefCell::new(Counter { counter: 0 })); 61 | 62 | let app = std::rc::Rc::new( 63 | picoserve::Router::new() 64 | .route("/", get(get_counters)) 65 | .route("/a", get(|| async { Redirect::to("/a/") })) 66 | .route("/b", get(|| async { Redirect::to("/b/") })) 67 | .nest("/a", make_app(a_counter.clone())) 68 | .nest("/b", make_app(b_counter.clone())) 69 | .with_state((a_counter, b_counter)), 70 | ); 71 | 72 | let config = picoserve::Config::new(picoserve::Timeouts { 73 | start_read_request: Some(Duration::from_secs(5)), 74 | persistent_start_read_request: Some(Duration::from_secs(1)), 75 | read_request: Some(Duration::from_secs(1)), 76 | write: Some(Duration::from_secs(1)), 77 | }) 78 | .keep_connection_alive(); 79 | 80 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 81 | 82 | println!("http://localhost:{port}/"); 83 | 84 | tokio::task::LocalSet::new() 85 | .run_until(async { 86 | loop { 87 | let (stream, remote_address) = socket.accept().await?; 88 | 89 | println!("Connection from {remote_address}"); 90 | 91 | let app = app.clone(); 92 | let config = config.clone(); 93 | 94 | tokio::task::spawn_local(async move { 95 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 96 | .serve(stream) 97 | .await 98 | { 99 | Ok(picoserve::DisconnectionInfo { 100 | handled_requests_count, 101 | .. 102 | }) => { 103 | println!( 104 | "{handled_requests_count} requests handled from {remote_address}" 105 | ) 106 | } 107 | Err(err) => println!("{err:?}"), 108 | } 109 | }); 110 | } 111 | }) 112 | .await 113 | } 114 | -------------------------------------------------------------------------------- /examples/graceful_shutdown/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::routing::get; 4 | 5 | #[derive(Clone)] 6 | enum ServerState { 7 | Running, 8 | Shutdown, 9 | } 10 | 11 | #[tokio::main(flavor = "current_thread")] 12 | async fn main() -> anyhow::Result<()> { 13 | let port = 8000; 14 | 15 | let (update_server_state, mut server_state) = tokio::sync::watch::channel(ServerState::Running); 16 | 17 | let app = std::rc::Rc::new( 18 | picoserve::Router::new() 19 | .route( 20 | "/", 21 | get(|| async { "Hello World\n\nNavigate to /shutdown to shutdown the server.\n" }), 22 | ) 23 | .route( 24 | "/shutdown", 25 | get(move || { 26 | let _ = update_server_state.send(ServerState::Shutdown); 27 | async { "Shutting Down\n" } 28 | }), 29 | ), 30 | ); 31 | 32 | // Larger timeouts to demonstrate rapid graceful shutdown 33 | let config = picoserve::Config::new(picoserve::Timeouts { 34 | start_read_request: Some(Duration::from_secs(10)), 35 | persistent_start_read_request: Some(Duration::from_secs(10)), 36 | read_request: Some(Duration::from_secs(1)), 37 | write: Some(Duration::from_secs(1)), 38 | }) 39 | .keep_connection_alive(); 40 | 41 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 42 | 43 | println!("http://localhost:{port}/"); 44 | 45 | let (wait_handle, waiter) = tokio::sync::oneshot::channel::(); 46 | let wait_handle = std::sync::Arc::new(wait_handle); 47 | 48 | tokio::task::LocalSet::new() 49 | .run_until(async { 50 | loop { 51 | let (stream, remote_address) = match futures_util::future::select( 52 | std::pin::pin!( 53 | server_state.wait_for(|state| matches!(state, ServerState::Shutdown)) 54 | ), 55 | std::pin::pin!(socket.accept()), 56 | ) 57 | .await 58 | { 59 | futures_util::future::Either::Left((_, _)) => break, 60 | futures_util::future::Either::Right((connection, _)) => connection?, 61 | }; 62 | 63 | println!("Connection from {remote_address}"); 64 | 65 | let app = app.clone(); 66 | let config = config.clone(); 67 | let mut server_state = server_state.clone(); 68 | let wait_handle = wait_handle.clone(); 69 | 70 | tokio::task::spawn_local(async move { 71 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 72 | .with_graceful_shutdown( 73 | server_state.wait_for(|state| matches!(state, ServerState::Shutdown)), 74 | Duration::from_secs(1), 75 | ) 76 | .serve(stream) 77 | .await 78 | { 79 | Ok(picoserve::DisconnectionInfo { 80 | handled_requests_count, 81 | shutdown_reason, 82 | }) => { 83 | println!( 84 | "{handled_requests_count} requests handled from {remote_address}" 85 | ); 86 | 87 | if shutdown_reason.is_some() { 88 | println!("Shutdown signal received"); 89 | } 90 | } 91 | Err(err) => println!("{err:?}"), 92 | } 93 | 94 | drop(wait_handle); 95 | }); 96 | } 97 | 98 | println!("Waiting for connections to close..."); 99 | drop(wait_handle); 100 | 101 | #[allow(clippy::single_match)] 102 | match waiter.await { 103 | Ok(never) => match never {}, 104 | Err(_) => (), 105 | } 106 | 107 | println!("All connections are closed"); 108 | 109 | Ok(()) 110 | }) 111 | .await 112 | } 113 | -------------------------------------------------------------------------------- /examples/embassy/hello_world_defmt/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use cyw43_pio::PioSpi; 6 | use embassy_rp::{ 7 | gpio::{Level, Output}, 8 | peripherals::{DMA_CH0, PIO0}, 9 | pio::Pio, 10 | }; 11 | 12 | use defmt_rtt as _; 13 | use embassy_time::Duration; 14 | use panic_probe as _; 15 | use picoserve::{make_static, routing::get, AppBuilder, AppRouter}; 16 | use rand::Rng; 17 | 18 | embassy_rp::bind_interrupts!(struct Irqs { 19 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 20 | }); 21 | 22 | #[embassy_executor::task] 23 | async fn wifi_task( 24 | runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>, 25 | ) -> ! { 26 | runner.run().await 27 | } 28 | 29 | #[embassy_executor::task] 30 | async fn net_task(mut stack: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { 31 | stack.run().await 32 | } 33 | 34 | struct AppProps; 35 | 36 | impl AppBuilder for AppProps { 37 | type PathRouter = impl picoserve::routing::PathRouter; 38 | 39 | fn build_app(self) -> picoserve::Router { 40 | picoserve::Router::new().route("/", get(|| async move { "Hello World" })) 41 | } 42 | } 43 | 44 | const WEB_TASK_POOL_SIZE: usize = 8; 45 | 46 | #[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)] 47 | async fn web_task( 48 | task_id: usize, 49 | stack: embassy_net::Stack<'static>, 50 | app: &'static AppRouter, 51 | config: &'static picoserve::Config, 52 | ) -> ! { 53 | let port = 80; 54 | let mut tcp_rx_buffer = [0; 1024]; 55 | let mut tcp_tx_buffer = [0; 1024]; 56 | let mut http_buffer = [0; 2048]; 57 | 58 | picoserve::Server::new(app, config, &mut http_buffer) 59 | .listen_and_serve(task_id, stack, port, &mut tcp_rx_buffer, &mut tcp_tx_buffer) 60 | .await 61 | .into_never() 62 | } 63 | 64 | #[embassy_executor::main] 65 | async fn main(spawner: embassy_executor::Spawner) { 66 | let p = embassy_rp::init(Default::default()); 67 | 68 | let fw = include_bytes!("../../cyw43-firmware/43439A0.bin"); 69 | let clm = include_bytes!("../../cyw43-firmware/43439A0_clm.bin"); 70 | 71 | let pwr = Output::new(p.PIN_23, Level::Low); 72 | let cs = Output::new(p.PIN_25, Level::High); 73 | let mut pio = Pio::new(p.PIO0, Irqs); 74 | let spi = cyw43_pio::PioSpi::new( 75 | &mut pio.common, 76 | pio.sm0, 77 | cyw43_pio::DEFAULT_CLOCK_DIVIDER, 78 | pio.irq0, 79 | cs, 80 | p.PIN_24, 81 | p.PIN_29, 82 | p.DMA_CH0, 83 | ); 84 | 85 | let state = make_static!(cyw43::State, cyw43::State::new()); 86 | let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; 87 | spawner.must_spawn(wifi_task(runner)); 88 | 89 | control.init(clm).await; 90 | 91 | let (stack, runner) = embassy_net::new( 92 | net_device, 93 | embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { 94 | address: embassy_net::Ipv4Cidr::new(core::net::Ipv4Addr::new(192, 168, 0, 1), 24), 95 | gateway: None, 96 | dns_servers: Default::default(), 97 | }), 98 | make_static!( 99 | embassy_net::StackResources::, 100 | embassy_net::StackResources::new() 101 | ), 102 | embassy_rp::clocks::RoscRng.gen(), 103 | ); 104 | 105 | spawner.must_spawn(net_task(runner)); 106 | 107 | control 108 | .start_ap_wpa2( 109 | example_secrets::WIFI_SSID, 110 | example_secrets::WIFI_PASSWORD, 111 | 8, 112 | ) 113 | .await; 114 | 115 | let app = make_static!(AppRouter, AppProps.build_app()); 116 | 117 | let config = make_static!( 118 | picoserve::Config::, 119 | picoserve::Config::new(picoserve::Timeouts { 120 | start_read_request: Some(Duration::from_secs(5)), 121 | persistent_start_read_request: Some(Duration::from_secs(1)), 122 | read_request: Some(Duration::from_secs(1)), 123 | write: Some(Duration::from_secs(1)), 124 | }) 125 | .keep_connection_alive() 126 | ); 127 | 128 | for task_id in 0..WEB_TASK_POOL_SIZE { 129 | spawner.must_spawn(web_task(task_id, stack, app, config)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /picoserve/src/response/with_state.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | io::{Read, Write}, 3 | ResponseSent, 4 | }; 5 | 6 | use super::{Connection, Content, IntoResponse, ResponseWriter}; 7 | 8 | /// [Content] which uses the State to calculate its properties 9 | pub trait ContentUsingState { 10 | fn content_type(&self, state: &State) -> &'static str; 11 | 12 | fn content_length(&self, state: &State) -> usize; 13 | 14 | async fn write_content(self, state: &State, writer: W) -> Result<(), W::Error>; 15 | 16 | /// Convert into a type which implements [Content] and thus can be passed into [Response::new](super::Response::new), 17 | /// or as the last field in a tuple. 18 | fn using_state(self, state: &State) -> ContentUsingStateWithState<'_, State, Self> 19 | where 20 | Self: Sized, 21 | { 22 | ContentUsingStateWithState { 23 | content: self, 24 | state, 25 | } 26 | } 27 | } 28 | 29 | /// A [Content] which passes the State to the [ContentUsingState]. 30 | pub struct ContentUsingStateWithState<'s, State, C: ContentUsingState> { 31 | content: C, 32 | state: &'s State, 33 | } 34 | 35 | impl> Content for ContentUsingStateWithState<'_, State, C> { 36 | fn content_type(&self) -> &'static str { 37 | self.content.content_type(self.state) 38 | } 39 | 40 | fn content_length(&self) -> usize { 41 | self.content.content_length(self.state) 42 | } 43 | 44 | async fn write_content(self, writer: W) -> Result<(), W::Error> { 45 | self.content.write_content(self.state, writer).await 46 | } 47 | } 48 | 49 | /// Trait for generating responses which use the State when writing themselves to the socket. 50 | /// 51 | /// Types that implement IntoResponseWithState can be returned from handlers. 52 | /// [IntoResponse] should be preferred, with [IntoResponseWithState] used if copying out the appropriate part of State is costly. 53 | pub trait IntoResponseWithState: Sized { 54 | /// Write the generated response into the given [ResponseWriter]. 55 | async fn write_to_with_state>( 56 | self, 57 | state: &State, 58 | connection: Connection<'_, R>, 59 | response_writer: W, 60 | ) -> Result; 61 | } 62 | 63 | impl IntoResponseWithState for T { 64 | async fn write_to_with_state>( 65 | self, 66 | _state: &State, 67 | connection: Connection<'_, R>, 68 | response_writer: W, 69 | ) -> Result { 70 | self.write_to(connection, response_writer).await 71 | } 72 | } 73 | 74 | /// A response which also updates the state. Returned by [`WithStateUpdate::with_state_update`] 75 | pub struct IntoResponseWithStateUpdate< 76 | State, 77 | T: IntoResponseWithState, 78 | F: AsyncFnOnce(&State), 79 | > { 80 | response: T, 81 | state_update: F, 82 | _state: core::marker::PhantomData, 83 | } 84 | 85 | impl, F: AsyncFnOnce(&State)> IntoResponseWithState 86 | for IntoResponseWithStateUpdate 87 | { 88 | async fn write_to_with_state>( 89 | self, 90 | state: &State, 91 | connection: Connection<'_, R>, 92 | response_writer: W, 93 | ) -> Result { 94 | let Self { 95 | response, 96 | state_update, 97 | _state: core::marker::PhantomData, 98 | } = self; 99 | 100 | state_update(state).await; 101 | 102 | response 103 | .write_to_with_state(state, connection, response_writer) 104 | .await 105 | } 106 | } 107 | 108 | /// An extension trait for updating the state as part of writing the response. 109 | /// 110 | /// Allows for easy state updates using data produced by the handler. 111 | pub trait WithStateUpdate: IntoResponseWithState { 112 | fn with_state_update( 113 | self, 114 | state_update: F, 115 | ) -> IntoResponseWithStateUpdate { 116 | IntoResponseWithStateUpdate { 117 | response: self, 118 | state_update, 119 | _state: core::marker::PhantomData, 120 | } 121 | } 122 | } 123 | 124 | impl> WithStateUpdate for T {} 125 | -------------------------------------------------------------------------------- /examples/response_using_state/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{ 4 | response::{Content, ContentUsingState}, 5 | routing::get, 6 | }; 7 | 8 | struct AppState { 9 | key: u8, 10 | } 11 | 12 | struct EncryptedContent { 13 | message: &'static str, 14 | } 15 | 16 | impl picoserve::response::ContentUsingState for EncryptedContent { 17 | fn content_type(&self, _state: &AppState) -> &'static str { 18 | self.message.content_type() 19 | } 20 | 21 | fn content_length(&self, _state: &AppState) -> usize { 22 | self.message.content_length() 23 | } 24 | 25 | async fn write_content( 26 | self, 27 | state: &AppState, 28 | mut writer: W, 29 | ) -> Result<(), W::Error> { 30 | let rotate = |d: u8| (d + (state.key % 26)) % 26; 31 | 32 | for decoded_byte in self.message.bytes() { 33 | let encoded_byte = if decoded_byte.is_ascii_lowercase() { 34 | rotate(decoded_byte - b'a') + b'a' 35 | } else if decoded_byte.is_ascii_uppercase() { 36 | rotate(decoded_byte - b'A') + b'A' 37 | } else { 38 | decoded_byte 39 | }; 40 | 41 | writer.write_all(&[encoded_byte]).await?; 42 | } 43 | 44 | Ok(()) 45 | } 46 | } 47 | 48 | struct EncryptedMessage { 49 | message: &'static str, 50 | } 51 | 52 | impl picoserve::response::IntoResponseWithState for EncryptedMessage { 53 | async fn write_to_with_state< 54 | R: picoserve::io::Read, 55 | W: picoserve::response::ResponseWriter, 56 | >( 57 | self, 58 | state: &AppState, 59 | connection: picoserve::response::Connection<'_, R>, 60 | response_writer: W, 61 | ) -> Result { 62 | use picoserve::response::IntoResponse; 63 | 64 | ( 65 | ("X-Encrypted", "true"), 66 | EncryptedContent { 67 | message: self.message, 68 | } 69 | .using_state(state), 70 | ) 71 | .write_to(connection, response_writer) 72 | .await 73 | } 74 | } 75 | 76 | #[tokio::main(flavor = "current_thread")] 77 | async fn main() -> anyhow::Result<()> { 78 | let port = 8000; 79 | 80 | let app = std::rc::Rc::new( 81 | picoserve::Router::new() 82 | .route( 83 | "/", 84 | get(|| async { (("X-Encrypted", "false"), "Hello World") }), 85 | ) 86 | .route( 87 | "/encrypted", 88 | get(|| async { 89 | EncryptedMessage { 90 | message: "Hello World", 91 | } 92 | }), 93 | ) 94 | .with_state(AppState { key: 13 }), 95 | ); 96 | 97 | let config = picoserve::Config::new(picoserve::Timeouts { 98 | start_read_request: Some(Duration::from_secs(5)), 99 | persistent_start_read_request: Some(Duration::from_secs(1)), 100 | read_request: Some(Duration::from_secs(1)), 101 | write: Some(Duration::from_secs(1)), 102 | }) 103 | .keep_connection_alive(); 104 | 105 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 106 | 107 | println!("http://localhost:{port}/"); 108 | 109 | tokio::task::LocalSet::new() 110 | .run_until(async { 111 | loop { 112 | let (stream, remote_address) = socket.accept().await?; 113 | 114 | println!("Connection from {remote_address}"); 115 | 116 | let app = app.clone(); 117 | let config = config.clone(); 118 | 119 | tokio::task::spawn_local(async move { 120 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 121 | .serve(stream) 122 | .await 123 | { 124 | Ok(picoserve::DisconnectionInfo { 125 | handled_requests_count, 126 | .. 127 | }) => { 128 | println!( 129 | "{handled_requests_count} requests handled from {remote_address}" 130 | ) 131 | } 132 | Err(err) => println!("{err:?}"), 133 | } 134 | }); 135 | } 136 | }) 137 | .await 138 | } 139 | -------------------------------------------------------------------------------- /examples/layers/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use picoserve::{ 4 | io::Read, 5 | request::Path, 6 | response::ResponseWriter, 7 | routing::{get, parse_path_segment}, 8 | }; 9 | 10 | struct TimedResponseWriter<'r, W> { 11 | path: Path<'r>, 12 | start_time: Instant, 13 | response_writer: W, 14 | } 15 | 16 | impl ResponseWriter for TimedResponseWriter<'_, W> { 17 | type Error = W::Error; 18 | 19 | async fn write_response< 20 | R: Read, 21 | H: picoserve::response::HeadersIter, 22 | B: picoserve::response::Body, 23 | >( 24 | self, 25 | connection: picoserve::response::Connection<'_, R>, 26 | response: picoserve::response::Response, 27 | ) -> Result { 28 | let status_code = response.status_code(); 29 | 30 | let result = self 31 | .response_writer 32 | .write_response(connection, response) 33 | .await; 34 | 35 | println!( 36 | "Path: {}; Status Code: {}; Response Time: {}ms", 37 | self.path, 38 | status_code, 39 | self.start_time.elapsed().as_secs_f32() * 1000.0 40 | ); 41 | 42 | result 43 | } 44 | } 45 | 46 | struct TimeLayer; 47 | 48 | impl picoserve::routing::Layer for TimeLayer { 49 | type NextState = State; 50 | type NextPathParameters = PathParameters; 51 | 52 | async fn call_layer< 53 | 'a, 54 | R: Read + 'a, 55 | NextLayer: picoserve::routing::Next<'a, R, Self::NextState, Self::NextPathParameters>, 56 | W: ResponseWriter, 57 | >( 58 | &self, 59 | next: NextLayer, 60 | state: &State, 61 | path_parameters: PathParameters, 62 | request_parts: picoserve::request::RequestParts<'_>, 63 | response_writer: W, 64 | ) -> Result { 65 | let path = request_parts.path(); 66 | 67 | next.run( 68 | state, 69 | path_parameters, 70 | TimedResponseWriter { 71 | path, 72 | start_time: Instant::now(), 73 | response_writer, 74 | }, 75 | ) 76 | .await 77 | } 78 | } 79 | 80 | #[tokio::main(flavor = "current_thread")] 81 | async fn main() -> anyhow::Result<()> { 82 | let port = 8000; 83 | 84 | let app = std::rc::Rc::new( 85 | picoserve::Router::new() 86 | .route("/", get(|| async { "Hello World" })) 87 | .route( 88 | ("/delay", parse_path_segment()), 89 | get(|millis| async move { 90 | tokio::time::sleep(std::time::Duration::from_millis(millis)).await; 91 | format!("Waited {millis}ms") 92 | }), 93 | ) 94 | .layer(TimeLayer), 95 | ); 96 | 97 | let config = picoserve::Config::new(picoserve::Timeouts { 98 | start_read_request: Some(Duration::from_secs(5)), 99 | persistent_start_read_request: Some(Duration::from_secs(1)), 100 | read_request: Some(Duration::from_secs(1)), 101 | write: Some(Duration::from_secs(1)), 102 | }) 103 | .keep_connection_alive(); 104 | 105 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 106 | 107 | println!("http://localhost:{port}/"); 108 | 109 | tokio::task::LocalSet::new() 110 | .run_until(async { 111 | loop { 112 | let (stream, remote_address) = socket.accept().await?; 113 | 114 | println!("Connection from {remote_address}"); 115 | 116 | let app = app.clone(); 117 | let config = config.clone(); 118 | 119 | tokio::task::spawn_local(async move { 120 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 121 | .serve(stream) 122 | .await 123 | { 124 | Ok(picoserve::DisconnectionInfo { 125 | handled_requests_count, 126 | .. 127 | }) => { 128 | println!( 129 | "{handled_requests_count} requests handled from {remote_address}" 130 | ) 131 | } 132 | Err(err) => println!("{err:?}"), 133 | } 134 | }); 135 | } 136 | }) 137 | .await 138 | } 139 | -------------------------------------------------------------------------------- /picoserve/src/response/chunked.rs: -------------------------------------------------------------------------------- 1 | //! A Response broken up into chunks, allowing for a response of a size not known ahead of time. 2 | 3 | /// A marker showing that all of the chunks have been written. 4 | pub struct ChunksWritten(()); 5 | 6 | /// Writing chunks to a [ChunkWriter] will send them to the client and flush the stream 7 | pub struct ChunkWriter { 8 | writer: W, 9 | } 10 | 11 | impl ChunkWriter { 12 | /// Write a chunk to the client. 13 | pub async fn write_chunk(&mut self, chunk: &[u8]) -> Result<(), W::Error> { 14 | use crate::io::WriteExt; 15 | 16 | if chunk.is_empty() { 17 | return Ok(()); 18 | } 19 | 20 | write!(&mut self.writer, "{:x}\r\n", chunk.len()).await?; 21 | 22 | self.writer.write_all(chunk).await?; 23 | self.writer.write_all(b"\r\n").await?; 24 | 25 | Ok(()) 26 | } 27 | 28 | /// Finish writing chunks and flush the buffer. 29 | pub async fn finalize(mut self) -> Result { 30 | self.writer.write_all(b"0\r\n\r\n").await?; 31 | self.writer.flush().await?; 32 | 33 | Ok(ChunksWritten(())) 34 | } 35 | 36 | /// Write formatted text as a single chunk. This is typically called using the `write!` macro. 37 | pub async fn write_fmt(&mut self, args: core::fmt::Arguments<'_>) -> Result<(), W::Error> { 38 | use crate::io::WriteExt; 39 | use core::fmt::Write; 40 | 41 | let mut chunk_size = 0; 42 | 43 | if super::MeasureFormatSize(&mut chunk_size) 44 | .write_fmt(args) 45 | .is_err() 46 | { 47 | log_warn!("Skipping writing chunk due to Format Error"); 48 | 49 | return Ok(()); 50 | } 51 | 52 | if chunk_size == 0 { 53 | return Ok(()); 54 | } 55 | 56 | write!(&mut self.writer, "{chunk_size:x}\r\n{args}\r\n",).await?; 57 | 58 | Ok(()) 59 | } 60 | 61 | /// Flush the underlying connection. 62 | pub async fn flush(&mut self) -> Result<(), W::Error> { 63 | self.writer.flush().await 64 | } 65 | } 66 | 67 | /// A series of chunks forming the response body 68 | pub trait Chunks { 69 | /// The Content Type of the response. 70 | fn content_type(&self) -> &'static str; 71 | 72 | /// Write the chunks to the [ChunkWriter] then finalize it. 73 | async fn write_chunks( 74 | self, 75 | chunk_writer: ChunkWriter, 76 | ) -> Result; 77 | } 78 | 79 | /// A response with a Chunked body. Implements [super::IntoResponse], so can be returned by handlers. 80 | /// By default, it sends a status code of 200 (OK), to customise the response, call [into_response](Self::into_response), 81 | /// which converts it into [response](super::Response) which can have the status code changed or headers added. 82 | pub struct ChunkedResponse { 83 | chunks: C, 84 | } 85 | 86 | impl ChunkedResponse { 87 | /// Create a response from [Chunks]. 88 | pub fn new(chunks: C) -> Self { 89 | Self { chunks } 90 | } 91 | 92 | /// Convert the response into a [Response](super::Response), which can then have its status code changed or headers added. 93 | pub fn into_response(self) -> super::Response { 94 | struct Body(C); 95 | 96 | impl super::Body for Body { 97 | async fn write_response_body< 98 | R: crate::io::Read, 99 | W: crate::io::Write, 100 | >( 101 | self, 102 | _connection: super::Connection<'_, R>, 103 | writer: W, 104 | ) -> Result<(), W::Error> { 105 | self.0 106 | .write_chunks(ChunkWriter { writer }) 107 | .await 108 | .map(|ChunksWritten(())| ()) 109 | } 110 | } 111 | 112 | let content_type = self.chunks.content_type(); 113 | 114 | super::Response { 115 | status_code: super::StatusCode::OK, 116 | headers: [ 117 | ("Content-Type", content_type), 118 | ("Transfer-Encoding", "chunked"), 119 | ], 120 | body: Body(self.chunks), 121 | } 122 | } 123 | } 124 | 125 | impl super::IntoResponse for ChunkedResponse { 126 | async fn write_to>( 127 | self, 128 | connection: super::Connection<'_, R>, 129 | response_writer: W, 130 | ) -> Result { 131 | response_writer 132 | .write_response(connection, self.into_response()) 133 | .await 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /picoserve/src/response/custom.rs: -------------------------------------------------------------------------------- 1 | //! Responses with a body that doesn't match a regular HTTP response. 2 | //! 3 | //! For example responses which send a partial response body, wait some amount of time, and send more data later on. 4 | //! 5 | //! Care should be taken when sending custom responses. 6 | //! For example, the Content-Length and Content-Type headers are not automatically sent, 7 | //! and thus must be manually included if need be. 8 | //! 9 | //! The only header that is automatically send is `Connection: close` 10 | 11 | use core::marker::PhantomData; 12 | 13 | use super::HeadersIter; 14 | 15 | /// The headers that are sent with every custom response. 16 | pub struct CustomHeaders; 17 | 18 | impl HeadersIter for CustomHeaders { 19 | async fn for_each_header( 20 | self, 21 | mut f: F, 22 | ) -> Result { 23 | f.call("Connection", "close").await?; 24 | f.finalize().await 25 | } 26 | } 27 | 28 | /// The body of a custom response. 29 | /// The writer is automatically flushed after `write_response_body` is called, but intermediate content must be manually flushed. 30 | pub trait CustomBody { 31 | async fn write_response_body(self, writer: W) -> Result<(), W::Error>; 32 | } 33 | 34 | /// A custom response. 35 | pub struct CustomResponse { 36 | status_code: super::StatusCode, 37 | headers: H, 38 | body: B, 39 | } 40 | 41 | impl CustomResponse { 42 | pub fn build(status_code: super::StatusCode) -> CustomResponseBuilder { 43 | CustomResponseBuilder { 44 | status_code, 45 | headers: CustomHeaders, 46 | _body: PhantomData, 47 | } 48 | } 49 | } 50 | 51 | impl super::IntoResponse for CustomResponse { 52 | async fn write_to>( 53 | self, 54 | connection: super::Connection<'_, R>, 55 | response_writer: W, 56 | ) -> Result { 57 | struct Body { 58 | body: B, 59 | } 60 | 61 | impl super::Body for Body { 62 | async fn write_response_body< 63 | R: crate::io::Read, 64 | W: crate::io::Write, 65 | >( 66 | self, 67 | connection: crate::response::Connection<'_, R>, 68 | mut writer: W, 69 | ) -> Result<(), W::Error> { 70 | connection 71 | .run_until_disconnection((), async { 72 | self.body.write_response_body(&mut writer).await?; 73 | writer.flush().await 74 | }) 75 | .await 76 | } 77 | } 78 | 79 | let Self { 80 | status_code, 81 | headers, 82 | body, 83 | } = self; 84 | 85 | response_writer 86 | .write_response( 87 | connection, 88 | super::Response { 89 | status_code, 90 | headers, 91 | body: Body { body }, 92 | }, 93 | ) 94 | .await 95 | } 96 | } 97 | 98 | /// Build a custom response. 99 | pub struct CustomResponseBuilder { 100 | status_code: super::StatusCode, 101 | headers: H, 102 | _body: PhantomData, 103 | } 104 | 105 | impl CustomResponseBuilder { 106 | /// Add a header to the response. 107 | pub fn with_header( 108 | self, 109 | name: &'static str, 110 | value: Value, 111 | ) -> CustomResponseBuilder { 112 | self.with_headers((name, value)) 113 | } 114 | 115 | /// Add a list of headers to the response. 116 | pub fn with_headers( 117 | self, 118 | headers: HS, 119 | ) -> CustomResponseBuilder { 120 | let Self { 121 | status_code, 122 | headers: current_headers, 123 | _body, 124 | } = self; 125 | 126 | CustomResponseBuilder { 127 | status_code, 128 | headers: super::HeadersChain(current_headers, headers), 129 | _body, 130 | } 131 | } 132 | 133 | /// Add the body to the response and finish building. 134 | pub fn with_body(self, body: B) -> CustomResponse { 135 | let Self { 136 | status_code, 137 | headers, 138 | _body, 139 | } = self; 140 | 141 | CustomResponse { 142 | status_code, 143 | headers, 144 | body, 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /examples/server_sent_events/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{ 4 | response::{self, ErrorWithStatusCode, StatusCode}, 5 | routing::{get, get_service, post}, 6 | }; 7 | 8 | #[derive(Debug, thiserror::Error, ErrorWithStatusCode)] 9 | #[status_code(BAD_REQUEST)] 10 | enum NewMessageRejection { 11 | #[error("Read Error")] 12 | ReadError, 13 | #[error("Body is not UTF-8: {0}")] 14 | NotUtf8(std::str::Utf8Error), 15 | } 16 | 17 | struct NewMessage(String); 18 | 19 | impl<'r, State> picoserve::extract::FromRequest<'r, State> for NewMessage { 20 | type Rejection = NewMessageRejection; 21 | 22 | async fn from_request( 23 | _state: &'r State, 24 | _request_parts: picoserve::request::RequestParts<'r>, 25 | request_body: picoserve::request::RequestBody<'r, R>, 26 | ) -> Result { 27 | core::str::from_utf8( 28 | request_body 29 | .read_all() 30 | .await 31 | .map_err(|_err| NewMessageRejection::ReadError)?, 32 | ) 33 | .map(|message| NewMessage(message.into())) 34 | .map_err(NewMessageRejection::NotUtf8) 35 | } 36 | } 37 | 38 | struct Events(tokio::sync::watch::Receiver); 39 | 40 | impl response::sse::EventSource for Events { 41 | async fn write_events( 42 | mut self, 43 | mut writer: response::sse::EventWriter<'_, W>, 44 | ) -> Result<(), W::Error> { 45 | loop { 46 | match tokio::time::timeout(std::time::Duration::from_secs(15), self.0.changed()).await { 47 | Ok(Ok(())) => { 48 | writer 49 | .write_event("message_changed", self.0.borrow_and_update().as_str()) 50 | .await? 51 | } 52 | Ok(Err(_)) => return Ok(()), 53 | Err(_) => writer.write_keepalive().await?, 54 | } 55 | } 56 | } 57 | } 58 | 59 | #[tokio::main(flavor = "current_thread")] 60 | async fn main() -> anyhow::Result<()> { 61 | let port = 8000; 62 | 63 | let (messages_tx, messages_rx) = tokio::sync::watch::channel(String::new()); 64 | 65 | let app = std::rc::Rc::new( 66 | picoserve::Router::new() 67 | .route( 68 | "/", 69 | get_service(response::File::html(include_str!("index.html"))), 70 | ) 71 | .route( 72 | "/index.css", 73 | get_service(response::File::css(include_str!("index.css"))), 74 | ) 75 | .route( 76 | "/index.js", 77 | get_service(response::File::javascript(include_str!("index.js"))), 78 | ) 79 | .route( 80 | "/set_message", 81 | post(async move |NewMessage(message)| { 82 | messages_tx 83 | .send(message) 84 | .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to send message")) 85 | }), 86 | ) 87 | .route( 88 | "/events", 89 | get(async move || response::EventStream(Events(messages_rx.clone()))), 90 | ), 91 | ); 92 | 93 | let config = picoserve::Config::new(picoserve::Timeouts { 94 | start_read_request: Some(Duration::from_secs(5)), 95 | persistent_start_read_request: Some(Duration::from_secs(1)), 96 | read_request: Some(Duration::from_secs(1)), 97 | write: Some(Duration::from_secs(1)), 98 | }) 99 | .keep_connection_alive(); 100 | 101 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 102 | 103 | println!("http://localhost:{port}/"); 104 | 105 | tokio::task::LocalSet::new() 106 | .run_until(async { 107 | loop { 108 | let (stream, remote_address) = socket.accept().await?; 109 | 110 | println!("Connection from {remote_address}"); 111 | 112 | let app = app.clone(); 113 | let config = config.clone(); 114 | 115 | tokio::task::spawn_local(async move { 116 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 117 | .serve(stream) 118 | .await 119 | { 120 | Ok(picoserve::DisconnectionInfo { 121 | handled_requests_count, 122 | .. 123 | }) => { 124 | println!( 125 | "{handled_requests_count} requests handled from {remote_address}" 126 | ) 127 | } 128 | Err(err) => println!("{err:?}"), 129 | } 130 | }); 131 | } 132 | }) 133 | .await 134 | } 135 | -------------------------------------------------------------------------------- /examples/embassy/hello_world/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use cyw43_pio::PioSpi; 6 | use embassy_rp::{ 7 | gpio::{Level, Output}, 8 | peripherals::{DMA_CH0, PIO0}, 9 | pio::Pio, 10 | }; 11 | 12 | use embassy_time::Duration; 13 | use panic_persist as _; 14 | use picoserve::{make_static, routing::get, AppBuilder, AppRouter}; 15 | use rand::Rng; 16 | 17 | embassy_rp::bind_interrupts!(struct Irqs { 18 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 19 | USBCTRL_IRQ => embassy_rp::usb::InterruptHandler; 20 | }); 21 | 22 | #[embassy_executor::task] 23 | async fn logger_task(usb: embassy_rp::Peri<'static, embassy_rp::peripherals::USB>) { 24 | let driver = embassy_rp::usb::Driver::new(usb, Irqs); 25 | embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); 26 | } 27 | 28 | #[embassy_executor::task] 29 | async fn wifi_task( 30 | runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>, 31 | ) -> ! { 32 | runner.run().await 33 | } 34 | 35 | #[embassy_executor::task] 36 | async fn net_task(mut stack: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { 37 | stack.run().await 38 | } 39 | 40 | struct AppProps; 41 | 42 | impl AppBuilder for AppProps { 43 | type PathRouter = impl picoserve::routing::PathRouter; 44 | 45 | fn build_app(self) -> picoserve::Router { 46 | picoserve::Router::new().route("/", get(|| async move { "Hello World" })) 47 | } 48 | } 49 | 50 | const WEB_TASK_POOL_SIZE: usize = 8; 51 | 52 | #[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)] 53 | async fn web_task( 54 | task_id: usize, 55 | stack: embassy_net::Stack<'static>, 56 | app: &'static AppRouter, 57 | config: &'static picoserve::Config, 58 | ) -> ! { 59 | let port = 80; 60 | let mut tcp_rx_buffer = [0; 1024]; 61 | let mut tcp_tx_buffer = [0; 1024]; 62 | let mut http_buffer = [0; 2048]; 63 | 64 | picoserve::Server::new(app, config, &mut http_buffer) 65 | .listen_and_serve(task_id, stack, port, &mut tcp_rx_buffer, &mut tcp_tx_buffer) 66 | .await 67 | .into_never() 68 | } 69 | 70 | #[embassy_executor::main] 71 | async fn main(spawner: embassy_executor::Spawner) { 72 | let p = embassy_rp::init(Default::default()); 73 | 74 | spawner.must_spawn(logger_task(p.USB)); 75 | 76 | if let Some(panic_message) = panic_persist::get_panic_message_utf8() { 77 | loop { 78 | log::error!("{panic_message}"); 79 | embassy_time::Timer::after_secs(5).await; 80 | } 81 | } 82 | 83 | let fw = include_bytes!("../../cyw43-firmware/43439A0.bin"); 84 | let clm = include_bytes!("../../cyw43-firmware/43439A0_clm.bin"); 85 | 86 | let pwr = Output::new(p.PIN_23, Level::Low); 87 | let cs = Output::new(p.PIN_25, Level::High); 88 | let mut pio = Pio::new(p.PIO0, Irqs); 89 | let spi = cyw43_pio::PioSpi::new( 90 | &mut pio.common, 91 | pio.sm0, 92 | cyw43_pio::DEFAULT_CLOCK_DIVIDER, 93 | pio.irq0, 94 | cs, 95 | p.PIN_24, 96 | p.PIN_29, 97 | p.DMA_CH0, 98 | ); 99 | 100 | let state = make_static!(cyw43::State, cyw43::State::new()); 101 | let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; 102 | spawner.must_spawn(wifi_task(runner)); 103 | 104 | control.init(clm).await; 105 | 106 | let (stack, runner) = embassy_net::new( 107 | net_device, 108 | embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { 109 | address: embassy_net::Ipv4Cidr::new(core::net::Ipv4Addr::new(192, 168, 0, 1), 24), 110 | gateway: None, 111 | dns_servers: Default::default(), 112 | }), 113 | make_static!( 114 | embassy_net::StackResources, 115 | embassy_net::StackResources::new() 116 | ), 117 | embassy_rp::clocks::RoscRng.gen(), 118 | ); 119 | 120 | spawner.must_spawn(net_task(runner)); 121 | 122 | control 123 | .start_ap_wpa2( 124 | example_secrets::WIFI_SSID, 125 | example_secrets::WIFI_PASSWORD, 126 | 8, 127 | ) 128 | .await; 129 | 130 | let app = make_static!(AppRouter, AppProps.build_app()); 131 | 132 | let config = make_static!( 133 | picoserve::Config, 134 | picoserve::Config::new(picoserve::Timeouts { 135 | start_read_request: Some(Duration::from_secs(5)), 136 | persistent_start_read_request: Some(Duration::from_secs(1)), 137 | read_request: Some(Duration::from_secs(1)), 138 | write: Some(Duration::from_secs(1)), 139 | }) 140 | .keep_connection_alive() 141 | ); 142 | 143 | for task_id in 0..WEB_TASK_POOL_SIZE { 144 | spawner.must_spawn(web_task(task_id, stack, app, config)); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /examples/embassy/app_with_props/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use cyw43_pio::PioSpi; 6 | use embassy_rp::{ 7 | gpio::{Level, Output}, 8 | peripherals::{DMA_CH0, PIO0}, 9 | pio::Pio, 10 | }; 11 | 12 | use embassy_time::Duration; 13 | use panic_persist as _; 14 | use picoserve::{make_static, routing::get, AppBuilder, AppRouter}; 15 | use rand::Rng; 16 | 17 | embassy_rp::bind_interrupts!(struct Irqs { 18 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 19 | USBCTRL_IRQ => embassy_rp::usb::InterruptHandler; 20 | }); 21 | 22 | #[embassy_executor::task] 23 | async fn logger_task(usb: embassy_rp::Peri<'static, embassy_rp::peripherals::USB>) { 24 | let driver = embassy_rp::usb::Driver::new(usb, Irqs); 25 | embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); 26 | } 27 | 28 | #[embassy_executor::task] 29 | async fn wifi_task( 30 | runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>, 31 | ) -> ! { 32 | runner.run().await 33 | } 34 | 35 | #[embassy_executor::task] 36 | async fn net_task(mut stack: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { 37 | stack.run().await 38 | } 39 | 40 | struct AppProps { 41 | message: &'static str, 42 | } 43 | 44 | impl AppBuilder for AppProps { 45 | type PathRouter = impl picoserve::routing::PathRouter; 46 | 47 | fn build_app(self) -> picoserve::Router { 48 | let Self { message } = self; 49 | 50 | picoserve::Router::new().route("/", get(move || async move { message })) 51 | } 52 | } 53 | 54 | const WEB_TASK_POOL_SIZE: usize = 8; 55 | 56 | #[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)] 57 | async fn web_task( 58 | task_id: usize, 59 | stack: embassy_net::Stack<'static>, 60 | app: &'static AppRouter, 61 | config: &'static picoserve::Config, 62 | ) -> ! { 63 | let port = 80; 64 | let mut tcp_rx_buffer = [0; 1024]; 65 | let mut tcp_tx_buffer = [0; 1024]; 66 | let mut http_buffer = [0; 2048]; 67 | 68 | picoserve::Server::new(app, config, &mut http_buffer) 69 | .listen_and_serve(task_id, stack, port, &mut tcp_rx_buffer, &mut tcp_tx_buffer) 70 | .await 71 | .into_never() 72 | } 73 | 74 | #[embassy_executor::main] 75 | async fn main(spawner: embassy_executor::Spawner) { 76 | let p = embassy_rp::init(Default::default()); 77 | 78 | spawner.must_spawn(logger_task(p.USB)); 79 | 80 | if let Some(panic_message) = panic_persist::get_panic_message_utf8() { 81 | loop { 82 | log::error!("{panic_message}"); 83 | embassy_time::Timer::after_secs(5).await; 84 | } 85 | } 86 | 87 | let fw = include_bytes!("../../cyw43-firmware/43439A0.bin"); 88 | let clm = include_bytes!("../../cyw43-firmware/43439A0_clm.bin"); 89 | 90 | let pwr = Output::new(p.PIN_23, Level::Low); 91 | let cs = Output::new(p.PIN_25, Level::High); 92 | let mut pio = Pio::new(p.PIO0, Irqs); 93 | let spi = cyw43_pio::PioSpi::new( 94 | &mut pio.common, 95 | pio.sm0, 96 | cyw43_pio::DEFAULT_CLOCK_DIVIDER, 97 | pio.irq0, 98 | cs, 99 | p.PIN_24, 100 | p.PIN_29, 101 | p.DMA_CH0, 102 | ); 103 | 104 | let state = make_static!(cyw43::State, cyw43::State::new()); 105 | let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; 106 | spawner.must_spawn(wifi_task(runner)); 107 | 108 | control.init(clm).await; 109 | 110 | let (stack, runner) = embassy_net::new( 111 | net_device, 112 | embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { 113 | address: embassy_net::Ipv4Cidr::new(core::net::Ipv4Addr::new(192, 168, 0, 1), 24), 114 | gateway: None, 115 | dns_servers: Default::default(), 116 | }), 117 | make_static!( 118 | embassy_net::StackResources, 119 | embassy_net::StackResources::new() 120 | ), 121 | embassy_rp::clocks::RoscRng.gen(), 122 | ); 123 | 124 | spawner.must_spawn(net_task(runner)); 125 | 126 | control 127 | .start_ap_wpa2( 128 | example_secrets::WIFI_SSID, 129 | example_secrets::WIFI_PASSWORD, 130 | 8, 131 | ) 132 | .await; 133 | 134 | let app = make_static!( 135 | AppRouter, 136 | AppProps { 137 | message: "Hello World" 138 | } 139 | .build_app() 140 | ); 141 | 142 | let config = make_static!( 143 | picoserve::Config, 144 | picoserve::Config::new(picoserve::Timeouts { 145 | start_read_request: Some(Duration::from_secs(5)), 146 | persistent_start_read_request: Some(Duration::from_secs(1)), 147 | read_request: Some(Duration::from_secs(1)), 148 | write: Some(Duration::from_secs(1)), 149 | }) 150 | .keep_connection_alive() 151 | ); 152 | 153 | for task_id in 0..WEB_TASK_POOL_SIZE { 154 | spawner.must_spawn(web_task(task_id, stack, app, config)); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /picoserve/src/response/status.rs: -------------------------------------------------------------------------------- 1 | //! HTTP status codes 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 4 | /// A HTTP response status code 5 | pub struct StatusCode(u16); 6 | 7 | impl StatusCode { 8 | /// Create a status code with the given numerical value. 9 | pub const fn new(status_code: u16) -> Self { 10 | Self(status_code) 11 | } 12 | 13 | /// Convert a status code into the underlying numerical value. 14 | pub const fn as_u16(self) -> u16 { 15 | self.0 16 | } 17 | 18 | /// Is the status code with the 1xx range 19 | pub const fn is_informational(&self) -> bool { 20 | 200 > self.0 && self.0 >= 100 21 | } 22 | 23 | /// Is the status code with the 2xx range 24 | pub const fn is_success(&self) -> bool { 25 | 300 > self.0 && self.0 >= 200 26 | } 27 | 28 | /// Is the status code with the 3xx range 29 | pub const fn is_redirection(&self) -> bool { 30 | 400 > self.0 && self.0 >= 300 31 | } 32 | 33 | /// Is the status code with the 4xx range 34 | pub const fn is_client_error(&self) -> bool { 35 | 500 > self.0 && self.0 >= 400 36 | } 37 | 38 | /// Is the status code with the 5xx range 39 | pub const fn is_server_error(&self) -> bool { 40 | 600 > self.0 && self.0 >= 500 41 | } 42 | } 43 | 44 | impl core::fmt::Display for StatusCode { 45 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 46 | self.0.fmt(f) 47 | } 48 | } 49 | 50 | impl super::IntoResponse for StatusCode { 51 | async fn write_to>( 52 | self, 53 | connection: super::Connection<'_, R>, 54 | response_writer: W, 55 | ) -> Result { 56 | super::Response::new(self, format_args!("Error {}", self.0)) 57 | .write_to(connection, response_writer) 58 | .await 59 | } 60 | } 61 | 62 | impl StatusCode { 63 | pub const CONTINUE: StatusCode = StatusCode(100); 64 | pub const SWITCHING_PROTOCOLS: StatusCode = StatusCode(101); 65 | pub const PROCESSING: StatusCode = StatusCode(102); 66 | pub const OK: StatusCode = StatusCode(200); 67 | pub const CREATED: StatusCode = StatusCode(201); 68 | pub const ACCEPTED: StatusCode = StatusCode(202); 69 | pub const NON_AUTHORITATIVE_INFORMATION: StatusCode = StatusCode(203); 70 | pub const NO_CONTENT: StatusCode = StatusCode(204); 71 | pub const RESET_CONTENT: StatusCode = StatusCode(205); 72 | pub const PARTIAL_CONTENT: StatusCode = StatusCode(206); 73 | pub const MULTI_STATUS: StatusCode = StatusCode(207); 74 | pub const ALREADY_REPORTED: StatusCode = StatusCode(208); 75 | pub const IM_USED: StatusCode = StatusCode(226); 76 | pub const MULTIPLE_CHOICES: StatusCode = StatusCode(300); 77 | pub const MOVED_PERMANENTLY: StatusCode = StatusCode(301); 78 | pub const FOUND: StatusCode = StatusCode(302); 79 | pub const SEE_OTHER: StatusCode = StatusCode(303); 80 | pub const NOT_MODIFIED: StatusCode = StatusCode(304); 81 | pub const USE_PROXY: StatusCode = StatusCode(305); 82 | pub const TEMPORARY_REDIRECT: StatusCode = StatusCode(307); 83 | pub const PERMANENT_REDIRECT: StatusCode = StatusCode(308); 84 | pub const BAD_REQUEST: StatusCode = StatusCode(400); 85 | pub const UNAUTHORIZED: StatusCode = StatusCode(401); 86 | pub const PAYMENT_REQUIRED: StatusCode = StatusCode(402); 87 | pub const FORBIDDEN: StatusCode = StatusCode(403); 88 | pub const NOT_FOUND: StatusCode = StatusCode(404); 89 | pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(405); 90 | pub const NOT_ACCEPTABLE: StatusCode = StatusCode(406); 91 | pub const PROXY_AUTHENTICATION_REQUIRED: StatusCode = StatusCode(407); 92 | pub const REQUEST_TIMEOUT: StatusCode = StatusCode(408); 93 | pub const CONFLICT: StatusCode = StatusCode(409); 94 | pub const GONE: StatusCode = StatusCode(410); 95 | pub const LENGTH_REQUIRED: StatusCode = StatusCode(411); 96 | pub const PRECONDITION_FAILED: StatusCode = StatusCode(412); 97 | pub const PAYLOAD_TOO_LARGE: StatusCode = StatusCode(413); 98 | pub const URI_TOO_LONG: StatusCode = StatusCode(414); 99 | pub const UNSUPPORTED_MEDIA_TYPE: StatusCode = StatusCode(415); 100 | pub const RANGE_NOT_SATISFIABLE: StatusCode = StatusCode(416); 101 | pub const EXPECTATION_FAILED: StatusCode = StatusCode(417); 102 | pub const IM_A_TEAPOT: StatusCode = StatusCode(418); 103 | pub const MISDIRECTED_REQUEST: StatusCode = StatusCode(421); 104 | pub const UNPROCESSABLE_ENTITY: StatusCode = StatusCode(422); 105 | pub const LOCKED: StatusCode = StatusCode(423); 106 | pub const FAILED_DEPENDENCY: StatusCode = StatusCode(424); 107 | pub const UPGRADE_REQUIRED: StatusCode = StatusCode(426); 108 | pub const PRECONDITION_REQUIRED: StatusCode = StatusCode(428); 109 | pub const TOO_MANY_REQUESTS: StatusCode = StatusCode(429); 110 | pub const REQUEST_HEADER_FIELDS_TOO_LARGE: StatusCode = StatusCode(431); 111 | pub const UNAVAILABLE_FOR_LEGAL_REASONS: StatusCode = StatusCode(451); 112 | pub const INTERNAL_SERVER_ERROR: StatusCode = StatusCode(500); 113 | pub const NOT_IMPLEMENTED: StatusCode = StatusCode(501); 114 | pub const BAD_GATEWAY: StatusCode = StatusCode(502); 115 | pub const SERVICE_UNAVAILABLE: StatusCode = StatusCode(503); 116 | pub const GATEWAY_TIMEOUT: StatusCode = StatusCode(504); 117 | pub const HTTP_VERSION_NOT_SUPPORTED: StatusCode = StatusCode(505); 118 | pub const VARIANT_ALSO_NEGOTIATES: StatusCode = StatusCode(506); 119 | pub const INSUFFICIENT_STORAGE: StatusCode = StatusCode(507); 120 | pub const LOOP_DETECTED: StatusCode = StatusCode(508); 121 | pub const NOT_EXTENDED: StatusCode = StatusCode(510); 122 | pub const NETWORK_AUTHENTICATION_REQUIRED: StatusCode = StatusCode(511); 123 | } 124 | -------------------------------------------------------------------------------- /examples/graceful_shutdown_server_sent_events/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{ 4 | response, 5 | routing::{get, get_service, post}, 6 | }; 7 | 8 | #[derive(Clone)] 9 | enum ServerState { 10 | Running, 11 | Shutdown, 12 | } 13 | 14 | #[tokio::main(flavor = "current_thread")] 15 | async fn main() -> anyhow::Result<()> { 16 | let port = 8000; 17 | 18 | let (update_server_state, mut server_state) = tokio::sync::watch::channel(ServerState::Running); 19 | 20 | let app = std::rc::Rc::new( 21 | picoserve::Router::new() 22 | .route( 23 | "/", 24 | get_service(response::File::html(include_str!("index.html"))), 25 | ) 26 | .route( 27 | "/index.css", 28 | get_service(response::File::css(include_str!("index.css"))), 29 | ) 30 | .route( 31 | "/index.js", 32 | get_service(response::File::javascript(include_str!("index.js"))), 33 | ) 34 | .route( 35 | "/counter", 36 | get(async || { 37 | struct Counter; 38 | 39 | impl picoserve::response::sse::EventSource for Counter { 40 | async fn write_events( 41 | self, 42 | mut writer: picoserve::response::sse::EventWriter<'_, W>, 43 | ) -> Result<(), W::Error> { 44 | let mut ticker = 45 | tokio::time::interval(std::time::Duration::from_millis(300)); 46 | 47 | for tick in 0_u32.. { 48 | ticker.tick().await; 49 | 50 | writer 51 | .write_event("tick", format_args!("Count: {tick}")) 52 | .await?; 53 | } 54 | 55 | Ok(()) 56 | } 57 | } 58 | 59 | picoserve::response::sse::EventStream(Counter) 60 | }), 61 | ) 62 | .route( 63 | "/shutdown", 64 | post(move || { 65 | let _ = update_server_state.send(ServerState::Shutdown); 66 | async { "Shutting Down\n" } 67 | }), 68 | ), 69 | ); 70 | 71 | // Larger timeouts to demonstrate rapid graceful shutdown 72 | let config = picoserve::Config::new(picoserve::Timeouts { 73 | start_read_request: Some(Duration::from_secs(10)), 74 | persistent_start_read_request: Some(Duration::from_secs(10)), 75 | read_request: Some(Duration::from_secs(1)), 76 | write: Some(Duration::from_secs(1)), 77 | }) 78 | .keep_connection_alive(); 79 | 80 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 81 | 82 | println!("http://localhost:{port}/"); 83 | 84 | let (wait_handle, waiter) = tokio::sync::oneshot::channel::(); 85 | let wait_handle = std::sync::Arc::new(wait_handle); 86 | 87 | tokio::task::LocalSet::new() 88 | .run_until(async { 89 | loop { 90 | let (stream, remote_address) = match futures_util::future::select( 91 | std::pin::pin!( 92 | server_state.wait_for(|state| matches!(state, ServerState::Shutdown)) 93 | ), 94 | std::pin::pin!(socket.accept()), 95 | ) 96 | .await 97 | { 98 | futures_util::future::Either::Left((_, _)) => break, 99 | futures_util::future::Either::Right((connection, _)) => connection?, 100 | }; 101 | 102 | println!("Connection from {remote_address}"); 103 | 104 | let app = app.clone(); 105 | let config = config.clone(); 106 | let mut server_state = server_state.clone(); 107 | let wait_handle = wait_handle.clone(); 108 | 109 | tokio::task::spawn_local(async move { 110 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 111 | .with_graceful_shutdown( 112 | server_state.wait_for(|state| matches!(state, ServerState::Shutdown)), 113 | Duration::from_secs(1), 114 | ) 115 | .serve(stream) 116 | .await 117 | { 118 | Ok(picoserve::DisconnectionInfo { 119 | handled_requests_count, 120 | shutdown_reason, 121 | }) => { 122 | println!( 123 | "{handled_requests_count} requests handled from {remote_address}" 124 | ); 125 | 126 | if shutdown_reason.is_some() { 127 | println!("Shutdown signal received"); 128 | } 129 | } 130 | Err(err) => println!("{err:?}"), 131 | } 132 | 133 | drop(wait_handle); 134 | }); 135 | } 136 | 137 | println!("Waiting for connections to close..."); 138 | drop(wait_handle); 139 | 140 | #[allow(clippy::single_match)] 141 | match waiter.await { 142 | Ok(never) => match never {}, 143 | Err(_) => (), 144 | } 145 | 146 | println!("All connections are closed"); 147 | 148 | Ok(()) 149 | }) 150 | .await 151 | } 152 | -------------------------------------------------------------------------------- /examples/path_parameters/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{ 4 | response::IntoResponse, 5 | routing::{get, get_service, parse_path_segment}, 6 | ResponseSent, 7 | }; 8 | 9 | struct PrefixOperation { 10 | operator: char, 11 | input: f32, 12 | output: f32, 13 | } 14 | 15 | impl IntoResponse for PrefixOperation { 16 | async fn write_to< 17 | R: picoserve::io::Read, 18 | W: picoserve::response::ResponseWriter, 19 | >( 20 | self, 21 | connection: picoserve::response::Connection<'_, R>, 22 | response_writer: W, 23 | ) -> Result { 24 | let Self { 25 | operator, 26 | input, 27 | output, 28 | } = self; 29 | 30 | format_args!("{operator}({input}) = {output}\n") 31 | .write_to(connection, response_writer) 32 | .await 33 | } 34 | } 35 | 36 | struct InfixOperation { 37 | input_0: f32, 38 | operator: char, 39 | input_1: f32, 40 | output: f32, 41 | } 42 | 43 | impl IntoResponse for InfixOperation { 44 | async fn write_to< 45 | R: picoserve::io::Read, 46 | W: picoserve::response::ResponseWriter, 47 | >( 48 | self, 49 | connection: picoserve::response::Connection<'_, R>, 50 | response_writer: W, 51 | ) -> Result { 52 | let Self { 53 | input_0, 54 | operator, 55 | input_1, 56 | output, 57 | } = self; 58 | 59 | format_args!("({input_0}) {operator} ({input_1}) = {output}\n") 60 | .write_to(connection, response_writer) 61 | .await 62 | } 63 | } 64 | 65 | struct MyService; 66 | 67 | impl picoserve::routing::RequestHandlerService for MyService { 68 | async fn call_request_handler_service< 69 | R: picoserve::io::Read, 70 | W: picoserve::response::ResponseWriter, 71 | >( 72 | &self, 73 | _state: &State, 74 | (n,): (f32,), 75 | request: picoserve::request::Request<'_, R>, 76 | response_writer: W, 77 | ) -> Result { 78 | picoserve::response::DebugValue(("n", n)) 79 | .write_to(request.body_connection.finalize().await?, response_writer) 80 | .await 81 | } 82 | } 83 | 84 | #[tokio::main(flavor = "current_thread")] 85 | async fn main() -> anyhow::Result<()> { 86 | let port = 8000; 87 | 88 | let app = std::rc::Rc::new( 89 | picoserve::Router::new() 90 | .route( 91 | ("/foo", parse_path_segment::()), 92 | get_service(MyService), 93 | ) 94 | .route( 95 | ("/neg", parse_path_segment::()), 96 | get(|input| async move { 97 | PrefixOperation { 98 | operator: '-', 99 | input, 100 | output: -input, 101 | } 102 | }), 103 | ) 104 | .route( 105 | ( 106 | "/add", 107 | parse_path_segment::(), 108 | parse_path_segment::(), 109 | ), 110 | get(|(input_0, input_1)| async move { 111 | InfixOperation { 112 | input_0, 113 | operator: '+', 114 | input_1, 115 | output: input_0 + input_1, 116 | } 117 | }), 118 | ) 119 | .route( 120 | ( 121 | "/sub", 122 | parse_path_segment::(), 123 | parse_path_segment::(), 124 | ), 125 | get(|(input_0, input_1)| async move { 126 | InfixOperation { 127 | input_0, 128 | operator: '-', 129 | input_1, 130 | output: input_0 - input_1, 131 | } 132 | }), 133 | ), 134 | ); 135 | 136 | let config = picoserve::Config::new(picoserve::Timeouts { 137 | start_read_request: Some(Duration::from_secs(5)), 138 | persistent_start_read_request: Some(Duration::from_secs(1)), 139 | read_request: Some(Duration::from_secs(1)), 140 | write: Some(Duration::from_secs(1)), 141 | }) 142 | .keep_connection_alive(); 143 | 144 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 145 | 146 | println!("http://localhost:{port}/"); 147 | 148 | tokio::task::LocalSet::new() 149 | .run_until(async { 150 | loop { 151 | let (stream, remote_address) = socket.accept().await?; 152 | 153 | println!("Connection from {remote_address}"); 154 | 155 | let app = app.clone(); 156 | let config = config.clone(); 157 | 158 | tokio::task::spawn_local(async move { 159 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 160 | .serve(stream) 161 | .await 162 | { 163 | Ok(picoserve::DisconnectionInfo { 164 | handled_requests_count, 165 | .. 166 | }) => { 167 | println!( 168 | "{handled_requests_count} requests handled from {remote_address}" 169 | ) 170 | } 171 | Err(err) => println!("{err:?}"), 172 | } 173 | }); 174 | } 175 | }) 176 | .await 177 | } 178 | -------------------------------------------------------------------------------- /picoserve/src/routing/layer.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | io::Read, 3 | request::{Path, Request, RequestParts}, 4 | ResponseSent, 5 | }; 6 | 7 | use super::{MethodHandler, PathRouter, ResponseWriter}; 8 | 9 | mod sealed { 10 | pub trait NextIsSealed {} 11 | } 12 | 13 | /// The remainer of a middleware stack, including the handler. 14 | pub trait Next<'a, R: Read + 'a, State, PathParameters>: sealed::NextIsSealed + Sized { 15 | /// Run the next layer, writing the final response to the [ResponseWriter] 16 | async fn run>( 17 | self, 18 | state: &State, 19 | path_parameters: PathParameters, 20 | response_writer: W, 21 | ) -> Result; 22 | 23 | fn into_request(self) -> Request<'a, R>; 24 | 25 | async fn into_connection( 26 | self, 27 | ) -> Result>, R::Error> { 28 | self.into_request().body_connection.finalize().await 29 | } 30 | } 31 | 32 | /// A middleware "layer", which can be used to inspect requests and transform responses. 33 | /// 34 | /// Layers can be used to: 35 | /// + inspect the request before it is passed to the inner handler 36 | /// + send a different state to the inner handler than the state passed to the layer 37 | /// + send different path parameters to the inner handler than the path parameters passed to the layer 38 | /// + send a response instead of passing the request to the inner handler 39 | /// + send a different response than the one returned by the inner handler 40 | /// + and more... 41 | /// 42 | /// To modify the response, create a struct that implements [ResponseWriter] and wraps `response_writer`, 43 | /// and pass an instance of that struct to `next` 44 | pub trait Layer { 45 | /// The state passed to the next layer 46 | type NextState; 47 | /// The parameters passed to the next layer 48 | type NextPathParameters; 49 | 50 | /// Call the current layer, passing inner layers 51 | async fn call_layer< 52 | 'a, 53 | R: Read + 'a, 54 | NextLayer: Next<'a, R, Self::NextState, Self::NextPathParameters>, 55 | W: ResponseWriter, 56 | >( 57 | &self, 58 | next: NextLayer, 59 | state: &State, 60 | path_parameters: PathParameters, 61 | request_parts: RequestParts<'_>, 62 | response_writer: W, 63 | ) -> Result; 64 | } 65 | 66 | struct NextMethodRouterLayer<'a, R: Read, N> { 67 | next: &'a N, 68 | request: Request<'a, R>, 69 | } 70 | 71 | impl sealed::NextIsSealed for NextMethodRouterLayer<'_, R, N> {} 72 | 73 | impl<'a, R: Read, State, PathParameters, N: MethodHandler> 74 | Next<'a, R, State, PathParameters> for NextMethodRouterLayer<'a, R, N> 75 | { 76 | async fn run>( 77 | self, 78 | state: &State, 79 | path_parameters: PathParameters, 80 | response_writer: W, 81 | ) -> Result { 82 | self.next 83 | .call_method_handler(state, path_parameters, self.request, response_writer) 84 | .await 85 | } 86 | 87 | fn into_request(self) -> Request<'a, R> { 88 | self.request 89 | } 90 | } 91 | 92 | pub(crate) struct MethodRouterLayer { 93 | pub(crate) layer: L, 94 | pub(crate) inner: I, 95 | } 96 | 97 | impl super::sealed::MethodHandlerIsSealed for MethodRouterLayer {} 98 | 99 | impl< 100 | L: Layer, 101 | I: MethodHandler, 102 | State, 103 | PathParameters, 104 | > MethodHandler for MethodRouterLayer 105 | { 106 | async fn call_method_handler>( 107 | &self, 108 | state: &State, 109 | path_parameters: PathParameters, 110 | request: Request<'_, R>, 111 | response_writer: W, 112 | ) -> Result { 113 | let request_parts = request.parts; 114 | 115 | self.layer 116 | .call_layer( 117 | NextMethodRouterLayer { 118 | next: &self.inner, 119 | request, 120 | }, 121 | state, 122 | path_parameters, 123 | request_parts, 124 | response_writer, 125 | ) 126 | .await 127 | } 128 | } 129 | 130 | struct NextPathRouterLayer<'a, R: Read, N> { 131 | next: &'a N, 132 | path: Path<'a>, 133 | request: Request<'a, R>, 134 | } 135 | 136 | impl sealed::NextIsSealed for NextPathRouterLayer<'_, R, N> {} 137 | 138 | impl<'a, R: Read, State, CurrentPathParameters, N: PathRouter> 139 | Next<'a, R, State, CurrentPathParameters> for NextPathRouterLayer<'a, R, N> 140 | { 141 | async fn run>( 142 | self, 143 | state: &State, 144 | current_path_parameters: CurrentPathParameters, 145 | response_writer: W, 146 | ) -> Result { 147 | self.next 148 | .call_path_router( 149 | state, 150 | current_path_parameters, 151 | self.path, 152 | self.request, 153 | response_writer, 154 | ) 155 | .await 156 | } 157 | 158 | fn into_request(self) -> Request<'a, R> { 159 | self.request 160 | } 161 | } 162 | 163 | pub(crate) struct PathRouterLayer { 164 | pub(crate) layer: L, 165 | pub(crate) inner: I, 166 | } 167 | 168 | impl super::sealed::PathRouterIsSealed for PathRouterLayer {} 169 | 170 | impl< 171 | L: Layer, 172 | I: PathRouter, 173 | State, 174 | CurrentPathParameters, 175 | > PathRouter for PathRouterLayer 176 | { 177 | async fn call_path_router>( 178 | &self, 179 | state: &State, 180 | current_path_parameters: CurrentPathParameters, 181 | path: Path<'_>, 182 | request: Request<'_, R>, 183 | response_writer: W, 184 | ) -> Result { 185 | let request_parts = request.parts; 186 | 187 | self.layer 188 | .call_layer( 189 | NextPathRouterLayer { 190 | next: &self.inner, 191 | path, 192 | request, 193 | }, 194 | state, 195 | current_path_parameters, 196 | request_parts, 197 | response_writer, 198 | ) 199 | .await 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /examples/embassy/graceful_shutdown_using_tasks/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use cyw43_pio::PioSpi; 6 | use embassy_rp::{ 7 | gpio::{Level, Output}, 8 | peripherals::{DMA_CH0, PIO0}, 9 | pio::Pio, 10 | }; 11 | 12 | use embassy_sync::watch::Watch; 13 | use embassy_time::Duration; 14 | use panic_persist as _; 15 | use picoserve::{make_static, routing::get, AppBuilder, AppRouter}; 16 | use rand::Rng; 17 | 18 | embassy_rp::bind_interrupts!(struct Irqs { 19 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 20 | USBCTRL_IRQ => embassy_rp::usb::InterruptHandler; 21 | }); 22 | 23 | #[embassy_executor::task] 24 | async fn logger_task(usb: embassy_rp::Peri<'static, embassy_rp::peripherals::USB>) { 25 | let driver = embassy_rp::usb::Driver::new(usb, Irqs); 26 | embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); 27 | } 28 | 29 | #[embassy_executor::task] 30 | async fn wifi_task( 31 | runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>, 32 | ) -> ! { 33 | runner.run().await 34 | } 35 | 36 | #[embassy_executor::task] 37 | async fn net_task(mut stack: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { 38 | stack.run().await 39 | } 40 | 41 | struct AppProps { 42 | spawner: embassy_executor::Spawner, 43 | } 44 | 45 | impl AppBuilder for AppProps { 46 | type PathRouter = impl picoserve::routing::PathRouter; 47 | 48 | fn build_app(self) -> picoserve::Router { 49 | let Self { spawner } = self; 50 | 51 | picoserve::Router::new() 52 | .route( 53 | "/", 54 | get(|| async { 55 | "Hello World\n\nNavigate to /suspend to temporarily shutdown the server." 56 | }), 57 | ) 58 | .route( 59 | "/suspend", 60 | get(move || async move { 61 | match spawner.spawn(suspend_server()) { 62 | Ok(()) => "Server suspended", 63 | Err(_) => "Failed to suspend server", 64 | } 65 | }), 66 | ) 67 | } 68 | } 69 | 70 | const WEB_TASK_POOL_SIZE: usize = 8; 71 | 72 | #[derive(Clone)] 73 | enum ServerState { 74 | Running, 75 | Shutdown, 76 | } 77 | 78 | impl ServerState { 79 | fn is_running(&self) -> bool { 80 | matches!(self, Self::Running) 81 | } 82 | 83 | fn is_shutdown(&self) -> bool { 84 | matches!(self, Self::Shutdown) 85 | } 86 | } 87 | 88 | static SERVER_STATE: Watch< 89 | embassy_sync::blocking_mutex::raw::ThreadModeRawMutex, 90 | ServerState, 91 | { 2 * WEB_TASK_POOL_SIZE }, 92 | > = Watch::new_with(ServerState::Running); 93 | 94 | #[embassy_executor::task] 95 | async fn suspend_server() { 96 | log::info!("Shutting down server"); 97 | SERVER_STATE.sender().send(ServerState::Shutdown); 98 | 99 | embassy_time::Timer::after_secs(5).await; 100 | 101 | log::info!("Resuming server"); 102 | SERVER_STATE.sender().send(ServerState::Running); 103 | } 104 | 105 | #[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)] 106 | async fn web_task( 107 | task_id: usize, 108 | stack: embassy_net::Stack<'static>, 109 | app: &'static AppRouter, 110 | config: &'static picoserve::Config, 111 | ) -> ! { 112 | let port = 80; 113 | let mut tcp_rx_buffer = [0; 1024]; 114 | let mut tcp_tx_buffer = [0; 1024]; 115 | let mut http_buffer = [0; 2048]; 116 | let shutdown_timeout = embassy_time::Duration::from_secs(3); 117 | 118 | let mut server_state = SERVER_STATE.receiver().unwrap(); 119 | 120 | loop { 121 | log::info!("{}: Waiting for startup", task_id); 122 | 123 | server_state.get_and(ServerState::is_running).await; 124 | 125 | picoserve::Server::new(app, config, &mut http_buffer) 126 | .with_graceful_shutdown( 127 | server_state.get_and(ServerState::is_shutdown), 128 | shutdown_timeout, 129 | ) 130 | .listen_and_serve(task_id, stack, port, &mut tcp_rx_buffer, &mut tcp_tx_buffer) 131 | .await; 132 | } 133 | } 134 | 135 | #[embassy_executor::main] 136 | async fn main(spawner: embassy_executor::Spawner) { 137 | let p = embassy_rp::init(Default::default()); 138 | 139 | spawner.must_spawn(logger_task(p.USB)); 140 | 141 | if let Some(panic_message) = panic_persist::get_panic_message_utf8() { 142 | loop { 143 | log::error!("{panic_message}"); 144 | embassy_time::Timer::after_secs(5).await; 145 | } 146 | } 147 | 148 | let fw = include_bytes!("../../cyw43-firmware/43439A0.bin"); 149 | let clm = include_bytes!("../../cyw43-firmware/43439A0_clm.bin"); 150 | 151 | let pwr = Output::new(p.PIN_23, Level::Low); 152 | let cs = Output::new(p.PIN_25, Level::High); 153 | let mut pio = Pio::new(p.PIO0, Irqs); 154 | let spi = cyw43_pio::PioSpi::new( 155 | &mut pio.common, 156 | pio.sm0, 157 | cyw43_pio::DEFAULT_CLOCK_DIVIDER, 158 | pio.irq0, 159 | cs, 160 | p.PIN_24, 161 | p.PIN_29, 162 | p.DMA_CH0, 163 | ); 164 | 165 | let state = make_static!(cyw43::State, cyw43::State::new()); 166 | let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; 167 | spawner.must_spawn(wifi_task(runner)); 168 | 169 | control.init(clm).await; 170 | 171 | let (stack, runner) = embassy_net::new( 172 | net_device, 173 | embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { 174 | address: embassy_net::Ipv4Cidr::new(core::net::Ipv4Addr::new(192, 168, 0, 1), 24), 175 | gateway: None, 176 | dns_servers: Default::default(), 177 | }), 178 | make_static!( 179 | embassy_net::StackResources, 180 | embassy_net::StackResources::new() 181 | ), 182 | embassy_rp::clocks::RoscRng.gen(), 183 | ); 184 | 185 | spawner.must_spawn(net_task(runner)); 186 | 187 | control 188 | .start_ap_wpa2( 189 | example_secrets::WIFI_SSID, 190 | example_secrets::WIFI_PASSWORD, 191 | 8, 192 | ) 193 | .await; 194 | 195 | let app = make_static!(AppRouter, AppProps { spawner }.build_app()); 196 | 197 | let config = make_static!( 198 | picoserve::Config, 199 | picoserve::Config::new(picoserve::Timeouts { 200 | start_read_request: Some(Duration::from_secs(5)), 201 | persistent_start_read_request: Some(Duration::from_secs(1)), 202 | read_request: Some(Duration::from_secs(1)), 203 | write: Some(Duration::from_secs(1)), 204 | }) 205 | .keep_connection_alive() 206 | ); 207 | 208 | for task_id in 0..WEB_TASK_POOL_SIZE { 209 | spawner.must_spawn(web_task(task_id, stack, app, config)); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /examples/embassy/set_pico_w_led/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use cyw43::Control; 6 | use cyw43_pio::PioSpi; 7 | use embassy_rp::{ 8 | gpio::{Level, Output}, 9 | peripherals::{DMA_CH0, PIO0}, 10 | pio::Pio, 11 | }; 12 | use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; 13 | use embassy_time::Duration; 14 | use panic_persist as _; 15 | use picoserve::{ 16 | make_static, 17 | response::DebugValue, 18 | routing::{get, get_service, parse_path_segment, PathRouter}, 19 | AppBuilder, AppRouter, 20 | }; 21 | use rand::Rng; 22 | 23 | use picoserve::extract::State; 24 | 25 | embassy_rp::bind_interrupts!(struct Irqs { 26 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 27 | USBCTRL_IRQ => embassy_rp::usb::InterruptHandler; 28 | }); 29 | 30 | #[embassy_executor::task] 31 | async fn logger_task(usb: embassy_rp::Peri<'static, embassy_rp::peripherals::USB>) { 32 | let driver = embassy_rp::usb::Driver::new(usb, Irqs); 33 | embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); 34 | } 35 | 36 | #[embassy_executor::task] 37 | async fn wifi_task( 38 | runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>, 39 | ) -> ! { 40 | runner.run().await 41 | } 42 | 43 | #[embassy_executor::task] 44 | async fn net_task(mut stack: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { 45 | stack.run().await 46 | } 47 | 48 | type ControlMutex = Mutex>; 49 | 50 | #[derive(Clone, Copy)] 51 | struct SharedControl(&'static ControlMutex); 52 | 53 | struct AppState { 54 | shared_control: SharedControl, 55 | } 56 | 57 | impl picoserve::extract::FromRef for SharedControl { 58 | fn from_ref(state: &AppState) -> Self { 59 | state.shared_control 60 | } 61 | } 62 | 63 | struct AppProps { 64 | state: AppState, 65 | } 66 | 67 | impl AppBuilder for AppProps { 68 | type PathRouter = impl PathRouter; 69 | 70 | fn build_app(self) -> picoserve::Router { 71 | let Self { state } = self; 72 | 73 | picoserve::Router::new() 74 | .route( 75 | "/", 76 | get_service(picoserve::response::File::html(include_str!("index.html"))), 77 | ) 78 | .route( 79 | "/index.css", 80 | get_service(picoserve::response::File::css(include_str!("index.css"))), 81 | ) 82 | .route( 83 | "/index.js", 84 | get_service(picoserve::response::File::javascript(include_str!( 85 | "index.js" 86 | ))), 87 | ) 88 | .route( 89 | ("/set_led", parse_path_segment()), 90 | get( 91 | |led_is_on, State(SharedControl(control)): State| async move { 92 | log::info!("Setting led to {}", if led_is_on { "ON" } else { "OFF" }); 93 | control.lock().await.gpio_set(0, led_is_on).await; 94 | DebugValue(led_is_on) 95 | }, 96 | ), 97 | ) 98 | .with_state(state) 99 | } 100 | } 101 | 102 | const WEB_TASK_POOL_SIZE: usize = 8; 103 | 104 | #[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)] 105 | async fn web_task( 106 | task_id: usize, 107 | stack: embassy_net::Stack<'static>, 108 | app: &'static AppRouter, 109 | config: &'static picoserve::Config, 110 | ) -> ! { 111 | let port = 80; 112 | let mut tcp_rx_buffer = [0; 1024]; 113 | let mut tcp_tx_buffer = [0; 1024]; 114 | let mut http_buffer = [0; 2048]; 115 | 116 | picoserve::Server::new(app, config, &mut http_buffer) 117 | .listen_and_serve(task_id, stack, port, &mut tcp_rx_buffer, &mut tcp_tx_buffer) 118 | .await 119 | .into_never() 120 | } 121 | 122 | #[embassy_executor::main] 123 | async fn main(spawner: embassy_executor::Spawner) { 124 | let p = embassy_rp::init(Default::default()); 125 | 126 | spawner.must_spawn(logger_task(p.USB)); 127 | 128 | if let Some(panic_message) = panic_persist::get_panic_message_utf8() { 129 | loop { 130 | log::error!("{panic_message}"); 131 | embassy_time::Timer::after_secs(5).await; 132 | } 133 | } 134 | 135 | let fw = include_bytes!("../../cyw43-firmware/43439A0.bin"); 136 | let clm = include_bytes!("../../cyw43-firmware/43439A0_clm.bin"); 137 | 138 | let pwr = Output::new(p.PIN_23, Level::Low); 139 | let cs = Output::new(p.PIN_25, Level::High); 140 | let mut pio = Pio::new(p.PIO0, Irqs); 141 | let spi = cyw43_pio::PioSpi::new( 142 | &mut pio.common, 143 | pio.sm0, 144 | cyw43_pio::DEFAULT_CLOCK_DIVIDER, 145 | pio.irq0, 146 | cs, 147 | p.PIN_24, 148 | p.PIN_29, 149 | p.DMA_CH0, 150 | ); 151 | 152 | let state = make_static!(cyw43::State, cyw43::State::new()); 153 | let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; 154 | spawner.must_spawn(wifi_task(runner)); 155 | 156 | control.init(clm).await; 157 | 158 | let (stack, runner) = embassy_net::new( 159 | net_device, 160 | embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { 161 | address: embassy_net::Ipv4Cidr::new(core::net::Ipv4Addr::new(192, 168, 0, 1), 24), 162 | gateway: None, 163 | dns_servers: Default::default(), 164 | }), 165 | make_static!( 166 | embassy_net::StackResources::, 167 | embassy_net::StackResources::new() 168 | ), 169 | embassy_rp::clocks::RoscRng.gen(), 170 | ); 171 | 172 | spawner.must_spawn(net_task(runner)); 173 | 174 | control 175 | .start_ap_wpa2( 176 | example_secrets::WIFI_SSID, 177 | example_secrets::WIFI_PASSWORD, 178 | 8, 179 | ) 180 | .await; 181 | 182 | let shared_control = SharedControl( 183 | make_static!(Mutex>, Mutex::new(control)), 184 | ); 185 | 186 | let app = make_static!( 187 | AppRouter, 188 | AppProps { 189 | state: AppState { shared_control } 190 | } 191 | .build_app() 192 | ); 193 | 194 | let config = make_static!( 195 | picoserve::Config::, 196 | picoserve::Config::new(picoserve::Timeouts { 197 | start_read_request: Some(Duration::from_secs(5)), 198 | persistent_start_read_request: Some(Duration::from_secs(1)), 199 | read_request: Some(Duration::from_secs(1)), 200 | write: Some(Duration::from_secs(1)), 201 | }) 202 | .keep_connection_alive() 203 | ); 204 | 205 | for task_id in 0..WEB_TASK_POOL_SIZE { 206 | spawner.must_spawn(web_task(task_id, stack, app, config)); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /examples/embassy/web_sockets/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | #![feature(impl_trait_in_assoc_type)] 4 | 5 | use cyw43_pio::PioSpi; 6 | use embassy_rp::{ 7 | gpio::{Level, Output}, 8 | peripherals::{DMA_CH0, PIO0}, 9 | pio::Pio, 10 | }; 11 | use embassy_time::Duration; 12 | use panic_persist as _; 13 | use picoserve::{ 14 | make_static, 15 | response::ws, 16 | routing::{get, get_service}, 17 | AppBuilder, AppRouter, 18 | }; 19 | use rand::Rng; 20 | 21 | embassy_rp::bind_interrupts!(struct Irqs { 22 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 23 | USBCTRL_IRQ => embassy_rp::usb::InterruptHandler; 24 | }); 25 | 26 | #[embassy_executor::task] 27 | async fn logger_task(usb: embassy_rp::Peri<'static, embassy_rp::peripherals::USB>) { 28 | let driver = embassy_rp::usb::Driver::new(usb, Irqs); 29 | embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); 30 | } 31 | 32 | #[embassy_executor::task] 33 | async fn wifi_task( 34 | runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>, 35 | ) -> ! { 36 | runner.run().await 37 | } 38 | 39 | #[embassy_executor::task] 40 | async fn net_task(mut stack: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { 41 | stack.run().await 42 | } 43 | 44 | struct AppProps; 45 | 46 | impl AppBuilder for AppProps { 47 | type PathRouter = impl picoserve::routing::PathRouter; 48 | 49 | fn build_app(self) -> picoserve::Router { 50 | picoserve::Router::new() 51 | .route( 52 | "/", 53 | get_service(picoserve::response::File::html(include_str!("index.html"))), 54 | ) 55 | .route( 56 | "/index.css", 57 | get_service(picoserve::response::File::css(include_str!("index.css"))), 58 | ) 59 | .route( 60 | "/index.js", 61 | get_service(picoserve::response::File::javascript(include_str!( 62 | "index.js" 63 | ))), 64 | ) 65 | .route( 66 | "/ws", 67 | get(async |upgrade: picoserve::response::WebSocketUpgrade| { 68 | upgrade.on_upgrade(WebsocketEcho).with_protocol("echo") 69 | }), 70 | ) 71 | } 72 | } 73 | 74 | const WEB_TASK_POOL_SIZE: usize = 8; 75 | 76 | #[embassy_executor::task(pool_size = WEB_TASK_POOL_SIZE)] 77 | async fn web_task( 78 | task_id: usize, 79 | stack: embassy_net::Stack<'static>, 80 | app: &'static AppRouter, 81 | config: &'static picoserve::Config, 82 | ) -> ! { 83 | let port = 80; 84 | let mut tcp_rx_buffer = [0; 1024]; 85 | let mut tcp_tx_buffer = [0; 1024]; 86 | let mut http_buffer = [0; 2048]; 87 | 88 | picoserve::Server::new(app, config, &mut http_buffer) 89 | .listen_and_serve(task_id, stack, port, &mut tcp_rx_buffer, &mut tcp_tx_buffer) 90 | .await 91 | .into_never() 92 | } 93 | 94 | struct WebsocketEcho; 95 | 96 | impl ws::WebSocketCallback for WebsocketEcho { 97 | async fn run>( 98 | self, 99 | mut rx: ws::SocketRx, 100 | mut tx: ws::SocketTx, 101 | ) -> Result<(), W::Error> { 102 | let mut buffer = [0; 1024]; 103 | 104 | let close_reason = loop { 105 | match rx 106 | .next_message(&mut buffer, core::future::pending()) 107 | .await? 108 | .ignore_never_b() 109 | { 110 | Ok(ws::Message::Text(data)) => tx.send_text(data).await, 111 | Ok(ws::Message::Binary(data)) => tx.send_binary(data).await, 112 | Ok(ws::Message::Close(reason)) => { 113 | log::info!("Websocket close reason: {reason:?}"); 114 | break None; 115 | } 116 | Ok(ws::Message::Ping(data)) => tx.send_pong(data).await, 117 | Ok(ws::Message::Pong(_)) => continue, 118 | Err(error) => { 119 | log::error!("Websocket Error: {error:?}"); 120 | 121 | break Some((error.code(), "Websocket Error")); 122 | } 123 | }?; 124 | }; 125 | 126 | tx.close(close_reason).await 127 | } 128 | } 129 | 130 | #[embassy_executor::main] 131 | async fn main(spawner: embassy_executor::Spawner) { 132 | let p = embassy_rp::init(Default::default()); 133 | 134 | spawner.must_spawn(logger_task(p.USB)); 135 | 136 | if let Some(panic_message) = panic_persist::get_panic_message_utf8() { 137 | loop { 138 | log::error!("{panic_message}"); 139 | embassy_time::Timer::after_secs(5).await; 140 | } 141 | } 142 | 143 | let fw = include_bytes!("../../cyw43-firmware/43439A0.bin"); 144 | let clm = include_bytes!("../../cyw43-firmware/43439A0_clm.bin"); 145 | 146 | let pwr = Output::new(p.PIN_23, Level::Low); 147 | let cs = Output::new(p.PIN_25, Level::High); 148 | let mut pio = Pio::new(p.PIO0, Irqs); 149 | let spi = cyw43_pio::PioSpi::new( 150 | &mut pio.common, 151 | pio.sm0, 152 | cyw43_pio::DEFAULT_CLOCK_DIVIDER, 153 | pio.irq0, 154 | cs, 155 | p.PIN_24, 156 | p.PIN_29, 157 | p.DMA_CH0, 158 | ); 159 | 160 | let state = make_static!(cyw43::State, cyw43::State::new()); 161 | 162 | let (net_device, mut control, runner) = cyw43::new(state, pwr, spi, fw).await; 163 | 164 | spawner.must_spawn(wifi_task(runner)); 165 | 166 | control.init(clm).await; 167 | 168 | let (stack, runner) = embassy_net::new( 169 | net_device, 170 | embassy_net::Config::ipv4_static(embassy_net::StaticConfigV4 { 171 | address: embassy_net::Ipv4Cidr::new(core::net::Ipv4Addr::new(192, 168, 0, 1), 24), 172 | gateway: None, 173 | dns_servers: Default::default(), 174 | }), 175 | make_static!( 176 | embassy_net::StackResources::, 177 | embassy_net::StackResources::new() 178 | ), 179 | embassy_rp::clocks::RoscRng.gen(), 180 | ); 181 | 182 | spawner.must_spawn(net_task(runner)); 183 | 184 | control 185 | .start_ap_wpa2( 186 | example_secrets::WIFI_SSID, 187 | example_secrets::WIFI_PASSWORD, 188 | 8, 189 | ) 190 | .await; 191 | 192 | let app = make_static!(AppRouter, AppProps.build_app()); 193 | 194 | let config = make_static!( 195 | picoserve::Config::, 196 | picoserve::Config::new(picoserve::Timeouts { 197 | start_read_request: Some(Duration::from_secs(5)), 198 | persistent_start_read_request: Some(Duration::from_secs(1)), 199 | read_request: Some(Duration::from_secs(1)), 200 | write: Some(Duration::from_secs(1)), 201 | }) 202 | .keep_connection_alive() 203 | ); 204 | 205 | for task_id in 0..WEB_TASK_POOL_SIZE { 206 | spawner.must_spawn(web_task(task_id, stack, app, config)); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /examples/web_sockets/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use picoserve::{ 4 | response::ws, 5 | routing::{get, get_service}, 6 | }; 7 | 8 | #[derive(Clone)] 9 | struct AppState { 10 | messages_tx: tokio::sync::broadcast::Sender, 11 | } 12 | 13 | struct WebsocketHandler; 14 | 15 | impl ws::WebSocketCallbackWithState for WebsocketHandler { 16 | async fn run_with_state>( 17 | self, 18 | state: &AppState, 19 | mut rx: ws::SocketRx, 20 | mut tx: ws::SocketTx, 21 | ) -> Result<(), W::Error> { 22 | use picoserve::response::ws::Message; 23 | 24 | let messages_tx = &state.messages_tx; 25 | let mut messages_rx = state.messages_tx.subscribe(); 26 | 27 | let mut message_buffer = [0; 128]; 28 | 29 | let close_reason = loop { 30 | let message = match rx 31 | .next_message(&mut message_buffer, messages_rx.recv()) 32 | .await? 33 | { 34 | picoserve::futures::Either::First(Ok(message)) => message, 35 | picoserve::futures::Either::First(Err(error)) => { 36 | eprintln!("Websocket error: {error:?}"); 37 | break Some((error.code(), "Websocket Error")); 38 | } 39 | picoserve::futures::Either::Second(message_changed) => match message_changed { 40 | Ok(message) => { 41 | tx.send_display(format_args!("Message: {message}")).await?; 42 | continue; 43 | } 44 | Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { 45 | tx.send_display(format_args!("Missed {n} messages")).await?; 46 | continue; 47 | } 48 | Err(tokio::sync::broadcast::error::RecvError::Closed) => { 49 | break Some((1011, "Server has an error")); 50 | } 51 | }, 52 | }; 53 | 54 | println!("Message: {message:?}"); 55 | match message { 56 | Message::Text(new_message) => { 57 | let _ = messages_tx.send(new_message.into()); 58 | } 59 | Message::Binary(message) => { 60 | println!("Ignoring binary message: {message:?}") 61 | } 62 | ws::Message::Close(reason) => { 63 | eprintln!("Websocket close reason: {reason:?}"); 64 | break None; 65 | } 66 | Message::Ping(ping) => tx.send_pong(ping).await?, 67 | Message::Pong(_) => (), 68 | }; 69 | }; 70 | 71 | tx.close(close_reason).await 72 | } 73 | } 74 | 75 | #[tokio::main(flavor = "current_thread")] 76 | async fn main() -> anyhow::Result<()> { 77 | let port = 8000; 78 | 79 | let (messages_tx, mut messages_rx) = tokio::sync::broadcast::channel(16); 80 | 81 | tokio::spawn(async move { 82 | loop { 83 | match messages_rx.recv().await { 84 | Ok(message) => println!("message: {message:?}"), 85 | Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { 86 | println!("Lost {n} messages") 87 | } 88 | Err(tokio::sync::broadcast::error::RecvError::Closed) => break, 89 | } 90 | } 91 | }); 92 | 93 | let state = AppState { messages_tx }; 94 | 95 | let app = std::rc::Rc::new( 96 | picoserve::Router::new() 97 | .route( 98 | "/", 99 | get_service(picoserve::response::File::html(include_str!("index.html"))), 100 | ) 101 | .nest_service( 102 | "/static", 103 | const { 104 | picoserve::response::Directory { 105 | files: &[ 106 | ( 107 | "index.css", 108 | picoserve::response::File::css(include_str!("index.css")), 109 | ), 110 | ( 111 | "index.js", 112 | picoserve::response::File::css(include_str!("index.js")), 113 | ), 114 | ], 115 | ..picoserve::response::Directory::DEFAULT 116 | } 117 | }, 118 | ) 119 | .route( 120 | "/index.css", 121 | get_service(picoserve::response::File::css(include_str!("index.css"))), 122 | ) 123 | .route( 124 | "/index.js", 125 | get_service(picoserve::response::File::javascript(include_str!( 126 | "index.js" 127 | ))), 128 | ) 129 | .route( 130 | "/ws", 131 | get(async move |upgrade: ws::WebSocketUpgrade| { 132 | if let Some(protocols) = upgrade.protocols() { 133 | println!("Protocols:"); 134 | for protocol in protocols { 135 | println!("\t{protocol}"); 136 | } 137 | } 138 | 139 | upgrade 140 | .on_upgrade_using_state(WebsocketHandler) 141 | .with_protocol("messages") 142 | }), 143 | ) 144 | .with_state(state), 145 | ); 146 | 147 | let config = picoserve::Config::new(picoserve::Timeouts { 148 | start_read_request: Some(Duration::from_secs(5)), 149 | persistent_start_read_request: Some(Duration::from_secs(1)), 150 | read_request: Some(Duration::from_secs(1)), 151 | write: Some(Duration::from_secs(1)), 152 | }) 153 | .keep_connection_alive(); 154 | 155 | let socket = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, port)).await?; 156 | 157 | println!("http://localhost:{port}/"); 158 | 159 | tokio::task::LocalSet::new() 160 | .run_until(async { 161 | loop { 162 | let (stream, remote_address) = socket.accept().await?; 163 | 164 | println!("Connection from {remote_address}"); 165 | 166 | let app = app.clone(); 167 | let config = config.clone(); 168 | 169 | tokio::task::spawn_local(async move { 170 | match picoserve::Server::new(&app, &config, &mut [0; 2048]) 171 | .serve(stream) 172 | .await 173 | { 174 | Ok(picoserve::DisconnectionInfo { 175 | handled_requests_count, 176 | .. 177 | }) => { 178 | println!( 179 | "{handled_requests_count} requests handled from {remote_address}" 180 | ) 181 | } 182 | Err(err) => println!("{err:?}"), 183 | } 184 | }); 185 | } 186 | }) 187 | .await 188 | } 189 | --------------------------------------------------------------------------------