diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b79afc..4ea3ed4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,56 +1,81 @@ +name: CI + on: push: branches: - master pull_request: -name: CI - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true +defaults: + run: + shell: bash jobs: - test: - name: Build & Test + rustfmt: + name: rustfmt runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 - - uses: dsherret/rust-toolchain-file@v1 - - name: Build - uses: actions-rs/cargo@v1 + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 with: - command: build - args: --all --target thumbv7em-none-eabihf + profile: minimal + toolchain: nightly + override: true + components: rustfmt - - name: Test + - name: Run rustfmt uses: actions-rs/cargo@v1 with: - command: test - args: --lib - env: - DEFMT_LOG: off - - rustfmt: - name: rustfmt - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - uses: dsherret/rust-toolchain-file@v1 - - name: Rustfmt - run: cargo fmt -- --check + command: fmt + args: --all -- --check --verbose clippy: name: clippy runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 - - uses: dsherret/rust-toolchain-file@v1 + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + - name: Run clippy uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: -- ${{ env.CLIPPY_PARAMS }} + args: --features odin-w2xx,ppp + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: thumbv7m-none-eabi + override: true + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --all --target thumbv7m-none-eabi --features odin-w2xx,ppp + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: --lib --features odin-w2xx,ppp diff --git a/.gitignore b/.gitignore index a1c9e07..1d240a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ *.fifo target/ *.o -.vscode Cargo.lock \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..78f5c6d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8fc7548 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "editor.formatOnSave": true, + "[toml]": { + "editor.formatOnSave": false + }, + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.check.allTargets": false, + "rust-analyzer.linkedProjects": [], + "rust-analyzer.cargo.features": [ + "odin-w2xx", + // "internal-network-stack" + "ppp" + ], + "rust-analyzer.server.extraEnv": { + "WIFI_NETWORK": "foo", + "WIFI_PASSWORD": "foo", + } +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c73ac94..650b27a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,88 @@ +[package] +name = "ublox-short-range-rs" +version = "0.1.1" +authors = ["Mads Andresen "] +description = "Driver crate for u-blox short range devices, implementation follows 'UBX-14044127 - R40'" +readme = "../README.md" +keywords = ["ublox", "wifi", "shortrange", "bluetooth"] +categories = ["embedded", "no-std"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/BlackbirdHQ/ublox-short-range-rs" +edition = "2021" + +[lib] +name = "ublox_short_range" +doctest = false + +[dependencies] +atat = { version = "0.23", features = ["derive", "bytes"] } + +heapless = { version = "^0.8", features = ["serde"] } +no-std-net = { version = "0.6", features = ["serde"] } +serde = { version = "^1", default-features = false, features = ["derive"] } +# ublox-sockets = { version = "0.5", optional = true } +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", optional = true } +portable-atomic = "1.6" + +log = { version = "^0.4", default-features = false, optional = true } +defmt = { version = "^0.3", optional = true } + +embedded-hal = "1.0" +embassy-time = "0.3" +embassy-sync = "0.6" +embassy-futures = "0.1" + +embedded-nal-async = { version = "0.7" } +futures-util = { version = "0.3.29", default-features = false } + +embedded-io-async = "0.6" + +embassy-net-ppp = { version = "0.1", optional = true } +embassy-net = { version = "0.4", features = [ + "proto-ipv4", + "medium-ip", +], optional = true } + + +[features] +default = ["socket-tcp", "socket-udp"] + +internal-network-stack = ["dep:ublox-sockets", "edm"] +edm = ["ublox-sockets?/edm"] + +ipv6 = ["embassy-net?/proto-ipv6"] + +# PPP mode requires UDP sockets enabled, to be able to do AT commands over UDP port 23 +ppp = ["dep:embassy-net-ppp", "dep:embassy-net", "socket-udp"] + +socket-tcp = ["ublox-sockets?/socket-tcp", "embassy-net?/tcp"] +socket-udp = ["ublox-sockets?/socket-udp", "embassy-net?/udp"] + +defmt = [ + "dep:defmt", + "heapless/defmt-03", + "atat/defmt", + "ublox-sockets?/defmt", + "embassy-net-ppp?/defmt", + "embassy-net?/defmt", +] +log = ["dep:log", "ublox-sockets?/log", "atat/log"] + +# Supported Ublox modules +odin-w2xx = [] +nina-w1xx = [] +nina-b1xx = [] +anna-b1xx = [] +nina-b2xx = [] +nina-b3xx = [] + [workspace] -resolver = "2" -members = [ "ublox-short-range" ] +members = [] +default-members = ["."] +exclude = ["examples"] [patch.crates-io] -atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "c5caaf7" } +no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } +atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "a466836" } +# atat = { path = "../atat/atat" } \ No newline at end of file diff --git a/Design_diagram.drawio b/Design_diagram.drawio deleted file mode 100644 index 016b7ea..0000000 --- a/Design_diagram.drawio +++ /dev/null @@ -1 +0,0 @@ -7V1rc6O4Ev01qdp7q5JCvPmYODOzs5uZzeZxZ/d+SRGj2GwweIHE8f76lWyEAYmneRlITdUYWcZGp9U63S11nwmz1ccXV18vvzkGtM54zvg4E67PeB4InIz+wy3bfYsGwL5h4ZpG0OnQcG/+A4NGLmh9Mw3oxTr6jmP55jreOHdsG879WJvuus4m3u3FseLfutYXkGq4n+sW3frDNPxl0Apk7fDGz9BcLIOvVnll/8ZKJ52DJ/GWuuFsIk3CpzNh5jqOv3+1+phBCw8eGZcfX7c/rJtX+csvv3t/649Xvz58/9/5/mafy3wkfAQX2n7lW1/LH5wJf3z9/5r/+e6bv+KfvLdzwEv7m7/r1lswYo/PlvNxPkeffrN099z1gsf3t2RMXefNNiC+L3cmXG2Wpg/v1/ocv7tBUoTalv7KQlcAvXwxLWvmWI6Lrm3HRp2udMtc2OjSgi/oea7eoeubCLHLoNl38C0M3VvuvgMEF7e670PX3rXwnIhaC45LMH74a+BHRCqCcfoCnRX03S3qErwrAmH/kUDmeSIsm4MEySAYuWVEeCQi9XogtYvw3gdk0IsAHDZQ4Iv68dk1Hm/51/PLjfrX5/sn5VzUKJwuHy4fRgcNiAOjcBQwAmlrBxhy4wgwP+uusUFPjr/u2fNdfe6bjo2uLH0L3W4gawoPXpRiiAgSBQiQVRoQoInHA8JUaQqFx7VrvuNx5x49xvB7G3Nl6buRfHFs/z54h4+M7BwNGvpk2tjOl6Zl3Ohb5w0/nOfr81dydbV0XPMfdFudAIjedv1gmeTlWI97/MlADFzooT63BC8QNt3onh/0mTuWpa8983n3g3GXle4uTPvK8X1nRW4Uk65wDdtd+K7zGq6KoEEpUWIyIgKGjDAmLSIf6TISfNcd4gy6vbDg4cskiYuLpEorCeb38XL863QLKzXdh1d4DD1KMsPnrC6stFa/h66p48/dfVCiikbbLzrhPaRCTHtxs+tzLR5a7oIHxk0O+uyLtROIpWkY0N4JjK/7+l6msJSsHdP2dyMiXaF/aNxm3IV0JqEfNEPX4HCN/uHurj9zbKz2zJ0QQSSwG4iFNksWAxXHMYUwc57nC+E2Dm9ZMahNM6lsqoWaZpaJH3ZoeDeFpMR3jCRj0f9qL5CCRkqC+6bbyKagF5pTR7Pb2auo7WHOJnrpqvrhj4lVdMAqeGIUBSICRNpMY8qIPCRewRRWmRJWSkItcyedEduCRi5HV62Q1sG3I8rpAQvu9TmgFJhAKzCBoaws/Rlat45n7uwlAfH2Xd+EEiu/6mTO59r5g9qQAuIpTDcushV/+s/gVpqmEBTEjtcQ2jY90P1pDenDGqIIBZeQDBEZxhJCWyvTEpIynU9mCaHdDS7UjfGuIKUB7HwFIdG3CFbQWECyNqAhWToLx9atT4fWhKY99LlxdrMNT7a/oO9vA+Wvv/lOfFqjUXS3f+DPX3CcTBr+3DfIGmm4/gi+Yn+1jV7dooUODQFerPaNH6b/B7k9er2/mRRcHe6EL8iNyi0CnvPmzokLOMMfg5a9BcyyVYn7Eg90ppC40NJ98z0eeUxdIS5dV99GOgTif7jzLW44LB5AjUelQBCa/VywP1nq0vqL3HH9BRALQKIX+yesdVXiRUqFXSItku5Dm0hU8yQKaIkgEMeiNTKtJoFWyRLnhTiNAmJ/aRQjwH4HvbVjQ+wr/P0NvuEHw+vY22qATsOCC3E4rY+jUgwRa2wl5mkfy+PdbAyYVnUE14Q80/vfKvK0GY+GCmlbK4L+resYb/MJ/brRZ8YB2kRfoAMBFMZjN4zLwwo4lmXFwLUp01ikiaUHbeOnS3+2MkZrIFcAEnStngU6ODtfwvnrEzIHx+vqqICk0LWqFYUUJN0dg/ZGHPuoAKdcUMM2NzHpCFb+ronJfm/Bfo/vqmWb7yxzGlSKgpyS+S4WMN9n/NnlcPl+QYUUTu5e7sdjY5ttwQ8b1lrNuPLgt7mFjw1+vhG/E4DJkdOYDHS+pU+g46EUxmM35cvDyjblW4xyqzTPHPlOqQogMs34NienSht/hrlAjz3BeKwN3yqMtDfGdnzzZTsZ8VXwZBrxreJJu78DPEftXqsApVbR6q0NSokVyZCt/VPv/zexZ+UFH0bl5UXwBvn/jBc+LtFLvP9itjIo5Dv03IjcSDw3ifPQEuM8NFumuIzjt8Pw3BALMyLdD9s1enWJGu+CtWdw6qpWk04qLo/kaG/nOo026w+oz5zVSreNG2gv0K+boK8Verlrd55Eu2opjMduzZeHVStoBzZlzEu0Gah7T89bH3oDZJsdz2FQOPLT2CSmffLoOZGR+Kbi0eU5NIbDg705QDsPskj0vpoXx51DZP7PzXf4hGj7mD1zFSDtPHQi0UfD4McaWQve5NOpAmjncRCJFQep5gjAhhUF/eQJaMETwFV0BSiD9wTItCdgMgzSVEDtpn5ThoFME4uJKR4BZ9eLkEx7bqouQrtdPP1ZgwRlpGuQrCjFhEpTBr8G0RRrBN7ogtpKLn8Ov6BgNaetpvQYDcAqF8y+0hSnIJx54hS1wKkUTPTaXL6lbrJlVEhtcciwcRbLrsFLZ9nJNWK5uKFtXOKc+ejy2XIwy8BNn00r3XNcNINGZow1mkIjk492nUJDSxAUXojKWG5/SVUTMtlAigviDupGaLmY0PJFpfZCisltn4WWODzyhRb0QmhFYpqFQlguL0vp/jl5X0DS0xTvf/SkYOfqYWTKRrIC8e+6h1Paly7MPVlKCg4r+T8reZ6UM0VOydxj53WnieReXOliKsNjj9kTuJfnw9gg0h5GCr0R2Xg1wVo0fFmDjZeSu104HaMggxnF8vKd48R8Ycv+3kDNYWHMvHzVyVamIsxNskdSiOWSrdQaTXmLSlnepQpJHiVm8qJkf1GRE8Ia4UX0p2XhAs2D8C9xL6BcCEA7/CWCdvvhDW5Z/3IGOGl4c6aspV3zzBCKzgy1qO3c2swgZnPojQ1cQ2kzI9lfkkGJmSFzcftH0IT48+yHukHhp/ek0X6lgMZRRCBRrCujfleqPRISBFZtsPgkKyejxam+Ctg2YnQV51ncrIayaymg0OHC/44NFD6eu1yUaYOoKVDYyYlJVpqOXFcXgqpFloodKeIzV4tyjij80Yb5UkO5himlGhZEDJP9Jm6RwijKLhX092Q7l9J+V6Me17AKaldyK0WklqtRYBsSzlzPKQBiI2JcWvr4OHXgpWzpUxJEJdG/KemTu5U+WeWTWjObY/dWa+Zy6TCtQdeCKcVLYvJKtmCqmpDVvxmfOyPPbU7Bwsnr3rzXPRkOEriCDjUAhrTLii2xdJToq236JhK+fxBuKebBybrea63IyJ9eQVVGptewntbKMYa3n65jwDuvu8pI0nz9/X6HMdIrg4O7MSDbPHHEnrlTEvUGYO088xpgwNiqKaMqUVO6gCnTzg6mjkwgNkbEnd61CaQmbPOcbUpaIryW6N+MCUQ2JEf01A/zs3m2SwpqI7KM9cBkBXW/90goqvpCX99wrSDGybiN+WI+jZokhTP5dMwbmU5oY0N/47ivA4WyVvOmPOCdmzeMI5Bzx34xF0/m8JwXjcHYuXHDOEU0GTdHw9qmccOGlbZZv+/V8cQAe8AARUG9KLj/PLmBaHgUUKEpoOcNcBGplTEoxWWwLxRRoSnifKkjy9QaHNSNgdg57VPo/YUTXzga1qI0sCm6oNFTMzyx9tWHq4k0dEAa1CRpIHk58qSErPXDpQwaK//OpIXYc7p2htCYFmLlKYlnVYI2Lm6l713ZqXmViO7qj9IS+ZEqLUkARS0dQDYHldNbqiDFvlGWK/pU2tBbjMxKs1sspWyJHQkPDid+8QxwLfJgdmIGGsnH6wnJ8khWna01ns6ZXKAN4Kp17AEFIJ9NFMnR+HBz3yciIYgjIRLJrcNiUZUPso6MpdMISYqbP4LaX/MHAFYS7EllpSiA4pkau9ZZPL0UmSs8Fk9zDM0LAmckBQjqgrRNopiCKR2KCzB1HQcBq094nhRdJKqWxvMVbicsG6CIzRlxdOTxk7FCDZd+UH4Nvf7h6us1o6D65AJvnwRKRV3gypBc4GzZpUM2lzhcEwru4NVQ9pTuZVyc/ZOnkGoDsLaYui3leIlCodj71LjgAifziqWa4rQwEVU9x0vqTeTGrvtdNGuu3EwGk9I5qhJBE1nOPl4iC0f2VzOyvdW1QKk0uaoWzdtTsk/vPTvEP5pqXZR0ksQj+Yf4M5TrMFiYSrMwVkGfkZAvtfQq3XkNbpUmX9doFFMVzoRkmmOmaxqt0vGei4uLCcC6i2c3BiAgXPwEGHMFzhsmh20ljStbKLTT4sXJzURyzrFrVTyuv5KT8E88tj/XQoJARvWJary7Z5UJR8O3Kaln5K8dK99m1Kq4hdg5T3IcDDBzVtEVv3R1g87Jt0aHRuMZKyY0T4iAEwI5EfBqALZJwNnJ++m4w6kWTqhOnI+rf0COilevf6DlLuE1lRxou5CCkFVipK6dccSdMwARjmT70jQlbjgCoQ/1ckjOl9xZQQ5EtjgrkkUgBC2RSqnxQhzdlhdoSBKDGgURSVS0HkiiVlQStaL55IgkJmobCblnK8tXbkrsc87zWST6k33LBSs3JY9ncQmTselp0XXVjWjKRZAzLdrItljzRCiRXL7sTCijk4+zI06o5t1BMYLEvgkgyFUUY0Rg5pbueea8Rm6bGcLMdQofX9srbxUvrTsT36AEFROKVr0r218OGE+zJTa4eg4uJXJ/vpv+lppX04no9t25Alf0PLSUKJx40geZUmR9OsiU60k6KITaT700tvkR0H76+V4RjeKERF04Sl176AGgXfTeXLcnFMtkd+raMx9WkoqgaJjeNCPLYwmKVjBqEEw6z7vpPc1JxGyCswycfIvnCVN2PWkUXr11FLK3MXFiP0zdXAuWOJ+aLlhKlT1LFiytKSQjaEnvZGx/UX5/rt6yFGwBJ8GzlgU8X7qqeAXLSWSuv66lCrpq3CTkOa6QPNL3kS+4yF8ywpe8bU11eUlQj+hspY0yu6Tq0yl5IeNqOXd3aaO+Rnb4paCi7sf2Uz4R/FFjkZbc7lpGYKY+MaX9JT2tBt2+Z5ss9/llpvohbkqcNeTV2RUIc2T3z4kECgnfuQjkCy3y13ZckM7o+mn1DJFtYTzZOp0hPC7Cm6Xpw/u1vhOHjauv4+L6gkRs5lgOZp62s/MmFrKFYqE8fHGr+9gnumvhObFBr7GY8BoHN4iYKZLIMlOkjPj0cXlP8o9zFkqnhtNdTlEIctFeJg0lwdVogWLbvSBvw8MpBSHYkk0fAXzYrtGrS9Q46PSstVakCBXECeXmYkVaxxt9qgnWoim6msqiR0CMoOqs4ThCFjVB2GbEgo0h7eMeU7SiJhhbDVawfzMd33ehPo4YRV0gthmiYE9GOga8cc1RJyItjyLZzNMdiozyW5bjTSiWQbGqGVNjWnOa3VSxw6+/9yqt+WjscOpcSkE7vGJxlJOywxkbGygZHb05Fs7/Pm4GTPnJtOtwAf2l4/nPW90w3BGvQOXRbHNLYMpvpolECKetr8ZMKMrD2YO9gfU49nfVjyZC0TqhkMWKhEIdkmM/RbRpTXXw7A+6XFdVz36Oiuilaz/lN09ZtZsAtkXnfspPpjcKjMa7XxeIPSAdI/YL14ViD44iMOpVjcczXBuOPTiDQHv4x+Mbrg3HNl38vy5/+f3t8fnTb+dXf//2/c9zCRjCeTcb7Y85ScJdKKWSa+wfMLFRs8Yto0rRZAhSJ1tEBTWRkCA48ZS6RTTRX5Kyt5QCclCF3b/sHmZ0iWvcRbu7+nr5zTEg7vEv \ No newline at end of file diff --git a/Design_diagram.png b/Design_diagram.png deleted file mode 100644 index e73bc90..0000000 Binary files a/Design_diagram.png and /dev/null differ diff --git a/README.md b/README.md index e9ca329..402312c 100644 --- a/README.md +++ b/README.md @@ -12,30 +12,27 @@ A driver crate for AT-command based serial ublox short range modules, built on top of [atat]. The driver aims to be compatible with the ublox short range modules: -- odin_w2xx -- nina_w1xx -- nina_b1xx -- anna_b1xx -- nina_b2xx -- nina_b3xx + +- odin-w2xx +- nina-w1xx +- nina-b1xx +- anna-b1xx +- nina-b2xx +- nina-b3xx [atat]: https://crates.io/crates/atat ## Documentation -Design diagram: -![design diagram](./Design_diagram.png "Design diagram") - Relevant docs: + - https://www.u-blox.com/en/docs/UBX-14044127 - https://www.u-blox.com/en/docs/UBX-14044126 - https://www.u-blox.com/en/docs/UBX-16024251 Relevant repos: -- https://github.com/u-blox/u-connectXpress-host-library -- https://github.com/particle-iot/device-os -- https://github.com/u-blox/ubxlib +- https://github.com/u-blox/ubxlib ## Tests @@ -51,12 +48,12 @@ The samples can be built using `cargo build -p linux_example --target x86_64-unk ## Features - device selection (must select one, and only one!): - - `odin_w2xx` - - `nina_w1xx` - - `nina_b1xx` - - `anna_b1xx` - - `nina_b2xx` - - `nina_b3xx` + - `odin-w2xx` + - `nina-w1xx` + - `nina-b1xx` + - `anna-b1xx` + - `nina-b2xx` + - `nina-b3xx` - `socket-tcp`: Enabled by default. Adds TCP socket capabilities, and implements [`TcpStack`] trait. - `socket-udp`: Enabled by default. Adds UDP socket capabilities, and implements [`UdpStack`] trait. - `defmt-default`: Disabled by default. Add log statements on trace (dev) or info (release) log levels to aid debugging. @@ -66,13 +63,12 @@ The samples can be built using `cargo build -p linux_example --target x86_64-unk - `defmt-warn`: Disabled by default. Add log statements on warn log levels to aid debugging. - `defmt-error`: Disabled by default. Add log statements on error log levels to aid debugging. - ## License Licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or - http://www.apache.org/licenses/LICENSE-2.0) + http://www.apache.org/licenses/LICENSE-2.0) - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. @@ -83,8 +79,8 @@ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - + [no-std-badge]: https://img.shields.io/badge/no__std-yes-blue [test]: https://github.com/BlackbirdHQ/ublox-short-range-rs/workflows/Test/badge.svg [codecov-badge]: https://codecov.io/gh/BlackbirdHQ/ublox-short-range-rs/branch/master/graph/badge.svg diff --git a/examples/linux.rs b/examples/linux.rs deleted file mode 100644 index 52804e9..0000000 --- a/examples/linux.rs +++ /dev/null @@ -1,152 +0,0 @@ -// use std::sync::{Arc, Mutex}; -// use std::thread; -// use std::time::Duration; - -// use linux_embedded_hal::Serial; -// use serial::{self, core::SerialPort}; - -// extern crate at_rs as at; -// extern crate env_logger; -// extern crate nb; - -// // Note this useful idiom: importing names from outer (for mod tests) scope. -// use ublox_short_range::command::*; -// use ublox_short_range::prelude::*; -// use ublox_short_range::wifi; - -// use heapless::{consts::*, spsc::Queue, String}; -// #[allow(unused_imports)] -// use defmt::{error, info, warn}; - -// #[derive(Clone, Copy)] -// struct MilliSeconds(u32); - -// trait U32Ext { -// fn s(self) -> MilliSeconds; -// fn ms(self) -> MilliSeconds; -// } - -// impl U32Ext for u32 { -// fn s(self) -> MilliSeconds { -// MilliSeconds(self / 1000) -// } -// fn ms(self) -> MilliSeconds { -// MilliSeconds(self) -// } -// } - -// struct Timer; - -// impl embedded_hal::timer::CountDown for Timer { -// type Time = MilliSeconds; -// fn start(&mut self, _duration: T) -// where -// T: Into, -// { -// // let dur = duration.into(); -// // self.timeout_time = Instant::now().checked_add(Duration::from_millis(dur.0.into())).expect(""); -// } - -// fn wait(&mut self) -> ::nb::Result<(), void::Void> { -// // if self.timeout_time - Instant::now() < Duration::from_secs(0) { -// // Ok(()) -// // } else { -// Err(nb::Error::WouldBlock) -// // } -// } -// } - -// impl embedded_hal::timer::Cancel for Timer { -// type Error = (); -// fn cancel(&mut self) -> Result<(), Self::Error> { -// Ok(()) -// } -// } - -// type SerialRxBufferLen = U4096; -// type ATRequestQueueLen = U5; -// type ATResponseQueueLen = U5; - -// static mut WIFI_REQ_Q: Option> = None; -// static mut WIFI_RES_Q: Option, ATResponseQueueLen, u8>> = -// None; - -// fn main() { -// env_logger::builder() -// .filter_level(defmt::LevelFilter::Trace) -// .init(); - -// // Serial port settings -// let settings = serial::PortSettings { -// baud_rate: serial::Baud115200, -// char_size: serial::Bits8, -// parity: serial::ParityNone, -// stop_bits: serial::Stop1, -// flow_control: serial::FlowNone, -// }; - -// // Open serial port -// let mut port = serial::open("/dev/ttyACM0").expect("Could not open serial port"); -// port.configure(&settings) -// .expect("Could not configure serial port"); - -// port.set_timeout(Duration::from_millis(2)) -// .expect("Could not set serial port timeout"); - -// unsafe { WIFI_REQ_Q = Some(Queue::u8()) }; -// unsafe { WIFI_RES_Q = Some(Queue::u8()) }; - -// let (wifi_client, parser) = at::new::<_, _, _, SerialRxBufferLen, _, _>( -// unsafe { (WIFI_REQ_Q.as_mut().unwrap(), WIFI_RES_Q.as_mut().unwrap()) }, -// Serial(port), -// Timer, -// 1000.ms(), -// ); - -// let ublox = ublox_short_range::UbloxClient::new(wifi_client); - -// let at_parser_arc = Arc::new(Mutex::new(parser)); - -// let at_parser = at_parser_arc.clone(); -// let serial_irq = thread::Builder::new() -// .name("serial_irq".to_string()) -// .spawn(move || loop { -// thread::sleep(Duration::from_millis(1)); -// if let Ok(mut at) = at_parser.lock() { -// at.handle_irq() -// } -// }) -// .unwrap(); - -// let serial_loop = thread::Builder::new() -// .name("serial_loop".to_string()) -// .spawn(move || loop { -// thread::sleep(Duration::from_millis(100)); -// if let Ok(mut at) = at_parser_arc.lock() { -// at.spin() -// } -// }) -// .unwrap(); - -// let main_loop = thread::Builder::new() -// .name("main_loop".to_string()) -// .spawn(move || { -// // let networks = wifi_client.scan().unwrap(); -// // networks.iter().for_each(|n| info!("{:?}", n.ssid)); - -// let options = wifi::options::ConnectionOptions::new() -// .ssid(String::from("E-NET1")) -// .password(String::from("pakhus47")); - -// // Attempt to connect to a wifi -// let connection = ublox.connect(options).expect("Cannot connect!"); -// info!("Connected! {:?}", connection.network); -// }) -// .unwrap(); - -// // needed otherwise it does not block till -// // the threads actually have been run -// serial_irq.join().unwrap(); -// serial_loop.join().unwrap(); -// main_loop.join().unwrap(); -// } diff --git a/examples/rpi-pico/.cargo/config.toml b/examples/rpi-pico/.cargo/config.toml new file mode 100644 index 0000000..f7e22c1 --- /dev/null +++ b/examples/rpi-pico/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = "probe-rs run --chip RP2040" + +[build] +target = "thumbv6m-none-eabi" + +[env] +DEFMT_LOG = "debug,atat=warn" \ No newline at end of file diff --git a/examples/rpi-pico/.vscode/settings.json b/examples/rpi-pico/.vscode/settings.json new file mode 100644 index 0000000..e786a02 --- /dev/null +++ b/examples/rpi-pico/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "editor.formatOnSave": true, + "[toml]": { + "editor.formatOnSave": false + }, + "rust-analyzer.cargo.features": [ + "ppp", + ], + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.check.allTargets": false, + "rust-analyzer.linkedProjects": [], + "rust-analyzer.server.extraEnv": { + "WIFI_NETWORK": "foo", + "WIFI_PASSWORD": "foo", + } +} \ No newline at end of file diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml new file mode 100644 index 0000000..3a26943 --- /dev/null +++ b/examples/rpi-pico/Cargo.toml @@ -0,0 +1,113 @@ +[package] +name = "ublox-short-range-examples-rpi-pico" +version = "0.1.0" +edition = "2021" + + +[dependencies] +ublox-short-range-rs = { path = "../../", features = ["odin-w2xx", "defmt"] } +embassy-executor = { version = "0.5", features = [ + "defmt", + "integrated-timers", + "nightly", + "arch-cortex-m", + "executor-thread", +] } +embassy-time = { version = "0.3", features = [ + "defmt", + "defmt-timestamp-uptime", +] } +embassy-sync = { version = "0.6" } +embassy-rp = { version = "0.1.0", features = [ + "defmt", + "unstable-pac", + "time-driver", +] } +embassy-futures = { version = "0.1.0" } +no-std-net = { version = "0.6", features = ["serde"] } + +static_cell = { version = "2", features = ["nightly"] } +defmt = "0.3.4" +defmt-rtt = "0.4" +panic-probe = { version = "0.3", features = ["print-defmt"] } + +cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } +cortex-m-rt = "0.7.0" +futures = { version = "0.3.17", default-features = false, features = [ + "async-await", + "cfg-target-has-atomic", + "unstable", +] } + +embedded-io-async = { version = "0.6" } +heapless = "0.8" +portable-atomic = { version = "*", features = ["unsafe-assume-single-core"] } + +embassy-net = { version = "0.4", optional = true, features = [ + "defmt", + "proto-ipv4", + "medium-ip", + "tcp", + "udp", + "dns" +] } +embassy-net-ppp = { version = "0.1", optional = true, features = ["defmt"] } +reqwless = { git = "https://github.com/drogue-iot/reqwless", features = ["defmt"] } +smoltcp = { version = "*", default-features = false, features = ["dns-max-server-count-4"]} +rand_chacha = { version = "0.3", default-features = false } +embedded-tls = { path = "../../../embedded-tls", default-features = false, features = ["defmt"] } + + +[features] +internal-network-stack = ["ublox-short-range-rs/internal-network-stack"] +ppp = ["dep:embassy-net", "dep:embassy-net-ppp", "ublox-short-range-rs/ppp"] + +[patch.crates-io] +# embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } +# embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } + + +embassy-rp = { path = "../../../embassy/embassy-rp" } +embassy-time = { path = "../../../embassy/embassy-time" } +embassy-sync = { path = "../../../embassy/embassy-sync" } +embassy-net = { path = "../../../embassy/embassy-net" } +embassy-net-ppp = { path = "../../../embassy/embassy-net-ppp" } +embassy-futures = { path = "../../../embassy/embassy-futures" } +embassy-executor = { path = "../../../embassy/embassy-executor" } +ublox-sockets = { path = "../../../ublox-sockets" } +no-std-net = { path = "../../../no-std-net" } +atat = { path = "../../../atat/atat" } + +[profile.dev] +debug = 2 +debug-assertions = true +opt-level = 1 +overflow-checks = true + +[profile.release] +codegen-units = 1 +debug = 2 +debug-assertions = false +incremental = false +lto = 'fat' +opt-level = 'z' +overflow-checks = false + +# do not optimize proc-macro crates = faster builds from scratch +[profile.dev.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false + +[profile.release.build-override] +codegen-units = 8 +debug = false +debug-assertions = false +opt-level = 0 +overflow-checks = false diff --git a/examples/rpi-pico/build.rs b/examples/rpi-pico/build.rs new file mode 100644 index 0000000..3f915f9 --- /dev/null +++ b/examples/rpi-pico/build.rs @@ -0,0 +1,36 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); + + println!("cargo:rustc-link-arg-bins=--nmagic"); + println!("cargo:rustc-link-arg-bins=-Tlink.x"); + println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); + println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); +} diff --git a/examples/rpi-pico/memory.x b/examples/rpi-pico/memory.x new file mode 100644 index 0000000..eb8c173 --- /dev/null +++ b/examples/rpi-pico/memory.x @@ -0,0 +1,5 @@ +MEMORY { + BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 + FLASH : ORIGIN = 0x10000100, LENGTH = 1024K - 0x100 + RAM : ORIGIN = 0x20000000, LENGTH = 256K +} \ No newline at end of file diff --git a/examples/rpi-pico/rust-toolchain.toml b/examples/rpi-pico/rust-toolchain.toml new file mode 100644 index 0000000..4e3b270 --- /dev/null +++ b/examples/rpi-pico/rust-toolchain.toml @@ -0,0 +1,7 @@ +[toolchain] +channel = "nightly-2024-01-17" +components = [ "rust-src", "rustfmt", "llvm-tools" ] +targets = [ + "thumbv6m-none-eabi", + "thumbv7em-none-eabihf" +] diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs new file mode 100644 index 0000000..81090c4 --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -0,0 +1,297 @@ +#![cfg(feature = "internal-network-stack")] +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] +#![allow(incomplete_features)] + +use core::fmt::Write as _; +use embassy_executor::Spawner; +use embassy_futures::select::{select, Either}; +use embassy_rp::gpio::{AnyPin, Input, Level, Output, Pull}; +use embassy_rp::peripherals::{PIN_26, UART1}; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUartTx}; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{Duration, Timer}; +use embedded_io_async::Write; +use no_std_net::{Ipv4Addr, SocketAddr}; +use static_cell::make_static; +use ublox_short_range::asynch::runner::Runner; +use ublox_short_range::asynch::ublox_stack::dns::DnsSocket; +use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; +use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; +use ublox_short_range::asynch::{new, Resources, State}; +use ublox_short_range::atat::{self, AtatIngress}; +use ublox_short_range::command::custom_digest::EdmDigester; +use ublox_short_range::command::edm::urc::EdmEvent; +use ublox_short_range::embedded_nal_async::AddrType; +use {defmt_rtt as _, panic_probe as _}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 1024; +const URC_CAPACITY: usize = 2; + +type AtClient = ublox_short_range::atat::asynch::Client< + 'static, + uart::BufferedUartTx<'static, UART1>, + INGRESS_BUF_SIZE, +>; + +#[embassy_executor::task] +async fn wifi_task( + runner: InternalRunner< + 'a, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + Output<'static, AnyPin>, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, +) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn net_task(stack: &'static UbloxStack) -> ! { + stack.run().await +} + +#[embassy_executor::task(pool_size = 2)] +async fn echo_task( + stack: &'static UbloxStack, + hostname: &'static str, + port: u16, + write_interval: Duration, +) { + let mut rx_buffer = [0; 128]; + let mut tx_buffer = [0; 128]; + let mut buf = [0; 128]; + let mut cnt = 0u32; + let mut msg = heapless::String::<64>::new(); + Timer::after(Duration::from_secs(1)).await; + + let ip_addr = match DnsSocket::new(stack).query(hostname, AddrType::IPv4).await { + Ok(ip) => ip, + Err(_) => { + error!("[{}] Failed to resolve IP addr", hostname); + return; + } + }; + + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + + info!("[{}] Connecting... {}", hostname, debug2Format(&ip_addr)); + if let Err(e) = socket.connect((ip_addr, port)).await { + warn!("[{}] connect error: {:?}", hostname, e); + return; + } + info!( + "[{}] Connected to {:?}", + hostname, + debug2Format(&socket.remote_endpoint()) + ); + + loop { + match select(Timer::after(write_interval), socket.read(&mut buf)).await { + Either::First(_) => { + msg.clear(); + write!(msg, "Hello {}! {}\n", ip_addr, cnt).unwrap(); + cnt = cnt.wrapping_add(1); + if let Err(e) = socket.write_all(msg.as_bytes()).await { + warn!("[{}] write error: {:?}", hostname, e); + break; + } + info!("[{}] txd: {}", hostname, msg); + Timer::after(Duration::from_millis(400)).await; + } + Either::Second(res) => { + let n = match res { + Ok(0) => { + warn!("[{}] read EOF", hostname); + break; + } + Ok(n) => n, + Err(e) => { + warn!("[{}] {:?}", hostname, e); + break; + } + }; + info!( + "[{}] rxd {}", + hostname, + core::str::from_utf8(&buf[..n]).unwrap() + ); + } + } + } +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + info!("Hello World!"); + + let p = embassy_rp::init(Default::default()); + + let rst = Output::new(p.PIN_26, Level::High); + let mut btn = Input::new(p.PIN_27, Pull::Up); + + static TX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + + let uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 16]), + RX_BUF.init([0; 16]), + uart::Config::default(), + ); + let (uart_rx, uart_tx) = uart.split(); + + static RESOURCES: StaticCell< + Resources, CMD_BUF_SIZE, INGRESS_BUF_SIZE, URC_CAPACITY>, + > = StaticCell::new(); + + let (net_device, mut control, runner) = ublox_short_range::asynch::new_internal( + uart_rx, + uart_tx, + RESOURCES.init(Resources::new()), + rst, + ); + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(UbloxStack::new( + net_device, + STACK_RESOURCES.init(StackResources::new()), + )); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(wifi_task(runner)).unwrap(); + + control + .set_hostname("Factbird-duo-wifi-test") + .await + .unwrap(); + + // And now we can use it! + info!("Device initialized!"); + + let mut rx_buffer = [0; 256]; + let mut tx_buffer = [0; 256]; + let mut buf = [0; 256]; + let mut cnt = 0u32; + let mut msg = heapless::String::<64>::new(); + + loop { + loop { + match control.join_wpa2("test", "1234abcd").await { + Ok(_) => { + info!("Network connected!"); + spawner + .spawn(echo_task( + &stack, + // "echo.u-blox.com", + // 7, + "tcpbin.com", + 4242, + Duration::from_secs(1), + )) + .unwrap(); + + spawner + .spawn(echo_task( + &stack, + "tcpbin.com", + 4242, + Duration::from_millis(500), + )) + .unwrap(); + break; + } + Err(err) => { + info!("join failed with error={:?}. Retrying in 1 second", err); + Timer::after(Duration::from_secs(1)).await; + } + } + } + 'outer: loop { + Timer::after(Duration::from_secs(1)).await; + + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + let remote: SocketAddr = (Ipv4Addr::new(192, 168, 1, 183), 4444).into(); + info!("Connecting... {}", debug2Format(&remote)); + if let Err(e) = socket.connect(remote).await { + warn!("connect error: {:?}", e); + continue; + } + info!("Connected to {:?}", debug2Format(&socket.remote_endpoint())); + + 'inner: loop { + match select(Timer::after(Duration::from_secs(3)), socket.read(&mut buf)).await { + Either::First(_) => { + msg.clear(); + write!(msg, "Hello world! {}\n", cnt).unwrap(); + cnt = cnt.wrapping_add(1); + if let Err(e) = socket.write_all(msg.as_bytes()).await { + warn!("write error: {:?}", e); + break; + } + info!("txd: {}", msg); + Timer::after(Duration::from_millis(400)).await; + } + Either::Second(res) => { + let n = match res { + Ok(0) => { + warn!("read EOF"); + break; + } + Ok(n) => n, + Err(e) => { + warn!("{:?}", e); + break; + } + }; + info!("rxd [{}] {}", n, core::str::from_utf8(&buf[..n]).unwrap()); + + match &buf[..n] { + b"c\n" => { + socket.close(); + break 'inner; + } + b"a\n" => { + socket.abort(); + break 'inner; + } + b"d\n" => { + drop(socket); + break 'inner; + } + b"f\n" => { + control.disconnect().await.unwrap(); + break 'outer; + } + _ => {} + } + } + } + } + info!("Press USER button to reconnect socket!"); + btn.wait_for_any_edge().await; + continue; + } + info!("Press USER button to reconnect to WiFi!"); + btn.wait_for_any_edge().await; + } +} diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs new file mode 100644 index 0000000..630415a --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -0,0 +1,316 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] +#![allow(incomplete_features)] + +use embassy_executor::Spawner; +use embassy_futures::join::join; +use embassy_rp::gpio::{Level, Output}; +use embassy_rp::peripherals::{PIN_26, UART1}; +use embassy_rp::uart::BufferedInterruptHandler; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{with_timeout, Duration, Timer}; +use no_std_net::Ipv4Addr; +use static_cell::make_static; +use ublox_short_range::asynch::runner::Runner; +use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; +use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; +use ublox_short_range::asynch::{new, State}; +use ublox_short_range::atat::{self, AtatIngress}; +use ublox_short_range::command::custom_digest::EdmDigester; +use ublox_short_range::command::edm::urc::EdmEvent; +use {defmt_rtt as _, panic_probe as _}; + +const RX_BUF_LEN: usize = 1024; +const URC_CAPACITY: usize = 3; + +type AtClient = ublox_short_range::atat::asynch::Client< + 'static, + common::TxWrap>, + RX_BUF_LEN, +>; + +#[embassy_executor::task] +async fn wifi_task( + runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8, URC_CAPACITY>, +) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn net_task(stack: &'static UbloxStack) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn ingress_task( + mut ingress: atat::Ingress<'static, EdmDigester, EdmEvent, RX_BUF_LEN, URC_CAPACITY, 2>, + mut rx: uart::BufferedUartRx<'static, UART1>, +) -> ! { + ingress.read_from(&mut rx).await +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + info!("Hello World!"); + + let p = embassy_rp::init(Default::default()); + + let rst_pin = OutputOpenDrain::new(p.PIN_26.degrade(), Level::High); + + static TX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + let wifi_uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 32]), + RX_BUF.init([0; 32]), + uart::Config::default(), + ); + + static RESOURCES: StaticCell> = + StaticCell::new(); + + let mut runner = Runner::new( + wifi_uart.split(), + RESOURCES.init(Resources::new()), + WifiConfig { rst_pin }, + ); + + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); + + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guaranteed to be random. + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( + net_device, + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, + )); + + static CONTROL_RESOURCES: StaticCell = StaticCell::new(); + let mut control = runner.control(CONTROL_RESOURCES.init(ControlResources::new()), &stack); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(ppp_task(runner, &stack)).unwrap(); + + stack.wait_config_up().await; + + Timer::after(Duration::from_secs(1)).await; + + loop { + match control.join_wpa2(WIFI_NETWORK, WIFI_PASSWORD).await { + Ok(_) => break, + Err(err) => { + defmt::panic!("join failed with status={}", err); + } + } + } + + // And now we can use it! + info!("Device initialized!"); + + let down = test_download(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; + let up = test_upload(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; + let updown = test_upload_download(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; + + // assert!(down > TEST_EXPECTED_DOWNLOAD_KBPS); + // assert!(up > TEST_EXPECTED_UPLOAD_KBPS); + // assert!(updown > TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS); + + info!("Test OK"); + cortex_m::asm::bkpt(); +} + +// Test-only wifi network, no internet access! +const WIFI_NETWORK: &str = "WiFimodem-7A76"; +const WIFI_PASSWORD: &str = "ndzwqzyhhd"; + +const TEST_DURATION: usize = 10; +const SETTLE_TIME: usize = 5; +const TEST_EXPECTED_DOWNLOAD_KBPS: usize = 300; +const TEST_EXPECTED_UPLOAD_KBPS: usize = 300; +const TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS: usize = 300; +const RX_BUFFER_SIZE: usize = 4096; +const TX_BUFFER_SIZE: usize = 4096; +const SERVER_ADDRESS: Ipv4Addr = Ipv4Addr::new(192, 168, 0, 8); +const DOWNLOAD_PORT: u16 = 4321; +const UPLOAD_PORT: u16 = 4322; +const UPLOAD_DOWNLOAD_PORT: u16 = 4323; + +async fn test_download(stack: &'static UbloxStack) -> usize { + info!("Testing download..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + info!( + "connecting to {:?}:{}...", + debug2Format(&SERVER_ADDRESS), + DOWNLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, DOWNLOAD_PORT)).await { + error!("connect error: {:?}", e); + return 0; + } + info!("connected, testing..."); + + let mut rx_buf = [0; 4096]; + let mut total: usize = 0; + with_timeout(Duration::from_secs(TEST_DURATION as _), async { + loop { + match socket.read(&mut rx_buf).await { + Ok(0) => { + error!("read EOF"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + error!("read error: {:?}", e); + return 0; + } + } + } + }) + .await + .ok(); + + let kbps = (total + 512) / 1024 / TEST_DURATION; + info!("download: {} kB/s", kbps); + kbps +} + +async fn test_upload(stack: &'static UbloxStack) -> usize { + info!("Testing upload..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + info!( + "connecting to {:?}:{}...", + debug2Format(&SERVER_ADDRESS), + UPLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_PORT)).await { + error!("connect error: {:?}", e); + return 0; + } + info!("connected, testing..."); + + let buf = [0; 4096]; + let mut total: usize = 0; + with_timeout(Duration::from_secs(TEST_DURATION as _), async { + loop { + match socket.write(&buf).await { + Ok(0) => { + error!("write zero?!??!?!"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + error!("write error: {:?}", e); + return 0; + } + } + } + }) + .await + .ok(); + + let kbps = (total + 512) / 1024 / TEST_DURATION; + info!("upload: {} kB/s", kbps); + kbps +} + +async fn test_upload_download(stack: &'static UbloxStack) -> usize { + info!("Testing upload+download..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + info!( + "connecting to {:?}:{}...", + debug2Format(&SERVER_ADDRESS), + UPLOAD_DOWNLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_DOWNLOAD_PORT)).await { + error!("connect error: {:?}", e); + return 0; + } + info!("connected, testing..."); + + let (mut reader, mut writer) = socket.split(); + + let tx_buf = [0; 4096]; + let mut rx_buf = [0; 4096]; + let mut total: usize = 0; + let tx_fut = async { + loop { + match writer.write(&tx_buf).await { + Ok(0) => { + error!("write zero?!??!?!"); + return 0; + } + Ok(_) => {} + Err(e) => { + error!("write error: {:?}", e); + return 0; + } + } + } + }; + + let rx_fut = async { + loop { + match reader.read(&mut rx_buf).await { + Ok(0) => { + error!("read EOF"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + error!("read error: {:?}", e); + return 0; + } + } + } + }; + + if with_timeout( + Duration::from_secs(TEST_DURATION as _), + join(tx_fut, rx_fut), + ) + .await + .is_err() + { + error!("Test timed out"); + } + + let kbps = (total + 512) / 1024 / TEST_DURATION; + info!("upload+download: {} kB/s", kbps); + kbps +} diff --git a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs new file mode 100644 index 0000000..e25d51e --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs @@ -0,0 +1,196 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(impl_trait_in_assoc_type)] + +#[cfg(not(feature = "ppp"))] +compile_error!("You must enable the `ppp` feature flag to build this example"); + +use defmt::*; +use embassy_executor::Spawner; +use embassy_net::tcp::TcpSocket; +use embassy_net::{Ipv4Address, Stack, StackResources}; +use embassy_rp::gpio::{AnyPin, Level, Output, OutputOpenDrain, Pin}; +use embassy_rp::peripherals::UART1; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUart, BufferedUartRx, BufferedUartTx}; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{Duration, Timer}; +use embedded_tls::TlsConfig; +use embedded_tls::TlsConnection; +use embedded_tls::TlsContext; +use embedded_tls::UnsecureProvider; +use embedded_tls::{Aes128GcmSha256, MaxFragmentLength}; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; +use reqwless::headers::ContentType; +use reqwless::request::Request; +use reqwless::request::RequestBuilder as _; +use reqwless::response::Response; +use static_cell::StaticCell; +use ublox_short_range::asynch::control::ControlResources; +use ublox_short_range::asynch::{Resources, Runner}; +use {defmt_rtt as _, panic_probe as _}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 512; +const URC_CAPACITY: usize = 2; + +pub struct WifiConfig { + pub rst_pin: OutputOpenDrain<'static>, +} + +impl<'a> ublox_short_range::WifiConfig<'a> for WifiConfig { + type ResetPin = OutputOpenDrain<'static>; + + const PPP_CONFIG: embassy_net_ppp::Config<'a> = embassy_net_ppp::Config { + username: b"", + password: b"", + }; + + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + Some(&mut self.rst_pin) + } +} + +#[embassy_executor::task] +async fn net_task(stack: &'static Stack>) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn ppp_task( + mut runner: Runner< + 'static, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + WifiConfig, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, + stack: &'static embassy_net::Stack>, +) -> ! { + runner.run(stack).await +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + let rst_pin = OutputOpenDrain::new(p.PIN_26.degrade(), Level::High); + + static TX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + let wifi_uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 32]), + RX_BUF.init([0; 32]), + uart::Config::default(), + ); + + static RESOURCES: StaticCell> = + StaticCell::new(); + + let mut runner = Runner::new( + wifi_uart.split(), + RESOURCES.init(Resources::new()), + WifiConfig { rst_pin }, + ); + + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); + + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guaranteed to be random. + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( + net_device, + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, + )); + + static CONTROL_RESOURCES: StaticCell = StaticCell::new(); + let mut control = runner.control(CONTROL_RESOURCES.init(ControlResources::new()), &stack); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(ppp_task(runner, &stack)).unwrap(); + + stack.wait_config_up().await; + + Timer::after(Duration::from_secs(1)).await; + + control.set_hostname("Ublox-wifi-test").await.ok(); + + control.join_wpa2("MyAccessPoint", "12345678").await; + + info!("We have network!"); + + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + socket.set_timeout(Some(Duration::from_secs(10))); + + let hostname = "ecdsa-test.germancoding.com"; + + let mut remote = stack + .dns_query(hostname, smoltcp::wire::DnsQueryType::A) + .await + .unwrap(); + let remote_endpoint = (remote.pop().unwrap(), 443); + info!("connecting to {:?}...", remote_endpoint); + let r = socket.connect(remote_endpoint).await; + if let Err(e) = r { + warn!("connect error: {:?}", e); + return; + } + info!("TCP connected!"); + + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = TlsConfig::new() + // .with_max_fragment_length(MaxFragmentLength::Bits11) + .with_server_name(hostname); + let mut tls = TlsConnection::new(socket, &mut read_record_buffer, &mut write_record_buffer); + + tls.open(TlsContext::new( + &config, + UnsecureProvider::new::(ChaCha8Rng::seed_from_u64(seed)), + )) + .await + .expect("error establishing TLS connection"); + + info!("TLS Established!"); + + let request = Request::get("/") + .host(hostname) + .content_type(ContentType::TextPlain) + .build(); + request.write(&mut tls).await.unwrap(); + + let mut rx_buf = [0; 1024]; + let mut body_buf = [0; 8192]; + let response = Response::read(&mut tls, reqwless::request::Method::GET, &mut rx_buf) + .await + .unwrap(); + let len = response + .body() + .reader() + .read_to_end(&mut body_buf) + .await + .unwrap(); + + info!("{=[u8]:a}", &body_buf[..len]); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3cd5460..1dca89f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,7 +1,7 @@ [toolchain] -channel = "nightly-2023-06-28" -components = [ "rust-src", "rustfmt", "llvm-tools-preview", "clippy" ] +channel = "1.79" +components = [ "rust-src", "rustfmt", "llvm-tools" ] targets = [ - "x86_64-unknown-linux-gnu", + "thumbv6m-none-eabi", "thumbv7em-none-eabihf" ] diff --git a/src/asynch/at_udp_socket.rs b/src/asynch/at_udp_socket.rs new file mode 100644 index 0000000..4428181 --- /dev/null +++ b/src/asynch/at_udp_socket.rs @@ -0,0 +1,70 @@ +use embassy_net::{udp::UdpSocket, Ipv4Address}; +use embedded_io_async::{Read, Write}; + +use crate::config::Transport; + +pub struct AtUdpSocket<'a>(pub(crate) UdpSocket<'a>); + +impl<'a> AtUdpSocket<'a> { + pub(crate) const PPP_AT_PORT: u16 = 23; +} + +impl<'a> embedded_io_async::ErrorType for &AtUdpSocket<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> Read for &AtUdpSocket<'a> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let (len, _) = self.0.recv_from(buf).await.unwrap(); + Ok(len) + } +} + +impl<'a> Write for &AtUdpSocket<'a> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.0 + .send_to( + buf, + (Ipv4Address::new(172, 30, 0, 251), AtUdpSocket::PPP_AT_PORT), + ) + .await + .unwrap(); + + Ok(buf.len()) + } +} + +impl<'a> Transport for AtUdpSocket<'a> { + fn set_baudrate(&mut self, _baudrate: u32) { + // Nothing to do here + } + + fn split_ref(&mut self) -> (impl Write, impl Read) { + (&*self, &*self) + } +} + +impl<'a> embedded_io_async::ErrorType for AtUdpSocket<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> Read for AtUdpSocket<'a> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let (len, _) = self.0.recv_from(buf).await.unwrap(); + Ok(len) + } +} + +impl<'a> Write for AtUdpSocket<'a> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.0 + .send_to( + buf, + (Ipv4Address::new(172, 30, 0, 251), AtUdpSocket::PPP_AT_PORT), + ) + .await + .unwrap(); + + Ok(buf.len()) + } +} diff --git a/src/asynch/control.rs b/src/asynch/control.rs new file mode 100644 index 0000000..67d648b --- /dev/null +++ b/src/asynch/control.rs @@ -0,0 +1,709 @@ +use core::cell::Cell; +use core::str::FromStr as _; + +use atat::AtatCmd; +use atat::{asynch::AtatClient, response_slot::ResponseSlotGuard, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Sender}; +use embassy_time::{with_timeout, Duration, Timer}; +use heapless::Vec; +use no_std_net::Ipv4Addr; + +use crate::command::general::responses::SoftwareVersionResponse; +use crate::command::general::types::FirmwareVersion; +use crate::command::general::SoftwareVersion; +use crate::command::gpio::responses::ReadGPIOResponse; +use crate::command::gpio::types::GPIOMode; +use crate::command::gpio::ConfigureGPIO; +use crate::command::network::responses::NetworkStatusResponse; +use crate::command::network::types::{NetworkStatus, NetworkStatusParameter}; +use crate::command::network::GetNetworkStatus; +use crate::command::ping::Ping; +use crate::command::system::responses::LocalAddressResponse; +use crate::command::system::types::InterfaceID; +use crate::command::system::GetLocalAddress; +use crate::command::wifi::{ExecWifiStationAction, GetWifiStatus, SetWifiStationConfig}; +use crate::command::OnOff; +use crate::command::{ + gpio::ReadGPIO, + wifi::{ + types::{ + AccessPointAction, Authentication, SecurityMode, SecurityModePSK, StatusId, + WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, + }, + WifiAPAction, + }, +}; +use crate::command::{ + gpio::{ + types::{GPIOId, GPIOValue}, + WriteGPIO, + }, + wifi::SetWifiAPConfig, +}; +use crate::command::{network::SetNetworkHostName, wifi::types::AccessPointConfig}; +use crate::command::{ + system::{RebootDCE, ResetToFactoryDefaults}, + wifi::types::AccessPointId, +}; +use crate::connection::{DnsServers, StaticConfigV4, WiFiState}; +use crate::error::Error; + +use super::runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}; +use super::state::LinkState; +use super::{state, UbloxUrc}; + +enum WifiAuthentication<'a> { + None, + Wpa2Passphrase(&'a str), + Wpa2Psk(&'a [u8; 32]), +} + +const CONFIG_ID: u8 = 0; + +pub(crate) struct ProxyClient<'a, const INGRESS_BUF_SIZE: usize> { + pub(crate) req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + pub(crate) res_slot: &'a atat::ResponseSlot, + cooldown_timer: Cell>, +} + +impl<'a, const INGRESS_BUF_SIZE: usize> ProxyClient<'a, INGRESS_BUF_SIZE> { + pub fn new( + req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + res_slot: &'a atat::ResponseSlot, + ) -> Self { + Self { + req_sender, + res_slot, + cooldown_timer: Cell::new(None), + } + } + + async fn wait_response( + &self, + timeout: Duration, + ) -> Result, atat::Error> { + with_timeout(timeout, self.res_slot.get()) + .await + .map_err(|_| atat::Error::Timeout) + } +} + +impl<'a, const INGRESS_BUF_SIZE: usize> atat::asynch::AtatClient + for &ProxyClient<'a, INGRESS_BUF_SIZE> +{ + async fn send(&mut self, cmd: &Cmd) -> Result { + let mut buf = [0u8; MAX_CMD_LEN]; + let len = cmd.write(&mut buf); + + if len < 50 { + debug!( + "Sending command: {:?}", + atat::helpers::LossyStr(&buf[..len]) + ); + } else { + debug!("Sending command with long payload ({} bytes)", len); + } + + if let Some(cooldown) = self.cooldown_timer.take() { + cooldown.await + } + + // TODO: Guard against race condition! + with_timeout( + Duration::from_secs(1), + self.req_sender.send(Vec::try_from(&buf[..len]).unwrap()), + ) + .await + .map_err(|_| atat::Error::Timeout)?; + + self.cooldown_timer.set(Some(Timer::after_millis(20))); + + if !Cmd::EXPECTS_RESPONSE_CODE { + cmd.parse(Ok(&[])) + } else { + let response = self + .wait_response(Duration::from_millis(Cmd::MAX_TIMEOUT_MS.into())) + .await?; + let response: &atat::Response = &response.borrow(); + cmd.parse(response.into()) + } + } +} + +pub struct Control<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + state_ch: state::Runner<'a>, + at_client: ProxyClient<'a, INGRESS_BUF_SIZE>, + urc_channel: &'a UrcChannel, +} + +impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY> +{ + pub(crate) fn new( + state_ch: state::Runner<'a>, + urc_channel: &'a UrcChannel, + req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + res_slot: &'a atat::ResponseSlot, + ) -> Self { + Self { + state_ch, + at_client: ProxyClient::new(req_sender, res_slot), + urc_channel: urc_channel, + } + } + + /// Set the hostname of the device + pub async fn set_hostname(&self, hostname: &str) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + (&self.at_client) + .send_retry(&SetNetworkHostName { + host_name: hostname, + }) + .await?; + Ok(()) + } + + /// Gets the firmware version of the device + pub async fn get_version(&self) -> Result { + self.state_ch.wait_for_initialized().await; + + let SoftwareVersionResponse { version } = + (&self.at_client).send_retry(&SoftwareVersion).await?; + Ok(version) + } + + /// Gets the MAC address of the device + pub async fn hardware_address(&mut self) -> Result<[u8; 6], Error> { + self.state_ch.wait_for_initialized().await; + + let LocalAddressResponse { mac } = (&self.at_client) + .send_retry(&GetLocalAddress { + interface_id: InterfaceID::WiFi, + }) + .await?; + + Ok(mac.to_be_bytes()[2..].try_into().unwrap()) + } + + async fn get_wifi_status(&self) -> Result { + match (&self.at_client) + .send_retry(&GetWifiStatus { + status_id: StatusId::Status, + }) + .await? + .status_id + { + WifiStatus::Status(s) => Ok(s), + _ => Err(Error::AT(atat::Error::InvalidResponse)), + } + } + + pub async fn wait_for_link_state(&self, link_state: LinkState) { + self.state_ch.wait_for_link_state(link_state).await + } + + pub async fn config_v4(&self) -> Result, Error> { + let NetworkStatusResponse { + status: NetworkStatus::IPv4Address(ipv4), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::IPv4Address, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv4_addr = core::str::from_utf8(ipv4.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::Gateway(gateway), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::Gateway, + }) + .await? + else { + return Err(Error::Network); + }; + + let gateway_addr = core::str::from_utf8(gateway.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::PrimaryDNS(primary), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::PrimaryDNS, + }) + .await? + else { + return Err(Error::Network); + }; + + let primary = core::str::from_utf8(primary.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::SecondaryDNS(secondary), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::SecondaryDNS, + }) + .await? + else { + return Err(Error::Network); + }; + + let secondary = core::str::from_utf8(secondary.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + Ok(ipv4_addr.map(|address| StaticConfigV4 { + address, + gateway: gateway_addr, + dns_servers: DnsServers { primary, secondary }, + })) + } + + pub async fn get_connected_ssid(&self) -> Result, Error> { + match (&self.at_client) + .send_retry(&GetWifiStatus { + status_id: StatusId::SSID, + }) + .await? + .status_id + { + WifiStatus::SSID(s) => Ok(s), + _ => Err(Error::AT(atat::Error::InvalidResponse)), + } + } + + pub async fn factory_reset(&self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + (&self.at_client) + .send_retry(&ResetToFactoryDefaults) + .await?; + (&self.at_client).send_retry(&RebootDCE).await?; + + Ok(()) + } + + async fn start_ap(&self, ssid: &str, _channel: u8) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + // Deactivate network id 0 + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Deactivate, + }) + .await?; + + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Reset, + }) + .await?; + + // // Disable DHCP Server (static IP address will be used) + // if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::IPv4Mode(IPv4Mode::Static), + // }) + // .await?; + // } + + // // Network IP address + // if let Some(ip) = options.ip { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::IPv4Address(ip), + // }) + // .await?; + // } + // // Network Subnet mask + // if let Some(subnet) = options.subnet { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::SubnetMask(subnet), + // }) + // .await?; + // } + // // Network Default gateway + // if let Some(gateway) = options.gateway { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::DefaultGateway(gateway), + // }) + // .await?; + // } + + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::DHCPServer(true.into()), + // }) + // .await?; + + // Wifi part + // Set the Network SSID to connect to + (&self.at_client) + .send_retry(&SetWifiAPConfig { + ap_config_id: AccessPointId::Id0, + ap_config_param: AccessPointConfig::SSID( + heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, + ), + }) + .await?; + + // if let Some(pass) = options.password.clone() { + // // Use WPA2 as authentication type + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::SecurityMode( + // SecurityMode::Wpa2AesCcmp, + // SecurityModePSK::PSK, + // ), + // }) + // .await?; + + // // Input passphrase + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::PSKPassphrase(PasskeyR::Passphrase(pass)), + // }) + // .await?; + // } else { + (&self.at_client) + .send_retry(&SetWifiAPConfig { + ap_config_id: AccessPointId::Id0, + ap_config_param: AccessPointConfig::SecurityMode( + SecurityMode::Open, + SecurityModePSK::Open, + ), + }) + .await?; + // } + + // if let Some(channel) = configuration.channel { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::Channel(channel as u8), + // }) + // .await?; + // } + + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Activate, + }) + .await?; + + Ok(()) + } + + /// Start open access point. + pub async fn start_ap_open(&mut self, ssid: &str, channel: u8) -> Result<(), Error> { + self.start_ap(ssid, channel).await + } + + /// Start WPA2 protected access point. + pub async fn start_ap_wpa2( + &mut self, + ssid: &str, + _passphrase: &str, + channel: u8, + ) -> Result<(), Error> { + self.start_ap(ssid, channel).await + } + + /// Closes access point. + pub async fn close_ap(&self) -> Result<(), Error> { + todo!() + } + + async fn join_sta(&self, ssid: &str, auth: WifiAuthentication<'_>) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { + // Wifi already connected. Check if the SSID is the same + let current_ssid = self.get_connected_ssid().await?; + if current_ssid.as_str() == ssid { + self.state_ch.set_should_connect(true); + return Ok(()); + } else { + self.leave().await?; + }; + } + + (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Reset, + }) + .await?; + + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), + }) + .await?; + + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::SSID(ssid), + }) + .await?; + + match auth { + WifiAuthentication::None => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::Open), + }) + .await?; + } + WifiAuthentication::Wpa2Passphrase(passphrase) => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + }) + .await?; + + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::WpaPskOrPassphrase(passphrase), + }) + .await?; + } + WifiAuthentication::Wpa2Psk(_psk) => { + unimplemented!() + // (&self.at_client) + // .send_retry(&SetWifiStationConfig { + // config_id: CONFIG_ID, + // config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + // }) + // .await?; + + // (&self.at_client) + // .send_retry(&SetWifiStationConfig { + // config_id: CONFIG_ID, + // config_param: WifiStationConfig::WpaPskOrPassphrase(todo!("hex values?!")), + // }) + // .await?; + } + } + + (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Activate, + }) + .await?; + + self.wait_for_join(ssid, Duration::from_secs(20)).await?; + self.state_ch.set_should_connect(true); + + Ok(()) + } + + /// Join an unprotected network with the provided ssid. + pub async fn join_open(&self, ssid: &str) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::None).await + } + + /// Join a protected network with the provided ssid and passphrase. + pub async fn join_wpa2(&self, ssid: &str, passphrase: &str) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::Wpa2Passphrase(passphrase)) + .await + } + + /// Join a protected network with the provided ssid and precomputed PSK. + pub async fn join_wpa2_psk(&mut self, ssid: &str, psk: &[u8; 32]) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::Wpa2Psk(psk)).await + } + + /// Leave the wifi, with which we are currently associated. + pub async fn leave(&self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + self.state_ch.set_should_connect(false); + + match self.get_wifi_status().await? { + WifiStatusVal::Disabled => {} + WifiStatusVal::Disconnected | WifiStatusVal::Connected => { + (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Deactivate, + }) + .await?; + } + } + + with_timeout( + Duration::from_secs(10), + self.state_ch.wait_connection_down(), + ) + .await + .map_err(|_| Error::Timeout)?; + + Ok(()) + } + + pub async fn wait_for_join(&self, ssid: &str, timeout: Duration) -> Result<(), Error> { + match with_timeout(timeout, self.state_ch.wait_for_link_state(LinkState::Up)).await { + Ok(_) => { + // Check that SSID matches + let current_ssid = self.get_connected_ssid().await?; + if ssid != current_ssid.as_str() { + return Err(Error::Network); + } + + Ok(()) + } + Err(_) if self.state_ch.wifi_state(None) == WiFiState::SecurityProblems => { + let _ = (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Deactivate, + }) + .await; + Err(Error::SecurityProblems) + } + Err(_) => Err(Error::Timeout), + } + } + + // /// Start a wifi scan + // /// + // /// Returns a `Stream` of networks found by the device + // /// + // /// # Note + // /// Device events are currently implemented using a bounded queue. + // /// To not miss any events, you should make sure to always await the stream. + // pub async fn scan(&mut self, scan_opts: ScanOptions) -> Scanner<'_> { + // todo!() + // } + + pub async fn send_at(&self, cmd: &Cmd) -> Result { + self.state_ch.wait_for_initialized().await; + Ok((&self.at_client).send_retry(cmd).await?) + } + + pub async fn gpio_configure(&self, id: GPIOId, mode: GPIOMode) -> Result<(), Error> { + self.send_at(&ConfigureGPIO { id, mode }).await?; + Ok(()) + } + + pub async fn gpio_set(&self, id: GPIOId, value: bool) -> Result<(), Error> { + let value = if value { + GPIOValue::High + } else { + GPIOValue::Low + }; + + self.send_at(&WriteGPIO { id, value }).await?; + Ok(()) + } + + pub async fn gpio_get(&self, id: GPIOId) -> Result { + let ReadGPIOResponse { value, .. } = self.send_at(&ReadGPIO { id }).await?; + Ok(value as u8 != 0) + } + + #[cfg(feature = "ppp")] + pub async fn ping( + &self, + hostname: &str, + ) -> Result { + let mut urc_sub = self.urc_channel.subscribe().map_err(|_| Error::Overflow)?; + + self.send_at(&Ping { + hostname, + retry_num: 1, + }) + .await?; + + let result_fut = async { + loop { + match urc_sub.next_message_pure().await { + crate::command::Urc::PingResponse(r) => return Ok(r), + crate::command::Urc::PingErrorResponse(e) => return Err(Error::Dns(e.error)), + _ => {} + } + } + }; + + with_timeout(Duration::from_secs(15), result_fut).await? + } + + // FIXME: This could probably be improved + // #[cfg(feature = "internal-network-stack")] + // pub async fn import_credentials( + // &mut self, + // data_type: SecurityDataType, + // name: &str, + // data: &[u8], + // md5_sum: Option<&str>, + // ) -> Result<(), atat::Error> { + // assert!(name.len() < 16); + + // info!("Importing {:?} bytes as {:?}", data.len(), name); + + // (&self.at_client) + // .send_retry(&PrepareSecurityDataImport { + // data_type, + // data_size: data.len(), + // internal_name: name, + // password: None, + // }) + // .await?; + + // let import_data = self + // .at_client + // .send_retry(&SendSecurityDataImport { + // data: atat::serde_bytes::Bytes::new(data), + // }) + // .await?; + + // if let Some(hash) = md5_sum { + // assert_eq!(import_data.md5_string.as_str(), hash); + // } + + // Ok(()) + // } +} diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs new file mode 100644 index 0000000..afb3f0f --- /dev/null +++ b/src/asynch/mod.rs @@ -0,0 +1,20 @@ +#[cfg(feature = "ppp")] +mod at_udp_socket; +pub mod control; +pub mod network; +mod resources; +pub mod runner; +#[cfg(feature = "internal-network-stack")] +pub mod ublox_stack; + +pub(crate) mod state; + +pub use resources::Resources; +pub use runner::Runner; +pub use state::LinkState; + +#[cfg(feature = "edm")] +pub type UbloxUrc = crate::command::edm::urc::EdmEvent; + +#[cfg(not(feature = "edm"))] +pub type UbloxUrc = crate::command::Urc; diff --git a/src/asynch/network.rs b/src/asynch/network.rs new file mode 100644 index 0000000..79532b0 --- /dev/null +++ b/src/asynch/network.rs @@ -0,0 +1,321 @@ +use core::str::FromStr as _; + +use atat::{asynch::AtatClient, UrcChannel, UrcSubscription}; +use embassy_time::{with_timeout, Duration, Timer}; +use embedded_hal::digital::OutputPin as _; +use no_std_net::{Ipv4Addr, Ipv6Addr}; + +use crate::{ + command::{ + network::{ + responses::NetworkStatusResponse, + types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, + urc::{NetworkDown, NetworkUp}, + GetNetworkStatus, + }, + system::{RebootDCE, StoreCurrentConfig}, + wifi::{ + types::DisconnectReason, + urc::{WifiLinkConnected, WifiLinkDisconnected}, + }, + Urc, + }, + connection::WiFiState, + error::Error, + network::WifiNetwork, + WifiConfig, +}; + +use super::{runner::URC_SUBSCRIBERS, state, UbloxUrc}; + +pub(crate) struct NetDevice<'a, 'b, C, A, const URC_CAPACITY: usize> { + ch: &'b state::Runner<'a>, + config: &'b mut C, + at_client: A, + urc_subscription: UrcSubscription<'a, UbloxUrc, URC_CAPACITY, { URC_SUBSCRIBERS }>, +} + +impl<'a, 'b, C, A, const URC_CAPACITY: usize> NetDevice<'a, 'b, C, A, URC_CAPACITY> +where + C: WifiConfig<'a>, + A: AtatClient, +{ + pub fn new( + ch: &'b state::Runner<'a>, + config: &'b mut C, + at_client: A, + urc_channel: &'a UrcChannel, + ) -> Self { + Self { + ch, + config, + at_client, + urc_subscription: urc_channel.subscribe().unwrap(), + } + } + + pub async fn run(&mut self) -> Result<(), Error> { + loop { + match embassy_futures::select::select( + self.urc_subscription.next_message_pure(), + self.ch.wait_for_wifi_state_change(), + ) + .await + { + embassy_futures::select::Either::First(event) => { + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + self.handle_urc(event).await?; + } + _ => {} + } + + if self.ch.wifi_state(None) == WiFiState::Inactive && self.ch.connection_down(None) { + return Ok(()); + } + } + } + + async fn handle_urc(&mut self, event: Urc) -> Result<(), Error> { + match event { + Urc::StartUp => { + error!("AT startup event?! Device restarted unintentionally!"); + } + Urc::WifiLinkConnected(WifiLinkConnected { + connection_id: _, + bssid, + channel, + }) => self.ch.update_connection_with(|con| { + con.wifi_state = WiFiState::Connected; + con.network + .replace(WifiNetwork::new_station(bssid, channel)); + }), + Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { + self.ch.update_connection_with(|con| { + con.wifi_state = match reason { + DisconnectReason::NetworkDisabled => { + con.network.take(); + warn!("Wifi network disabled!"); + WiFiState::Inactive + } + DisconnectReason::SecurityProblems => { + error!("Wifi Security Problems"); + WiFiState::SecurityProblems + } + _ => WiFiState::NotConnected, + } + }) + } + Urc::WifiAPUp(_) => warn!("Not yet implemented [WifiAPUp]"), + Urc::WifiAPDown(_) => warn!("Not yet implemented [WifiAPDown]"), + Urc::WifiAPStationConnected(_) => warn!("Not yet implemented [WifiAPStationConnected]"), + Urc::WifiAPStationDisconnected(_) => { + warn!("Not yet implemented [WifiAPStationDisconnected]") + } + Urc::EthernetLinkUp(_) => warn!("Not yet implemented [EthernetLinkUp]"), + Urc::EthernetLinkDown(_) => warn!("Not yet implemented [EthernetLinkDown]"), + Urc::NetworkUp(NetworkUp { interface_id }) => { + self.network_status_callback(interface_id).await?; + } + Urc::NetworkDown(NetworkDown { interface_id }) => { + self.network_status_callback(interface_id).await?; + } + Urc::NetworkError(_) => warn!("Not yet implemented [NetworkError]"), + _ => {} + } + + Ok(()) + } + + async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { + // Normally a check for this interface type being + // `InterfaceType::WifiStation`` should be made but there is a bug in + // uConnect which gives the type `InterfaceType::Unknown` when the + // credentials have been restored from persistent memory. This although + // the wifi station has been started. So we assume that this type is + // also ok. + let NetworkStatusResponse { + status: + NetworkStatus::InterfaceType(InterfaceType::WifiStation | InterfaceType::Unknown), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::InterfaceType, + }) + .await? + else { + return Err(Error::Network); + }; + + let NetworkStatusResponse { + status: NetworkStatus::IPv4Address(ipv4), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv4Address, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv4_up = core::str::from_utf8(ipv4.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + #[cfg(feature = "ipv6")] + let ipv6_up = { + let NetworkStatusResponse { + status: NetworkStatus::IPv6Address1(ipv6), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6Address1, + }) + .await? + else { + return Err(Error::Network); + }; + + core::str::from_utf8(ipv6.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default() + }; + + let NetworkStatusResponse { + status: NetworkStatus::IPv6LinkLocalAddress(ipv6_link_local), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6LinkLocalAddress, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv6_link_local_up = core::str::from_utf8(ipv6_link_local.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + // Use `ipv4_addr` & `ipv6_addr` to determine link state + self.ch.update_connection_with(|con| { + con.ipv6_link_local_up = ipv6_link_local_up; + con.ipv4_up = ipv4_up; + + #[cfg(feature = "ipv6")] + { + con.ipv6_up = ipv6_up + } + }); + + Ok(()) + } + + async fn wait_startup(&mut self, timeout: Duration) -> Result<(), Error> { + let fut = async { + loop { + let event = self.urc_subscription.next_message_pure().await; + + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + if let Urc::StartUp = event { + return; + } + } + }; + + with_timeout(timeout, fut).await.map_err(|_| Error::Timeout) + } + + pub async fn reset(&mut self) -> Result<(), Error> { + if let Some(reset_pin) = self.config.reset_pin() { + warn!("Reset pin found! Hard resetting Ublox Short Range"); + reset_pin.set_low().ok(); + Timer::after(Duration::from_millis(100)).await; + reset_pin.set_high().ok(); + } else { + warn!("No reset pin found! Soft resetting Ublox Short Range"); + self.at_client.send_retry(&RebootDCE).await?; + } + + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; + + #[cfg(feature = "edm")] + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn restart(&mut self, store: bool) -> Result<(), Error> { + warn!("Soft resetting Ublox Short Range"); + if store { + self.at_client.send_retry(&StoreCurrentConfig).await?; + } + + self.at_client.send_retry(&RebootDCE).await?; + + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; + + info!("Module started again"); + #[cfg(feature = "edm")] + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } + + #[cfg(feature = "edm")] + pub async fn enter_edm(&mut self, timeout: Duration) -> Result<(), Error> { + info!("Entering EDM mode"); + + // Switch to EDM on Init. If in EDM, fail and check with autosense + let fut = async { + loop { + // Ignore AT results until we are successful in EDM mode + if let Ok(_) = self + .at_client + .send_retry(&crate::command::edm::SwitchToEdmCommand) + .await + { + // After executing the data mode command or the extended data + // mode command, a delay of 50 ms is required before start of + // data transmission. + Timer::after(Duration::from_millis(50)).await; + break; + } + Timer::after(Duration::from_millis(10)).await; + } + }; + + with_timeout(timeout, fut) + .await + .map_err(|_| Error::Timeout)?; + + Ok(()) + } +} diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs new file mode 100644 index 0000000..20db742 --- /dev/null +++ b/src/asynch/resources.rs @@ -0,0 +1,39 @@ +use atat::{ResponseSlot, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; + +use super::{ + runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}, + state, UbloxUrc, +}; + +pub struct Resources { + pub(crate) ch: state::State, + + pub(crate) res_slot: ResponseSlot, + pub(crate) req_slot: Channel, 1>, + pub(crate) urc_channel: UrcChannel, + pub(crate) ingress_buf: [u8; INGRESS_BUF_SIZE], +} + +impl Default + for Resources +{ + fn default() -> Self { + Self::new() + } +} + +impl + Resources +{ + pub fn new() -> Self { + Self { + ch: state::State::new(), + + res_slot: ResponseSlot::new(), + req_slot: Channel::new(), + urc_channel: UrcChannel::new(), + ingress_buf: [0; INGRESS_BUF_SIZE], + } + } +} diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs new file mode 100644 index 0000000..5153b77 --- /dev/null +++ b/src/asynch/runner.rs @@ -0,0 +1,486 @@ +use super::{control::Control, network::NetDevice, state, Resources, UbloxUrc}; +use crate::{ + asynch::control::ProxyClient, + command::{ + data_mode::{self, ChangeMode}, + general::SoftwareVersion, + system::{ + types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, + SetEcho, SetRS232Settings, + }, + wifi::{ + types::{PowerSaveMode, WifiConfig as WifiConfigParam}, + SetWifiConfig, + }, + OnOff, AT, + }, + config::Transport, + error::Error, + WifiConfig, DEFAULT_BAUD_RATE, +}; +use atat::{ + asynch::{AtatClient as _, SimpleClient}, + AtatIngress as _, UrcChannel, +}; +use embassy_futures::select::Either; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; +use embassy_time::{Duration, Timer}; +use embedded_io_async::{BufRead, Write}; + +#[cfg(feature = "ppp")] +pub(crate) const URC_SUBSCRIBERS: usize = 2; +#[cfg(feature = "ppp")] +type Digester = atat::AtDigester; + +#[cfg(feature = "internal-network-stack")] +pub(crate) const URC_SUBSCRIBERS: usize = 3; +#[cfg(feature = "internal-network-stack")] +type Digester = crate::command::custom_digest::EdmDigester; + +pub(crate) const MAX_CMD_LEN: usize = 256; + +async fn at_bridge<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize>( + transport: &mut impl Transport, + req_slot: &Channel, 1>, + ingress: &mut atat::Ingress< + 'a, + Digester, + UbloxUrc, + INGRESS_BUF_SIZE, + URC_CAPACITY, + { URC_SUBSCRIBERS }, + >, +) -> ! { + ingress.clear(); + + let (mut tx, rx) = transport.split_ref(); + + let tx_fut = async { + loop { + let msg = req_slot.receive().await; + let _ = tx.write_all(&msg).await; + } + }; + + embassy_futures::join::join(tx_fut, ingress.read_from(rx)).await; + + unreachable!() +} + +/// Background runner for the Ublox Module. +/// +/// You must call `.run()` in a background task for the Ublox Module to operate. +pub struct Runner<'a, T: Transport, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + transport: T, + + ch: state::Runner<'a>, + config: C, + + pub urc_channel: &'a UrcChannel, + + pub ingress: + atat::Ingress<'a, Digester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, { URC_SUBSCRIBERS }>, + pub res_slot: &'a atat::ResponseSlot, + pub req_slot: &'a Channel, 1>, + + #[cfg(feature = "ppp")] + ppp_runner: Option>, +} + +impl<'a, T, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Runner<'a, T, C, INGRESS_BUF_SIZE, URC_CAPACITY> +where + T: Transport + BufRead, + C: WifiConfig<'a> + 'a, +{ + pub fn new( + transport: T, + resources: &'a mut Resources, + config: C, + ) -> (Self, Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY>) { + let ch_runner = state::Runner::new(&mut resources.ch); + + let ingress = atat::Ingress::new( + Digester::new(), + &mut resources.ingress_buf, + &resources.res_slot, + &resources.urc_channel, + ); + + let control = Control::new( + ch_runner.clone(), + &resources.urc_channel, + resources.req_slot.sender(), + &resources.res_slot, + ); + + ( + Self { + transport, + + ch: ch_runner, + config, + urc_channel: &resources.urc_channel, + + ingress, + res_slot: &resources.res_slot, + req_slot: &resources.req_slot, + + #[cfg(feature = "ppp")] + ppp_runner: None, + }, + control, + ) + } + + #[cfg(feature = "ppp")] + pub fn ppp_stack<'d: 'a, const N_RX: usize, const N_TX: usize>( + &mut self, + ppp_state: &'d mut embassy_net_ppp::State, + ) -> embassy_net_ppp::Device<'d> { + let (net_device, ppp_runner) = embassy_net_ppp::new(ppp_state); + self.ppp_runner.replace(ppp_runner); + net_device + } + + #[cfg(feature = "internal-network-stack")] + pub fn internal_stack( + &mut self, + ) -> super::ublox_stack::Device<'a, INGRESS_BUF_SIZE, URC_CAPACITY> { + super::ublox_stack::Device { + state_ch: self.ch.clone(), + at_client: core::cell::RefCell::new(ProxyClient::new( + self.req_slot.sender(), + &self.res_slot, + )), + urc_channel: &self.urc_channel, + } + } + + /// Probe a given baudrate with the goal of establishing initial + /// communication with the module, so we can reconfigure it for desired + /// baudrate + async fn probe_baud(&mut self, baudrate: BaudRate) -> Result<(), Error> { + info!("Probing wifi module using baud rate: {}", baudrate as u32); + self.transport.set_baudrate(baudrate as u32); + + let baud_fut = async { + let at_client = ProxyClient::new(self.req_slot.sender(), self.res_slot); + + // Hard reset module + NetDevice::new(&self.ch, &mut self.config, &at_client, self.urc_channel) + .reset() + .await?; + + (&at_client).send_retry(&AT).await?; + + // Lets take a shortcut if we are probing for the desired baudrate + if baudrate == C::BAUD_RATE { + info!("Successfully shortcut the baud probing!"); + return Ok(None); + } + + let flow_control = if C::FLOW_CONTROL { + FlowControl::On + } else { + FlowControl::Off + }; + + (&at_client) + .send_retry(&SetRS232Settings { + baud_rate: C::BAUD_RATE, + flow_control, + data_bits: 8, + stop_bits: StopBits::One, + parity: Parity::None, + change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, + }) + .await?; + + Ok::<_, Error>(Some(C::BAUD_RATE)) + }; + + match embassy_futures::select::select( + baud_fut, + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await + { + Either::First(Ok(Some(baud))) => { + self.transport.set_baudrate(baud as u32); + Timer::after_millis(40).await; + Ok(()) + } + Either::First(r) => r.map(drop), + Either::Second(_) => unreachable!(), + } + } + + async fn init(&mut self) -> Result<(), Error> { + // Initialize a new ublox device to a known state + debug!("Initializing WiFi module"); + + // Probe all possible baudrates with the goal of establishing initial + // communication with the module, so we can reconfigure it for desired + // baudrate. + // + // Start with the two most likely + let mut found_baudrate = false; + + for baudrate in [ + C::BAUD_RATE, + DEFAULT_BAUD_RATE, + BaudRate::B9600, + BaudRate::B14400, + BaudRate::B19200, + BaudRate::B28800, + BaudRate::B38400, + BaudRate::B57600, + BaudRate::B76800, + BaudRate::B115200, + BaudRate::B230400, + BaudRate::B250000, + BaudRate::B460800, + BaudRate::B921600, + BaudRate::B3000000, + BaudRate::B5250000, + ] { + if self.probe_baud(baudrate).await.is_ok() { + if baudrate != C::BAUD_RATE { + // Attempt to store the desired baudrate, so we can shortcut + // this probing next time. Ignore any potential failures, as + // this is purely an optimization. + let _ = embassy_futures::select::select( + NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), self.res_slot), + self.urc_channel, + ) + .restart(true), + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await; + } + found_baudrate = true; + break; + } + } + + if !found_baudrate { + return Err(Error::BaudDetection); + } + + let at_client = ProxyClient::new(self.req_slot.sender(), self.res_slot); + + let setup_fut = async { + (&at_client).send_retry(&SoftwareVersion).await?; + + (&at_client) + .send_retry(&SetEcho { on: EchoOn::Off }) + .await?; + (&at_client) + .send_retry(&SetWifiConfig { + config_param: WifiConfigParam::DropNetworkOnLinkLoss(OnOff::On), + }) + .await?; + + // Disable all power savings for now + (&at_client) + .send_retry(&SetWifiConfig { + config_param: WifiConfigParam::PowerSaveMode(PowerSaveMode::ActiveMode), + }) + .await?; + + #[cfg(feature = "internal-network-stack")] + if let Some(size) = C::TLS_IN_BUFFER_SIZE { + (&at_client) + .send_retry(&crate::command::data_mode::SetPeerConfiguration { + parameter: crate::command::data_mode::types::PeerConfigParameter::TlsInBuffer( + size, + ), + }) + .await?; + } + + #[cfg(feature = "internal-network-stack")] + if let Some(size) = C::TLS_OUT_BUFFER_SIZE { + (&at_client) + .send_retry(&crate::command::data_mode::SetPeerConfiguration { + parameter: + crate::command::data_mode::types::PeerConfigParameter::TlsOutBuffer( + size, + ), + }) + .await?; + } + + Ok::<(), Error>(()) + }; + + match embassy_futures::select::select( + setup_fut, + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await + { + Either::First(r) => r?, + Either::Second(_) => unreachable!(), + } + + self.ch.mark_initialized(); + + Ok(()) + } + + #[cfg(feature = "internal-network-stack")] + pub async fn run(&mut self) -> ! { + loop { + if self.init().await.is_err() { + continue; + } + + embassy_futures::select::select( + NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), &self.res_slot), + self.urc_channel, + ) + .run(), + at_bridge(&mut self.transport, &self.req_slot, &mut self.ingress), + ) + .await; + } + } + + #[cfg(feature = "ppp")] + pub async fn run( + &mut self, + stack: &embassy_net::Stack, + ) -> ! { + loop { + if self.init().await.is_err() { + continue; + } + + debug!("Done initializing WiFi module"); + + let network_fut = async { + // Allow control to send/receive AT commands directly on the + // UART, until we are ready to establish connection using PPP + let _ = embassy_futures::select::select( + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + self.ch.wait_connected(), + ) + .await; + + #[cfg(feature = "ppp")] + let ppp_fut = async { + self.ch.wait_for_link_state(state::LinkState::Up).await; + + { + let mut buf = [0u8; 8]; + let mut at_client = SimpleClient::new( + &mut self.transport, + atat::AtDigester::::new(), + &mut buf, + C::AT_CONFIG, + ); + + // Send AT command `ATO3` to enter PPP mode + let res = at_client + .send_retry(&ChangeMode { + mode: data_mode::types::Mode::PPPMode, + }) + .await; + + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + return; + } + + // Drain the UART + let _ = embassy_time::with_timeout(Duration::from_millis(500), async { + loop { + self.transport.read(&mut buf).await.ok(); + } + }) + .await; + } + + info!("RUNNING PPP"); + let _ = self + .ppp_runner + .as_mut() + .unwrap() + .run(&mut self.transport, C::PPP_CONFIG, |ipv4| { + debug!("Running on_ipv4_up for wifi!"); + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = + dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = + embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }) + .await; + + info!("ppp failed"); + }; + + let at_fut = async { + use crate::asynch::at_udp_socket::AtUdpSocket; + use embassy_net::udp::{PacketMetadata, UdpSocket}; + + let mut rx_meta = [PacketMetadata::EMPTY; 1]; + let mut tx_meta = [PacketMetadata::EMPTY; 1]; + let mut socket_rx_buf = [0u8; 64]; + let mut socket_tx_buf = [0u8; 64]; + let mut socket = UdpSocket::new( + stack, + &mut rx_meta, + &mut socket_rx_buf, + &mut tx_meta, + &mut socket_tx_buf, + ); + + socket.bind(AtUdpSocket::PPP_AT_PORT).unwrap(); + let mut at_socket = AtUdpSocket(socket); + + at_bridge(&mut at_socket, self.req_slot, &mut self.ingress).await; + }; + + embassy_futures::select::select(ppp_fut, at_fut).await; + }; + + let device_fut = async { + let _ = NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), self.res_slot), + self.urc_channel, + ) + .run() + .await; + + warn!("Breaking to reboot device"); + }; + + embassy_futures::select::select(device_fut, network_fut).await; + } + } +} diff --git a/src/asynch/state.rs b/src/asynch/state.rs new file mode 100644 index 0000000..4dc3784 --- /dev/null +++ b/src/asynch/state.rs @@ -0,0 +1,216 @@ +#![allow(dead_code)] + +use core::cell::RefCell; +use core::future::poll_fn; +use core::task::{Context, Poll}; + +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use embassy_sync::waitqueue::WakerRegistration; + +use crate::connection::{WiFiState, WifiConnection}; + +/// The link state of a network device. +#[derive(PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum LinkState { + /// Device is not yet initialized. + Uninitialized, + /// The link is down. + Down, + /// The link is up. + Up, +} + +pub(crate) struct State { + shared: Mutex>, +} + +impl State { + pub(crate) const fn new() -> Self { + Self { + shared: Mutex::new(RefCell::new(Shared { + should_connect: false, + link_state: LinkState::Uninitialized, + wifi_connection: WifiConnection::new(), + state_waker: WakerRegistration::new(), + connection_waker: WakerRegistration::new(), + })), + } + } +} + +/// State of the LinkState +pub(crate) struct Shared { + link_state: LinkState, + should_connect: bool, + wifi_connection: WifiConnection, + state_waker: WakerRegistration, + connection_waker: WakerRegistration, +} + +#[derive(Clone)] +pub(crate) struct Runner<'d> { + shared: &'d Mutex>, +} + +impl<'d> Runner<'d> { + pub(crate) fn new(state: &'d mut State) -> Self { + Self { + shared: &state.shared, + } + } + + pub(crate) fn mark_initialized(&self) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = LinkState::Down; + s.state_waker.wake(); + }) + } + + pub(crate) fn mark_uninitialized(&self) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = LinkState::Uninitialized; + s.state_waker.wake(); + }) + } + + pub(crate) fn set_should_connect(&self, should_connect: bool) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.connection_waker.wake(); + s.should_connect = should_connect; + }) + } + + pub(crate) async fn wait_for_initialized(&self) { + if self.link_state(None) != LinkState::Uninitialized { + return; + } + + poll_fn(|cx| { + if self.link_state(Some(cx)) != LinkState::Uninitialized { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn link_state(&self, cx: Option<&mut Context>) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.state_waker.register(cx.waker()); + } + s.link_state + }) + } + + pub(crate) async fn wait_for_link_state(&self, ls: LinkState) { + if self.link_state(None) == ls { + return; + } + + poll_fn(|cx| { + if self.link_state(Some(cx)) == ls { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn update_connection_with(&self, f: impl FnOnce(&mut WifiConnection)) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + f(&mut s.wifi_connection); + info!( + "Connection status changed! Connected: {:?}", + s.wifi_connection.is_connected() + ); + + s.link_state = if s.wifi_connection.is_connected() { + LinkState::Up + } else { + LinkState::Down + }; + + s.state_waker.wake(); + s.connection_waker.wake(); + }) + } + + pub(crate) fn connection_down(&self, cx: Option<&mut Context>) -> bool { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + !s.wifi_connection.ipv4_up && !s.wifi_connection.ipv6_link_local_up + }) + } + + pub(crate) async fn wait_connection_down(&self) { + if self.connection_down(None) { + return; + } + + poll_fn(|cx| { + if self.connection_down(Some(cx)) { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn is_connected(&self, cx: Option<&mut Context>) -> bool { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + s.wifi_connection.is_connected() && s.should_connect + }) + } + + pub(crate) async fn wait_connected(&self) { + if self.is_connected(None) { + return; + } + + poll_fn(|cx| { + if self.is_connected(Some(cx)) { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn wifi_state(&self, cx: Option<&mut Context>) -> WiFiState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + s.wifi_connection.wifi_state + }) + } + + pub(crate) async fn wait_for_wifi_state_change(&self) -> WiFiState { + let old_state = self.wifi_state(None); + + poll_fn(|cx| { + let new_state = self.wifi_state(Some(cx)); + if old_state != new_state { + return Poll::Ready(new_state); + } + Poll::Pending + }) + .await + } +} diff --git a/src/asynch/ublox_stack/device.rs b/src/asynch/ublox_stack/device.rs new file mode 100644 index 0000000..bf728c5 --- /dev/null +++ b/src/asynch/ublox_stack/device.rs @@ -0,0 +1,11 @@ +use core::cell::RefCell; + +use atat::UrcChannel; + +use crate::asynch::{control::ProxyClient, runner::URC_SUBSCRIBERS, state, UbloxUrc}; + +pub struct Device<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + pub(crate) state_ch: state::Runner<'a>, + pub(crate) at_client: RefCell>, + pub(crate) urc_channel: &'a UrcChannel, +} diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs new file mode 100644 index 0000000..00550a2 --- /dev/null +++ b/src/asynch/ublox_stack/dns.rs @@ -0,0 +1,183 @@ +use core::{cell::RefCell, future::poll_fn, task::Poll}; + +use embassy_sync::waitqueue::WakerRegistration; +use embedded_nal_async::AddrType; +use no_std_net::IpAddr; + +use crate::command::ping::types::PingError; + +use super::{SocketStack, UbloxStack}; + +/// Errors returned by DnsSocket. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// Invalid name + InvalidName, + /// Name too long + NameTooLong, + /// Name lookup failed + Failed, +} + +/// From u-connectXpress AT commands manual: +/// depends on the . For internet domain names, the maximum +/// length is 64 characters. +/// Domain name length is 128 for NINA-W13 and NINA-W15 software version 4.0 +/// .0 or later. +#[cfg(not(feature = "nina-w1xx"))] +pub const MAX_DOMAIN_NAME_LENGTH: usize = 64; + +#[cfg(feature = "nina-w1xx")] +pub const MAX_DOMAIN_NAME_LENGTH: usize = 128; + +pub struct DnsTableEntry { + pub domain_name: heapless::String, + pub state: DnsState, + pub waker: WakerRegistration, +} + +#[derive(PartialEq, Clone)] +pub enum DnsState { + New, + Pending, + Resolved(IpAddr), + Error(PingError), +} + +impl DnsTableEntry { + pub const fn new(domain_name: heapless::String) -> Self { + Self { + domain_name, + state: DnsState::New, + waker: WakerRegistration::new(), + } + } +} + +pub struct DnsTable { + pub table: heapless::Deque, +} + +impl DnsTable { + pub const fn new() -> Self { + Self { + table: heapless::Deque::new(), + } + } + pub fn upsert(&mut self, new_entry: DnsTableEntry) { + if let Some(entry) = self + .table + .iter_mut() + .find(|e| e.domain_name == new_entry.domain_name) + { + entry.state = new_entry.state; + return; + } + + if self.table.is_full() { + self.table.pop_front(); + } + unsafe { + self.table.push_back_unchecked(new_entry); + } + } + + pub fn get(&self, domain_name: &str) -> Option<&DnsTableEntry> { + self.table + .iter() + .find(|e| e.domain_name.as_str() == domain_name) + } + + pub fn get_mut(&mut self, domain_name: &str) -> Option<&mut DnsTableEntry> { + self.table + .iter_mut() + .find(|e| e.domain_name.as_str() == domain_name) + } + + pub fn reverse_lookup(&self, ip: IpAddr) -> Option<&str> { + self.table + .iter() + .find(|e| e.state == DnsState::Resolved(ip)) + .map(|e| e.domain_name.as_str()) + } +} + +/// DNS client compatible with the `embedded-nal-async` traits. +/// +/// This exists only for compatibility with crates that use `embedded-nal-async`. +/// Prefer using [`Stack::dns_query`](crate::Stack::dns_query) directly if you're +/// not using `embedded-nal-async`. +pub struct DnsSocket<'a> { + stack: &'a RefCell, +} + +impl<'a> DnsSocket<'a> { + /// Create a new DNS socket using the provided stack. + pub fn new( + stack: &'a UbloxStack, + ) -> Self { + Self { + stack: &stack.socket, + } + } + + /// Make a query for a given name and return the corresponding IP addresses. + pub async fn query(&self, name: &str, addr_type: AddrType) -> Result { + match addr_type { + AddrType::IPv4 => { + if let Ok(ip) = name.parse().map(IpAddr::V4) { + return Ok(ip); + } + } + AddrType::IPv6 => { + if let Ok(ip) = name.parse().map(IpAddr::V6) { + return Ok(ip); + } + } + _ => {} + } + + let name_string = heapless::String::try_from(name).map_err(|_| Error::NameTooLong)?; + + { + let mut s = self.stack.borrow_mut(); + s.dns_table.upsert(DnsTableEntry::new(name_string.clone())); + s.waker.wake(); + } + + poll_fn(|cx| { + let mut s = self.stack.borrow_mut(); + let query = s.dns_table.get_mut(&name_string).unwrap(); + match query.state { + DnsState::Resolved(ip) => Poll::Ready(Ok(ip)), + DnsState::Error(_e) => Poll::Ready(Err(Error::Failed)), + _ => { + query.waker.register(cx.waker()); + Poll::Pending + } + } + }) + .await + } +} + +impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { + type Error = Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: AddrType, + ) -> Result { + self.query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } +} diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs new file mode 100644 index 0000000..07247b6 --- /dev/null +++ b/src/asynch/ublox_stack/mod.rs @@ -0,0 +1,529 @@ +#[cfg(feature = "socket-tcp")] +pub mod tcp; +#[cfg(feature = "socket-tcp")] +pub mod tls; +#[cfg(feature = "socket-udp")] +pub mod udp; + +mod device; +pub mod dns; +mod peer_builder; + +pub use device::Device; + +use core::cell::RefCell; +use core::future::poll_fn; +use core::ops::{DerefMut, Rem}; +use core::task::Poll; + +use crate::command::data_mode::responses::ConnectPeerResponse; +use crate::command::data_mode::urc::PeerDisconnected; +use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; +use crate::command::edm::types::{DataEvent, Protocol}; +use crate::command::edm::urc::EdmEvent; +use crate::command::edm::{EdmAtCmdWrapper, EdmDataCommand}; +use crate::command::ping::types::PingError; +use crate::command::ping::urc::{PingErrorResponse, PingResponse}; +use crate::command::ping::Ping; +use crate::command::Urc; +use peer_builder::{PeerUrlBuilder, SecurityCredentials}; + +use self::dns::{DnsSocket, DnsState, DnsTable}; + +use super::control::ProxyClient; + +use embassy_futures::select; +use embassy_sync::waitqueue::WakerRegistration; +use embassy_time::{Duration, Ticker}; +use embedded_nal_async::SocketAddr; +use no_std_net::IpAddr; +use portable_atomic::{AtomicBool, AtomicU8, Ordering}; +use ublox_sockets::{ + AnySocket, ChannelId, PeerHandle, Socket, SocketHandle, SocketSet, SocketStorage, +}; + +#[cfg(feature = "socket-tcp")] +use ublox_sockets::TcpState; + +#[cfg(feature = "socket-udp")] +use ublox_sockets::UdpState; + +const MAX_EGRESS_SIZE: usize = 2048; + +pub struct StackResources { + sockets: [SocketStorage<'static>; SOCK], +} + +impl Default for StackResources { + fn default() -> Self { + Self::new() + } +} + +impl StackResources { + pub fn new() -> Self { + Self { + sockets: [SocketStorage::EMPTY; SOCK], + } + } +} + +pub struct UbloxStack { + socket: RefCell, + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, + last_tx_socket: AtomicU8, + should_tx: AtomicBool, +} + +pub(crate) struct SocketStack { + sockets: SocketSet<'static>, + waker: WakerRegistration, + dns_table: DnsTable, + dropped_sockets: heapless::Vec, + credential_map: heapless::FnvIndexMap, +} + +impl + UbloxStack +{ + pub fn new( + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, + resources: &'static mut StackResources, + ) -> Self { + let sockets = SocketSet::new(&mut resources.sockets[..]); + + let socket = SocketStack { + sockets, + dns_table: DnsTable::new(), + waker: WakerRegistration::new(), + dropped_sockets: heapless::Vec::new(), + credential_map: heapless::IndexMap::new(), + }; + + Self { + socket: RefCell::new(socket), + device, + last_tx_socket: AtomicU8::new(0), + should_tx: AtomicBool::new(false), + } + } + + pub async fn run(&self) -> ! { + let mut tx_buf = [0u8; MAX_EGRESS_SIZE]; + + let Device { + urc_channel, + state_ch, + at_client, + } = &self.device; + + let mut urc_subscription = urc_channel.subscribe().unwrap(); + + loop { + // FIXME: It feels like this can be written smarter/simpler? + let should_tx = poll_fn(|cx| match self.should_tx.load(Ordering::Relaxed) { + true => { + self.should_tx.store(false, Ordering::Relaxed); + Poll::Ready(()) + } + false => { + self.should_tx.store(true, Ordering::Relaxed); + self.socket.borrow_mut().waker.register(cx.waker()); + Poll::<()>::Pending + } + }); + + let ticker = Ticker::every(Duration::from_millis(100)); + futures_util::pin_mut!(ticker); + + match select::select3( + urc_subscription.next_message_pure(), + should_tx, + ticker.next(), + ) + .await + { + select::Either3::First(event) => { + Self::socket_rx(event, &self.socket); + } + select::Either3::Second(_) | select::Either3::Third(_) => { + if let Some(ev) = self.tx_event(&mut tx_buf) { + Self::socket_tx(ev, &self.socket, &at_client).await; + } + } + } + } + } + + /// Make a query for a given name and return the corresponding IP addresses. + // #[cfg(feature = "dns")] + pub async fn dns_query( + &self, + name: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(self).query(name, addr_type).await + } + + fn socket_rx(event: EdmEvent, socket: &RefCell) { + match event { + EdmEvent::IPv4ConnectEvent(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, socket); + } + EdmEvent::IPv6ConnectEvent(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, socket); + } + EdmEvent::DisconnectEvent(channel_id) => { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.edm_channel == Some(channel_id) => { + udp.edm_channel = None; + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.edm_channel == Some(channel_id) => { + tcp.edm_channel = None; + break; + } + _ => {} + } + } + } + EdmEvent::DataEvent(DataEvent { channel_id, data }) => { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) + if udp.edm_channel == Some(channel_id) => + // FIXME: + // if udp.edm_channel == Some(channel_id) && udp.may_recv() => + { + let n = udp.rx_enqueue_slice(&data); + if n < data.len() { + error!( + "[{}] UDP RX data overflow! Discarding {} bytes", + udp.peer_handle, + data.len() - n + ); + } + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) + if tcp.edm_channel == Some(channel_id) && tcp.may_recv() => + { + let n = tcp.rx_enqueue_slice(&data); + if n < data.len() { + error!( + "[{}] TCP RX data overflow! Discarding {} bytes", + tcp.peer_handle, + data.len() - n + ); + } + break; + } + _ => {} + } + } + } + EdmEvent::ATEvent(Urc::PeerDisconnected(PeerDisconnected { handle })) => { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.peer_handle == Some(handle) => { + udp.peer_handle = None; + // FIXME: + // udp.set_state(UdpState::TimeWait); + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.peer_handle == Some(handle) => { + tcp.peer_handle = None; + tcp.set_state(TcpState::TimeWait); + break; + } + _ => {} + } + } + } + EdmEvent::ATEvent(Urc::PingResponse(PingResponse { + ip, hostname, rtt, .. + })) => { + let mut s = socket.borrow_mut(); + if let Some(query) = s.dns_table.get_mut(&hostname) { + match query.state { + DnsState::Pending if rtt == -1 => { + // According to AT manual, rtt = -1 means the PING has timed out + query.state = DnsState::Error(PingError::Timeout); + query.waker.wake(); + } + DnsState::Pending => { + query.state = DnsState::Resolved(ip); + query.waker.wake(); + } + _ => {} + } + } + } + EdmEvent::ATEvent(Urc::PingErrorResponse(PingErrorResponse { error })) => { + let mut s = socket.borrow_mut(); + for query in s.dns_table.table.iter_mut() { + match query.state { + DnsState::Pending => { + query.state = DnsState::Error(error); + query.waker.wake(); + } + _ => {} + } + } + } + _ => {} + } + } + + fn tx_event<'data>(&self, buf: &'data mut [u8]) -> Option> { + let mut s = self.socket.borrow_mut(); + for query in s.dns_table.table.iter_mut() { + if let DnsState::New = query.state { + query.state = DnsState::Pending; + buf[..query.domain_name.len()].copy_from_slice(query.domain_name.as_bytes()); + return Some(TxEvent::Dns { + hostname: core::str::from_utf8(&buf[..query.domain_name.len()]).unwrap(), + }); + } + } + + // Handle delayed close-by-drop here + if let Some(dropped_peer_handle) = s.dropped_sockets.pop() { + warn!("Handling dropped socket {}", dropped_peer_handle); + return Some(TxEvent::Close { + peer_handle: dropped_peer_handle, + }); + } + + // Make sure to give all sockets an even opportunity to TX + // let skip = self + // .last_tx_socket + // .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| { + // let next = v + 1; + // Some(next.rem(s.sockets.sockets.len() as u8)) + // }) + // .unwrap(); + let skip = 0; + + let SocketStack { + sockets, + dns_table, + credential_map, + .. + } = s.deref_mut(); + + for (handle, socket) in sockets.iter_mut().skip(skip as usize) { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(_udp) => todo!(), + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) => { + tcp.poll(); + + match tcp.state() { + TcpState::Closed => { + if let Some(addr) = tcp.remote_endpoint() { + let mut builder = PeerUrlBuilder::new(); + + if let Some(hostname) = dns_table.reverse_lookup(addr.ip()) { + builder.hostname(hostname).port(addr.port()) + } else { + builder.address(&addr) + }; + + if let Some(creds) = credential_map.get(&handle) { + info!("Found credentials {} for {}", creds, handle); + builder.creds(creds); + } + + let url = + builder.set_local_port(tcp.local_port).tcp::<128>().unwrap(); + + // FIXME: Write directly into `buf` instead + buf[..url.len()].copy_from_slice(url.as_bytes()); + + return Some(TxEvent::Connect { + socket_handle: handle, + url: core::str::from_utf8(&buf[..url.len()]).unwrap(), + }); + } + } + // We transmit data in all states where we may have data in the buffer, + // or the transmit half of the connection is still open. + TcpState::Established | TcpState::CloseWait | TcpState::LastAck => { + if let Some(edm_channel) = tcp.edm_channel { + return tcp.tx_dequeue(|payload| { + let len = core::cmp::min(payload.len(), MAX_EGRESS_SIZE); + let res = if len != 0 { + buf[..len].copy_from_slice(&payload[..len]); + Some(TxEvent::Send { + edm_channel, + data: &buf[..len], + }) + } else { + None + }; + + (len, res) + }); + } + } + TcpState::FinWait1 => { + return Some(TxEvent::Close { + peer_handle: tcp.peer_handle.unwrap(), + }); + } + TcpState::Listen => todo!(), + TcpState::SynReceived => todo!(), + _ => {} + }; + } + _ => {} + }; + } + + None + } + + async fn socket_tx<'data>( + ev: TxEvent<'data>, + socket: &RefCell, + at_client: &RefCell>, + ) { + use atat::asynch::AtatClient; + + let mut at = at_client.borrow_mut(); + match ev { + TxEvent::Connect { socket_handle, url } => { + match at + .send_retry(&EdmAtCmdWrapper(ConnectPeer { url: &url })) + .await + { + Ok(ConnectPeerResponse { peer_handle }) => { + let mut s = socket.borrow_mut(); + let tcp = s + .sockets + .get_mut::(socket_handle); + tcp.peer_handle = Some(peer_handle); + tcp.set_state(TcpState::SynSent); + } + Err(e) => { + error!("Failed to connect?! {}", e) + } + } + } + TxEvent::Send { edm_channel, data } => { + warn!("Sending {} bytes on {}", data.len(), edm_channel); + at.send_retry(&EdmDataCommand { + channel: edm_channel, + data, + }) + .await + .ok(); + } + TxEvent::Close { peer_handle } => { + at.send_retry(&EdmAtCmdWrapper(ClosePeerConnection { peer_handle })) + .await + .ok(); + } + TxEvent::Dns { hostname } => { + match at + .send_retry(&EdmAtCmdWrapper(Ping { + hostname: &hostname, + retry_num: 1, + })) + .await + { + Ok(_) => {} + Err(_) => { + let mut s = socket.borrow_mut(); + if let Some(query) = s.dns_table.get_mut(&hostname) { + match query.state { + DnsState::Pending => { + query.state = DnsState::Error(PingError::Other); + query.waker.wake(); + } + _ => {} + } + } + } + } + } + } + } + + fn connect_event( + channel_id: ChannelId, + protocol: Protocol, + endpoint: SocketAddr, + socket: &RefCell, + ) { + let mut s = socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match protocol { + #[cfg(feature = "socket-tcp")] + Protocol::TCP => match ublox_sockets::tcp::Socket::downcast_mut(socket) { + Some(tcp) if tcp.remote_endpoint == Some(endpoint) => { + tcp.edm_channel = Some(channel_id); + tcp.set_state(TcpState::Established); + break; + } + _ => {} + }, + #[cfg(feature = "socket-udp")] + Protocol::UDP => match ublox_sockets::udp::Socket::downcast_mut(socket) { + Some(udp) if udp.endpoint == Some(endpoint) => { + udp.edm_channel = Some(channel_id); + udp.set_state(UdpState::Established); + break; + } + _ => {} + }, + _ => {} + } + } + } +} + +// TODO: This extra data clone step can probably be avoided by adding a +// waker/context based API to ATAT. +enum TxEvent<'data> { + Connect { + socket_handle: SocketHandle, + url: &'data str, + }, + Send { + edm_channel: ChannelId, + data: &'data [u8], + }, + Close { + peer_handle: PeerHandle, + }, + Dns { + hostname: &'data str, + }, +} + +#[cfg(feature = "defmt")] +impl defmt::Format for TxEvent<'_> { + fn format(&self, fmt: defmt::Formatter) { + match self { + TxEvent::Connect { .. } => defmt::write!(fmt, "TxEvent::Connect"), + TxEvent::Send { .. } => defmt::write!(fmt, "TxEvent::Send"), + TxEvent::Close { .. } => defmt::write!(fmt, "TxEvent::Close"), + TxEvent::Dns { .. } => defmt::write!(fmt, "TxEvent::Dns"), + } + } +} diff --git a/ublox-short-range/src/wifi/peer_builder.rs b/src/asynch/ublox_stack/peer_builder.rs similarity index 53% rename from ublox-short-range/src/wifi/peer_builder.rs rename to src/asynch/ublox_stack/peer_builder.rs index 1ad56a0..2e65114 100644 --- a/ublox-short-range/src/wifi/peer_builder.rs +++ b/src/asynch/ublox_stack/peer_builder.rs @@ -1,16 +1,22 @@ -use crate::{client::SecurityCredentials, error::Error}; +use crate::error::Error; use core::fmt::Write; -/// Handles receiving data from sockets -/// implements TCP and UDP for WiFi client -use embedded_nal::{IpAddr, SocketAddr}; use heapless::String; +use no_std_net::{IpAddr, SocketAddr}; + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SecurityCredentials { + pub ca_cert_name: heapless::String<16>, + pub c_cert_name: heapless::String<16>, + pub c_key_name: heapless::String<16>, +} #[derive(Default)] pub(crate) struct PeerUrlBuilder<'a> { hostname: Option<&'a str>, ip_addr: Option, port: Option, - creds: Option, + creds: Option<&'a SecurityCredentials>, local_port: Option, } @@ -20,7 +26,7 @@ impl<'a> PeerUrlBuilder<'a> { Self::default() } - pub fn write_domain(&self, s: &mut String<128>) -> Result<(), Error> { + fn write_domain(&self, s: &mut String) -> Result<(), Error> { let port = self.port.ok_or(Error::Network)?; let addr = self .ip_addr @@ -32,45 +38,42 @@ impl<'a> PeerUrlBuilder<'a> { addr.xor(host).ok_or(Error::Network) } - pub fn udp(&self) -> Result, Error> { + pub fn udp(&self) -> Result, Error> { let mut s = String::new(); - write!(&mut s, "udp://").ok(); + write!(&mut s, "udp://").map_err(|_| Error::Overflow)?; self.write_domain(&mut s)?; // Start writing query parameters - write!(&mut s, "?").ok(); - self.local_port - .map(|v| write!(&mut s, "local_port={}&", v).ok()); + write!(&mut s, "?").map_err(|_| Error::Overflow)?; + + if let Some(v) = self.local_port { + write!(&mut s, "local_port={}&", v).map_err(|_| Error::Overflow)?; + } + // Remove trailing '&' or '?' if no query. s.pop(); Ok(s) } - pub fn tcp(&mut self) -> Result, Error> { + pub fn tcp(&mut self) -> Result, Error> { let mut s = String::new(); - write!(&mut s, "tcp://").ok(); + write!(&mut s, "tcp://").map_err(|_| Error::Overflow)?; self.write_domain(&mut s)?; // Start writing query parameters - write!(&mut s, "?").ok(); - self.local_port - .map(|v| write!(&mut s, "local_port={}&", v).ok()); + write!(&mut s, "?").map_err(|_| Error::Overflow)?; - if let Some(creds) = self.creds.as_ref() { - creds - .ca_cert_name - .as_ref() - .map(|v| write!(&mut s, "ca={}&", v).ok()); - creds - .c_cert_name - .as_ref() - .map(|v| write!(&mut s, "cert={}&", v).ok()); - creds - .c_key_name - .as_ref() - .map(|v| write!(&mut s, "privKey={}&", v).ok()); + if let Some(v) = self.local_port { + write!(&mut s, "local_port={}&", v).map_err(|_| Error::Overflow)?; } + + if let Some(creds) = self.creds.as_ref() { + write!(&mut s, "ca={}&", creds.ca_cert_name).map_err(|_| Error::Overflow)?; + write!(&mut s, "cert={}&", creds.c_cert_name).map_err(|_| Error::Overflow)?; + write!(&mut s, "privKey={}&", creds.c_key_name).map_err(|_| Error::Overflow)?; + }; + // Remove trailing '&' or '?' if no query. s.pop(); @@ -86,19 +89,34 @@ impl<'a> PeerUrlBuilder<'a> { self } + pub fn set_hostname(&mut self, hostname: Option<&'a str>) -> &mut Self { + self.hostname = hostname; + self + } + /// maximum length 64 pub fn ip_addr(&mut self, ip_addr: IpAddr) -> &mut Self { self.ip_addr.replace(ip_addr); self } + pub fn set_ip_addr(&mut self, ip_addr: Option) -> &mut Self { + self.ip_addr = ip_addr; + self + } + /// port number pub fn port(&mut self, port: u16) -> &mut Self { self.port.replace(port); self } - pub fn creds(&mut self, creds: SecurityCredentials) -> &mut Self { + pub fn set_port(&mut self, port: Option) -> &mut Self { + self.port = port; + self + } + + pub fn creds(&mut self, creds: &'a SecurityCredentials) -> &mut Self { self.creds.replace(creds); self } @@ -107,6 +125,11 @@ impl<'a> PeerUrlBuilder<'a> { self.local_port.replace(local_port); self } + + pub fn set_local_port(&mut self, local_port: Option) -> &mut Self { + self.local_port = local_port; + self + } } #[cfg(test)] @@ -116,7 +139,10 @@ mod test { #[test] fn udp_ipv4_url() { let address = "192.168.0.1:8080".parse().unwrap(); - let url = PeerUrlBuilder::new().address(&address).udp().unwrap(); + let url = PeerUrlBuilder::new() + .address(&address) + .udp::<128>() + .unwrap(); assert_eq!(url, "udp://192.168.0.1:8080/"); } @@ -125,7 +151,10 @@ mod test { let address = "[FE80:0000:0000:0000:0202:B3FF:FE1E:8329]:8080" .parse() .unwrap(); - let url = PeerUrlBuilder::new().address(&address).udp().unwrap(); + let url = PeerUrlBuilder::new() + .address(&address) + .udp::<128>() + .unwrap(); assert_eq!(url, "udp://[fe80::202:b3ff:fe1e:8329]:8080/"); } @@ -135,7 +164,7 @@ mod test { .hostname("example.org") .port(2000) .local_port(2001) - .udp() + .udp::<128>() .unwrap(); assert_eq!(url, "udp://example.org:2000/?local_port=2001"); } @@ -145,13 +174,14 @@ mod test { let url = PeerUrlBuilder::new() .hostname("example.org") .port(2000) - .creds(SecurityCredentials { - c_cert_name: Some(heapless::String::from("client.crt")), - ca_cert_name: Some(heapless::String::from("ca.crt")), - c_key_name: Some(heapless::String::from("client.key")), + .creds(&SecurityCredentials { + c_cert_name: heapless::String::try_from("client.crt").unwrap(), + ca_cert_name: heapless::String::try_from("ca.crt").unwrap(), + c_key_name: heapless::String::try_from("client.key").unwrap(), }) - .tcp() + .tcp::<128>() .unwrap(); + assert_eq!( url, "tcp://example.org:2000/?ca=ca.crt&cert=client.crt&privKey=client.key" diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs new file mode 100644 index 0000000..8706313 --- /dev/null +++ b/src/asynch/ublox_stack/tcp.rs @@ -0,0 +1,831 @@ +use core::cell::RefCell; +use core::future::poll_fn; +use core::mem; +use core::task::Poll; + +use embassy_time::Duration; +use embedded_nal_async::SocketAddr; +use ublox_sockets::{tcp, SocketHandle, TcpState}; + +use super::{SocketStack, UbloxStack}; + +/// Error returned by TcpSocket read/write functions. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// The connection was reset. + /// + /// This can happen on receiving a RST packet, or on timeout. + ConnectionReset, +} + +/// Error returned by [`TcpSocket::connect`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConnectError { + /// The socket is already connected or listening. + InvalidState, + /// The remote host rejected the connection with a RST packet. + ConnectionReset, + /// Connect timed out. + TimedOut, + /// No route to host. + NoRoute, +} + +/// Error returned by [`TcpSocket::accept`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AcceptError { + /// The socket is already connected or listening. + InvalidState, + /// Invalid listen port + InvalidPort, + /// The remote host rejected the connection with a RST packet. + ConnectionReset, +} + +/// A TCP socket. +pub struct TcpSocket<'a> { + pub(crate) io: TcpIo<'a>, +} + +/// The reader half of a TCP socket. +pub struct TcpReader<'a> { + pub(crate) io: TcpIo<'a>, +} + +/// The writer half of a TCP socket. +pub struct TcpWriter<'a> { + pub(crate) io: TcpIo<'a>, +} + +impl<'a> TcpReader<'a> { + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.read_with(f).await + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn recv_capacity(&self) -> usize { + self.io.recv_capacity() + } +} + +impl<'a> TcpWriter<'a> { + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TcpSocket::abort) it will wait for the TCP RST packet to be sent. + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.write_with(f).await + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.io.send_capacity() + } +} + +impl<'a> TcpSocket<'a> { + /// Create a new TCP socket on the given stack, with the given buffers. + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + ) -> Self { + let s = &mut *stack.socket.borrow_mut(); + let rx_buffer: &'static mut [u8] = unsafe { mem::transmute(rx_buffer) }; + let tx_buffer: &'static mut [u8] = unsafe { mem::transmute(tx_buffer) }; + let handle = s.sockets.add(tcp::Socket::new( + tcp::SocketBuffer::new(rx_buffer), + tcp::SocketBuffer::new(tx_buffer), + )); + + Self { + io: TcpIo { + stack: &stack.socket, + handle, + }, + } + } + + /// Return the maximum number of bytes inside the recv buffer. + pub fn recv_capacity(&self) -> usize { + self.io.recv_capacity() + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.io.send_capacity() + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.write_with(f).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.read_with(f).await + } + + /// Split the socket into reader and a writer halves. + pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { + (TcpReader { io: self.io }, TcpWriter { io: self.io }) + } + + /// Connect to a remote host. + pub async fn connect(&mut self, remote_endpoint: T) -> Result<(), ConnectError> + where + T: Into, + { + match { self.io.with_mut(|s| s.connect(remote_endpoint, None)) } { + Ok(()) => {} + Err(_) => return Err(ConnectError::InvalidState), + // Err(tcp::ConnectError::Unaddressable) => return Err(ConnectError::NoRoute), + } + + poll_fn(|cx| { + self.io.with_mut(|s| match s.state() { + tcp::State::TimeWait => Poll::Ready(Err(ConnectError::ConnectionReset)), + tcp::State::Listen => unreachable!(), + tcp::State::Closed | tcp::State::SynSent | tcp::State::SynReceived => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + _ => Poll::Ready(Ok(())), + }) + }) + .await + } + + // /// Accept a connection from a remote host. + // /// + // /// This function puts the socket in listening mode, and waits until a connection is received. + // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + // where + // T: Into, + // { + // todo!() + // // match self.io.with_mut(|s, _| s.listen(local_endpoint)) { + // // Ok(()) => {} + // // Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), + // // Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), + // // } + + // // poll_fn(|cx| { + // // self.io.with_mut(|s, _| match s.state() { + // // tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + // // s.register_send_waker(cx.waker()); + // // Poll::Pending + // // } + // // _ => Poll::Ready(Ok(())), + // // }) + // // }) + // // .await + // } + + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TcpSocket::abort) it will wait for the TCP RST packet to be sent. + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } + + /// Set the timeout for the socket. + /// + /// If the timeout is set, the socket will be closed if no data is received for the + /// specified duration. + pub fn set_timeout(&mut self, _duration: Option) { + todo!() + // self.io.with_mut(|s| s.set_timeout(duration)) + } + + /// Set the keep-alive interval for the socket. + /// + /// If the keep-alive interval is set, the socket will send keep-alive packets after + /// the specified duration of inactivity. + /// + /// If not set, the socket will not send keep-alive packets. + pub fn set_keep_alive(&mut self, _interval: Option) { + todo!() + // self.io + // .with_mut(|s| s.set_keep_alive(interval.map(duration_to_smoltcp))) + } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.io.with_mut(|s| s.set_hop_limit(hop_limit)) + // } + + /// Get the local endpoint of the socket. + /// + /// Returns `None` if the socket is not bound (listening) or not connected. + pub fn local_endpoint(&self) -> Option { + todo!() + // self.io.with(|s| s.local_endpoint()) + } + + /// Get the remote endpoint of the socket. + /// + /// Returns `None` if the socket is not connected. + pub fn remote_endpoint(&self) -> Option { + self.io.with(|s| s.remote_endpoint()) + } + + /// Get the state of the socket. + pub fn state(&self) -> TcpState { + self.io.with(|s| s.state()) + } + + /// Close the write half of the socket. + /// + /// This closes only the write half of the socket. The read half side remains open, the + /// socket can still receive data. + /// + /// Data that has been written to the socket and not yet sent (or not yet ACKed) will still + /// still sent. The last segment of the pending to send data is sent with the FIN flag set. + pub fn close(&mut self) { + self.io.with_mut(|s| s.close()) + } + + /// Forcibly close the socket. + /// + /// This instantly closes both the read and write halves of the socket. Any pending data + /// that has not been sent will be lost. + /// + /// Note that the TCP RST packet is not sent immediately - if the `TcpSocket` is dropped too soon + /// the remote host may not know the connection has been closed. + /// `abort()` callers should wait for a [`flush()`](TcpSocket::flush) call to complete before + /// dropping or reusing the socket. + pub fn abort(&mut self) { + self.io.with_mut(|s| s.abort()) + } + + /// Get whether the socket is ready to send data, i.e. whether there is space in the send buffer. + pub fn may_send(&self) -> bool { + self.io.with(|s| s.may_send()) + } + + /// return whether the receive half of the full-duplex connection is open. + /// This function returns true if it’s possible to receive data from the remote endpoint. + /// It will return true while there is data in the receive buffer, and if there isn’t, + /// as long as the remote endpoint has not closed the connection. + pub fn may_recv(&self) -> bool { + self.io.with(|s| s.may_recv()) + } + + /// Get whether the socket is ready to receive data, i.e. whether there is some pending data in the receive buffer. + pub fn can_recv(&self) -> bool { + self.io.with(|s| s.can_recv()) + } +} + +impl<'a> Drop for TcpSocket<'a> { + fn drop(&mut self) { + if matches!( + self.state(), + TcpState::Listen | TcpState::Established | TcpState::FinWait1 + ) { + if let Some(peer_handle) = self.io.with(|s| s.peer_handle) { + self.io + .stack + .borrow_mut() + .dropped_sockets + .push(peer_handle) + .ok(); + } + } + let mut stack = self.io.stack.borrow_mut(); + stack.sockets.remove(self.io.handle); + stack.waker.wake(); + } +} + +// ======================= + +#[derive(Copy, Clone)] +pub(crate) struct TcpIo<'a> { + pub(crate) stack: &'a RefCell, + pub(crate) handle: SocketHandle, +} + +impl<'d> TcpIo<'d> { + fn with(&self, f: impl FnOnce(&tcp::Socket) -> R) -> R { + let s = &*self.stack.borrow(); + let socket = s.sockets.get::(self.handle); + f(socket) + } + + fn with_mut(&mut self, f: impl FnOnce(&mut tcp::Socket) -> R) -> R { + let s = &mut *self.stack.borrow_mut(); + let socket = s.sockets.get_mut::(self.handle); + let res = f(socket); + s.waker.wake(); + res + } + + async fn read(&mut self, buf: &mut [u8]) -> Result { + poll_fn(move |cx| { + // CAUTION: smoltcp semantics around EOF are different to what you'd expect + // from posix-like IO, so we have to tweak things here. + self.with_mut(|s| match s.recv_slice(buf) { + // No data ready + Ok(0) if buf.is_empty() => { + // embedded_io_async::Read's contract is to not block if buf is empty. While + // this function is not a direct implementor of the trait method, we still don't + // want our future to never resolve. + Poll::Ready(Ok(0)) + } + // No data ready + Ok(0) => { + s.register_recv_waker(cx.waker()); + Poll::Pending + } + // Data ready! + Ok(n) => Poll::Ready(Ok(n)), + // EOF + Err(_) => Poll::Ready(Ok(0)), + // FIXME: + // Err(tcp::RecvError::Finished) => Poll::Ready(Ok(0)), + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::RecvError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + async fn write(&mut self, buf: &[u8]) -> Result { + poll_fn(move |cx| { + self.with_mut(|s| match s.send_slice(buf) { + // Not ready to send (no space in the tx buffer) + Ok(0) => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + // Some data sent + Ok(n) => Poll::Ready(Ok(n)), + // Connection reset. TODO: this can also be timeouts etc, investigate. + Err(_) => Poll::Ready(Err(Error::ConnectionReset)), + // FIXME: + // Err(tcp::SendError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + let mut f = Some(f); + + poll_fn(move |cx| { + self.with_mut(|s| { + if !s.can_send() { + if s.may_send() { + // socket buffer is full wait until it has atleast one byte free + s.register_send_waker(cx.waker()); + Poll::Pending + } else { + // if we can't transmit because the transmit half of the duplex connection is closed then return an error + Poll::Ready(Err(Error::ConnectionReset)) + } + } else { + Poll::Ready(match s.send(f.take().unwrap()) { + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::SendError::InvalidState) => Err(Error::ConnectionReset), + Err(_) => Err(Error::ConnectionReset), + Ok(r) => Ok(r), + }) + } + }) + }) + .await + } + + async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + let mut f = Some(f); + poll_fn(move |cx| { + self.with_mut(|s| { + if !s.can_recv() { + if s.may_recv() { + // socket buffer is empty wait until it has atleast one byte has arrived + s.register_recv_waker(cx.waker()); + Poll::Pending + } else { + // if we can't receive because the receive half of the duplex connection is closed then return an error + Poll::Ready(Err(Error::ConnectionReset)) + } + } else { + Poll::Ready(match s.recv(f.take().unwrap()) { + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::RecvError::Finished) | Err(tcp::RecvError::InvalidState) => { + // Err(Error::ConnectionReset) + // } + Err(_) => Err(Error::ConnectionReset), + Ok(r) => Ok(r), + }) + } + }) + }) + .await + } + + async fn flush(&mut self) -> Result<(), Error> { + poll_fn(move |cx| { + self.with_mut(|s| { + // If there are outstanding send operations, register for wake up and wait + // smoltcp issues wake-ups when octets are dequeued from the send buffer + if s.send_queue() > 0 { + s.register_send_waker(cx.waker()); + Poll::Pending + // No outstanding sends, socket is flushed + } else { + Poll::Ready(Ok(())) + } + }) + }) + .await + } + + fn recv_capacity(&self) -> usize { + self.with(|s| s.recv_capacity()) + } + + fn send_capacity(&self) -> usize { + self.with(|s| s.send_capacity()) + } +} + +mod embedded_io_impls { + use super::*; + + impl embedded_io_async::Error for ConnectError { + fn kind(&self) -> embedded_io_async::ErrorKind { + match self { + ConnectError::ConnectionReset => embedded_io_async::ErrorKind::ConnectionReset, + ConnectError::TimedOut => embedded_io_async::ErrorKind::TimedOut, + ConnectError::NoRoute => embedded_io_async::ErrorKind::NotConnected, + ConnectError::InvalidState => embedded_io_async::ErrorKind::Other, + } + } + } + + impl embedded_io_async::Error for Error { + fn kind(&self) -> embedded_io_async::ErrorKind { + match self { + Error::ConnectionReset => embedded_io_async::ErrorKind::ConnectionReset, + } + } + } + + impl<'d> embedded_io_async::ErrorType for TcpSocket<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Read for TcpSocket<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + } + + impl<'d> embedded_io_async::ReadReady for TcpSocket<'d> { + fn read_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_recv())) + } + } + + impl<'d> embedded_io_async::Write for TcpSocket<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.io.flush().await + } + } + + impl<'d> embedded_io_async::WriteReady for TcpSocket<'d> { + fn write_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_send())) + } + } + + impl<'d> embedded_io_async::ErrorType for TcpReader<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Read for TcpReader<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + } + + impl<'d> embedded_io_async::ReadReady for TcpReader<'d> { + fn read_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_recv())) + } + } + + impl<'d> embedded_io_async::ErrorType for TcpWriter<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Write for TcpWriter<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.io.flush().await + } + } + + impl<'d> embedded_io_async::WriteReady for TcpWriter<'d> { + fn write_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_send())) + } + } +} + +/// TCP client compatible with `embedded-nal-async` traits. +pub mod client { + use core::cell::{Cell, UnsafeCell}; + use core::mem::MaybeUninit; + use core::ptr::NonNull; + + use crate::asynch::ublox_stack::dns::DnsSocket; + + use super::*; + + /// TCP client connection pool compatible with `embedded-nal-async` traits. + /// + /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. + pub struct TcpClient< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize = 1024, + const RX_SZ: usize = 1024, + > { + pub(crate) stack: &'d UbloxStack, + pub(crate) state: &'d TcpClientState, + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::Dns + for TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + type Error = crate::asynch::ublox_stack::dns::Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(self.stack).query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: no_std_net::IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + /// Create a new `TcpClient`. + pub fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + ) -> Self { + Self { stack, state } + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::TcpConnect + for TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + type Error = Error; + type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; + + async fn connect<'a>( + &'a self, + remote: SocketAddr, + ) -> Result, Self::Error> { + let remote_endpoint = (remote.ip(), remote.port()); + let mut socket = TcpConnection::new(self.stack, self.state)?; + socket + .socket + .connect(remote_endpoint) + .await + .map_err(|_| Error::ConnectionReset)?; + Ok(socket) + } + } + + /// Opened TCP connection in a [`TcpClient`]. + pub struct TcpConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { + socket: TcpSocket<'d>, + state: &'d TcpClientState, + bufs: NonNull<([u8; TX_SZ], [u8; RX_SZ])>, + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> + TcpConnection<'d, N, TX_SZ, RX_SZ> + { + fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + ) -> Result { + let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; + Ok(Self { + socket: unsafe { + TcpSocket::new(stack, &mut bufs.as_mut().1, &mut bufs.as_mut().0) + }, + state, + bufs, + }) + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + fn drop(&mut self) { + unsafe { + self.socket.close(); + self.state.pool.free(self.bufs); + } + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::ErrorType + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + type Error = Error; + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Read + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.socket.read(buf).await + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Write + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + async fn write(&mut self, buf: &[u8]) -> Result { + self.socket.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.socket.flush().await + } + } + + /// State for TcpClient + pub struct TcpClientState { + pub(crate) pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, + } + + impl TcpClientState { + /// Create a new `TcpClientState`. + pub const fn new() -> Self { + Self { pool: Pool::new() } + } + } + + pub(crate) struct Pool { + used: [Cell; N], + data: [UnsafeCell>; N], + } + + impl Pool { + const VALUE: Cell = Cell::new(false); + const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); + + pub(crate) const fn new() -> Self { + Self { + used: [Self::VALUE; N], + data: [Self::UNINIT; N], + } + } + } + + impl Pool { + pub(crate) fn alloc(&self) -> Option> { + for n in 0..N { + // this can't race because Pool is not Sync. + if !self.used[n].get() { + self.used[n].set(true); + let p = self.data[n].get() as *mut T; + return Some(unsafe { NonNull::new_unchecked(p) }); + } + } + None + } + + /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. + pub(crate) unsafe fn free(&self, p: NonNull) { + let origin = self.data.as_ptr() as *mut T; + let n = p.as_ptr().offset_from(origin); + assert!(n >= 0); + assert!((n as usize) < N); + self.used[n as usize].set(false); + } + } +} diff --git a/src/asynch/ublox_stack/tls.rs b/src/asynch/ublox_stack/tls.rs new file mode 100644 index 0000000..327f811 --- /dev/null +++ b/src/asynch/ublox_stack/tls.rs @@ -0,0 +1,435 @@ +use embassy_time::Duration; +use no_std_net::SocketAddr; +use ublox_sockets::TcpState as State; + +use super::peer_builder::SecurityCredentials; + +use super::{ + tcp::{ConnectError, Error, TcpIo, TcpReader, TcpSocket, TcpWriter}, + UbloxStack, +}; + +pub struct TlsSocket<'a> { + inner: TcpSocket<'a>, +} + +impl<'a> TlsSocket<'a> { + /// Create a new TCP socket on the given stack, with the given buffers. + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + credentials: SecurityCredentials, + ) -> Self { + let tcp_socket = TcpSocket::new(stack, rx_buffer, tx_buffer); + + let TcpIo { stack, handle } = tcp_socket.io; + + let s = &mut *stack.borrow_mut(); + info!("Associating credentials {} with {}", credentials, handle); + s.credential_map.insert(handle, credentials).unwrap(); + + Self { inner: tcp_socket } + } + + /// Return the maximum number of bytes inside the recv buffer. + pub fn recv_capacity(&self) -> usize { + self.inner.recv_capacity() + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.inner.send_capacity() + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.inner.write_with(f).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.inner.read_with(f).await + } + + /// Split the socket into reader and a writer halves. + pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { + ( + TcpReader { io: self.inner.io }, + TcpWriter { io: self.inner.io }, + ) + } + + /// Connect to a remote host. + pub async fn connect(&mut self, remote_endpoint: T) -> Result<(), ConnectError> + where + T: Into, + { + self.inner.connect(remote_endpoint).await + } + + // /// Accept a connection from a remote host. + // /// + // /// This function puts the socket in listening mode, and waits until a connection is received. + // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + // where + // T: Into, + // { + // todo!() + // // match self.io.with_mut(|s, _| s.listen(local_endpoint)) { + // // Ok(()) => {} + // // Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), + // // Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), + // // } + + // // poll_fn(|cx| { + // // self.io.with_mut(|s, _| match s.state() { + // // tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + // // s.register_send_waker(cx.waker()); + // // Poll::Pending + // // } + // // _ => Poll::Ready(Ok(())), + // // }) + // // }) + // // .await + // } + + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.inner.read(buf).await + } + + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.inner.write(buf).await + } + + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TlsSocket::abort) it will wait for the TCP RST packet to be sent. + pub async fn flush(&mut self) -> Result<(), Error> { + self.inner.flush().await + } + + /// Set the timeout for the socket. + /// + /// If the timeout is set, the socket will be closed if no data is received for the + /// specified duration. + pub fn set_timeout(&mut self, _duration: Option) { + todo!() + // self.inner.set_timeout(duration) + } + + /// Set the keep-alive interval for the socket. + /// + /// If the keep-alive interval is set, the socket will send keep-alive packets after + /// the specified duration of inactivity. + /// + /// If not set, the socket will not send keep-alive packets. + pub fn set_keep_alive(&mut self, interval: Option) { + self.inner.set_keep_alive(interval) + } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.inner.set_hop_limit() + // } + + /// Get the local endpoint of the socket. + /// + /// Returns `None` if the socket is not bound (listening) or not connected. + pub fn local_endpoint(&self) -> Option { + todo!() + // self.inner.local_endpoint() + } + + /// Get the remote endpoint of the socket. + /// + /// Returns `None` if the socket is not connected. + pub fn remote_endpoint(&self) -> Option { + self.inner.remote_endpoint() + } + + /// Get the state of the socket. + pub fn state(&self) -> State { + self.inner.state() + } + + /// Close the write half of the socket. + /// + /// This closes only the write half of the socket. The read half side remains open, the + /// socket can still receive data. + /// + /// Data that has been written to the socket and not yet sent (or not yet ACKed) will still + /// still sent. The last segment of the pending to send data is sent with the FIN flag set. + pub fn close(&mut self) { + self.inner.close() + } + + /// Forcibly close the socket. + /// + /// This instantly closes both the read and write halves of the socket. Any pending data + /// that has not been sent will be lost. + /// + /// Note that the TCP RST packet is not sent immediately - if the `TlsSocket` is dropped too soon + /// the remote host may not know the connection has been closed. + /// `abort()` callers should wait for a [`flush()`](TlsSocket::flush) call to complete before + /// dropping or reusing the socket. + pub fn abort(&mut self) { + self.inner.abort() + } + + /// Get whether the socket is ready to send data, i.e. whether there is space in the send buffer. + pub fn may_send(&self) -> bool { + self.inner.may_send() + } + + /// return whether the receive half of the full-duplex connection is open. + /// This function returns true if it’s possible to receive data from the remote endpoint. + /// It will return true while there is data in the receive buffer, and if there isn’t, + /// as long as the remote endpoint has not closed the connection. + pub fn may_recv(&self) -> bool { + self.inner.may_recv() + } + + /// Get whether the socket is ready to receive data, i.e. whether there is some pending data in the receive buffer. + pub fn can_recv(&self) -> bool { + self.inner.can_recv() + } +} + +impl<'a> Drop for TlsSocket<'a> { + fn drop(&mut self) { + let mut stack = self.inner.io.stack.borrow_mut(); + stack.credential_map.remove(&self.inner.io.handle); + } +} + +mod embedded_io_impls { + use super::*; + + impl<'d> embedded_io_async::ErrorType for TlsSocket<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Read for TlsSocket<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.inner.read(buf).await + } + } + + impl<'d> embedded_io_async::ReadReady for TlsSocket<'d> { + fn read_ready(&mut self) -> Result { + self.inner.read_ready() + } + } + + impl<'d> embedded_io_async::Write for TlsSocket<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.inner.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.inner.flush().await + } + } + + impl<'d> embedded_io_async::WriteReady for TlsSocket<'d> { + fn write_ready(&mut self) -> Result { + self.inner.write_ready() + } + } +} + +/// TLS client compatible with `embedded-nal-async` traits. +pub mod client { + use core::ptr::NonNull; + + use crate::asynch::ublox_stack::dns::DnsSocket; + use crate::asynch::ublox_stack::tcp::client::TcpClientState; + + use super::*; + + /// TLS client connection pool compatible with `embedded-nal-async` traits. + /// + /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. + pub struct TlsClient< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize = 1024, + const RX_SZ: usize = 1024, + > { + pub(crate) stack: &'d UbloxStack, + pub(crate) state: &'d TcpClientState, + pub(crate) credentials: SecurityCredentials, + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::Dns + for TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + type Error = crate::asynch::ublox_stack::dns::Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(self.stack).query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: no_std_net::IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + /// Create a new `TlsClient`. + pub fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + credentials: SecurityCredentials, + ) -> Self { + Self { + stack, + state, + credentials, + } + } + } + + impl< + 'd, + const INGRESS_BUF_SIZE: usize, + const URC_CAPACITY: usize, + const N: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::TcpConnect + for TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> + { + type Error = Error; + type Connection<'m> = TlsConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; + + async fn connect<'a>( + &'a self, + remote: SocketAddr, + ) -> Result, Self::Error> { + let remote_endpoint = (remote.ip(), remote.port()); + let mut socket = TlsConnection::new(self.stack, self.state, self.credentials.clone())?; + socket + .socket + .connect(remote_endpoint) + .await + .map_err(|_| Error::ConnectionReset)?; + Ok(socket) + } + } + + /// Opened TLS connection in a [`TlsClient`]. + pub struct TlsConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { + socket: TlsSocket<'d>, + state: &'d TcpClientState, + bufs: NonNull<([u8; TX_SZ], [u8; RX_SZ])>, + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> + TlsConnection<'d, N, TX_SZ, RX_SZ> + { + fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + credentials: SecurityCredentials, + ) -> Result { + let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; + Ok(Self { + socket: unsafe { + TlsSocket::new( + stack, + &mut bufs.as_mut().1, + &mut bufs.as_mut().0, + credentials, + ) + }, + state, + bufs, + }) + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + fn drop(&mut self) { + unsafe { + self.socket.close(); + self.state.pool.free(self.bufs); + } + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::ErrorType + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + type Error = Error; + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Read + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.socket.read(buf).await + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Write + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + async fn write(&mut self, buf: &[u8]) -> Result { + self.socket.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.socket.flush().await + } + } +} diff --git a/src/asynch/ublox_stack/udp.rs b/src/asynch/ublox_stack/udp.rs new file mode 100644 index 0000000..f561f69 --- /dev/null +++ b/src/asynch/ublox_stack/udp.rs @@ -0,0 +1,247 @@ +//! UDP sockets. +use core::cell::RefCell; + +use core::mem; + +use embedded_nal_async::SocketAddr; +use ublox_sockets::{udp, SocketHandle, UdpState}; + +use super::{SocketStack, UbloxStack}; + +/// Error returned by [`UdpSocket::bind`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum BindError { + /// The socket was already open. + InvalidState, + /// No route to host. + NoRoute, +} + +/// Error returned by [`UdpSocket::recv_from`] and [`UdpSocket::send_to`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SendError { + /// No route to host. + NoRoute, + /// Socket not bound to an outgoing port. + SocketNotBound, +} + +/// Error returned by [`UdpSocket::recv_from`] and [`UdpSocket::send_to`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum RecvError { + /// Provided buffer was smaller than the received packet. + Truncated, +} + +/// An UDP socket. +pub struct UdpSocket<'a> { + stack: &'a RefCell, + handle: SocketHandle, +} + +impl<'a> UdpSocket<'a> { + /// Create a new UDP socket using the provided stack and buffers. + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + ) -> Self { + let s = &mut *stack.socket.borrow_mut(); + let rx_buffer: &'static mut [u8] = unsafe { mem::transmute(rx_buffer) }; + let tx_buffer: &'static mut [u8] = unsafe { mem::transmute(tx_buffer) }; + let handle = s.sockets.add(udp::Socket::new( + udp::SocketBuffer::new(rx_buffer), + udp::SocketBuffer::new(tx_buffer), + )); + + Self { + stack: &stack.socket, + handle, + } + } + + // /// Bind the socket to a local endpoint. + // pub fn bind(&mut self, endpoint: T) -> Result<(), BindError> + // where + // T: Into, + // { + // let mut endpoint = endpoint.into(); + + // if endpoint.port == 0 { + // // If user didn't specify port allocate a dynamic port. + // endpoint.port = self.stack.borrow_mut().get_local_port(); + // } + + // match self.with_mut(|s| s.bind(endpoint)) { + // Ok(()) => Ok(()), + // // Err(udp::BindError::InvalidState) => Err(BindError::InvalidState), + // // Err(udp::BindError::Unaddressable) => Err(BindError::NoRoute), + // Err(_) => Err(BindError::InvalidState), + // } + // } + + fn with(&self, f: impl FnOnce(&udp::Socket) -> R) -> R { + let s = &*self.stack.borrow(); + let socket = s.sockets.get::(self.handle); + f(socket) + } + + fn with_mut(&self, f: impl FnOnce(&mut udp::Socket) -> R) -> R { + let s = &mut *self.stack.borrow_mut(); + let socket = s.sockets.get_mut::(self.handle); + let res = f(socket); + s.waker.wake(); + res + } + + // /// Receive a datagram. + // /// + // /// This method will wait until a datagram is received. + // /// + // /// Returns the number of bytes received and the remote endpoint. + // pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, IpEndpoint), RecvError> { + // poll_fn(move |cx| self.poll_recv_from(buf, cx)).await + // } + + // /// Receive a datagram. + // /// + // /// When no datagram is available, this method will return `Poll::Pending` and + // /// register the current task to be notified when a datagram is received. + // /// + // /// When a datagram is received, this method will return `Poll::Ready` with the + // /// number of bytes received and the remote endpoint. + // pub fn poll_recv_from( + // &self, + // buf: &mut [u8], + // cx: &mut Context<'_>, + // ) -> Poll> { + // self.with_mut(|s| match s.recv_slice(buf) { + // Ok((n, meta)) => Poll::Ready(Ok((n, meta.endpoint))), + // // No data ready + // // Err(udp::RecvError::Truncated) => Poll::Ready(Err(RecvError::Truncated)), + // // Err(udp::RecvError::Exhausted) => { + // Err(_) => { + // s.register_recv_waker(cx.waker()); + // Poll::Pending + // } + // }) + // } + + // /// Send a datagram to the specified remote endpoint. + // /// + // /// This method will wait until the datagram has been sent. + // /// + // /// When the remote endpoint is not reachable, this method will return `Err(SendError::NoRoute)` + // pub async fn send_to(&self, buf: &[u8], remote_endpoint: T) -> Result<(), SendError> + // where + // T: Into, + // { + // let remote_endpoint: IpEndpoint = remote_endpoint.into(); + // poll_fn(move |cx| self.poll_send_to(buf, remote_endpoint, cx)).await + // } + + // /// Send a datagram to the specified remote endpoint. + // /// + // /// When the datagram has been sent, this method will return `Poll::Ready(Ok())`. + // /// + // /// When the socket's send buffer is full, this method will return `Poll::Pending` + // /// and register the current task to be notified when the buffer has space available. + // /// + // /// When the remote endpoint is not reachable, this method will return `Poll::Ready(Err(Error::NoRoute))`. + // pub fn poll_send_to( + // &self, + // buf: &[u8], + // remote_endpoint: T, + // cx: &mut Context<'_>, + // ) -> Poll> + // where + // T: Into, + // { + // self.with_mut(|s| match s.send_slice(buf, remote_endpoint) { + // // Entire datagram has been sent + // Ok(()) => Poll::Ready(Ok(())), + // Err(udp::SendError::BufferFull) => { + // s.register_send_waker(cx.waker()); + // Poll::Pending + // } + // Err(udp::SendError::Unaddressable) => { + // // If no sender/outgoing port is specified, there is not really "no route" + // if s.endpoint().port == 0 { + // Poll::Ready(Err(SendError::SocketNotBound)) + // } else { + // Poll::Ready(Err(SendError::NoRoute)) + // } + // } + // }) + // } + + /// Returns the local endpoint of the socket. + pub fn endpoint(&self) -> Option { + self.with(|s| s.endpoint()) + } + + /// Returns whether the socket is open. + pub fn is_open(&self) -> bool { + self.with(|s| s.is_open()) + } + + /// Close the socket. + pub fn close(&mut self) { + self.with_mut(|s| s.close()) + } + + // /// Returns whether the socket is ready to send data, i.e. it has enough buffer space to hold a packet. + // pub fn may_send(&self) -> bool { + // self.with(|s| s.can_send()) + // } + + /// Returns whether the socket is ready to receive data, i.e. it has received a packet that's now in the buffer. + pub fn may_recv(&self) -> bool { + self.with(|s| s.can_recv()) + } + + // /// Return the maximum number packets the socket can receive. + // pub fn packet_recv_capacity(&self) -> usize { + // self.with(|s| s.packet_recv_capacity()) + // } + + // /// Return the maximum number packets the socket can receive. + // pub fn packet_send_capacity(&self) -> usize { + // self.with(|s| s.packet_send_capacity()) + // } + + // /// Return the maximum number of bytes inside the recv buffer. + // pub fn payload_recv_capacity(&self) -> usize { + // self.with(|s| s.payload_recv_capacity()) + // } + + // /// Return the maximum number of bytes inside the transmit buffer. + // pub fn payload_send_capacity(&self) -> usize { + // self.with(|s| s.payload_send_capacity()) + // } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.with_mut(|s| s.set_hop_limit(hop_limit)) + // } +} + +impl<'a> Drop for UdpSocket<'a> { + fn drop(&mut self) { + if matches!(self.with(|s| s.state()), UdpState::Established) { + if let Some(peer_handle) = self.with(|s| s.peer_handle) { + self.stack + .borrow_mut() + .dropped_sockets + .push(peer_handle) + .ok(); + } + } + let mut stack = self.stack.borrow_mut(); + stack.sockets.remove(self.handle); + stack.waker.wake(); + } +} diff --git a/ublox-short-range/src/command/custom_digest.rs b/src/command/custom_digest.rs similarity index 89% rename from ublox-short-range/src/command/custom_digest.rs rename to src/command/custom_digest.rs index 20c4dde..2b4b537 100644 --- a/ublox-short-range/src/command/custom_digest.rs +++ b/src/command/custom_digest.rs @@ -10,6 +10,12 @@ use super::edm::types::{AUTOCONNECTMESSAGE, STARTUPMESSAGE}; #[derive(Debug, Default)] pub struct EdmDigester; +impl EdmDigester { + pub fn new() -> Self { + Self + } +} + impl Digester for EdmDigester { fn digest<'a>(&mut self, buf: &'a [u8]) -> (DigestResult<'a>, usize) { // TODO: Handle module restart, tests and set default startupmessage in client, and optimize this! @@ -18,12 +24,12 @@ impl Digester for EdmDigester { return (DigestResult::None, 0); } - defmt::trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(buf)); if buf.len() >= STARTUPMESSAGE.len() && buf[..2] == *b"\r\n" { if let Some(i) = buf[2..].windows(2).position(|x| x == *b"\r\n") { // Two for starting position, one for index -> len and one for the window size. let len = i + 4; - defmt::trace!("Digest common at {:?}; i: {:?}", LossyStr(&buf[..len]), i); + trace!("Digest common at {:?}; i: {:?}", LossyStr(&buf[..len]), i); if buf[..len] == *STARTUPMESSAGE { return ( DigestResult::Urc(&buf[..STARTUPMESSAGE.len()]), @@ -67,7 +73,7 @@ impl Digester for EdmDigester { // Debug statement for trace properly if !buf.is_empty() { - defmt::trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(buf)); } // Filter message by payload @@ -84,8 +90,7 @@ impl Digester for EdmDigester { }; (return_val, edm_len) } - PayloadType::StartEvent => (DigestResult::Urc(&buf[..edm_len]), edm_len), - // PayloadType::StartEvent => (DigestResult::Response(Ok(&buf[..edm_len])), edm_len), + PayloadType::StartEvent => (DigestResult::Response(Ok(&buf[..edm_len])), edm_len), PayloadType::ATEvent | PayloadType::ConnectEvent | PayloadType::DataEvent @@ -103,49 +108,21 @@ impl Digester for EdmDigester { // #[cfg(test)] // mod test { - // use super::*; -// use atat::bbqueue::framed::FrameConsumer; -// use atat::{frame::Frame, AtatIngress, Buffers, Ingress, Response}; +// use atat::Config; +// use atat::{AtatIngress, Buffers, Response, blocking::AtatClient}; // const TEST_RX_BUF_LEN: usize = 256; // const TEST_RES_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; // const TEST_URC_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; -// // macro_rules! setup_ingressmanager { -// // () => {{ -// // static mut RES_Q: BBBuffer = BBBuffer::new(); -// // let (res_p, res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; - -// // static mut URC_Q: BBBuffer = BBBuffer::new(); -// // let (urc_p, urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; - -// // ( -// // IngressManager::<_, TEST_RX_BUF_LEN, TEST_RES_CAPACITY, TEST_URC_CAPACITY>::new( -// // res_p, -// // urc_p, -// // EdmDigester::default(), -// // ), -// // res_c, -// // urc_c, -// // ) -// // }}; -// // } +// struct MockWriter; // /// Removed functionality used to change OK responses to empty responses. // #[test] // fn ok_response<'a>() { -// let mut at_pars: Ingress< -// 'a, -// EdmDigester, -// TEST_RX_BUF_LEN, -// TEST_RES_CAPACITY, -// TEST_URC_CAPACITY, -// >; -// let mut res_c: FrameConsumer<'a, TEST_RES_CAPACITY>; -// let mut urc_c: FrameConsumer<'a, TEST_URC_CAPACITY>; // let buf = Buffers::::new(); -// (at_pars, res_c, urc_c) = buf.to_ingress(EdmDigester::default()); +// (at_pars, client) = buf.split_blocking(MockWriter, EdmDigester::default(), Config::new()); // // Payload: "OK\r\n" // let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; @@ -154,7 +131,7 @@ impl Digester for EdmDigester { // let ingress_buf = at_pars.write_buf(); // let len = usize::min(data.len(), ingress_buf.len()); // ingress_buf[..len].copy_from_slice(&data[..len]); -// at_pars.try_advance(len); +// at_pars.try_advance(len).unwrap(); // let mut grant = res_c.read().unwrap(); // grant.auto_release(true); @@ -167,6 +144,7 @@ impl Digester for EdmDigester { // assert_eq!(res, Ok(&empty_ok_response[..])); // assert_eq!(urc_c.read(), None); // } +// } // #[test] // fn error_response() { @@ -243,7 +221,7 @@ impl Digester for EdmDigester { // assert_eq!(urc_c.read(), None); // } -// /// Regular response with traling regular response.. +// /// Regular response with trailing regular response.. // #[test] // fn at_urc() { // let mut at_pars: Ingress< diff --git a/ublox-short-range/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs similarity index 93% rename from ublox-short-range/src/command/data_mode/mod.rs rename to src/command/data_mode/mod.rs index 18062e9..ca9cdfb 100644 --- a/ublox-short-range/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -8,7 +8,7 @@ use heapless::String; use responses::*; use types::*; -use super::{NoResponse, PeerHandle}; +use super::NoResponse; /// 5.1 Enter data mode O /// @@ -27,8 +27,9 @@ pub struct ChangeMode { /// Connects to an enabled service on a remote device. When the host connects to a /// service on a remote device, it implicitly registers to receive the "Connection Closed" /// event. +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] -#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 1000)] +#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 5000)] pub struct ConnectPeer<'a> { #[at_arg(position = 0, len = 128)] pub url: &'a str, @@ -37,11 +38,12 @@ pub struct ConnectPeer<'a> { /// 5.3 Close peer connection +UDCPC /// /// Closes an existing peer connection. +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCPC", NoResponse, timeout_ms = 1000)] pub struct ClosePeerConnection { - #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + #[at_arg(position = 0, len = 1)] + pub peer_handle: ublox_sockets::PeerHandle, } /// 5.4 Default remote peer +UDDRP @@ -64,6 +66,7 @@ pub struct SetDefaultRemotePeer<'a> { /// 5.5 Peer list +UDLP /// /// This command reads the connected peers (peer handle). +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDLP?", PeerListResponse, timeout_ms = 1000)] pub struct PeerList; @@ -126,7 +129,7 @@ pub struct SetWatchdogSettings { /// /// Writes peer configuration. /// -/// Suported parameter tags | Software Version +/// Supported parameter tags | Software Version /// ------------------------|------------------ /// 0,1 | All versions /// 2 | >= 4.0.0 diff --git a/ublox-short-range/src/command/data_mode/responses.rs b/src/command/data_mode/responses.rs similarity index 64% rename from ublox-short-range/src/command/data_mode/responses.rs rename to src/command/data_mode/responses.rs index 3f64833..fddac87 100644 --- a/ublox-short-range/src/command/data_mode/responses.rs +++ b/src/command/data_mode/responses.rs @@ -1,26 +1,26 @@ //! Responses for Data Mode -use super::PeerHandle; use atat::atat_derive::AtatResp; -use heapless::String; /// 5.2 Connect peer +UDCP +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct ConnectPeerResponse { #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, } /// 5.5 Peer list +UDLP +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct PeerListResponse { #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, #[at_arg(position = 1)] - pub protocol: String<64>, + pub protocol: heapless::String<64>, #[at_arg(position = 2)] - pub local_address: String<64>, + pub local_address: heapless::String<64>, #[at_arg(position = 3)] - pub remote_address: String<64>, + pub remote_address: heapless::String<64>, } /// 5.12 Bind +UDBIND diff --git a/ublox-short-range/src/command/data_mode/types.rs b/src/command/data_mode/types.rs similarity index 92% rename from ublox-short-range/src/command/data_mode/types.rs rename to src/command/data_mode/types.rs index 9bbc5f6..6db7c29 100644 --- a/ublox-short-range/src/command/data_mode/types.rs +++ b/src/command/data_mode/types.rs @@ -5,6 +5,7 @@ use heapless::String; use crate::command::OnOff; #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum Mode { /// Command mode @@ -19,6 +20,7 @@ pub enum Mode { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ConnectScheme { /// Always connected - Keep the peer connected when not in command mode. @@ -38,11 +40,13 @@ pub enum ConnectScheme { Both = 0b110, } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ServerConfig { Type(ServerType), Url(String<128>), } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ServerType { #[at_arg(value = 0)] Disabled, @@ -63,6 +67,7 @@ pub enum ServerType { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum Interface { TCP = 1, @@ -75,6 +80,7 @@ pub enum Interface { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum UDPBehaviour { /// No connect. This will trigger an +UUDPC URC immediately (with @@ -90,6 +96,7 @@ pub enum UDPBehaviour { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ImmediateFlush { Disable = 0, @@ -97,6 +104,7 @@ pub enum ImmediateFlush { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum IPVersion { /// Default @@ -105,6 +113,7 @@ pub enum IPVersion { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum RemoteConfiguration { Disable = 0, @@ -112,6 +121,7 @@ pub enum RemoteConfiguration { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum WatchdogSetting { /// SPP (and all SPP based protocols like DUN) write timeout: is the time in @@ -158,6 +168,7 @@ pub enum WatchdogSetting { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum PeerConfigParameter { /// Keep remote peer in the command mode /// - Off: Disconnect peers when entering the command mode @@ -218,6 +229,7 @@ pub enum PeerConfigParameter { } #[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ConnectionType { Bluetooth = 1, @@ -236,6 +248,7 @@ pub enum ConnectionType { // } #[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum IPProtocol { TCP = 0, diff --git a/ublox-short-range/src/command/data_mode/urc.rs b/src/command/data_mode/urc.rs similarity index 52% rename from ublox-short-range/src/command/data_mode/urc.rs rename to src/command/data_mode/urc.rs index cdda3af..b768654 100644 --- a/ublox-short-range/src/command/data_mode/urc.rs +++ b/src/command/data_mode/urc.rs @@ -1,15 +1,14 @@ //! Unsolicited responses for Data mode Commands -use crate::command::PeerHandle; - +#[allow(unused_imports)] use super::types::*; -use atat::atat_derive::AtatResp; -use atat::heapless_bytes::Bytes; /// 5.10 Peer connected +UUDPC -#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg(feature = "internal-network-stack")] +#[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PeerConnected { #[at_arg(position = 0)] - pub handle: PeerHandle, + pub handle: ublox_sockets::PeerHandle, #[at_arg(position = 1)] pub connection_type: ConnectionType, #[at_arg(position = 2)] @@ -17,20 +16,23 @@ pub struct PeerConnected { // #[at_arg(position = 3)] // pub local_address: IpAddr, #[at_arg(position = 3)] - pub local_address: Bytes<40>, + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] + pub local_address: atat::heapless_bytes::Bytes<40>, #[at_arg(position = 4)] pub local_port: u16, // #[at_arg(position = 5)] // pub remote_address: IpAddr, #[at_arg(position = 5)] - pub remote_address: Bytes<40>, + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] + pub remote_address: atat::heapless_bytes::Bytes<40>, #[at_arg(position = 6)] pub remote_port: u16, } /// 5.11 Peer disconnected +UUDPD -#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg(feature = "internal-network-stack")] +#[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] pub struct PeerDisconnected { #[at_arg(position = 0)] - pub handle: PeerHandle, + pub handle: ublox_sockets::PeerHandle, } diff --git a/ublox-short-range/src/command/edm/mod.rs b/src/command/edm/mod.rs similarity index 68% rename from ublox-short-range/src/command/edm/mod.rs rename to src/command/edm/mod.rs index 324b196..61694fb 100644 --- a/ublox-short-range/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -6,11 +6,12 @@ use core::convert::TryInto; use crate::command::{data_mode, data_mode::ChangeMode}; use crate::command::{NoResponse, Urc}; -use crate::wifi::EGRESS_CHUNK_SIZE; +// use crate::wifi::EGRESS_CHUNK_SIZE; /// Containing EDM structs with custom serialaization and deserilaisation. use atat::AtatCmd; -use heapless::Vec; + use types::*; +use ublox_sockets::ChannelId; pub(crate) fn calc_payload_len(resp: &[u8]) -> usize { (u16::from_be_bytes(resp[1..3].try_into().unwrap()) & EDM_FULL_SIZE_FILTER) as usize @@ -22,97 +23,30 @@ pub(crate) fn calc_payload_len(resp: &[u8]) -> usize { // using the parameter. Instead the parameter must // be set to 0 and the serial settings will take effect when the module is reset. #[derive(Debug, Clone)] -pub(crate) struct EdmAtCmdWrapper, const LEN: usize>(pub T); +pub(crate) struct EdmAtCmdWrapper(pub T); -impl atat::AtatCmd<1024> for EdmAtCmdWrapper -where - T: AtatCmd, -{ +impl atat::AtatCmd for EdmAtCmdWrapper { type Response = T::Response; + const MAX_LEN: usize = T::MAX_LEN + 6; + const MAX_TIMEOUT_MS: u32 = T::MAX_TIMEOUT_MS; - fn as_bytes(&self) -> Vec { - let at_vec = self.0.as_bytes(); - let payload_len = (at_vec.len() + 2) as u16; - [ + fn write(&self, buf: &mut [u8]) -> usize { + let at_len = self.0.write(&mut buf[5..]); + let payload_len = (at_len + 2) as u16; + + buf[0..5].copy_from_slice(&[ STARTBYTE, (payload_len >> 8) as u8 & EDM_SIZE_FILTER, (payload_len & 0xffu16) as u8, 0x00, PayloadType::ATRequest as u8, - ] - .iter() - .cloned() - .chain(at_vec) - .chain(core::iter::once(ENDBYTE)) - .collect() - } - - fn parse( - &self, - resp: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - let resp = resp.and_then(|resp| { - if resp.len() < PAYLOAD_OVERHEAD - || !resp.starts_with(&[STARTBYTE]) - || !resp.ends_with(&[ENDBYTE]) - { - return Err(atat::InternalError::InvalidResponse); - }; - - let payload_len = calc_payload_len(resp); - - if resp.len() != payload_len + EDM_OVERHEAD - || resp[4] != PayloadType::ATConfirmation as u8 - { - return Err(atat::InternalError::InvalidResponse); - } - - // Recieved OK response code in EDM response? - match resp - .windows(b"\r\nOK".len()) - .position(|window| window == b"\r\nOK") - { - // Cutting OK out leaves an empth string for NoResponse, for - // other responses just removes "\r\nOK\r\n" - Some(pos) => Ok(&resp[AT_COMMAND_POSITION..pos]), - // Isolate the AT_response - None => Ok(&resp[AT_COMMAND_POSITION..PAYLOAD_POSITION + payload_len]), - } - }); - - self.0.parse(resp) - } -} - -/////////////////////// Temp Solution for fixed size /////////////////////// -#[derive(Debug, Clone)] -pub(crate) struct BigEdmAtCmdWrapper, const LEN: usize>(pub T); - -impl atat::AtatCmd<2054> for BigEdmAtCmdWrapper -where - T: AtatCmd, -{ - type Response = T::Response; + ]); - const MAX_TIMEOUT_MS: u32 = T::MAX_TIMEOUT_MS; + buf[5 + at_len] = ENDBYTE; - fn as_bytes(&self) -> Vec { - let at_vec = self.0.as_bytes(); - let payload_len = (at_vec.len() + 2) as u16; - [ - STARTBYTE, - (payload_len >> 8) as u8 & EDM_SIZE_FILTER, - (payload_len & 0xffu16) as u8, - 0x00, - PayloadType::ATRequest as u8, - ] - .iter() - .cloned() - .chain(at_vec) - .chain(core::iter::once(ENDBYTE)) - .collect() + 5 + at_len + 1 } fn parse( @@ -135,7 +69,7 @@ where return Err(atat::InternalError::InvalidResponse); } - // Recieved OK response code in EDM response? + // Received OK response code in EDM response? match resp .windows(b"\r\nOK".len()) .position(|window| window == b"\r\nOK") @@ -151,7 +85,6 @@ where self.0.parse(resp) } } -////////////////////////////////////////////////////////////////////////////////////////////// #[derive(Debug, Clone)] pub struct EdmDataCommand<'a> { @@ -159,54 +92,57 @@ pub struct EdmDataCommand<'a> { pub data: &'a [u8], } // wifi::socket::EGRESS_CHUNK_SIZE + PAYLOAD_OVERHEAD = 512 + 6 + 1 = 519 -impl<'a> atat::AtatCmd<{ EGRESS_CHUNK_SIZE + 7 }> for EdmDataCommand<'a> { +impl<'a> atat::AtatCmd for EdmDataCommand<'a> { type Response = NoResponse; + const MAX_LEN: usize = DATA_PACKAGE_SIZE + 7; + const EXPECTS_RESPONSE_CODE: bool = false; - fn as_bytes(&self) -> Vec { + fn parse( + &self, + _resp: Result<&[u8], atat::InternalError>, + ) -> core::result::Result { + Ok(NoResponse) + } + + fn write(&self, buf: &mut [u8]) -> usize { let payload_len = (self.data.len() + 3) as u16; - [ + buf[0..6].copy_from_slice(&[ STARTBYTE, (payload_len >> 8) as u8 & EDM_SIZE_FILTER, (payload_len & 0xffu16) as u8, 0x00, PayloadType::DataCommand as u8, self.channel.0, - ] - .iter() - .cloned() - .chain(self.data.iter().cloned()) - .chain(core::iter::once(ENDBYTE)) - .collect() - } + ]); - fn parse( - &self, - _resp: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - Ok(NoResponse) + buf[6..6 + self.data.len()].copy_from_slice(self.data); + buf[6 + self.data.len()] = ENDBYTE; + + 6 + self.data.len() + 1 } } #[derive(Debug, Clone)] pub struct EdmResendConnectEventsCommand; -impl atat::AtatCmd<6> for EdmResendConnectEventsCommand { +impl atat::AtatCmd for EdmResendConnectEventsCommand { type Response = NoResponse; - fn as_bytes(&self) -> Vec { - [ + const MAX_LEN: usize = 6; + + fn write(&self, buf: &mut [u8]) -> usize { + buf[0..6].copy_from_slice(&[ STARTBYTE, 0x00, 0x02, 0x00, PayloadType::ResendConnectEventsCommand as u8, ENDBYTE, - ] - .iter() - .cloned() - .collect() + ]); + + 6 } fn parse( @@ -220,32 +156,30 @@ impl atat::AtatCmd<6> for EdmResendConnectEventsCommand { #[derive(Debug, Clone)] pub struct SwitchToEdmCommand; -impl atat::AtatCmd<6> for SwitchToEdmCommand { +impl atat::AtatCmd for SwitchToEdmCommand { type Response = NoResponse; + const MAX_LEN: usize = 6; + const MAX_TIMEOUT_MS: u32 = 2000; - fn as_bytes(&self) -> Vec { + fn write(&self, buf: &mut [u8]) -> usize { ChangeMode { mode: data_mode::types::Mode::ExtendedDataMode, } - .as_bytes() - .into_iter() - .collect() + .write(buf) } fn parse( &self, - _resp: Result<&[u8], atat::InternalError>, + resp: Result<&[u8], atat::InternalError>, ) -> core::result::Result { - // let resp = resp?; - // // Parse EDM startup command - // let correct = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; // &[0xAA,0x00,0x06,0x00,0x45,0x4f,0x4b,0x0D,0x0a,0x55]; // AA 00 06 00 44 41 54 0D 0A 0D 0A 4F 4B 0D 0A 55 ? - // if resp.len() != correct.len() - // || resp[.. correct.len()] != *correct { - // // TODO: check this - // return Err(atat::Error::InvalidResponse); - // } + let resp = resp?; + // Parse EDM startup command + let correct = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; + if resp.len() != correct.len() || resp[..correct.len()] != *correct { + return Err(atat::Error::InvalidResponse); + } Ok(NoResponse) } } @@ -278,7 +212,11 @@ mod test { PayloadType::ATConfirmation as u8, 0x55, ]; - assert_eq!(parse.as_bytes(), correct_cmd); + + let mut buf = [0u8; as AtatCmd>::MAX_LEN]; + let len = parse.write(&mut buf); + + assert_eq!(buf[..len], correct_cmd); assert_eq!(parse.parse(Ok(response)), Ok(correct_response)); let parse = EdmAtCmdWrapper(SystemStatus { @@ -318,7 +256,10 @@ mod test { 0x0A, 0x55, ]; - assert_eq!(parse.as_bytes(), correct); + let mut buf = [0u8; as AtatCmd>::MAX_LEN]; + let len = parse.write(&mut buf); + + assert_eq!(buf[..len], correct); assert_eq!(parse.parse(Ok(response)), Ok(correct_response)); } @@ -450,7 +391,11 @@ mod test { fn change_to_edm_cmd() { let resp = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; let correct = Vec::<_, 6>::from_slice(b"ATO2\r\n").unwrap(); - assert_eq!(SwitchToEdmCommand.as_bytes(), correct); + + let mut buf = [0u8; SwitchToEdmCommand::MAX_LEN]; + let len = SwitchToEdmCommand.write(&mut buf); + + assert_eq!(buf[..len], correct); assert_eq!(SwitchToEdmCommand.parse(Ok(resp)).unwrap(), NoResponse); } } diff --git a/ublox-short-range/src/command/edm/types.rs b/src/command/edm/types.rs similarity index 90% rename from ublox-short-range/src/command/edm/types.rs rename to src/command/edm/types.rs index 2b3e5b6..08690cf 100644 --- a/ublox-short-range/src/command/edm/types.rs +++ b/src/command/edm/types.rs @@ -1,29 +1,12 @@ -use atat::atat_derive::AtatLen; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::Vec; +use no_std_net::{Ipv4Addr, Ipv6Addr}; use serde::{Deserialize, Serialize}; +use ublox_sockets::ChannelId; /// Start byte, Length: u16, Id+Type: u16, Endbyte // type EdmAtCmdOverhead = (u8, u16, u16, u8); -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - AtatLen, - Ord, - Default, - Serialize, - Deserialize, - defmt::Format, - hash32_derive::Hash32, -)] -pub struct ChannelId(pub u8); - -pub const DATA_PACKAGE_SIZE: usize = 2304; +pub const DATA_PACKAGE_SIZE: usize = 4096; pub const STARTBYTE: u8 = 0xAA; pub const ENDBYTE: u8 = 0x55; pub const EDM_SIZE_FILTER: u8 = 0x0F; @@ -98,7 +81,7 @@ pub struct BluetoothConnectEvent { pub frame_size: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IPv4ConnectEvent { pub channel_id: ChannelId, pub protocol: Protocol, @@ -107,7 +90,7 @@ pub struct IPv4ConnectEvent { pub local_ip: Ipv4Addr, pub local_port: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct IPv6ConnectEvent { pub channel_id: ChannelId, pub protocol: Protocol, @@ -117,7 +100,7 @@ pub struct IPv6ConnectEvent { pub local_port: u16, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct DataEvent { pub channel_id: ChannelId, pub data: Vec, @@ -151,7 +134,7 @@ impl From for ConnectType { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[repr(u8)] pub enum Protocol { TCP = 0x00, diff --git a/ublox-short-range/src/command/edm/urc.rs b/src/command/edm/urc.rs similarity index 93% rename from ublox-short-range/src/command/edm/urc.rs rename to src/command/edm/urc.rs index 34ee3ad..1f78606 100644 --- a/ublox-short-range/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -3,8 +3,9 @@ use super::types::*; use super::Urc; use atat::helpers::LossyStr; use atat::AtatUrc; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::Vec; +use no_std_net::{Ipv4Addr, Ipv6Addr}; +use ublox_sockets::ChannelId; #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, PartialEq)] @@ -20,13 +21,22 @@ pub enum EdmEvent { StartUp, } +impl EdmEvent { + pub fn extract_urc(self) -> Option { + match self { + EdmEvent::ATEvent(urc) => Some(urc), + _ => None, + } + } +} + impl AtatUrc for EdmEvent { /// The type of the response. Usually the enum this trait is implemented on. - type Response = EdmEvent; + type Response = Self; /// Parse the response into a `Self::Response` instance. fn parse(resp: &[u8]) -> Option { - defmt::trace!("[Parse URC] {:?}", LossyStr(resp)); + trace!("[Parse URC] {:?}", LossyStr(resp)); // Startup message? // TODO: simplify mayby no packet check. if resp.len() >= STARTUPMESSAGE.len() @@ -59,12 +69,12 @@ impl AtatUrc for EdmEvent { || !resp.starts_with(&[STARTBYTE]) || !resp.ends_with(&[ENDBYTE]) { - defmt::error!("[Parse URC Start/End byte Error] {:?}", LossyStr(&resp)); + error!("[Parse URC Start/End byte Error] {:?}", LossyStr(resp)); return None; }; let payload_len = calc_payload_len(resp); if resp.len() != payload_len + EDM_OVERHEAD { - defmt::error!("[Parse URC lenght Error] {:?}", LossyStr(resp)); + error!("[Parse URC length Error] {:?}", LossyStr(resp)); return None; } @@ -156,7 +166,7 @@ impl AtatUrc for EdmEvent { PayloadType::StartEvent => EdmEvent::StartUp.into(), _ => { - defmt::error!("[Parse URC Error] {:?}", LossyStr(resp)); + error!("[Parse URC Error] {:?}", LossyStr(resp)); None } } @@ -166,10 +176,9 @@ impl AtatUrc for EdmEvent { #[cfg(test)] mod test { use super::*; - use crate::command::{ - data_mode::urc::PeerConnected, edm::types::DATA_PACKAGE_SIZE, PeerHandle, Urc, - }; + use crate::command::{data_mode::urc::PeerConnected, edm::types::DATA_PACKAGE_SIZE, Urc}; use atat::{heapless::Vec, heapless_bytes::Bytes, AtatUrc}; + use ublox_sockets::PeerHandle; #[test] fn parse_at_urc() { diff --git a/ublox-short-range/src/command/ethernet/mod.rs b/src/command/ethernet/mod.rs similarity index 100% rename from ublox-short-range/src/command/ethernet/mod.rs rename to src/command/ethernet/mod.rs diff --git a/ublox-short-range/src/command/ethernet/responses.rs b/src/command/ethernet/responses.rs similarity index 100% rename from ublox-short-range/src/command/ethernet/responses.rs rename to src/command/ethernet/responses.rs diff --git a/ublox-short-range/src/command/ethernet/types.rs b/src/command/ethernet/types.rs similarity index 99% rename from ublox-short-range/src/command/ethernet/types.rs rename to src/command/ethernet/types.rs index eca3ffe..8c15549 100644 --- a/ublox-short-range/src/command/ethernet/types.rs +++ b/src/command/ethernet/types.rs @@ -1,7 +1,7 @@ //! Argument and parameter types used by Ethernet Commands and Responses use atat::atat_derive::AtatEnum; -use embedded_nal::Ipv4Addr; +use no_std_net::Ipv4Addr; use crate::command::OnOff; diff --git a/ublox-short-range/src/command/ethernet/urc.rs b/src/command/ethernet/urc.rs similarity index 100% rename from ublox-short-range/src/command/ethernet/urc.rs rename to src/command/ethernet/urc.rs diff --git a/ublox-short-range/src/command/general/mod.rs b/src/command/general/mod.rs similarity index 87% rename from ublox-short-range/src/command/general/mod.rs rename to src/command/general/mod.rs index 66e1b65..08ce8d8 100644 --- a/ublox-short-range/src/command/general/mod.rs +++ b/src/command/general/mod.rs @@ -61,8 +61,8 @@ pub struct SerialNumber2; /// /// Identificationinformation. #[derive(Clone, AtatCmd)] -#[at_cmd("I0", IdentificationInfomationTypeCodeResponse, timeout_ms = 1000)] -pub struct IdentificationInfomationTypeCode; +#[at_cmd("I0", IdentificationInformationTypeCodeResponse, timeout_ms = 1000)] +pub struct IdentificationInformationTypeCode; /// 3.9 Identification information I9 /// @@ -70,17 +70,17 @@ pub struct IdentificationInfomationTypeCode; #[derive(Clone, AtatCmd)] #[at_cmd( "I9", - IdentificationInfomationSoftwareVersionResponse, + IdentificationInformationSoftwareVersionResponse, timeout_ms = 1000 )] -pub struct IdentificationInfomationSoftwareVersion; +pub struct IdentificationInformationSoftwareVersion; /// 3.9 Identification information I10 /// /// Identificationinformation. #[derive(Clone, AtatCmd)] -#[at_cmd("I10", IdentificationInfomationMCUIDResponse, timeout_ms = 1000)] -pub struct IdentificationInfomationMCUID; +#[at_cmd("I10", IdentificationInformationMCUIDResponse, timeout_ms = 1000)] +pub struct IdentificationInformationMCUID; /// 3.11 Set greeting text +CSGT /// diff --git a/ublox-short-range/src/command/general/responses.rs b/src/command/general/responses.rs similarity index 90% rename from ublox-short-range/src/command/general/responses.rs rename to src/command/general/responses.rs index aea6181..155c8d3 100644 --- a/ublox-short-range/src/command/general/responses.rs +++ b/src/command/general/responses.rs @@ -37,7 +37,7 @@ pub struct SerialNumberResponse { /// 3.10 Identification information I0 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationTypeCodeResponse { +pub struct IdentificationInformationTypeCodeResponse { /// Text string that identifies the serial number. #[at_arg(position = 0)] pub serial_number: String<64>, @@ -45,7 +45,7 @@ pub struct IdentificationInfomationTypeCodeResponse { /// 3.10 Identification information I9 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationSoftwareVersionResponse { +pub struct IdentificationInformationSoftwareVersionResponse { /// Text string that identifies the firmware version. #[at_arg(position = 0)] pub version: String<64>, @@ -53,7 +53,7 @@ pub struct IdentificationInfomationSoftwareVersionResponse { /// 3.10 Identification information I10 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationMCUIDResponse { +pub struct IdentificationInformationMCUIDResponse { /// Text string that identifies the serial number. #[at_arg(position = 0)] pub serial_number: String<64>, diff --git a/ublox-short-range/src/command/general/types.rs b/src/command/general/types.rs similarity index 98% rename from ublox-short-range/src/command/general/types.rs rename to src/command/general/types.rs index 55f927f..58f3aa2 100644 --- a/ublox-short-range/src/command/general/types.rs +++ b/src/command/general/types.rs @@ -89,7 +89,7 @@ impl core::str::FromStr for FirmwareVersion { let (patch, meta) = match patch_meta.split_once('-') { Some((patch_str, meta)) => ( patch_str.parse().map_err(|_| DeserializeError)?, - Some(heapless::String::from(meta)), + heapless::String::try_from(meta).ok(), ), None => (patch_meta.parse().map_err(|_| DeserializeError)?, None), }; @@ -103,6 +103,7 @@ impl core::str::FromStr for FirmwareVersion { } } +#[cfg(feature = "defmt")] impl defmt::Format for FirmwareVersion { fn format(&self, fmt: defmt::Formatter) { if let Some(meta) = &self.meta { diff --git a/ublox-short-range/src/command/gpio/mod.rs b/src/command/gpio/mod.rs similarity index 95% rename from ublox-short-range/src/command/gpio/mod.rs rename to src/command/gpio/mod.rs index a5d4ab5..b73c4f2 100644 --- a/ublox-short-range/src/command/gpio/mod.rs +++ b/src/command/gpio/mod.rs @@ -41,7 +41,7 @@ pub struct ReadGPIO { /// Writes the value of an enabled GPIO pin configured as output. /// Supported by ODIN-W2 from software version 3.0.0 onwards only. #[derive(Clone, AtatCmd)] -#[at_cmd("+UGPIOW", NoResponse, value_sep = false, timeout_ms = 1000)] +#[at_cmd("+UGPIOW", NoResponse, timeout_ms = 1000)] pub struct WriteGPIO { #[at_arg(position = 0)] pub id: GPIOId, diff --git a/ublox-short-range/src/command/gpio/responses.rs b/src/command/gpio/responses.rs similarity index 83% rename from ublox-short-range/src/command/gpio/responses.rs rename to src/command/gpio/responses.rs index e51e308..9a4bbac 100644 --- a/ublox-short-range/src/command/gpio/responses.rs +++ b/src/command/gpio/responses.rs @@ -6,7 +6,7 @@ use atat::atat_derive::AtatResp; #[derive(Clone, PartialEq, AtatResp)] pub struct ReadGPIOResponse { #[at_arg(position = 0)] - id: GPIOId, + pub id: GPIOId, #[at_arg(position = 1)] - value: GPIOValue, + pub value: GPIOValue, } diff --git a/ublox-short-range/src/command/gpio/types.rs b/src/command/gpio/types.rs similarity index 100% rename from ublox-short-range/src/command/gpio/types.rs rename to src/command/gpio/types.rs diff --git a/ublox-short-range/src/command/mod.rs b/src/command/mod.rs similarity index 84% rename from ublox-short-range/src/command/mod.rs rename to src/command/mod.rs index a11c14d..864dec2 100644 --- a/ublox-short-range/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,8 +1,10 @@ //! AT Commands for U-Blox short range module family\ //! Following the [u-connect ATCommands Manual](https://www.u-blox.com/sites/default/files/u-connect-ATCommands-Manual_(UBX-14044127).pdf) +#[cfg(feature = "edm")] pub mod custom_digest; pub mod data_mode; +#[cfg(feature = "edm")] pub mod edm; pub mod ethernet; pub mod general; @@ -13,42 +15,26 @@ pub mod security; pub mod system; pub mod wifi; -use atat::atat_derive::{AtatCmd, AtatEnum, AtatLen, AtatResp, AtatUrc}; -use serde::{Deserialize, Serialize}; +use atat::atat_derive::{AtatCmd, AtatEnum, AtatResp, AtatUrc}; #[derive(Debug, Clone, AtatResp, PartialEq)] pub struct NoResponse; #[derive(Debug, Clone, AtatCmd)] -#[at_cmd("", NoResponse, timeout_ms = 1000)] +#[at_cmd("", NoResponse, attempts = 3, timeout_ms = 1000)] pub struct AT; -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - AtatLen, - Ord, - Default, - Serialize, - Deserialize, - defmt::Format, - hash32_derive::Hash32, -)] -pub struct PeerHandle(pub u8); - #[derive(Debug, PartialEq, Clone, AtatUrc)] pub enum Urc { /// Startup Message #[at_urc("+STARTUP")] StartUp, /// 5.10 Peer connected +UUDPC + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPC")] PeerConnected(data_mode::urc::PeerConnected), /// 5.11 Peer disconnected +UUDPD + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPD")] PeerDisconnected(data_mode::urc::PeerDisconnected), /// 7.15 Wi-Fi Link connected +UUWLE @@ -91,6 +77,7 @@ pub enum Urc { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum OnOff { On = 1, @@ -105,3 +92,12 @@ impl From for OnOff { } } } + +impl From for bool { + fn from(val: OnOff) -> Self { + match val { + OnOff::On => true, + OnOff::Off => false, + } + } +} diff --git a/ublox-short-range/src/command/network/mod.rs b/src/command/network/mod.rs similarity index 97% rename from ublox-short-range/src/command/network/mod.rs rename to src/command/network/mod.rs index c80b06e..81d4c8a 100644 --- a/ublox-short-range/src/command/network/mod.rs +++ b/src/command/network/mod.rs @@ -23,7 +23,7 @@ pub struct SetNetworkHostName<'a> { /// /// Shows current status of the network interface id. #[derive(Clone, AtatCmd)] -#[at_cmd("+UNSTAT", NetworkStatusResponse, timeout_ms = 1000)] +#[at_cmd("+UNSTAT", NetworkStatusResponse, attempts = 3, timeout_ms = 1000)] pub struct GetNetworkStatus { #[at_arg(position = 0)] pub interface_id: u8, diff --git a/ublox-short-range/src/command/network/responses.rs b/src/command/network/responses.rs similarity index 100% rename from ublox-short-range/src/command/network/responses.rs rename to src/command/network/responses.rs diff --git a/ublox-short-range/src/command/network/types.rs b/src/command/network/types.rs similarity index 99% rename from ublox-short-range/src/command/network/types.rs rename to src/command/network/types.rs index d27d7cc..97c8a4a 100644 --- a/ublox-short-range/src/command/network/types.rs +++ b/src/command/network/types.rs @@ -31,7 +31,7 @@ pub enum NetworkStatus { /// 101: The is the currently used IPv4_Addr (omitted if no IP address has /// been acquired). #[at_arg(value = 101)] - IPv4Address(#[at_arg(len = 16)] Bytes<40>), + IPv4Address(#[at_arg(len = 16)] Bytes<16>), /// 102: The is the currently used subnet mask (omitted if no IP address /// has been acquired). #[at_arg(value = 102)] diff --git a/ublox-short-range/src/command/network/urc.rs b/src/command/network/urc.rs similarity index 100% rename from ublox-short-range/src/command/network/urc.rs rename to src/command/network/urc.rs diff --git a/ublox-short-range/src/command/ping/mod.rs b/src/command/ping/mod.rs similarity index 100% rename from ublox-short-range/src/command/ping/mod.rs rename to src/command/ping/mod.rs diff --git a/ublox-short-range/src/command/ping/types.rs b/src/command/ping/types.rs similarity index 94% rename from ublox-short-range/src/command/ping/types.rs rename to src/command/ping/types.rs index cfe102c..3999593 100644 --- a/ublox-short-range/src/command/ping/types.rs +++ b/src/command/ping/types.rs @@ -18,14 +18,15 @@ use atat::atat_derive::AtatEnum; /// provides the TTL value received in the incoming packet. /// - Range: 1-255 /// - Default value: 32 -// pub type TTL = (u8, Option); +// pub type TTL = (u8, Option); /// The time in milliseconds to wait after an echo reply response before sending the next /// echo request. /// - Range: 0-60000 /// - Default value: 1000 -// pub type Inteval = u16; +// pub type Interval = u16; -#[derive(Debug, PartialEq, Clone, Copy, AtatEnum, defmt::Format)] +#[derive(Debug, PartialEq, Clone, Copy, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum PingError { /// 1 - 6: Internal error (ping level) diff --git a/ublox-short-range/src/command/ping/urc.rs b/src/command/ping/urc.rs similarity index 93% rename from ublox-short-range/src/command/ping/urc.rs rename to src/command/ping/urc.rs index 28231cf..0abc5c7 100644 --- a/ublox-short-range/src/command/ping/urc.rs +++ b/src/command/ping/urc.rs @@ -1,8 +1,8 @@ //! Responses for Ping Commands use super::types::*; use atat::atat_derive::AtatResp; -use embedded_nal::IpAddr; use heapless::String; +use no_std_net::IpAddr; /// 16.1 Ping command +UPING /// /// The ping command is the common method to know if a remote host is reachable on the Internet. @@ -24,7 +24,6 @@ use heapless::String; /// in another way. #[derive(Debug, PartialEq, Clone, AtatResp)] pub struct PingResponse { - /// Text string that identifies the serial number. #[at_arg(position = 0)] pub retrynum: u32, #[at_arg(position = 1)] @@ -39,7 +38,8 @@ pub struct PingResponse { pub rtt: i32, } -#[derive(Debug, PartialEq, Clone, AtatResp, defmt::Format)] +#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PingErrorResponse { #[at_arg(position = 0)] pub error: PingError, diff --git a/ublox-short-range/src/command/security/mod.rs b/src/command/security/mod.rs similarity index 99% rename from ublox-short-range/src/command/security/mod.rs rename to src/command/security/mod.rs index 388057a..81fd730 100644 --- a/ublox-short-range/src/command/security/mod.rs +++ b/src/command/security/mod.rs @@ -41,7 +41,7 @@ pub struct PrepareSecurityDataImport<'a> { "", SecurityDataImport, value_sep = false, - timeout_ms = 1000, + timeout_ms = 3000, cmd_prefix = "", termination = "" )] diff --git a/ublox-short-range/src/command/security/responses.rs b/src/command/security/responses.rs similarity index 100% rename from ublox-short-range/src/command/security/responses.rs rename to src/command/security/responses.rs diff --git a/ublox-short-range/src/command/security/types.rs b/src/command/security/types.rs similarity index 100% rename from ublox-short-range/src/command/security/types.rs rename to src/command/security/types.rs diff --git a/ublox-short-range/src/command/system/mod.rs b/src/command/system/mod.rs similarity index 95% rename from ublox-short-range/src/command/system/mod.rs rename to src/command/system/mod.rs index ec1ac13..9ff5be2 100644 --- a/ublox-short-range/src/command/system/mod.rs +++ b/src/command/system/mod.rs @@ -29,7 +29,7 @@ pub struct SetToDefaultConfig; /// Reset to factory defined defaults. A reboot is required before using the new settings. #[derive(Debug, PartialEq, Clone, AtatCmd)] #[at_cmd("+UFACTORY", NoResponse, timeout_ms = 1000)] -pub struct ResetToFacroryDefaults; +pub struct ResetToFactoryDefaults; /// 4.4 Circuit 108/2 (DTR) behavior &D /// @@ -175,7 +175,7 @@ pub struct ModuleStart { #[at_cmd("+UMLA", NoResponse, timeout_ms = 1000)] pub struct SetLocalAddress<'a> { #[at_arg(position = 0)] - pub interface_id: InserfaceID, + pub interface_id: InterfaceID, /// MAC address of the interface id. If the address is set to 000000000000, the local /// address will be restored to factory-programmed value. /// The least significant bit of the first octet of the
must be 0; that is, the @@ -188,10 +188,10 @@ pub struct SetLocalAddress<'a> { /// /// Reads the local address of the interface id. #[derive(Debug, PartialEq, Clone, AtatCmd)] -#[at_cmd("+UMSM", LocalAddressResponse, timeout_ms = 1000)] +#[at_cmd("+UMLA", LocalAddressResponse, timeout_ms = 1000)] pub struct GetLocalAddress { #[at_arg(position = 0)] - pub interface_id: InserfaceID, + pub interface_id: InterfaceID, } /// 4.15 System status +UMSTAT @@ -215,14 +215,6 @@ pub struct SystemStatus { pub struct SetRS232Settings { #[at_arg(position = 0)] pub baud_rate: BaudRate, - // #[at_arg(position = 1)] - // pub settings: Option<( - // FlowControl, - // Option<( - // u8, - // Option<(StopBits, Option<(Parity, Option)>)>, - // )>, - // )>, #[at_arg(position = 1)] pub flow_control: FlowControl, #[at_arg(position = 2)] diff --git a/ublox-short-range/src/command/system/responses.rs b/src/command/system/responses.rs similarity index 92% rename from ublox-short-range/src/command/system/responses.rs rename to src/command/system/responses.rs index da05fd9..fcd5de8 100644 --- a/ublox-short-range/src/command/system/responses.rs +++ b/src/command/system/responses.rs @@ -1,6 +1,6 @@ //! Responses for System Commands use super::types::*; -use atat::atat_derive::AtatResp; +use atat::{atat_derive::AtatResp, serde_at::HexStr}; use heapless::String; /// 4.11 Software update +UFWUPD @@ -17,7 +17,7 @@ pub struct LocalAddressResponse { /// MAC address of the interface id. If the address is set to 000000000000, the local /// address will be restored to factory-programmed value. #[at_arg(position = 0)] - pub mac: String<64>, + pub mac: HexStr, } /// 4.15 System status +UMSTAT diff --git a/ublox-short-range/src/command/system/types.rs b/src/command/system/types.rs similarity index 96% rename from ublox-short-range/src/command/system/types.rs rename to src/command/system/types.rs index 9d4b17d..b00b2a7 100644 --- a/ublox-short-range/src/command/system/types.rs +++ b/src/command/system/types.rs @@ -40,7 +40,7 @@ pub enum DSRAssertMode { /// DSR line when no remote peers are connected. See Connect Peer +UDCP and Default /// remote peer +UDDRP for definition of the remote peer. This applies to both incoming /// and outgoing connections. - WhenPeersConected = 2, + WhenPeersConnected = 2, } /// Echo on @@ -81,10 +81,11 @@ pub enum ModuleStartMode { #[derive(Debug, Clone, PartialEq, AtatEnum)] #[repr(u8)] -pub enum InserfaceID { - Bluetooth = 0, - WiFi = 1, - Ethernet = 2, +pub enum InterfaceID { + Bluetooth = 1, + WiFi = 2, + Ethernet = 3, + WiFiAP = 4, } #[derive(Debug, Clone, PartialEq, AtatEnum)] @@ -98,7 +99,7 @@ pub enum StatusID { SavedStatus = 1, } -#[derive(Debug, Clone, PartialEq, AtatEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, AtatEnum)] #[at_enum(u32)] /// ODIN-W2: /// 19200 - 5250000. The module will set a baud rate as close as possible to the diff --git a/ublox-short-range/src/command/wifi/mod.rs b/src/command/wifi/mod.rs similarity index 97% rename from ublox-short-range/src/command/wifi/mod.rs rename to src/command/wifi/mod.rs index f0c9223..9dd319a 100644 --- a/ublox-short-range/src/command/wifi/mod.rs +++ b/src/command/wifi/mod.rs @@ -34,14 +34,29 @@ impl<'a> atat::AtatLen for SetWifiStationConfig<'a> { const ATAT_SETWIFISTATIONCONFIG_LEN: usize = as atat::AtatLen>::LEN + ::LEN + 1usize; #[automatically_derived] -impl<'a> atat::AtatCmd<{ ATAT_SETWIFISTATIONCONFIG_LEN + 12usize }> for SetWifiStationConfig<'a> { +impl<'a> atat::AtatCmd for SetWifiStationConfig<'a> { type Response = NoResponse; const MAX_TIMEOUT_MS: u32 = 1000u32; #[inline] - fn as_bytes(&self) -> atat::heapless::Vec { - match atat::serde_at::to_vec( + fn parse( + &self, + res: Result<&[u8], atat::InternalError>, + ) -> core::result::Result { + match res { + Ok(resp) => { + atat::serde_at::from_slice::(resp).map_err(|_e| atat::Error::Parse) + } + Err(e) => Err(e.into()), + } + } + + const MAX_LEN: usize = ATAT_SETWIFISTATIONCONFIG_LEN + 12usize; + + fn write(&self, buf: &mut [u8]) -> usize { + match atat::serde_at::to_slice( self, "+UWSC", + buf, atat::serde_at::SerializeOptions { value_sep: true, cmd_prefix: "AT", @@ -53,18 +68,6 @@ impl<'a> atat::AtatCmd<{ ATAT_SETWIFISTATIONCONFIG_LEN + 12usize }> for SetWifiS Err(_) => panic!("Failed to serialize command"), } } - #[inline] - fn parse( - &self, - res: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - match res { - Ok(resp) => { - atat::serde_at::from_slice::(resp).map_err(|e| atat::Error::Parse) - } - Err(e) => Err(e.into()), - } - } } #[automatically_derived] impl<'a> atat::serde_at::serde::Serialize for SetWifiStationConfig<'a> { diff --git a/ublox-short-range/src/command/wifi/responses.rs b/src/command/wifi/responses.rs similarity index 100% rename from ublox-short-range/src/command/wifi/responses.rs rename to src/command/wifi/responses.rs diff --git a/ublox-short-range/src/command/wifi/types.rs b/src/command/wifi/types.rs similarity index 97% rename from ublox-short-range/src/command/wifi/types.rs rename to src/command/wifi/types.rs index 62e6566..25097a5 100644 --- a/ublox-short-range/src/command/wifi/types.rs +++ b/src/command/wifi/types.rs @@ -3,8 +3,8 @@ use crate::command::OnOff; use atat::atat_derive::AtatEnum; use atat::heapless_bytes::Bytes; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::{String, Vec}; +use no_std_net::{Ipv4Addr, Ipv6Addr}; use serde::Deserialize; #[derive(Clone, PartialEq, AtatEnum)] @@ -106,7 +106,7 @@ pub enum WifiStationConfigParameter { /// is the Wi-Fi beacon listen interval in units of beacon /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 - WiFiBeaconListenInteval = 300, + WiFiBeaconListenInterval = 300, /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -244,7 +244,7 @@ pub enum WifiStationConfig<'a> { /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 #[at_arg(value = 300)] - WiFiBeaconListenInteval(u8), + WiFiBeaconListenInterval(u8), /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -384,7 +384,7 @@ pub enum WifiStationConfigR { /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 #[at_arg(value = 300)] - WiFiBeaconListenInteval(u8), + WiFiBeaconListenInterval(u8), /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -426,7 +426,8 @@ pub enum WifiStationAction { Deactivate = 4, } -#[derive(Debug, Clone, PartialEq, AtatEnum, defmt::Format)] +#[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum OperationMode { Infrastructure = 1, @@ -434,6 +435,7 @@ pub enum OperationMode { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum StatusId { SSID = 0, @@ -447,7 +449,7 @@ pub enum StatusId { Status = 3, /// The is the RSSI value of the current connection; will /// return-32768, if not connected. - RSSI = 6, + Rssi = 6, /// The is the mobility domain of the last or current /// connection This tag is supported by ODIN-W2 from software version 6.0.0 /// onwards only. @@ -497,7 +499,7 @@ pub enum WifiStatus { /// The is the RSSI value of the current connection; will /// return-32768, if not connected. #[at_arg(value = 6)] - RSSI(i16), + Rssi(u32), /// The is the mobility domain of the last or current /// connection This tag is supported by ODIN-W2 from software version 6.0.0 /// onwards only. @@ -594,17 +596,35 @@ pub enum WifiConfigParameter { /// onwards RemainOnChannel = 15, /// Station TX rates bit mask where bit masks are defined according to: - /// 0x00000001: Rate 1 Mbps 0x00000002: Rate 2 Mbps 0x00000004: Rate 5.5 - /// Mbps 0x00000008: Rate 11 Mbps 0x00000010: Rate 6 Mbps 0x00000020: Rate 9 - /// Mbps 0x00000040: Rate 12 Mbps 0x00000080: Rate 18 Mbps 0x00000100: Rate - /// 24 Mbps 0x00000200: Rate 36 Mbps 0x00000400: Rate 48 Mbps 0x00000800: - /// Rate 54 Mbps 0x00001000: Rate MCS 0 0x00002000: Rate MCS 1 0x00004000: - /// Rate MCS 2 0x00008000: Rate MCS 3 0x00010000: Rate MCS 4 0x00020000: - /// Rate MCS 5 0x00040000: Rate MCS 6 0x00080000: Rate MCS 7 0x00100000: - /// Rate MCS 8 0x00200000: Rate MCS 9 0x00400000: Rate MCS 10 0x00800000: - /// Rate MCS 11 0x01000000: Rate MCS 12 0x02000000: Rate MCS 13 0x04000000: - /// Rate MCS 14 0x08000000: Rate MCS 15 Default value is 0, which means that - /// all rates are enabled. Supported software versions 7.0.0 onwards + /// - 0x00000001: Rate 1 Mbps + /// - 0x00000002: Rate 2 Mbps + /// - 0x00000004: Rate 5.5 Mbps + /// - 0x00000008: Rate 11 Mbps + /// - 0x00000010: Rate 6 Mbps + /// - 0x00000020: Rate 9 Mbps + /// - 0x00000040: Rate 12 Mbps + /// - 0x00000080: Rate 18 Mbps + /// - 0x00000100: Rate 24 Mbps + /// - 0x00000200: Rate 36 Mbps + /// - 0x00000400: Rate 48 Mbps + /// - 0x00000800: Rate 54 Mbps + /// - 0x00001000: Rate MCS 0 + /// - 0x00002000: Rate MCS 1 + /// - 0x00004000: Rate MCS 2 + /// - 0x00008000: Rate MCS 3 + /// - 0x00010000: Rate MCS 4 + /// - 0x00020000: Rate MCS 5 + /// - 0x00040000: Rate MCS 6 + /// - 0x00080000: Rate MCS 7 + /// - 0x00100000: Rate MCS 8 + /// - 0x00200000: Rate MCS 9 + /// - 0x00400000: Rate MCS 10 + /// - 0x00800000: Rate MCS 11 + /// - 0x01000000: Rate MCS 12 + /// - 0x02000000: Rate MCS 13 + /// - 0x04000000: Rate MCS 14 + /// - 0x08000000: Rate MCS 15 Default value is 0, which means that all rates + /// are enabled. Supported software versions 7.0.0 onwards StationTxRates = 16, /// Station short packet retry limit. Default value is 0x00141414. The /// definition of retry limits are listed below: diff --git a/ublox-short-range/src/command/wifi/urc.rs b/src/command/wifi/urc.rs similarity index 100% rename from ublox-short-range/src/command/wifi/urc.rs rename to src/command/wifi/urc.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5380f32 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,31 @@ +use embedded_hal::digital::OutputPin; +use embedded_io_async::{Read, Write}; + +use crate::{command::system::types::BaudRate, DEFAULT_BAUD_RATE}; + +pub trait WifiConfig<'a> { + type ResetPin: OutputPin; + + const AT_CONFIG: atat::Config = atat::Config::new(); + + // Transport settings + const FLOW_CONTROL: bool = false; + const BAUD_RATE: BaudRate = DEFAULT_BAUD_RATE; + + #[cfg(feature = "internal-network-stack")] + const TLS_IN_BUFFER_SIZE: Option = None; + #[cfg(feature = "internal-network-stack")] + const TLS_OUT_BUFFER_SIZE: Option = None; + + #[cfg(feature = "ppp")] + const PPP_CONFIG: embassy_net_ppp::Config<'a>; + + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + None + } +} + +pub trait Transport: Write + Read { + fn set_baudrate(&mut self, baudrate: u32); + fn split_ref(&mut self) -> (impl Write, impl Read); +} diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 0000000..b5f7292 --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,87 @@ +use no_std_net::Ipv4Addr; + +use crate::network::{WifiMode, WifiNetwork}; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum WiFiState { + Inactive, + /// Searching for Wifi + NotConnected, + SecurityProblems, + Connected, +} + +/// Static IP address configuration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StaticConfigV4 { + /// IP address and subnet mask. + pub address: Ipv4Addr, + /// Default gateway. + pub gateway: Option, + /// DNS servers. + pub dns_servers: DnsServers, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DnsServers { + pub primary: Option, + pub secondary: Option, +} + +pub struct WifiConnection { + pub wifi_state: WiFiState, + pub ipv6_link_local_up: bool, + pub ipv4_up: bool, + #[cfg(feature = "ipv6")] + pub ipv6_up: bool, + pub network: Option, +} + +impl WifiConnection { + pub(crate) const fn new() -> Self { + WifiConnection { + wifi_state: WiFiState::Inactive, + ipv6_link_local_up: false, + network: None, + ipv4_up: false, + #[cfg(feature = "ipv6")] + ipv6_up: false, + } + } + + #[allow(dead_code)] + pub fn is_station(&self) -> bool { + self.network + .as_ref() + .map(|n| n.mode == WifiMode::Station) + .unwrap_or_default() + } + + #[allow(dead_code)] + pub fn is_access_point(&self) -> bool { + !self.is_station() + } + + /// Get whether the network stack has a valid IP configuration. + /// This is true if the network stack has a static IP configuration or if DHCP has completed + pub fn is_config_up(&self) -> bool { + let v6_up; + let v4_up = self.ipv4_up; + + #[cfg(feature = "ipv6")] + { + v6_up = self.ipv6_up; + } + #[cfg(not(feature = "ipv6"))] + { + v6_up = false; + } + + (v4_up || v6_up) && self.ipv6_link_local_up + } + + pub fn is_connected(&self) -> bool { + self.is_config_up() && self.wifi_state == WiFiState::Connected + } +} diff --git a/ublox-short-range/src/error.rs b/src/error.rs similarity index 79% rename from ublox-short-range/src/error.rs rename to src/error.rs index ea0b24a..fd38a1b 100644 --- a/ublox-short-range/src/error.rs +++ b/src/error.rs @@ -1,10 +1,13 @@ +#[cfg(feature = "internal-network-stack")] pub use ublox_sockets::Error as SocketError; -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { Overflow, SetState, BadLength, + SecurityProblems, Network, Pin, BaudDetection, @@ -13,9 +16,10 @@ pub enum Error { SocketNotFound, SocketNotConnected, MissingSocketSet, - NetworkState(crate::wifi::connection::NetworkState), + // NetworkState(crate::wifi::connection::NetworkState), NoWifiSetup, - WifiState(crate::wifi::connection::WiFiState), + // WifiState(crate::wifi::connection::WiFiState), + #[cfg(feature = "internal-network-stack")] Socket(ublox_sockets::Error), AT(atat::Error), Busy, @@ -26,8 +30,10 @@ pub enum Error { Unimplemented, SocketMemory, SocketMapMemory, + Supplicant, Timeout, ShadowStoreBug, + AlreadyConnected, _Unknown, } @@ -37,6 +43,13 @@ impl From for Error { } } +impl From for Error { + fn from(_: embassy_time::TimeoutError) -> Self { + Error::Timeout + } +} + +#[cfg(feature = "internal-network-stack")] impl From for Error { fn from(e: ublox_sockets::Error) -> Self { Error::Socket(e) @@ -44,7 +57,8 @@ impl From for Error { } /// Error that occurs when attempting to connect to a wireless network. -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiConnectionError { /// Failed to connect to wireless network. FailedToConnect, @@ -70,7 +84,8 @@ impl From for WifiConnectionError { } } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiError { // The specified wifi is currently disabled. Try switching it on. WifiDisabled, @@ -86,9 +101,10 @@ pub enum WifiError { // Other, } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiHotspotError { - /// Failed to ceate wireless hotspot. + /// Failed to create wireless hotspot. CreationFailed, /// Failed to stop wireless hotspot service. Try turning off /// the wireless interface via ```wifi.turn_off()```. diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..35b929f --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,274 @@ +#![macro_use] +#![allow(unused)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +#[collapse_debuginfo(yes)] +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + ::core::unreachable!($($x)*) + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unreachable { + ($($x:tt)*) => { + ::defmt::unreachable!($($x)*) + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[collapse_debuginfo(yes)] +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +pub(crate) struct Bytes<'a>(pub &'a [u8]); + +impl<'a> Debug for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> Display for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> LowerHex for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for Bytes<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} diff --git a/ublox-short-range/src/hex.rs b/src/hex.rs similarity index 100% rename from ublox-short-range/src/hex.rs rename to src/hex.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..283875a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,34 @@ +#![cfg_attr(not(test), no_std)] +#![allow(async_fn_in_trait)] + +#[cfg(all(feature = "ppp", feature = "internal-network-stack"))] +compile_error!("You may not enable both `ppp` and `internal-network-stack` features."); + +#[cfg(not(any( + feature = "odin-w2xx", + feature = "nina-w1xx", + feature = "nina-b1xx", + feature = "anna-b1xx", + feature = "nina-b2xx", + feature = "nina-b3xx" +)))] +compile_error!("No module feature activated. You must activate exactly one of the following features: odin-w2xx, nina-w1xx, nina-b1xx, anna-b1xx, nina-b2xx, nina-b3xx"); + +mod fmt; + +pub mod asynch; + +mod config; +mod connection; +mod network; + +mod hex; + +pub use atat; + +pub mod command; +pub mod error; +pub use config::{Transport, WifiConfig}; + +use command::system::types::BaudRate; +pub const DEFAULT_BAUD_RATE: BaudRate = BaudRate::B115200; diff --git a/ublox-short-range/src/wifi/network.rs b/src/network.rs similarity index 67% rename from ublox-short-range/src/wifi/network.rs rename to src/network.rs index 0ecb82f..0d9204c 100644 --- a/ublox-short-range/src/wifi/network.rs +++ b/src/network.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use crate::command::wifi::types::{OperationMode, ScannedWifiNetwork}; use crate::error::WifiError; use crate::hex::from_hex; @@ -6,15 +8,16 @@ use heapless::String; use core::convert::TryFrom; -#[derive(PartialEq, Debug, defmt::Format)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WifiMode { Station, AccessPoint, } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct WifiNetwork { - #[defmt(Debug2Format)] + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] pub bssid: Bytes<20>, pub op_mode: OperationMode, pub ssid: String<64>, @@ -26,6 +29,22 @@ pub struct WifiNetwork { pub mode: WifiMode, } +impl WifiNetwork { + pub fn new_station(bssid: Bytes<20>, channel: u8) -> Self { + Self { + bssid, + op_mode: OperationMode::Infrastructure, + ssid: String::new(), + channel, + rssi: 1, + authentication_suites: 0, + unicast_ciphers: 0, + group_ciphers: 0, + mode: WifiMode::Station, + } + } +} + impl TryFrom for WifiNetwork { type Error = WifiError; diff --git a/ublox-short-range/Cargo.toml b/ublox-short-range/Cargo.toml deleted file mode 100644 index 3097326..0000000 --- a/ublox-short-range/Cargo.toml +++ /dev/null @@ -1,56 +0,0 @@ -[package] -name = "ublox-short-range-rs" -version = "0.1.1" -authors = ["Mads Andresen "] -description = "Driver crate for u-blox short range devices, implementation follows 'UBX-14044127 - R40'" -readme = "../README.md" -keywords = ["ublox", "wifi", "shortrange", "bluetooth"] -categories = ["embedded", "no-std"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/BlackbirdHQ/ublox-short-range-rs" -edition = "2021" - -[lib] -name = "ublox_short_range" -doctest = false - -[dependencies] -# atat = { version = "0.18.0", features = ["derive", "defmt", "bytes"] } -atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "c5caaf7", features = ["derive", "defmt", "bytes"] } -heapless = { version = "^0.7", features = ["serde", "defmt-impl"] } -no-std-net = { version = "^0.5", features = ["serde"] } -serde = { version = "^1", default-features = false, features = ["derive"] } -# ublox-sockets = { version = "0.5", features = ["defmt"] } -ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "b1ff942", features = ["defmt"] } - -hash32 = "^0.2.1" -hash32-derive = "^0.1.0" - -defmt = { version = "0.3" } -embedded-hal = "=1.0.0-rc.1" -embedded-io = "0.5" -embedded-nal = "0.6.0" - -embassy-time = "0.1" - -[dev-dependencies] -embedded-io = "0.4" - -[features] -default = ["odin_w2xx", "wifi_ap", "wifi_sta", "socket-udp", "socket-tcp"] - -async = ["atat/async"] - -odin_w2xx = [] -nina_w1xx = [] -nina_b1xx = [] -anna_b1xx = [] -nina_b2xx = [] -nina_b3xx = [] - -socket-tcp = ["ublox-sockets/socket-tcp"] -socket-udp = ["ublox-sockets/socket-udp"] - -wifi_ap = [] -wifi_sta = [] -bluetooth = [] diff --git a/ublox-short-range/src/blocking_timer.rs b/ublox-short-range/src/blocking_timer.rs deleted file mode 100644 index 15be27b..0000000 --- a/ublox-short-range/src/blocking_timer.rs +++ /dev/null @@ -1,21 +0,0 @@ -use embassy_time::{Duration, Instant}; - -pub struct BlockingTimer { - expires_at: Instant, -} - -impl BlockingTimer { - pub fn after(duration: Duration) -> Self { - Self { - expires_at: Instant::now() + duration, - } - } - - pub fn wait(self) { - loop { - if self.expires_at <= Instant::now() { - break; - } - } - } -} diff --git a/ublox-short-range/src/client.rs b/ublox-short-range/src/client.rs deleted file mode 100644 index 77ea337..0000000 --- a/ublox-short-range/src/client.rs +++ /dev/null @@ -1,925 +0,0 @@ -use core::str::FromStr; - -use crate::{ - blocking_timer::BlockingTimer, - command::{ - custom_digest::EdmDigester, - data_mode::{ - types::{IPProtocol, PeerConfigParameter}, - SetPeerConfiguration, - }, - edm::{types::Protocol, urc::EdmEvent, EdmAtCmdWrapper, SwitchToEdmCommand}, - general::{types::FirmwareVersion, SoftwareVersion}, - network::SetNetworkHostName, - ping::types::PingError, - system::{ - types::{BaudRate, ChangeAfterConfirm, FlowControl, Parity, StopBits}, - RebootDCE, SetRS232Settings, StoreCurrentConfig, - }, - wifi::{ - responses::WifiStatusResponse, - types::{DisconnectReason, StatusId, WifiConfig, WifiStatus}, - GetWifiStatus, SetWifiConfig, - }, - Urc, - }, - config::Config, - error::Error, - wifi::{ - connection::{NetworkState, WiFiState, WifiConnection}, - network::{WifiMode, WifiNetwork}, - SocketMap, - }, - UbloxWifiBuffers, UbloxWifiIngress, UbloxWifiUrcChannel, -}; -use atat::{blocking::AtatClient, heapless_bytes::Bytes, AtatUrcChannel, UrcSubscription}; -use defmt::{debug, error, trace}; -use embassy_time::{Duration, Instant}; -use embedded_hal::digital::OutputPin; -use embedded_nal::{IpAddr, Ipv4Addr, SocketAddr}; -use ublox_sockets::{ - udp_listener::UdpListener, AnySocket, SocketHandle, SocketSet, SocketType, TcpSocket, TcpState, - UdpSocket, UdpState, -}; - -#[derive(PartialEq, Eq, Copy, Clone)] -pub enum SerialMode { - Cmd, - ExtendedData, -} - -#[derive(PartialEq, Copy, Clone)] -pub enum DNSState { - Resolving, - Resolved(IpAddr), - Error(PingError), -} - -/// From u-connectXpress AT commands manual: -/// depends on the . For internet domain names, the maximum -/// length is 64 characters. -/// Domain name length is 128 for NINA-W13 and NINA-W15 software version 4.0 -/// .0 or later. -#[cfg(not(feature = "nina_w1xx"))] -pub const MAX_DOMAIN_NAME_LENGTH: usize = 64; - -#[cfg(feature = "nina_w1xx")] -pub const MAX_DOMAIN_NAME_LENGTH: usize = 128; - -pub struct DNSTableEntry { - domain_name: heapless::String, - state: DNSState, -} - -impl DNSTableEntry { - pub const fn new( - state: DNSState, - domain_name: heapless::String, - ) -> Self { - Self { domain_name, state } - } -} - -pub struct DNSTable { - pub table: heapless::Deque, -} - -impl DNSTable { - const fn new() -> Self { - Self { - table: heapless::Deque::new(), - } - } - pub fn upsert(&mut self, new_entry: DNSTableEntry) { - if let Some(entry) = self - .table - .iter_mut() - .find(|e| e.domain_name == new_entry.domain_name) - { - entry.state = new_entry.state; - return; - } - - if self.table.is_full() { - self.table.pop_front(); - } - unsafe { - self.table.push_back_unchecked(new_entry); - } - } - - pub fn get_state( - &self, - domain_name: heapless::String, - ) -> Option { - self.table - .iter() - .find(|e| e.domain_name == domain_name) - .map(|x| x.state) - } - pub fn reverse_lookup(&self, ip: IpAddr) -> Option<&heapless::String> { - match self - .table - .iter() - .find(|e| e.state == DNSState::Resolved(ip)) - { - Some(entry) => Some(&entry.domain_name), - None => None, - } - } -} - -#[derive(PartialEq, Eq, Clone, Default)] -pub struct SecurityCredentials { - pub ca_cert_name: Option>, - pub c_cert_name: Option>, // TODO: Make &str with lifetime - pub c_key_name: Option>, -} - -/// Creates new socket numbers -/// Properly not Async safe -pub fn new_socket_num(sockets: &SocketSet) -> Result { - let mut num = 0; - while sockets.socket_type(SocketHandle(num)).is_some() { - num += 1; - if num == u8::MAX { - return Err(()); - } - } - Ok(num) -} - -pub(crate) const URC_CAPACITY: usize = 2; -pub(crate) const URC_SUBSCRIBERS: usize = 1; - -pub struct UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> -where - RST: OutputPin, -{ - pub(crate) module_started: bool, - pub(crate) initialized: bool, - serial_mode: SerialMode, - pub(crate) dns_table: DNSTable, - pub(crate) wifi_connection: Option, - pub(crate) wifi_config_active_on_startup: Option, - pub(crate) client: AtCl, - pub(crate) config: Config, - pub(crate) sockets: Option<&'static mut SocketSet>, - pub(crate) security_credentials: SecurityCredentials, - pub(crate) socket_map: SocketMap, - pub(crate) udp_listener: UdpListener<2, N>, - urc_subscription: UrcSubscription<'sub, EdmEvent, URC_CAPACITY, URC_SUBSCRIBERS>, - _urc_channel: &'buf AtUrcCh, -} - -impl<'buf, 'sub, W, RST, const INGRESS_BUF_SIZE: usize, const N: usize, const L: usize> - UbloxClient< - 'buf, - 'sub, - atat::blocking::Client<'buf, W, INGRESS_BUF_SIZE>, - UbloxWifiUrcChannel, - RST, - N, - L, - > -where - 'buf: 'sub, - W: embedded_io::Write, - RST: OutputPin, -{ - /// Create new u-blox device - /// - /// Look for [`data_service`](Device::data_service) how to handle data connection automatically. - /// - pub fn from_buffers( - buffers: &'buf UbloxWifiBuffers, - tx: W, - config: Config, - ) -> (UbloxWifiIngress, Self) { - let (ingress, client) = - buffers.split_blocking(tx, EdmDigester::default(), atat::Config::default()); - - ( - ingress, - UbloxClient::new(client, &buffers.urc_channel, config), - ) - } -} - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - AtUrcCh: AtatUrcChannel, - RST: OutputPin, -{ - pub fn new(client: AtCl, _urc_channel: &'buf AtUrcCh, config: Config) -> Self { - let urc_subscription = _urc_channel.subscribe().unwrap(); - UbloxClient { - module_started: false, - initialized: false, - serial_mode: SerialMode::Cmd, - dns_table: DNSTable::new(), - wifi_connection: None, - wifi_config_active_on_startup: None, - client, - config, - sockets: None, - security_credentials: SecurityCredentials::default(), - socket_map: SocketMap::default(), - udp_listener: UdpListener::new(), - urc_subscription, - _urc_channel, - } - } -} - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - pub fn set_socket_storage(&mut self, socket_set: &'static mut SocketSet) { - socket_set.prune(); - self.sockets.replace(socket_set); - } - - pub fn take_socket_storage(&mut self) -> Option<&'static mut SocketSet> { - self.sockets.take() - } - - pub fn has_socket_storage(&self) -> bool { - self.sockets.is_some() - } - - pub fn init(&mut self) -> Result<(), Error> { - // Initilize a new ublox device to a known state (set RS232 settings) - - debug!("Initializing wifi"); - // Hard reset module - self.reset()?; - - // Switch to EDM on Init. If in EDM, fail and check with autosense - // if self.serial_mode != SerialMode::ExtendedData { - // self.retry_send(&SwitchToEdmCommand, 5)?; - // self.serial_mode = SerialMode::ExtendedData; - // } - - while self.serial_mode != SerialMode::ExtendedData { - self.send_internal(&SwitchToEdmCommand, true).ok(); - BlockingTimer::after(Duration::from_millis(100)).wait(); - self.handle_urc()?; - } - - // TODO: handle EDM settings quirk see EDM datasheet: 2.2.5.1 AT Request Serial settings - self.retry_send( - &EdmAtCmdWrapper(SetRS232Settings { - baud_rate: BaudRate::B115200, - flow_control: FlowControl::On, - data_bits: 8, - stop_bits: StopBits::One, - parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, - }), - 5, - )?; - - if let Some(hostname) = self.config.hostname.clone() { - self.send_internal( - &EdmAtCmdWrapper(SetNetworkHostName { - host_name: hostname.as_str(), - }), - false, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(SetWifiConfig { - config_param: WifiConfig::RemainOnChannel(0), - }), - false, - )?; - - self.send_internal(&EdmAtCmdWrapper(StoreCurrentConfig), false)?; - - self.software_reset()?; - - while self.serial_mode != SerialMode::ExtendedData { - self.send_internal(&SwitchToEdmCommand, true).ok(); - BlockingTimer::after(Duration::from_millis(100)).wait(); - self.handle_urc()?; - } - - let response = self.send_internal(&EdmAtCmdWrapper(SoftwareVersion), true)?; - if response.version < FirmwareVersion::new(8, 0, 0) { - self.config.network_up_bug = true; - } else { - if let Some(size) = self.config.tls_in_buffer_size { - self.send_internal( - &EdmAtCmdWrapper(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsInBuffer(size), - }), - false, - )?; - } - - if let Some(size) = self.config.tls_out_buffer_size { - self.send_internal( - &EdmAtCmdWrapper(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsOutBuffer(size), - }), - false, - )?; - } - } - - self.load_wifi_config(); - - self.initialized = true; - - Ok(()) - } - - pub fn firmware_version(&mut self) -> Result { - let response = self.send_at(SoftwareVersion)?; - Ok(response.version) - } - - pub fn signal_strength(&mut self) -> Result { - if let WifiStatusResponse { - status_id: WifiStatus::RSSI(rssi), - } = self.send_at(GetWifiStatus { - status_id: StatusId::RSSI, - })? { - Ok(rssi) - } else { - Err(Error::_Unknown) - } - } - - fn load_wifi_config(&mut self) { - //Check if we have active network config if so then update wifi_connection - if let Ok(active) = self.is_active_on_startup() { - if active { - if let Ok(ssid) = self.get_ssid() { - defmt::info!("Found network in module configuring as active network"); - self.wifi_connection.replace(WifiConnection::new( - WifiNetwork { - bssid: Bytes::new(), - op_mode: crate::command::wifi::types::OperationMode::Infrastructure, - ssid: ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::Station, - }, - WiFiState::NotConnected, - )); - } - } - } - } - - pub fn retry_send( - &mut self, - cmd: &A, - attempts: usize, - ) -> Result - where - A: atat::AtatCmd, - { - for _ in 0..attempts { - match self.send_internal(cmd, true) { - Ok(resp) => { - return Ok(resp); - } - Err(_e) => {} - }; - } - Err(Error::BaudDetection) - } - - pub fn reset(&mut self) -> Result<(), Error> { - self.serial_mode = SerialMode::Cmd; - self.initialized = false; - self.module_started = false; - self.wifi_connection = None; - self.wifi_config_active_on_startup = None; - self.security_credentials = SecurityCredentials::default(); - self.socket_map = SocketMap::default(); - self.udp_listener = UdpListener::new(); - - self.clear_buffers()?; - - if let Some(ref mut pin) = self.config.rst_pin { - defmt::warn!("Hard resetting Ublox Short Range"); - pin.set_low().ok(); - - BlockingTimer::after(Duration::from_millis(50)).wait(); - - pin.set_high().ok(); - - let expiration = Instant::now() + Duration::from_secs(4); - - while Instant::now() < expiration { - self.handle_urc().ok(); - if self.module_started { - return Ok(()); - } - } - return Err(Error::Timeout); - } - - Ok(()) - } - - pub fn software_reset(&mut self) -> Result<(), Error> { - self.serial_mode = SerialMode::Cmd; - self.initialized = false; - self.module_started = false; - self.wifi_connection = None; - self.wifi_config_active_on_startup = None; - self.security_credentials = SecurityCredentials::default(); - self.socket_map = SocketMap::default(); - self.udp_listener = UdpListener::new(); - - defmt::warn!("Soft resetting Ublox Short Range"); - self.send_internal(&EdmAtCmdWrapper(RebootDCE), false)?; - self.clear_buffers()?; - - let expiration = Instant::now() + Duration::from_secs(4); - while Instant::now() < expiration { - self.handle_urc().ok(); - if self.module_started { - return Ok(()); - } - } - Err(Error::Timeout) - } - - pub(crate) fn clear_buffers(&mut self) -> Result<(), Error> { - if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - sockets.prune(); - } - - Ok(()) - } - - pub fn spin(&mut self) -> Result<(), Error> { - if !self.initialized { - return Err(Error::Uninitialized); - } - - self.handle_urc()?; - - self.connected_to_network()?; - - Ok(()) - } - - pub(crate) fn send_internal( - &mut self, - req: &A, - check_urc: bool, - ) -> Result - where - A: atat::AtatCmd, - { - if check_urc { - if let Err(e) = self.handle_urc() { - error!("Failed handle URC: {:?}", e); - } - } - - self.client.send(req).map_err(|e| { - error!("{:?}: {=[u8]:a}", e, req.as_bytes()); - e.into() - }) - } - - fn handle_urc(&mut self) -> Result<(), Error> { - if let Some(edm_urc) = self.urc_subscription.try_next_message_pure() { - match edm_urc { - EdmEvent::ATEvent(urc) => { - match urc { - Urc::StartUp => { - debug!("[URC] Startup"); - self.module_started = true; - self.initialized = false; - self.serial_mode = SerialMode::Cmd; - } - Urc::PeerConnected(event) => { - debug!("[URC] PeerConnected"); - - // TODO: - // - // We should probably move - // `tcp.set_state(TcpState::Connected(endpoint));` - // + `udp.set_state(UdpState::Established);` as - // well as `tcp.update_handle(*socket);` + - // `udp.update_handle(*socket);` here, to make - // sure that part also works without EDM mode - - if let Some(sockets) = self.sockets.as_deref_mut() { - let remote_ip = Ipv4Addr::from_str( - core::str::from_utf8(event.remote_address.as_slice()).unwrap(), - ) - .unwrap(); - - let remote = SocketAddr::new(remote_ip.into(), event.remote_port); - - if let Some(queue) = self.udp_listener.incoming(event.local_port) { - trace!("[UDP Server] Server socket incomming"); - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - // TODO Is this correct? - if !sockets.recycle() {} - } - let peer_handle = event.handle; - let socket_handle = - SocketHandle(new_socket_num(sockets).unwrap()); - let mut new_socket = UdpSocket::new(socket_handle.0); - new_socket.set_state(UdpState::Established); - if new_socket.bind(remote).is_err() { - error!("[UDP_URC] Binding connecting socket Error"); - } - if sockets.add(new_socket).is_err() { - error!("[UDP_URC] Opening socket Error: Socket set full"); - } - if self - .socket_map - .insert_peer(peer_handle, socket_handle) - .is_err() - { - error!("[UDP_URC] Opening socket Error: Socket Map full"); - } - debug!( - "[URC] Binding remote {=[u8]:a} to UDP server on port: {:?} with handle: {:?}", - event.remote_address.as_slice(), - event.local_port, - socket_handle - ); - queue.enqueue((socket_handle, remote)).ok(); - } else { - match event.protocol { - IPProtocol::TCP => { - // if let Ok(mut tcp) = - // sockets.get::>(event.handle) - // { - // debug!( - // "Binding remote {=[u8]:a} to TCP socket {:?}", - // event.remote_address.as_slice(), - // event.handle - // ); - // tcp.set_state(TcpState::Connected(remote)); - // return true; - // } - } - IPProtocol::UDP => { - // if let Ok(mut udp) = - // sockets.get::>(event.handle) - // { - // debug!( - // "Binding remote {=[u8]:a} to UDP socket {:?}", - // event.remote_address.as_slice(), - // event.handle - // ); - // udp.bind(remote).unwrap(); - // udp.set_state(UdpState::Established); - // return true; - // } - } - } - } - } - } - Urc::PeerDisconnected(msg) => { - debug!("[URC] PeerDisconnected"); - if let Some(sockets) = self.sockets.as_deref_mut() { - if let Some(handle) = self.socket_map.peer_to_socket(&msg.handle) { - match sockets.socket_type(*handle) { - Some(SocketType::Tcp) => { - if let Ok(mut tcp) = - sockets.get::>(*handle) - { - tcp.closed_by_remote(); - } - } - Some(SocketType::Udp) => { - if let Ok(mut udp) = - sockets.get::>(*handle) - { - udp.close(); - } - sockets.remove(*handle).ok(); - } - _ => {} - } - self.socket_map.remove_peer(&msg.handle); - } - } - } - Urc::WifiLinkConnected(msg) => { - debug!("[URC] WifiLinkConnected"); - if let Some(ref mut con) = self.wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = msg.bssid; - con.network.channel = msg.channel; - } else { - debug!("[URC] Active network config discovered"); - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: msg.bssid, - op_mode: crate::command::wifi::types::OperationMode::Infrastructure, - ssid: heapless::String::new(), - channel: msg.channel, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::Station, - }, - WiFiState::Connected, - ).activate() - ); - } - } - Urc::WifiLinkDisconnected(msg) => { - debug!("[URC] WifiLinkDisconnected"); - if let Some(ref mut con) = self.wifi_connection { - match msg.reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } - } - } - } - Urc::WifiAPUp(_) => { - debug!("[URC] WifiAPUp"); - } - Urc::WifiAPDown(_) => { - debug!("[URC] WifiAPDown"); - } - Urc::WifiAPStationConnected(client) => { - debug!( - "[URC] WifiAPStationConnected {=[u8]:a}", - client.mac_addr.into_inner() - ); - } - Urc::WifiAPStationDisconnected(_) => { - debug!("[URC] WifiAPStationDisconnected"); - } - Urc::EthernetLinkUp(_) => { - debug!("[URC] EthernetLinkUp"); - } - Urc::EthernetLinkDown(_) => { - debug!("[URC] EthernetLinkDown"); - } - Urc::NetworkUp(_) => { - debug!("[URC] NetworkUp"); - if let Some(ref mut con) = self.wifi_connection { - if self.config.network_up_bug { - match con.network_state { - NetworkState::Attached => (), - NetworkState::AlmostAttached => { - con.network_state = NetworkState::Attached - } - NetworkState::Unattached => { - con.network_state = NetworkState::AlmostAttached - } - } - } else { - con.network_state = NetworkState::Attached; - } - } - } - Urc::NetworkDown(_) => { - debug!("[URC] NetworkDown"); - if let Some(ref mut con) = self.wifi_connection { - con.network_state = NetworkState::Unattached; - } - } - Urc::NetworkError(_) => { - debug!("[URC] NetworkError"); - } - Urc::PingResponse(resp) => { - debug!("[URC] PingResponse"); - self.dns_table.upsert(DNSTableEntry { - domain_name: resp.hostname, - state: DNSState::Resolved(resp.ip), - }); - } - Urc::PingErrorResponse(resp) => { - debug!("[URC] PingErrorResponse: {:?}", resp.error); - } - } - } // end match urc - EdmEvent::StartUp => { - debug!("[EDM_URC] STARTUP"); - self.module_started = true; - self.serial_mode = SerialMode::ExtendedData; - } - EdmEvent::IPv4ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv4ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = self.sockets.as_deref_mut() { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = self.udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _)) = - queue.into_iter().find(|(_, remote)| remote == &endpoint) - { - self.socket_map - .insert_channel(event.channel_id, *socket_handle) - .ok(); - } - } else { - for (h, s) in sockets.iter_mut() { - match (&event.protocol, s.get_type()) { - (Protocol::TCP, SocketType::Tcp) => { - let mut tcp = TcpSocket::downcast(s)?; - if tcp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - tcp.set_state(TcpState::Connected(endpoint)); - } - } - (Protocol::UDP, SocketType::Udp) => { - let mut udp = UdpSocket::downcast(s)?; - if udp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - udp.set_state(UdpState::Established); - } - } - _ => {} - } - } - } - } - } - EdmEvent::IPv6ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv6ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = self.sockets.as_deref_mut() { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = self.udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _)) = - queue.into_iter().find(|(_, remote)| remote == &endpoint) - { - self.socket_map - .insert_channel(event.channel_id, *socket_handle) - .ok(); - } - } else { - for (h, s) in sockets.iter_mut() { - match (&event.protocol, s.get_type()) { - (Protocol::TCP, SocketType::Tcp) => { - let mut tcp = TcpSocket::downcast(s)?; - if tcp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - tcp.set_state(TcpState::Connected(endpoint)); - } - } - (Protocol::UDP, SocketType::Udp) => { - let mut udp = UdpSocket::downcast(s)?; - if udp.endpoint() == Some(endpoint) { - self.socket_map - .insert_channel(event.channel_id, h) - .ok(); - udp.set_state(UdpState::Established); - } - } - _ => {} - } - } - } - } - } - EdmEvent::BluetoothConnectEvent(_) => { - debug!("[EDM_URC] BluetoothConnectEvent"); - } - EdmEvent::DisconnectEvent(channel_id) => { - debug!("[EDM_URC] DisconnectEvent! Channel_id: {:?}", channel_id); - self.socket_map.remove_channel(&channel_id); - } - EdmEvent::DataEvent(event) => { - debug!("[EDM_URC] DataEvent! Channel_id: {:?}", event.channel_id); - if let Some(sockets) = self.sockets.as_deref_mut() { - if !event.data.is_empty() { - if let Some(socket_handle) = - self.socket_map.channel_to_socket(&event.channel_id) - { - match sockets.socket_type(*socket_handle) { - Some(SocketType::Tcp) => { - // Handle tcp socket - let mut tcp = - sockets.get::>(*socket_handle).unwrap(); - if tcp.can_recv() { - tcp.rx_enqueue_slice(&event.data); - } - } - Some(SocketType::Udp) => { - // Handle udp socket - let mut udp = - sockets.get::>(*socket_handle).unwrap(); - - if udp.can_recv() { - udp.rx_enqueue_slice(&event.data); - } - } - _ => { - error!("SocketNotFound {:?}", socket_handle); - } - } - } - } - } - } - }; - }; - Ok(()) - } - - /// Send AT command - /// Automaticaly waraps commands in EDM context - pub fn send_at(&mut self, cmd: A) -> Result - where - A: atat::AtatCmd, - { - if !self.initialized { - return Err(Error::Uninitialized); - } - match self.serial_mode { - SerialMode::ExtendedData => self.send_internal(&EdmAtCmdWrapper(cmd), true), - SerialMode::Cmd => self.send_internal(&cmd, true), - } - } - - /// Is the module attached to a WiFi and ready to open sockets - pub(crate) fn connected_to_network(&self) -> Result<(), Error> { - if let Some(ref con) = self.wifi_connection { - if !self.initialized { - Err(Error::Uninitialized) - } else if !con.is_connected() { - Err(Error::WifiState(con.wifi_state)) - } else if self.sockets.is_none() { - Err(Error::MissingSocketSet) - } else { - Ok(()) - } - } else { - Err(Error::NoWifiSetup) - } - } - - /// Is the module attached to a WiFi - /// - // TODO: handle this case for better stability - // WiFi connection can disconnect momentarily, but if the network state does not change - // the current context is safe. - pub fn attached_to_wifi(&self) -> Result<(), Error> { - if let Some(ref con) = self.wifi_connection { - if !self.initialized { - Err(Error::Uninitialized) - // } else if !(con.network_state == NetworkState::Attached) { - } else if !con.is_connected() { - if con.wifi_state == WiFiState::Connected { - Err(Error::NetworkState(con.network_state)) - } else { - Err(Error::WifiState(con.wifi_state)) - } - } else { - Ok(()) - } - } else { - Err(Error::NoWifiSetup) - } - } -} diff --git a/ublox-short-range/src/config.rs b/ublox-short-range/src/config.rs deleted file mode 100644 index 2002734..0000000 --- a/ublox-short-range/src/config.rs +++ /dev/null @@ -1,94 +0,0 @@ -use embedded_hal::digital::{ErrorType, OutputPin}; -use heapless::String; - -pub struct NoPin; - -impl ErrorType for NoPin { - type Error = core::convert::Infallible; -} - -impl OutputPin for NoPin { - fn set_low(&mut self) -> Result<(), Self::Error> { - Ok(()) - } - - fn set_high(&mut self) -> Result<(), Self::Error> { - Ok(()) - } -} - -#[derive(Debug)] -pub struct Config { - pub(crate) rst_pin: Option, - pub(crate) hostname: Option>, - pub(crate) tls_in_buffer_size: Option, - pub(crate) tls_out_buffer_size: Option, - pub(crate) network_up_bug: bool, -} - -impl Default for Config { - fn default() -> Self { - Config { - rst_pin: None, - hostname: None, - tls_in_buffer_size: None, - tls_out_buffer_size: None, - network_up_bug: true, - } - } -} - -impl Config -where - RST: OutputPin, -{ - pub fn new() -> Self { - Config { - rst_pin: None, - hostname: None, - tls_in_buffer_size: None, - tls_out_buffer_size: None, - network_up_bug: true, - } - } - - pub fn with_rst(self, rst_pin: RST) -> Self { - Config { - rst_pin: Some(rst_pin), - ..self - } - } - - pub fn with_hostname(self, hostname: &str) -> Self { - Config { - hostname: Some(String::from(hostname)), - ..self - } - } - - /// Experimental use of undocumented setting for TLS buffers - /// - /// For Odin: - /// Minimum is 512 and maximum is 16K (16384). - /// DEFAULT_TLS_IN_BUFFER_SIZE (7800) - pub fn tls_in_buffer_size(self, bytes: u16) -> Self { - assert!(bytes > 512); - Config { - tls_in_buffer_size: Some(bytes), - ..self - } - } - - /// Experimental use of undocumented setting for TLS buffers - /// - /// For Odin: - /// Minimum is 512 and maximum is 16K (16384). - /// DEFAULT_TLS_OUT_BUFFER_SIZE (3072) - pub fn tls_out_buffer_size(self, bytes: u16) -> Self { - assert!(bytes > 512); - Config { - tls_out_buffer_size: Some(bytes), - ..self - } - } -} diff --git a/ublox-short-range/src/lib.rs b/ublox-short-range/src/lib.rs deleted file mode 100644 index bd58012..0000000 --- a/ublox-short-range/src/lib.rs +++ /dev/null @@ -1,32 +0,0 @@ -#![cfg_attr(not(test), no_std)] - -mod blocking_timer; -mod client; -mod hex; - -pub use atat; -pub use client::UbloxClient; -use client::{URC_CAPACITY, URC_SUBSCRIBERS}; - -pub mod command; -pub mod config; -pub mod error; -pub mod wifi; - -use command::edm::urc::EdmEvent; -#[cfg(any(feature = "socket-udp", feature = "socket-tcp"))] -pub use wifi::tls::TLS; - -pub type UbloxWifiBuffers = - atat::Buffers; - -pub type UbloxWifiIngress<'a, const INGRESS_BUF_SIZE: usize> = atat::Ingress< - 'a, - command::custom_digest::EdmDigester, - EdmEvent, - INGRESS_BUF_SIZE, - URC_CAPACITY, - URC_SUBSCRIBERS, ->; - -pub type UbloxWifiUrcChannel = atat::UrcChannel; diff --git a/ublox-short-range/src/wifi/ap.rs b/ublox-short-range/src/wifi/ap.rs deleted file mode 100644 index e561c52..0000000 --- a/ublox-short-range/src/wifi/ap.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::{ - client::UbloxClient, - command::{ - edm::EdmAtCmdWrapper, - wifi::{ - self, - types::{ - AccessPointAction, AccessPointConfig, AccessPointId, IPv4Mode, PasskeyR, - SecurityMode, SecurityModePSK, - }, - SetWifiAPConfig, WifiAPAction, - }, - }, - error::WifiHotspotError, - wifi::{ - network::{WifiMode, WifiNetwork}, - options::{ConnectionOptions, HotspotOptions}, - }, -}; -use atat::blocking::AtatClient; -use atat::heapless_bytes::Bytes; -use embedded_hal::digital::OutputPin; - -use super::connection::{WiFiState, WifiConnection}; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - /// Creates wireless hotspot service for host machine. - pub fn create_hotspot( - &mut self, - options: ConnectionOptions, - configuration: HotspotOptions, - ) -> Result<(), WifiHotspotError> { - let ap_config_id = AccessPointId::Id0; - - // Network part - // Deactivate network id 0 - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Deactivate, - }), - true, - )?; - - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Reset, - }), - true, - )?; - - if let Some(ref con) = self.wifi_connection { - if con.activated { - return Err(WifiHotspotError::CreationFailed); - } - } - - // Disable DHCP Server (static IP address will be used) - if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::IPv4Mode(IPv4Mode::Static), - }), - true, - )?; - } - - // Network IP address - if let Some(ip) = options.ip { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::IPv4Address(ip), - }), - true, - )?; - } - // Network Subnet mask - if let Some(subnet) = options.subnet { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SubnetMask(subnet), - }), - true, - )?; - } - // Network Default gateway - if let Some(gateway) = options.gateway { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::DefaultGateway(gateway), - }), - true, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::DHCPServer(true.into()), - }), - true, - )?; - - // Active on startup - // self.send_internal(&SetWifiAPConfig{ - // ap_config_id, - // ap_config_param: AccessPointConfig::ActiveOnStartup(true), - // }, true)?; - - // Wifi part - // Set the Network SSID to connect to - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SSID(options.ssid.clone()), - }), - true, - )?; - - if let Some(pass) = options.password.clone() { - // Use WPA2 as authentication type - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SecurityMode( - SecurityMode::Wpa2AesCcmp, - SecurityModePSK::PSK, - ), - }), - true, - )?; - - // Input passphrase - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::PSKPassphrase(PasskeyR::Passphrase(pass)), - }), - true, - )?; - } else { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SecurityMode( - SecurityMode::Open, - SecurityModePSK::Open, - ), - }), - true, - )?; - } - - if let Some(channel) = configuration.channel { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::Channel(channel as u8), - }), - true, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Activate, - }), - true, - )?; - - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: Bytes::new(), - op_mode: wifi::types::OperationMode::AdHoc, - ssid: options.ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::AccessPoint, - }, - WiFiState::NotConnected, - ) - .activate(), - ); - Ok(()) - } - - /// Stop serving a wireless network. - /// - /// **NOTE: All users connected will automatically be disconnected.** - pub fn stop_hotspot(&mut self) -> Result<(), WifiHotspotError> { - let ap_config_id = AccessPointId::Id0; - - if let Some(ref con) = self.wifi_connection { - if con.activated { - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Deactivate, - }), - true, - )?; - } - } else { - return Err(WifiHotspotError::FailedToStop); - } - if let Some(ref mut con) = self.wifi_connection { - con.deactivate() - } - - Ok(()) - } -} diff --git a/ublox-short-range/src/wifi/connection.rs b/ublox-short-range/src/wifi/connection.rs deleted file mode 100644 index c3717e9..0000000 --- a/ublox-short-range/src/wifi/connection.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::wifi::network::{WifiMode, WifiNetwork}; - -#[derive(Debug, Clone, Copy, PartialEq, defmt::Format)] -pub enum WiFiState { - Inactive, - /// Searching for Wifi - NotConnected, - Connected, -} - -/// Describes whether device is connected to a network and has an IP or not. -/// It is possible to be attached to a network but have no Wifi connection. -#[derive(Debug, Clone, Copy, PartialEq, defmt::Format)] -pub enum NetworkState { - Attached, - AlmostAttached, - Unattached, -} - -// Fold into wifi connectivity -#[derive(defmt::Format)] -pub struct WifiConnection { - /// Keeps track of connection state on module - pub wifi_state: WiFiState, - pub network_state: NetworkState, - pub network: WifiNetwork, - /// Keeps track of activation of the config by driver - pub activated: bool, -} - -impl WifiConnection { - pub(crate) fn new(network: WifiNetwork, wifi_state: WiFiState) -> Self { - WifiConnection { - wifi_state, - network_state: NetworkState::Unattached, - network, - activated: false, - } - } - - pub(crate) fn is_connected(&self) -> bool { - self.network_state == NetworkState::Attached && self.wifi_state == WiFiState::Connected - } - - pub fn is_station(&self) -> bool { - self.network.mode == WifiMode::Station - } - - pub fn is_access_point(&self) -> bool { - !self.is_station() - } - - pub(crate) fn activate(mut self) -> Self { - self.activated = true; - self - } - - pub(crate) fn deactivate(&mut self) { - self.activated = false; - } -} diff --git a/ublox-short-range/src/wifi/dns.rs b/ublox-short-range/src/wifi/dns.rs deleted file mode 100644 index 624949a..0000000 --- a/ublox-short-range/src/wifi/dns.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::client::{DNSState, DNSTableEntry}; -use atat::blocking::AtatClient; -use embassy_time::{Duration, Instant}; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, AddrType, Dns, IpAddr}; -use heapless::String; - -use crate::{command::ping::*, UbloxClient}; -use ublox_sockets::Error; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> Dns - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - type Error = Error; - - fn get_host_by_address(&mut self, _ip_addr: IpAddr) -> nb::Result, Self::Error> { - unimplemented!() - } - - fn get_host_by_name( - &mut self, - hostname: &str, - _addr_type: AddrType, - ) -> nb::Result { - defmt::debug!("Lookup hostname: {}", hostname); - self.send_at(Ping { - hostname, - retry_num: 1, - }) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - self.dns_table.upsert(DNSTableEntry::new( - DNSState::Resolving, - String::from(hostname), - )); - - let expiration = Instant::now() + Duration::from_secs(8); - - while let Some(DNSState::Resolving) = self.dns_table.get_state(String::from(hostname)) { - self.spin().map_err(|_| nb::Error::Other(Error::Illegal))?; - - if Instant::now() >= expiration { - break; - } - } - - match self.dns_table.get_state(String::from(hostname)) { - Some(DNSState::Resolved(ip)) => Ok(ip), - Some(DNSState::Resolving) => { - self.dns_table.upsert(DNSTableEntry::new( - DNSState::Error(types::PingError::Timeout), - String::from(hostname), - )); - Err(nb::Error::Other(Error::Timeout)) - } - _ => Err(nb::Error::Other(Error::Illegal)), - } - } -} diff --git a/ublox-short-range/src/wifi/mod.rs b/ublox-short-range/src/wifi/mod.rs deleted file mode 100644 index b570b1d..0000000 --- a/ublox-short-range/src/wifi/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::command::PeerHandle; -pub use ublox_sockets::SocketHandle; - -use crate::command::edm::types::ChannelId; - -pub mod ap; -pub mod connection; -pub mod dns; -pub mod network; -pub mod options; -pub mod sta; -pub mod tls; - -pub mod peer_builder; - -#[cfg(feature = "socket-udp")] -pub mod udp_stack; - -#[cfg(feature = "socket-tcp")] -pub mod tcp_stack; - -pub(crate) const EGRESS_CHUNK_SIZE: usize = 512; -/// The socket map, keeps mappings between `ublox::sockets`s `SocketHandle`, -/// and the modems `PeerHandle` and `ChannelId`. The peer handle is used -/// for controlling the connection, while the channel id is used for sending -/// data over the connection in EDM mode. - -pub enum SocketMapError { - Full, - NotFound, -} - -pub struct SocketMap { - channel_map: heapless::FnvIndexMap, - peer_map: heapless::FnvIndexMap, -} - -impl defmt::Format for SocketMap { - fn format(&self, fmt: defmt::Formatter) { - defmt::write!(fmt, "ChannelMap:\n"); - for (channel, socket) in self.channel_map.iter() { - defmt::write!(fmt, "channelId: {}, Handle: {}\n", channel.0, socket.0) - } - defmt::write!(fmt, "PeerMap:\n"); - for (peer, socket) in self.peer_map.iter() { - defmt::write!(fmt, "PeerId: {}, Handle: {}\n", peer.0, socket.0) - } - } -} - -impl Default for SocketMap { - fn default() -> Self { - Self::new() - } -} - -impl SocketMap { - fn new() -> Self { - Self { - channel_map: heapless::FnvIndexMap::new(), - peer_map: heapless::FnvIndexMap::new(), - } - } - - pub fn insert_channel( - &mut self, - channel_id: ChannelId, - socket_handle: SocketHandle, - ) -> Result<(), SocketMapError> { - defmt::trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, channel_id); - match self.channel_map.insert(channel_id, socket_handle) { - Ok(_) => Ok(()), - Err(_) => { - defmt::error!("Failed inserting channel SocketMap full"); - Err(SocketMapError::Full) - } - } - } - - pub fn remove_channel(&mut self, channel_id: &ChannelId) { - defmt::trace!("[SOCK_MAP] {:?} removed", channel_id); - self.channel_map.remove(channel_id); - } - - pub fn channel_to_socket(&self, channel_id: &ChannelId) -> Option<&SocketHandle> { - self.channel_map.get(channel_id) - } - - pub fn socket_to_channel_id(&self, socket_handle: &SocketHandle) -> Option<&ChannelId> { - self.channel_map - .iter() - .find_map(|(c, s)| if s == socket_handle { Some(c) } else { None }) - } - - pub fn insert_peer( - &mut self, - peer: PeerHandle, - socket_handle: SocketHandle, - ) -> Result<(), SocketMapError> { - defmt::trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, peer); - if self.peer_map.insert(peer, socket_handle).is_err() { - defmt::error!("Insert peer failed SocketMap is FULL"); - return Err(SocketMapError::Full); - }; - Ok(()) - } - - pub fn remove_peer(&mut self, peer: &PeerHandle) { - defmt::trace!("[SOCK_MAP] {:?} removed", peer); - self.peer_map.remove(peer); - } - - pub fn peer_to_socket(&self, peer: &PeerHandle) -> Option<&SocketHandle> { - self.peer_map.get(peer) - } - - pub fn socket_to_peer(&self, socket_handle: &SocketHandle) -> Option<&PeerHandle> { - self.peer_map - .iter() - .find_map(|(c, s)| if s == socket_handle { Some(c) } else { None }) - } -} diff --git a/ublox-short-range/src/wifi/options.rs b/ublox-short-range/src/wifi/options.rs deleted file mode 100644 index f76b581..0000000 --- a/ublox-short-range/src/wifi/options.rs +++ /dev/null @@ -1,136 +0,0 @@ -use embedded_nal::Ipv4Addr; -use heapless::String; -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -#[derive(Debug, Clone, Copy)] -/// Channel to broadcast wireless hotspot on. -pub enum Channel { - /// Channel 1 - One = 1, - /// Channel 2 - Two = 2, - /// Channel 3 - Three = 3, - /// Channel 4 - Four = 4, - /// Channel 5 - Five = 5, - /// Channel 6 - Six = 6, -} - -#[allow(dead_code)] -#[derive(Debug)] -/// Band type of wireless hotspot. -pub enum Band { - /// Band `A` - A, - /// Band `BG` - Bg, -} - -#[derive(Debug, Default)] -pub struct HotspotOptions { - pub(crate) channel: Option, - pub(crate) band: Option, -} - -impl HotspotOptions { - pub fn new() -> Self { - Self { - channel: Some(Channel::One), - band: Some(Band::Bg), - } - } - - pub fn channel(mut self, channel: Channel) -> Self { - self.channel = Some(channel); - self - } - - pub fn band(mut self, band: Band) -> Self { - self.band = Some(band); - self - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, defmt::Format)] -pub struct ConnectionOptions { - pub ssid: String<64>, - pub password: Option>, - - #[defmt(Debug2Format)] - pub ip: Option, - #[defmt(Debug2Format)] - pub subnet: Option, - #[defmt(Debug2Format)] - pub gateway: Option, -} - -impl ConnectionOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn ssid(mut self, ssid: String<64>) -> Self { - self.ssid = ssid; - self - } - - pub fn password(mut self, password: String<64>) -> Self { - self.password = Some(password); - self - } - - pub fn ip_address(mut self, ip_addr: Ipv4Addr) -> Self { - self.ip = Some(ip_addr); - self.subnet = if let Some(subnet) = self.subnet { - Some(subnet) - } else { - Some(Ipv4Addr::new(255, 255, 255, 0)) - }; - - self.gateway = if let Some(gateway) = self.gateway { - Some(gateway) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - self - } - - pub fn subnet_address(mut self, subnet_addr: Ipv4Addr) -> Self { - self.subnet = Some(subnet_addr); - - self.ip = if let Some(ip) = self.ip { - Some(ip) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - - self.gateway = if let Some(gateway) = self.gateway { - Some(gateway) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - - self - } - - pub fn gateway_address(mut self, gateway_addr: Ipv4Addr) -> Self { - self.gateway = Some(gateway_addr); - - self.subnet = if let Some(subnet) = self.subnet { - Some(subnet) - } else { - Some(Ipv4Addr::new(255, 255, 255, 0)) - }; - - self.ip = if let Some(ip) = self.ip { - Some(ip) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - self - } -} diff --git a/ublox-short-range/src/wifi/sta.rs b/ublox-short-range/src/wifi/sta.rs deleted file mode 100644 index 2d850bb..0000000 --- a/ublox-short-range/src/wifi/sta.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::{ - client::UbloxClient, - command::{ - edm::EdmAtCmdWrapper, - wifi::{types::*, *}, - *, - }, - error::{WifiConnectionError, WifiError}, - wifi::{ - connection::{WiFiState, WifiConnection}, - network::{WifiMode, WifiNetwork}, - options::ConnectionOptions, - }, -}; - -use atat::{blocking::AtatClient, heapless_bytes::Bytes}; -use core::convert::TryFrom; -use embedded_hal::digital::OutputPin; -use heapless::Vec; - -const CONFIG_ID: u8 = 0; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> - UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - /// Attempts to connect to a wireless network with the given connection options. - pub fn connect(&mut self, options: &ConnectionOptions) -> Result<(), WifiConnectionError> { - defmt::info!("Connecting to {:?}", options); - // Network part - - if let Some(ref con) = self.wifi_connection { - defmt::warn!("WaitingForWifiDeactivation {:#?}", con); - if con.wifi_state != WiFiState::Inactive { - return Err(WifiConnectionError::WaitingForWifiDeactivation); - } - } - - // Disable DHCP Client (static IP address will be used) - if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::IPv4Mode(IPv4Mode::Static), - }), - true, - )?; - } - - // Network IP address - if let Some(ip) = options.ip { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::IPv4Address(ip), - }), - true, - )?; - } - // Network Subnet mask - if let Some(subnet) = options.subnet { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::SubnetMask(subnet), - }), - true, - )?; - } - // Network Default gateway - if let Some(gateway) = options.gateway { - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::DefaultGateway(gateway), - }), - true, - )?; - } - - // Wifi part - // Set the Network SSID to connect to - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::SSID(&options.ssid), - }), - true, - )?; - - if let Some(ref pass) = options.password { - // Use WPA2 as authentication type - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }), - true, - )?; - - // Input passphrase - self.send_internal( - &EdmAtCmdWrapper(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::WpaPskOrPassphrase(&pass), - }), - true, - )?; - } - - self.wifi_connection.replace(WifiConnection::new( - WifiNetwork { - bssid: Bytes::new(), - op_mode: wifi::types::OperationMode::Infrastructure, - ssid: options.ssid.clone(), - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::Station, - }, - WiFiState::NotConnected, - )); - self.send_internal( - &EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Activate, - }), - true, - )?; - - // TODO: Await connected event? - - Ok(()) - } - - pub fn activate(&mut self) -> Result<(), WifiConnectionError> { - self.send_internal( - &EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Activate, - }), - true, - )?; - return Ok(()); - } - - pub fn scan(&mut self) -> Result, WifiError> { - match self.send_internal(&EdmAtCmdWrapper(WifiScan { ssid: None }), true) { - Ok(resp) => resp - .network_list - .into_iter() - .map(WifiNetwork::try_from) - .collect(), - Err(_) => Err(WifiError::UnexpectedResponse), - } - } - - pub fn is_connected(&self) -> bool { - if !self.initialized { - return false; - } - - self.wifi_connection - .as_ref() - .map(|c| c.is_connected()) - .unwrap_or_default() - } - - pub fn is_active_on_startup(&mut self) -> Result { - if let Ok(resp) = self.send_internal( - &EdmAtCmdWrapper(GetWifiStationConfig { - config_id: CONFIG_ID, - parameter: Some(WifiStationConfigParameter::ActiveOnStartup), - }), - false, - ) { - if let WifiStationConfigR::ActiveOnStartup(active) = resp.parameter { - return Ok(active == OnOff::On); - } - } - Err(WifiConnectionError::Illegal) - } - - pub fn get_ssid(&mut self) -> Result, WifiConnectionError> { - if let Ok(resp) = self.send_internal( - &EdmAtCmdWrapper(GetWifiStationConfig { - config_id: CONFIG_ID, - parameter: Some(WifiStationConfigParameter::SSID), - }), - false, - ) { - if let WifiStationConfigR::SSID(ssid) = resp.parameter { - return Ok(ssid); - } - }; - return Err(WifiConnectionError::Illegal); - } - - pub fn reset_config_profile(&mut self) -> Result<(), WifiConnectionError> { - self.send_at(EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Reset, - }))?; - Ok(()) - } - - pub fn disconnect(&mut self) -> Result<(), WifiConnectionError> { - defmt::debug!("Disconnecting"); - self.send_at(EdmAtCmdWrapper(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Deactivate, - }))?; - Ok(()) - } -} diff --git a/ublox-short-range/src/wifi/tcp_stack.rs b/ublox-short-range/src/wifi/tcp_stack.rs deleted file mode 100644 index 9f5800c..0000000 --- a/ublox-short-range/src/wifi/tcp_stack.rs +++ /dev/null @@ -1,245 +0,0 @@ -use crate::{ - client::new_socket_num, - command::data_mode::*, - command::edm::{EdmAtCmdWrapper, EdmDataCommand}, - wifi::peer_builder::PeerUrlBuilder, - UbloxClient, -}; -use atat::blocking::AtatClient; -use embedded_hal::digital::OutputPin; -/// Handles receiving data from sockets -/// implements TCP and UDP for WiFi client -use embedded_nal::{nb, SocketAddr, TcpClientStack}; - -use ublox_sockets::{Error, SocketHandle, TcpSocket, TcpState}; - -use super::EGRESS_CHUNK_SIZE; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> TcpClientStack - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the UbloxClient, - // as the Socket object itself provides no value without accessing it though the client. - type TcpSocket = SocketHandle; - - /// Open a new TCP socket to the given address and port. The socket starts in the unconnected state. - fn socket(&mut self) -> Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - // Check if there are any unused sockets available - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - if !sockets.recycle() { - return Err(Error::SocketSetFull); - } - } - - defmt::debug!("[TCP] Opening socket"); - - let socket_id = new_socket_num(sockets).unwrap(); - sockets.add(TcpSocket::new(socket_id)).map_err(|e| { - defmt::error!("[TCP] Opening socket Error: {:?}", e); - e - }) - } else { - Err(Error::Illegal) - } - } - - /// Connect to the given remote host and port. - fn connect( - &mut self, - socket: &mut Self::TcpSocket, - remote: SocketAddr, - ) -> nb::Result<(), Self::Error> { - if self.sockets.is_none() { - return Err(Error::Illegal.into()); - } - - defmt::debug!("[TCP] Connect socket"); - self.connected_to_network().map_err(|_| Error::Illegal)?; - - let url = if let Some(hostname) = self.dns_table.reverse_lookup(remote.ip()) { - PeerUrlBuilder::new() - .hostname(hostname.as_str()) - .port(remote.port()) - .creds(self.security_credentials.clone()) - .tcp() - .map_err(|_| Error::Unaddressable)? - } else { - PeerUrlBuilder::new() - .ip_addr(remote.ip()) - .port(remote.port()) - .creds(self.security_credentials.clone()) - .tcp() - .map_err(|_| Error::Unaddressable)? - }; - - defmt::debug!("[TCP] Connecting socket: {:?} to url: {=str}", socket, url); - - // If no socket is found we stop here - let mut tcp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)?; - - tcp.set_state(TcpState::WaitingForConnect(remote)); - - match self - .send_internal(&EdmAtCmdWrapper(ConnectPeer { url: &url }), false) - .map_err(|_| Error::Unaddressable) - { - Ok(resp) => self - .socket_map - .insert_peer(resp.peer_handle, *socket) - .map_err(|_| Error::InvalidSocket)?, - Err(e) => { - let mut tcp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)?; - tcp.set_state(TcpState::Created); - return Err(nb::Error::Other(e)); - } - } - - defmt::debug!("[TCP] Connecting socket: {:?} to url: {=str}", socket, url); - - // TODO: Timeout? - // TODO: Fix the fact that it doesen't wait for both connect messages - while { - matches!( - self.sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)? - .state(), - TcpState::WaitingForConnect(_) - ) - } { - self.spin().map_err(|_| Error::Illegal)?; - } - Ok(()) - } - - /// Check if this socket is still connected - fn is_connected(&mut self, socket: &Self::TcpSocket) -> Result { - if self.connected_to_network().is_err() { - return Ok(false); - } - if let Some(ref mut sockets) = self.sockets { - let tcp = sockets.get::>(*socket)?; - Ok(tcp.is_connected()) - } else { - Err(Error::Illegal) - } - } - - /// Write to the stream. Returns the number of bytes written is returned - /// (which may be less than `buffer.len()`), or an error. - fn send( - &mut self, - socket: &mut Self::TcpSocket, - buffer: &[u8], - ) -> nb::Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - let tcp = sockets - .get::>(*socket) - .map_err(nb::Error::Other)?; - - if !tcp.is_connected() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(socket) - .ok_or(nb::Error::Other(Error::SocketClosed))?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - true, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - Ok(buffer.len()) - } else { - Err(Error::Illegal.into()) - } - } - - fn receive( - &mut self, - socket: &mut Self::TcpSocket, - buffer: &mut [u8], - ) -> nb::Result { - // TODO: Handle error states - self.spin().map_err(|_| nb::Error::Other(Error::Illegal))?; - if let Some(ref mut sockets) = self.sockets { - // Enable detecting closed socket from receive function - sockets.recycle(); - - let mut tcp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - Ok(tcp.recv_slice(buffer).map_err(Self::Error::from)?) - } else { - Err(Error::Illegal.into()) - } - } - - /// Close an existing TCP socket. - fn close(&mut self, socket: Self::TcpSocket) -> Result<(), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - defmt::debug!("[TCP] Closing socket: {:?}", socket); - // If the socket is not found it is already removed - if let Ok(ref tcp) = sockets.get::>(socket) { - // If socket is not closed that means a connection excists which has to be closed - if !matches!( - tcp.state(), - TcpState::ShutdownForWrite(_) | TcpState::Created - ) { - if let Some(peer_handle) = self.socket_map.socket_to_peer(&tcp.handle()) { - let peer_handle = *peer_handle; - match self.send_at(ClosePeerConnection { peer_handle }) { - Err(crate::error::Error::AT(atat::Error::InvalidResponse)) | Ok(_) => { - () - } - Err(_) => return Err(Error::Unaddressable), - } - } else { - defmt::error!( - "Illigal state! Socket connected but not in socket map: {:?}", - tcp.handle() - ); - return Err(Error::Illegal); - } - } else { - // No connection exists the socket should be removed from the set here - sockets.remove(socket)?; - } - } - Ok(()) - } else { - Err(Error::Illegal) - } - } -} diff --git a/ublox-short-range/src/wifi/tls.rs b/ublox-short-range/src/wifi/tls.rs deleted file mode 100644 index 0ffc35e..0000000 --- a/ublox-short-range/src/wifi/tls.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::{ - command::edm::BigEdmAtCmdWrapper, - command::security::{types::*, *}, - error::Error, - UbloxClient, -}; -use embedded_hal::digital::OutputPin; -use heapless::String; - -pub trait TLS { - fn import_certificate(&mut self, name: &str, certificate: &[u8]) -> Result<(), Error>; - fn import_root_ca(&mut self, name: &str, root_ca: &[u8]) -> Result<(), Error>; - fn import_private_key( - &mut self, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error>; -} - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> TLS - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: atat::blocking::AtatClient, - RST: OutputPin, -{ - /// Importing credentials enabeles their use for all further TCP connections - fn import_certificate(&mut self, name: &str, certificate: &[u8]) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientCertificate, - data_size: certificate.len(), - internal_name: name, - password: None, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(certificate), - }), - false, - )?; - - self.security_credentials - .c_cert_name - .replace(String::from(name)); - - Ok(()) - } - - /// Importing credentials enabeles their use for all further TCP connections - fn import_root_ca(&mut self, name: &str, root_ca: &[u8]) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::TrustedRootCA, - data_size: root_ca.len(), - internal_name: name, - password: None, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(root_ca), - }), - false, - )?; - - self.security_credentials - .ca_cert_name - .replace(String::from(name)); - - Ok(()) - } - - /// Importing credentials enabeles their use for all further TCP connections - fn import_private_key( - &mut self, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientPrivateKey, - data_size: private_key.len(), - internal_name: name, - password, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(private_key), - }), - false, - )?; - - self.security_credentials - .c_key_name - .replace(String::from(name)); - - Ok(()) - } -} diff --git a/ublox-short-range/src/wifi/udp_stack.rs b/ublox-short-range/src/wifi/udp_stack.rs deleted file mode 100644 index 60f7f61..0000000 --- a/ublox-short-range/src/wifi/udp_stack.rs +++ /dev/null @@ -1,411 +0,0 @@ -use crate::{ - client::new_socket_num, - command::data_mode::*, - command::{ - data_mode::types::{IPVersion, ServerType, UDPBehaviour}, - edm::{EdmAtCmdWrapper, EdmDataCommand}, - }, - wifi::peer_builder::PeerUrlBuilder, - UbloxClient, -}; -use atat::blocking::AtatClient; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, SocketAddr, UdpFullStack}; - -use embedded_nal::UdpClientStack; -use ublox_sockets::{Error, SocketHandle, UdpSocket, UdpState}; - -use super::EGRESS_CHUNK_SIZE; - -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> UdpClientStack - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the UbloxClient, - // as the Socket object itself provides no value without accessing it though the client. - type UdpSocket = SocketHandle; - - fn socket(&mut self) -> Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - // Check if there are any unused sockets available - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - if !sockets.recycle() { - return Err(Error::SocketSetFull); - } - } - - let socket_id = new_socket_num(sockets).unwrap(); - defmt::debug!("[UDP] Opening socket"); - sockets.add(UdpSocket::new(socket_id)).map_err(|_| { - defmt::error!("[UDP] Opening socket Error: Socket set full"); - Error::SocketSetFull - }) - } else { - defmt::error!("[UDP] Opening socket Error: Missing socket set"); - Err(Error::Illegal) - } - } - - /// Connect a UDP socket with a peer using a dynamically selected port. - /// Selects a port number automatically and initializes for read/writing. - fn connect( - &mut self, - socket: &mut Self::UdpSocket, - remote: SocketAddr, - ) -> Result<(), Self::Error> { - let mut peer_handle = crate::command::PeerHandle(0); - - if self.sockets.is_none() { - defmt::error!("[UDP] Connecting socket Error: Missing socket set"); - return Err(Error::Illegal); - } - let url = PeerUrlBuilder::new() - .address(&remote) - .udp() - .map_err(|_| Error::Unaddressable)?; - defmt::debug!("[UDP] Connecting Socket: {:?} to URL: {=str}", socket, url); - - self.connected_to_network().map_err(|_| Error::Illegal)?; - - // First look to see if socket is valid - let mut udp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)?; - udp.bind(remote)?; - - // Then connect modem - match self - .send_internal(&EdmAtCmdWrapper(ConnectPeer { url: &url }), true) - .map_err(|_| Error::Unaddressable) - { - Ok(resp) => { - peer_handle = resp.peer_handle; - - self.socket_map - .insert_peer(resp.peer_handle, *socket) - .map_err(|_| Error::InvalidSocket)? - } - - Err(e) => { - let mut udp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)?; - udp.close(); - return Err(e); - } - } - while self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)? - .state() - == UdpState::Closed - { - match self.spin() { - Ok(_) => {} - Err(_) => { - defmt::error!("ERROR connection UDP removing peer"); - self.socket_map.remove_peer(&peer_handle); - return Err(Error::Illegal); - } - }; - } - Ok(()) - } - - /// Send a datagram to the remote host. - fn send(&mut self, socket: &mut Self::UdpSocket, buffer: &[u8]) -> nb::Result<(), Self::Error> { - self.spin().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - // No send for server sockets! - if self.udp_listener.is_bound(*socket) { - return Err(nb::Error::Other(Error::Illegal)); - } - - let udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - if !udp.is_open() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(socket) - .ok_or(nb::Error::Other(Error::SocketClosed))?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - true, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - Ok(()) - } else { - Err(Error::Illegal.into()) - } - } - - /// Read a datagram the remote host has sent to us. Returns `Ok(n)`, which - /// means a datagram of size `n` has been received and it has been placed - /// in `&buffer[0..n]`, or an error. - fn receive( - &mut self, - socket: &mut Self::UdpSocket, - buffer: &mut [u8], - ) -> nb::Result<(usize, SocketAddr), Self::Error> { - self.spin().ok(); - let udp_listener = &mut self.udp_listener; - // Handle server sockets - if udp_listener.is_bound(*socket) { - // Nothing available, would block - if !udp_listener.available(*socket).unwrap_or(false) { - return Err(nb::Error::WouldBlock); - } - - let (connection_handle, remote) = self - .udp_listener - .peek_remote(*socket) - .map_err(|_| Error::NotBound)?; - - if let Some(ref mut sockets) = self.sockets { - let mut udp = sockets - .get::>(*connection_handle) - .map_err(|_| Self::Error::InvalidSocket)?; - - let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; - Ok((bytes, *remote)) - } else { - Err(Error::Illegal.into()) - } - - // Handle reciving for udp normal sockets - } else if let Some(ref mut sockets) = self.sockets { - let mut udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; - - let endpoint = udp.endpoint().ok_or(Error::SocketClosed)?; - Ok((bytes, endpoint)) - } else { - Err(Error::Illegal.into()) - } - } - - /// Close an existing UDP socket. - fn close(&mut self, socket: Self::UdpSocket) -> Result<(), Self::Error> { - self.spin().ok(); - // Close server socket - if self.udp_listener.is_bound(socket) { - defmt::debug!("[UDP] Closing Server socket: {:?}", socket); - - // ID 2 used by UDP server - self.send_internal( - &EdmAtCmdWrapper(ServerConfiguration { - id: 2, - server_config: ServerType::Disabled, - }), - true, - ) - .map_err(|_| Error::Unaddressable)?; - - // Borrow socket set to close server socket - if let Some(ref mut sockets) = self.sockets { - // If socket in socket set close - if sockets.remove(socket).is_err() { - defmt::error!( - "[UDP] Closing server socket error: No socket matching: {:?}", - socket - ); - return Err(Error::InvalidSocket); - } - } else { - return Err(Error::Illegal); - } - - // Close incomming connections - while self.udp_listener.available(socket).unwrap_or(false) { - if let Ok((connection_handle, _)) = self.udp_listener.get_remote(socket) { - defmt::debug!( - "[UDP] Closing incomming socket for Server: {:?}", - connection_handle - ); - self.close(connection_handle)?; - } else { - defmt::error!("[UDP] Incomming socket for server error - Listener says available, while nothing present"); - } - } - - // Unbind server socket in listener - self.udp_listener.unbind(socket).map_err(|_| { - defmt::error!( - "[UDP] Closing socket error: No server socket matching: {:?}", - socket - ); - Error::Illegal - }) - // Handle normal sockets - } else if let Some(ref mut sockets) = self.sockets { - defmt::debug!("[UDP] Closing socket: {:?}", socket); - // If no sockets exists, nothing to close. - if let Ok(ref mut udp) = sockets.get::>(socket) { - defmt::trace!("[UDP] Closing socket state: {:?}", udp.state()); - match udp.state() { - UdpState::Closed => { - sockets.remove(socket).ok(); - } - UdpState::Established => { - udp.close(); - if let Some(peer_handle) = self.socket_map.socket_to_peer(&udp.handle()) { - let peer_handle = *peer_handle; - self.send_at(ClosePeerConnection { peer_handle }) - .map_err(|_| Error::Unaddressable)?; - } - } - } - } else { - defmt::error!( - "[UDP] Closing socket error: No socket matching: {:?}", - socket - ); - return Err(Error::InvalidSocket); - } - Ok(()) - } else { - Err(Error::Illegal) - } - } -} - -/// UDP Full Stack -/// -/// This fullstack is build for request-response type servers due to HW/SW limitations -/// Limitations: -/// - The driver can only send to Socket addresses that have send data first. -/// - The driver can only call send_to once after reciving data once. -/// - The driver has to call send_to after reciving data, to release the socket bound by remote host, -/// even if just sending no bytes. Else these sockets will be held open until closure of server socket. -/// -impl<'buf, 'sub, AtCl, AtUrcCh, RST, const N: usize, const L: usize> UdpFullStack - for UbloxClient<'buf, 'sub, AtCl, AtUrcCh, RST, N, L> -where - 'buf: 'sub, - AtCl: AtatClient, - RST: OutputPin, -{ - fn bind(&mut self, socket: &mut Self::UdpSocket, local_port: u16) -> Result<(), Self::Error> { - if self.connected_to_network().is_err() || self.udp_listener.is_port_bound(local_port) { - return Err(Error::Illegal); - } - - defmt::debug!( - "[UDP] binding socket: {:?} to port: {:?}", - socket, - local_port - ); - - // ID 2 used by UDP server - self.send_internal( - &EdmAtCmdWrapper(ServerConfiguration { - id: 2, - server_config: ServerType::UDP( - local_port, - UDPBehaviour::AutoConnect, - IPVersion::IPv4, - ), - }), - true, - ) - .map_err(|_| Error::Unaddressable)?; - - self.udp_listener - .bind(*socket, local_port) - .map_err(|_| Error::Illegal)?; - - Ok(()) - } - - fn send_to( - &mut self, - socket: &mut Self::UdpSocket, - remote: SocketAddr, - buffer: &[u8], - ) -> nb::Result<(), Self::Error> { - self.spin().map_err(|_| Error::Illegal)?; - // Protect against non server sockets - if !self.udp_listener.is_bound(*socket) { - return Err(Error::Illegal.into()); - } - // Check incomming sockets for the socket address - if let Some(connection_socket) = self.udp_listener.get_outgoing(socket, remote) { - if let Some(ref mut sockets) = self.sockets { - if buffer.is_empty() { - self.close(connection_socket)?; - return Ok(()); - } - - let udp = sockets - .get::>(connection_socket) - .map_err(Self::Error::from)?; - - if !udp.is_open() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(&connection_socket) - .ok_or(nb::Error::WouldBlock)?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - self.close(connection_socket).unwrap(); - Ok(()) - } else { - Err(Error::Illegal.into()) - } - } else { - Err(Error::Illegal.into()) - } - - ////// Do with URC - // Crate a new SocketBuffer allocation for the incoming connection - // let mut tcp = self - // .sockets - // .as_mut() - // .ok_or(Error::Illegal)? - // .get::>(data_socket) - // .map_err(Self::Error::from)?; - - // tcp.update_handle(handle); - // tcp.set_state(TcpState::Connected(remote.clone())); - } -}