├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE │ └── adafruit_circuitpython_pr.md └── workflows │ ├── build.yml │ ├── failure-help-text.yml │ ├── release_gh.yml │ └── release_pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── LICENSES ├── CC-BY-4.0.txt ├── MIT.txt └── Unlicense.txt ├── README.rst ├── README.rst.license ├── adafruit_wiznet5k ├── __init__.py ├── adafruit_wiznet5k.py ├── adafruit_wiznet5k_debug.py ├── adafruit_wiznet5k_dhcp.py ├── adafruit_wiznet5k_dns.py └── adafruit_wiznet5k_socketpool.py ├── docs ├── _static │ ├── favicon.ico │ └── favicon.ico.license ├── api.rst ├── api.rst.license ├── conf.py ├── examples.rst ├── examples.rst.license ├── index.rst ├── index.rst.license └── requirements.txt ├── examples ├── wiznet5k_aio_post.py ├── wiznet5k_cheerlights.py ├── wiznet5k_cpython_client_for_simpleserver.py ├── wiznet5k_httpserver.py ├── wiznet5k_simpleserver.py ├── wiznet5k_simpletest.py └── wiznet5k_simpletest_manual_network.py ├── optional_requirements.txt ├── pyproject.toml ├── requirements.txt └── ruff.toml /.gitattributes: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | .py text eol=lf 6 | .rst text eol=lf 7 | .txt text eol=lf 8 | .yaml text eol=lf 9 | .toml text eol=lf 10 | .license text eol=lf 11 | .md text eol=lf 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | Thank you for contributing! Before you submit a pull request, please read the following. 6 | 7 | Make sure any changes you're submitting are in line with the CircuitPython Design Guide, available here: https://docs.circuitpython.org/en/latest/docs/design_guide.html 8 | 9 | If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs 10 | 11 | Before submitting the pull request, make sure you've run Pylint and Black locally on your code. You can do this manually or using pre-commit. Instructions are available here: https://adafru.it/check-your-code 12 | 13 | Please remove all of this text before submitting. Include an explanation or list of changes included in your PR, as well as, if applicable, a link to any related issues. 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Build CI 6 | 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Run Build CI workflow 14 | uses: adafruit/workflows-circuitpython-libs/build@main 15 | -------------------------------------------------------------------------------- /.github/workflows/failure-help-text.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Failure help text 6 | 7 | on: 8 | workflow_run: 9 | workflows: ["Build CI"] 10 | types: 11 | - completed 12 | 13 | jobs: 14 | post-help: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }} 17 | steps: 18 | - name: Post comment to help 19 | uses: adafruit/circuitpython-action-library-ci-failed@v1 20 | -------------------------------------------------------------------------------- /.github/workflows/release_gh.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: GitHub Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run GitHub Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-gh@main 17 | with: 18 | github-token: ${{ secrets.GITHUB_TOKEN }} 19 | upload-url: ${{ github.event.release.upload_url }} 20 | -------------------------------------------------------------------------------- /.github/workflows/release_pypi.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: PyPI Release Actions 6 | 7 | on: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | upload-release-assets: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Run PyPI Release CI workflow 16 | uses: adafruit/workflows-circuitpython-libs/release-pypi@main 17 | with: 18 | pypi-username: ${{ secrets.pypi_username }} 19 | pypi-password: ${{ secrets.pypi_password }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Kattni Rembor, written for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Do not include files and directories created by your personal work environment, such as the IDE 6 | # you use, except for those already listed here. Pull requests including changes to this file will 7 | # not be accepted. 8 | 9 | # This .gitignore file contains rules for files generated by working with CircuitPython libraries, 10 | # including building Sphinx, testing with pip, and creating a virual environment, as well as the 11 | # MacOS and IDE-specific files generated by using MacOS in general, or the PyCharm or VSCode IDEs. 12 | 13 | # If you find that there are files being generated on your machine that should not be included in 14 | # your git commit, you should create a .gitignore_global file on your computer to include the 15 | # files created by your personal setup. To do so, follow the two steps below. 16 | 17 | # First, create a file called .gitignore_global somewhere convenient for you, and add rules for 18 | # the files you want to exclude from git commits. 19 | 20 | # Second, configure Git to use the exclude file for all Git repositories by running the 21 | # following via commandline, replacing "path/to/your/" with the actual path to your newly created 22 | # .gitignore_global file: 23 | # git config --global core.excludesfile path/to/your/.gitignore_global 24 | 25 | # CircuitPython-specific files 26 | *.mpy 27 | 28 | # Python-specific files 29 | __pycache__ 30 | *.pyc 31 | 32 | # Sphinx build-specific files 33 | _build 34 | 35 | # This file results from running `pip -e install .` in a local repository 36 | *.egg-info 37 | 38 | # Virtual environment-specific files 39 | .env 40 | .venv 41 | 42 | # MacOS-specific files 43 | *.DS_Store 44 | 45 | # IDE-specific files 46 | .idea 47 | .vscode 48 | *~ 49 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: check-yaml 10 | - id: end-of-file-fixer 11 | - id: trailing-whitespace 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: v0.3.4 14 | hooks: 15 | - id: ruff-format 16 | - id: ruff 17 | args: ["--fix"] 18 | - repo: https://github.com/fsfe/reuse-tool 19 | rev: v3.0.1 20 | hooks: 21 | - id: reuse 22 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | # Read the Docs configuration file 6 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 7 | 8 | # Required 9 | version: 2 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | build: 15 | os: ubuntu-20.04 16 | tools: 17 | python: "3" 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | - requirements: requirements.txt 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Adafruit Community Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and leaders pledge to making participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, gender identity and expression, level or type of 15 | experience, education, socio-economic status, nationality, personal appearance, 16 | race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | We are committed to providing a friendly, safe and welcoming environment for 21 | all. 22 | 23 | Examples of behavior that contributes to creating a positive environment 24 | include: 25 | 26 | * Be kind and courteous to others 27 | * Using welcoming and inclusive language 28 | * Being respectful of differing viewpoints and experiences 29 | * Collaborating with other community members 30 | * Gracefully accepting constructive criticism 31 | * Focusing on what is best for the community 32 | * Showing empathy towards other community members 33 | 34 | Examples of unacceptable behavior by participants include: 35 | 36 | * The use of sexualized language or imagery and sexual attention or advances 37 | * The use of inappropriate images, including in a community member's avatar 38 | * The use of inappropriate language, including in a community member's nickname 39 | * Any spamming, flaming, baiting or other attention-stealing behavior 40 | * Excessive or unwelcome helping; answering outside the scope of the question 41 | asked 42 | * Trolling, insulting/derogatory comments, and personal or political attacks 43 | * Promoting or spreading disinformation, lies, or conspiracy theories against 44 | a person, group, organisation, project, or community 45 | * Public or private harassment 46 | * Publishing others' private information, such as a physical or electronic 47 | address, without explicit permission 48 | * Other conduct which could reasonably be considered inappropriate 49 | 50 | The goal of the standards and moderation guidelines outlined here is to build 51 | and maintain a respectful community. We ask that you don’t just aim to be 52 | "technically unimpeachable", but rather try to be your best self. 53 | 54 | We value many things beyond technical expertise, including collaboration and 55 | supporting others within our community. Providing a positive experience for 56 | other community members can have a much more significant impact than simply 57 | providing the correct answer. 58 | 59 | ## Our Responsibilities 60 | 61 | Project leaders are responsible for clarifying the standards of acceptable 62 | behavior and are expected to take appropriate and fair corrective action in 63 | response to any instances of unacceptable behavior. 64 | 65 | Project leaders have the right and responsibility to remove, edit, or 66 | reject messages, comments, commits, code, issues, and other contributions 67 | that are not aligned to this Code of Conduct, or to ban temporarily or 68 | permanently any community member for other behaviors that they deem 69 | inappropriate, threatening, offensive, or harmful. 70 | 71 | ## Moderation 72 | 73 | Instances of behaviors that violate the Adafruit Community Code of Conduct 74 | may be reported by any member of the community. Community members are 75 | encouraged to report these situations, including situations they witness 76 | involving other community members. 77 | 78 | You may report in the following ways: 79 | 80 | In any situation, you may send an email to . 81 | 82 | On the Adafruit Discord, you may send an open message from any channel 83 | to all Community Moderators by tagging @community moderators. You may 84 | also send an open message from any channel, or a direct message to 85 | @kattni#1507, @tannewt#4653, @Dan Halbert#1614, @cater#2442, 86 | @sommersoft#0222, @Mr. Certainly#0472 or @Andon#8175. 87 | 88 | Email and direct message reports will be kept confidential. 89 | 90 | In situations on Discord where the issue is particularly egregious, possibly 91 | illegal, requires immediate action, or violates the Discord terms of service, 92 | you should also report the message directly to Discord. 93 | 94 | These are the steps for upholding our community’s standards of conduct. 95 | 96 | 1. Any member of the community may report any situation that violates the 97 | Adafruit Community Code of Conduct. All reports will be reviewed and 98 | investigated. 99 | 2. If the behavior is an egregious violation, the community member who 100 | committed the violation may be banned immediately, without warning. 101 | 3. Otherwise, moderators will first respond to such behavior with a warning. 102 | 4. Moderators follow a soft "three strikes" policy - the community member may 103 | be given another chance, if they are receptive to the warning and change their 104 | behavior. 105 | 5. If the community member is unreceptive or unreasonable when warned by a 106 | moderator, or the warning goes unheeded, they may be banned for a first or 107 | second offense. Repeated offenses will result in the community member being 108 | banned. 109 | 110 | ## Scope 111 | 112 | This Code of Conduct and the enforcement policies listed above apply to all 113 | Adafruit Community venues. This includes but is not limited to any community 114 | spaces (both public and private), the entire Adafruit Discord server, and 115 | Adafruit GitHub repositories. Examples of Adafruit Community spaces include 116 | but are not limited to meet-ups, audio chats on the Adafruit Discord, or 117 | interaction at a conference. 118 | 119 | This Code of Conduct applies both within project spaces and in public spaces 120 | when an individual is representing the project or its community. As a community 121 | member, you are representing our community, and are expected to behave 122 | accordingly. 123 | 124 | ## Attribution 125 | 126 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 127 | version 1.4, available at 128 | , 129 | and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). 130 | 131 | For other projects adopting the Adafruit Community Code of 132 | Conduct, please contact the maintainers of those projects for enforcement. 133 | If you wish to use this code of conduct for your own project, consider 134 | explicitly mentioning your moderation policy or making a copy with your 135 | own moderation policy so as to avoid confusion. 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Arduino LLC. All right reserved. 2 | 3 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. 4 | 5 | This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 6 | 7 | You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 8 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution 4.0 International Creative Commons Corporation 2 | ("Creative Commons") is not a law firm and does not provide legal services 3 | or legal advice. Distribution of Creative Commons public licenses does not 4 | create a lawyer-client or other relationship. Creative Commons makes its licenses 5 | and related information available on an "as-is" basis. Creative Commons gives 6 | no warranties regarding its licenses, any material licensed under their terms 7 | and conditions, or any related information. Creative Commons disclaims all 8 | liability for damages resulting from their use to the fullest extent possible. 9 | 10 | Using Creative Commons Public Licenses 11 | 12 | Creative Commons public licenses provide a standard set of terms and conditions 13 | that creators and other rights holders may use to share original works of 14 | authorship and other material subject to copyright and certain other rights 15 | specified in the public license below. The following considerations are for 16 | informational purposes only, are not exhaustive, and do not form part of our 17 | licenses. 18 | 19 | Considerations for licensors: Our public licenses are intended for use by 20 | those authorized to give the public permission to use material in ways otherwise 21 | restricted by copyright and certain other rights. Our licenses are irrevocable. 22 | Licensors should read and understand the terms and conditions of the license 23 | they choose before applying it. Licensors should also secure all rights necessary 24 | before applying our licenses so that the public can reuse the material as 25 | expected. Licensors should clearly mark any material not subject to the license. 26 | This includes other CC-licensed material, or material used under an exception 27 | or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 28 | 29 | Considerations for the public: By using one of our public licenses, a licensor 30 | grants the public permission to use the licensed material under specified 31 | terms and conditions. If the licensor's permission is not necessary for any 32 | reason–for example, because of any applicable exception or limitation to copyright–then 33 | that use is not regulated by the license. Our licenses grant only permissions 34 | under copyright and certain other rights that a licensor has authority to 35 | grant. Use of the licensed material may still be restricted for other reasons, 36 | including because others have copyright or other rights in the material. A 37 | licensor may make special requests, such as asking that all changes be marked 38 | or described. Although not required by our licenses, you are encouraged to 39 | respect those requests where reasonable. More considerations for the public 40 | : wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution 41 | 4.0 International Public License 42 | 43 | By exercising the Licensed Rights (defined below), You accept and agree to 44 | be bound by the terms and conditions of this Creative Commons Attribution 45 | 4.0 International Public License ("Public License"). To the extent this Public 46 | License may be interpreted as a contract, You are granted the Licensed Rights 47 | in consideration of Your acceptance of these terms and conditions, and the 48 | Licensor grants You such rights in consideration of benefits the Licensor 49 | receives from making the Licensed Material available under these terms and 50 | conditions. 51 | 52 | Section 1 – Definitions. 53 | 54 | a. Adapted Material means material subject to Copyright and Similar Rights 55 | that is derived from or based upon the Licensed Material and in which the 56 | Licensed Material is translated, altered, arranged, transformed, or otherwise 57 | modified in a manner requiring permission under the Copyright and Similar 58 | Rights held by the Licensor. For purposes of this Public License, where the 59 | Licensed Material is a musical work, performance, or sound recording, Adapted 60 | Material is always produced where the Licensed Material is synched in timed 61 | relation with a moving image. 62 | 63 | b. Adapter's License means the license You apply to Your Copyright and Similar 64 | Rights in Your contributions to Adapted Material in accordance with the terms 65 | and conditions of this Public License. 66 | 67 | c. Copyright and Similar Rights means copyright and/or similar rights closely 68 | related to copyright including, without limitation, performance, broadcast, 69 | sound recording, and Sui Generis Database Rights, without regard to how the 70 | rights are labeled or categorized. For purposes of this Public License, the 71 | rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 72 | 73 | d. Effective Technological Measures means those measures that, in the absence 74 | of proper authority, may not be circumvented under laws fulfilling obligations 75 | under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, 76 | and/or similar international agreements. 77 | 78 | e. Exceptions and Limitations means fair use, fair dealing, and/or any other 79 | exception or limitation to Copyright and Similar Rights that applies to Your 80 | use of the Licensed Material. 81 | 82 | f. Licensed Material means the artistic or literary work, database, or other 83 | material to which the Licensor applied this Public License. 84 | 85 | g. Licensed Rights means the rights granted to You subject to the terms and 86 | conditions of this Public License, which are limited to all Copyright and 87 | Similar Rights that apply to Your use of the Licensed Material and that the 88 | Licensor has authority to license. 89 | 90 | h. Licensor means the individual(s) or entity(ies) granting rights under this 91 | Public License. 92 | 93 | i. Share means to provide material to the public by any means or process that 94 | requires permission under the Licensed Rights, such as reproduction, public 95 | display, public performance, distribution, dissemination, communication, or 96 | importation, and to make material available to the public including in ways 97 | that members of the public may access the material from a place and at a time 98 | individually chosen by them. 99 | 100 | j. Sui Generis Database Rights means rights other than copyright resulting 101 | from Directive 96/9/EC of the European Parliament and of the Council of 11 102 | March 1996 on the legal protection of databases, as amended and/or succeeded, 103 | as well as other essentially equivalent rights anywhere in the world. 104 | 105 | k. You means the individual or entity exercising the Licensed Rights under 106 | this Public License. Your has a corresponding meaning. 107 | 108 | Section 2 – Scope. 109 | 110 | a. License grant. 111 | 112 | 1. Subject to the terms and conditions of this Public License, the Licensor 113 | hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, 114 | irrevocable license to exercise the Licensed Rights in the Licensed Material 115 | to: 116 | 117 | A. reproduce and Share the Licensed Material, in whole or in part; and 118 | 119 | B. produce, reproduce, and Share Adapted Material. 120 | 121 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions 122 | and Limitations apply to Your use, this Public License does not apply, and 123 | You do not need to comply with its terms and conditions. 124 | 125 | 3. Term. The term of this Public License is specified in Section 6(a). 126 | 127 | 4. Media and formats; technical modifications allowed. The Licensor authorizes 128 | You to exercise the Licensed Rights in all media and formats whether now known 129 | or hereafter created, and to make technical modifications necessary to do 130 | so. The Licensor waives and/or agrees not to assert any right or authority 131 | to forbid You from making technical modifications necessary to exercise the 132 | Licensed Rights, including technical modifications necessary to circumvent 133 | Effective Technological Measures. For purposes of this Public License, simply 134 | making modifications authorized by this Section 2(a)(4) never produces Adapted 135 | Material. 136 | 137 | 5. Downstream recipients. 138 | 139 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed 140 | Material automatically receives an offer from the Licensor to exercise the 141 | Licensed Rights under the terms and conditions of this Public License. 142 | 143 | B. No downstream restrictions. You may not offer or impose any additional 144 | or different terms or conditions on, or apply any Effective Technological 145 | Measures to, the Licensed Material if doing so restricts exercise of the Licensed 146 | Rights by any recipient of the Licensed Material. 147 | 148 | 6. No endorsement. Nothing in this Public License constitutes or may be construed 149 | as permission to assert or imply that You are, or that Your use of the Licensed 150 | Material is, connected with, or sponsored, endorsed, or granted official status 151 | by, the Licensor or others designated to receive attribution as provided in 152 | Section 3(a)(1)(A)(i). 153 | 154 | b. Other rights. 155 | 156 | 1. Moral rights, such as the right of integrity, are not licensed under this 157 | Public License, nor are publicity, privacy, and/or other similar personality 158 | rights; however, to the extent possible, the Licensor waives and/or agrees 159 | not to assert any such rights held by the Licensor to the limited extent necessary 160 | to allow You to exercise the Licensed Rights, but not otherwise. 161 | 162 | 2. Patent and trademark rights are not licensed under this Public License. 163 | 164 | 3. To the extent possible, the Licensor waives any right to collect royalties 165 | from You for the exercise of the Licensed Rights, whether directly or through 166 | a collecting society under any voluntary or waivable statutory or compulsory 167 | licensing scheme. In all other cases the Licensor expressly reserves any right 168 | to collect such royalties. 169 | 170 | Section 3 – License Conditions. 171 | 172 | Your exercise of the Licensed Rights is expressly made subject to the following 173 | conditions. 174 | 175 | a. Attribution. 176 | 177 | 1. If You Share the Licensed Material (including in modified form), You must: 178 | 179 | A. retain the following if it is supplied by the Licensor with the Licensed 180 | Material: 181 | 182 | i. identification of the creator(s) of the Licensed Material and any others 183 | designated to receive attribution, in any reasonable manner requested by the 184 | Licensor (including by pseudonym if designated); 185 | 186 | ii. a copyright notice; 187 | 188 | iii. a notice that refers to this Public License; 189 | 190 | iv. a notice that refers to the disclaimer of warranties; 191 | 192 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 193 | 194 | B. indicate if You modified the Licensed Material and retain an indication 195 | of any previous modifications; and 196 | 197 | C. indicate the Licensed Material is licensed under this Public License, and 198 | include the text of, or the URI or hyperlink to, this Public License. 199 | 200 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner 201 | based on the medium, means, and context in which You Share the Licensed Material. 202 | For example, it may be reasonable to satisfy the conditions by providing a 203 | URI or hyperlink to a resource that includes the required information. 204 | 205 | 3. If requested by the Licensor, You must remove any of the information required 206 | by Section 3(a)(1)(A) to the extent reasonably practicable. 207 | 208 | 4. If You Share Adapted Material You produce, the Adapter's License You apply 209 | must not prevent recipients of the Adapted Material from complying with this 210 | Public License. 211 | 212 | Section 4 – Sui Generis Database Rights. 213 | 214 | Where the Licensed Rights include Sui Generis Database Rights that apply to 215 | Your use of the Licensed Material: 216 | 217 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, 218 | reuse, reproduce, and Share all or a substantial portion of the contents of 219 | the database; 220 | 221 | b. if You include all or a substantial portion of the database contents in 222 | a database in which You have Sui Generis Database Rights, then the database 223 | in which You have Sui Generis Database Rights (but not its individual contents) 224 | is Adapted Material; and 225 | 226 | c. You must comply with the conditions in Section 3(a) if You Share all or 227 | a substantial portion of the contents of the database. 228 | 229 | For the avoidance of doubt, this Section 4 supplements and does not replace 230 | Your obligations under this Public License where the Licensed Rights include 231 | other Copyright and Similar Rights. 232 | 233 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 234 | 235 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, 236 | the Licensor offers the Licensed Material as-is and as-available, and makes 237 | no representations or warranties of any kind concerning the Licensed Material, 238 | whether express, implied, statutory, or other. This includes, without limitation, 239 | warranties of title, merchantability, fitness for a particular purpose, non-infringement, 240 | absence of latent or other defects, accuracy, or the presence or absence of 241 | errors, whether or not known or discoverable. Where disclaimers of warranties 242 | are not allowed in full or in part, this disclaimer may not apply to You. 243 | 244 | b. To the extent possible, in no event will the Licensor be liable to You 245 | on any legal theory (including, without limitation, negligence) or otherwise 246 | for any direct, special, indirect, incidental, consequential, punitive, exemplary, 247 | or other losses, costs, expenses, or damages arising out of this Public License 248 | or use of the Licensed Material, even if the Licensor has been advised of 249 | the possibility of such losses, costs, expenses, or damages. Where a limitation 250 | of liability is not allowed in full or in part, this limitation may not apply 251 | to You. 252 | 253 | c. The disclaimer of warranties and limitation of liability provided above 254 | shall be interpreted in a manner that, to the extent possible, most closely 255 | approximates an absolute disclaimer and waiver of all liability. 256 | 257 | Section 6 – Term and Termination. 258 | 259 | a. This Public License applies for the term of the Copyright and Similar Rights 260 | licensed here. However, if You fail to comply with this Public License, then 261 | Your rights under this Public License terminate automatically. 262 | 263 | b. Where Your right to use the Licensed Material has terminated under Section 264 | 6(a), it reinstates: 265 | 266 | 1. automatically as of the date the violation is cured, provided it is cured 267 | within 30 days of Your discovery of the violation; or 268 | 269 | 2. upon express reinstatement by the Licensor. 270 | 271 | c. For the avoidance of doubt, this Section 6(b) does not affect any right 272 | the Licensor may have to seek remedies for Your violations of this Public 273 | License. 274 | 275 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material 276 | under separate terms or conditions or stop distributing the Licensed Material 277 | at any time; however, doing so will not terminate this Public License. 278 | 279 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 280 | 281 | Section 7 – Other Terms and Conditions. 282 | 283 | a. The Licensor shall not be bound by any additional or different terms or 284 | conditions communicated by You unless expressly agreed. 285 | 286 | b. Any arrangements, understandings, or agreements regarding the Licensed 287 | Material not stated herein are separate from and independent of the terms 288 | and conditions of this Public License. 289 | 290 | Section 8 – Interpretation. 291 | 292 | a. For the avoidance of doubt, this Public License does not, and shall not 293 | be interpreted to, reduce, limit, restrict, or impose conditions on any use 294 | of the Licensed Material that could lawfully be made without permission under 295 | this Public License. 296 | 297 | b. To the extent possible, if any provision of this Public License is deemed 298 | unenforceable, it shall be automatically reformed to the minimum extent necessary 299 | to make it enforceable. If the provision cannot be reformed, it shall be severed 300 | from this Public License without affecting the enforceability of the remaining 301 | terms and conditions. 302 | 303 | c. No term or condition of this Public License will be waived and no failure 304 | to comply consented to unless expressly agreed to by the Licensor. 305 | 306 | d. Nothing in this Public License constitutes or may be interpreted as a limitation 307 | upon, or waiver of, any privileges and immunities that apply to the Licensor 308 | or You, including from the legal processes of any jurisdiction or authority. 309 | 310 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative 311 | Commons may elect to apply one of its public licenses to material it publishes 312 | and in those instances will be considered the "Licensor." The text of the 313 | Creative Commons public licenses is dedicated to the public domain under the 314 | CC0 Public Domain Dedication. Except for the limited purpose of indicating 315 | that material is shared under a Creative Commons public license or as otherwise 316 | permitted by the Creative Commons policies published at creativecommons.org/policies, 317 | Creative Commons does not authorize the use of the trademark "Creative Commons" 318 | or any other trademark or logo of Creative Commons without its prior written 319 | consent including, without limitation, in connection with any unauthorized 320 | modifications to any of its public licenses or any other arrangements, understandings, 321 | or agreements concerning use of licensed material. For the avoidance of doubt, 322 | this paragraph does not form part of the public licenses. 323 | 324 | Creative Commons may be contacted at creativecommons.org. 325 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice (including the next 11 | paragraph) shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 18 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 19 | OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute 4 | this software, either in source code form or as a compiled binary, for any 5 | purpose, commercial or non-commercial, and by any means. 6 | 7 | In jurisdictions that recognize copyright laws, the author or authors of this 8 | software dedicate any and all copyright interest in the software to the public 9 | domain. We make this dedication for the benefit of the public at large and 10 | to the detriment of our heirs and successors. We intend this dedication to 11 | be an overt act of relinquishment in perpetuity of all present and future 12 | rights to this software under copyright law. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 17 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH 19 | THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information, 20 | please refer to 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | .. image:: https://readthedocs.org/projects/wiznet5k/badge/?version=latest 5 | :target: https://docs.circuitpython.org/projects/wiznet5k/en/latest/ 6 | :alt: Documentation Status 7 | 8 | .. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg 9 | :target: https://adafru.it/discord 10 | :alt: Discord 11 | 12 | .. image:: https://github.com/adafruit/Adafruit_CircuitPython_Wiznet5k/workflows/Build%20CI/badge.svg 13 | :target: https://github.com/adafruit/Adafruit_CircuitPython_Wiznet5k/actions 14 | :alt: Build Status 15 | 16 | .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json 17 | :target: https://github.com/astral-sh/ruff 18 | :alt: Code Style: Ruff 19 | 20 | Pure-Python interface for WIZNET 5k ethernet modules. 21 | 22 | Dependencies 23 | ============= 24 | This driver depends on: 25 | 26 | * `Adafruit CircuitPython `_ 27 | * `Bus Device `_ 28 | 29 | Please ensure all dependencies are available on the CircuitPython filesystem. 30 | This is easily achieved by downloading 31 | `the Adafruit library and driver bundle `_. 32 | 33 | Installing from PyPI 34 | ===================== 35 | On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from 36 | PyPI `_. To install for current user: 37 | 38 | .. code-block:: shell 39 | 40 | pip3 install adafruit-circuitpython-wiznet5k 41 | 42 | To install system-wide (this may be required in some cases): 43 | 44 | .. code-block:: shell 45 | 46 | sudo pip3 install adafruit-circuitpython-wiznet5k 47 | 48 | To install in a virtual environment in your current project: 49 | 50 | .. code-block:: shell 51 | 52 | mkdir project-name && cd project-name 53 | python3 -m venv .venv 54 | source .venv/bin/activate 55 | pip3 install adafruit-circuitpython-wiznet5k 56 | 57 | Usage Example 58 | ============= 59 | This example demonstrates making a HTTP GET request to 60 | wifitest.adafruit.com. 61 | 62 | .. code-block:: python 63 | 64 | import board 65 | import busio 66 | import digitalio 67 | import adafruit_connection_manager 68 | import adafruit_requests 69 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 70 | 71 | print("Wiznet5k WebClient Test") 72 | 73 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 74 | JSON_URL = "http://api.coindesk.com/v1/bpi/currentprice/USD.json" 75 | 76 | cs = digitalio.DigitalInOut(board.D10) 77 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 78 | 79 | # Initialize ethernet interface with DHCP 80 | eth = WIZNET5K(spi_bus, cs) 81 | 82 | # Initialize a requests session 83 | pool = adafruit_connection_manager.get_radio_socketpool(eth) 84 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(eth) 85 | requests = adafruit_requests.Session(pool, ssl_context) 86 | 87 | print("Chip Version:", eth.chip) 88 | print("MAC Address:", [hex(i) for i in eth.mac_address]) 89 | print("My IP address is:", eth.pretty_ip(eth.ip_address)) 90 | print("IP lookup adafruit.com: %s" %eth.pretty_ip(eth.get_host_by_name("adafruit.com"))) 91 | 92 | 93 | #eth._debug = True 94 | print("Fetching text from", TEXT_URL) 95 | r = requests.get(TEXT_URL) 96 | print('-'*40) 97 | print(r.text) 98 | print('-'*40) 99 | r.close() 100 | 101 | print() 102 | print("Fetching json from", JSON_URL) 103 | r = requests.get(JSON_URL) 104 | print('-'*40) 105 | print(r.json()) 106 | print('-'*40) 107 | r.close() 108 | 109 | print("Done!") 110 | 111 | Documentation 112 | ============= 113 | 114 | API documentation for this library can be found on `Read the Docs `_. 115 | 116 | For information on building library documentation, please check out `this guide `_. 117 | 118 | Contributing 119 | ============ 120 | 121 | Contributions are welcome! Please read our `Code of Conduct 122 | `_ 123 | before contributing to help this project stay welcoming. 124 | 125 | License 126 | ============= 127 | 128 | This library was written by `Arduino LLC `_. We've converted it to work 129 | with `CircuitPython `_ and made changes so it works similarly to `CircuitPython's WIZNET5k wrapper for the WIZnet 130 | 5500 Ethernet interface `_ and CPython's `Socket low-level 131 | networking interface module `_. 132 | 133 | This open source code is licensed under the LGPL license (see LICENSE for details). 134 | -------------------------------------------------------------------------------- /README.rst.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries 2 | 3 | SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /adafruit_wiznet5k/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Wiznet5k/1cab7a21682c508b0d8a46e875ecffdfa5736c24/adafruit_wiznet5k/__init__.py -------------------------------------------------------------------------------- /adafruit_wiznet5k/adafruit_wiznet5k.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2010 WIZnet 2 | # SPDX-FileCopyrightText: 2010 Arduino LLC 3 | # SPDX-FileCopyrightText: 2008 Bjoern Hartmann 4 | # SPDX-FileCopyrightText: 2018 Paul Stoffregen 5 | # SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries 6 | # SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck 7 | # SPDX-FileCopyrightText: 2021 Adam Cummick 8 | # SPDX-FileCopyrightText: 2023 Martin Stephens 9 | # 10 | # SPDX-License-Identifier: MIT 11 | """ 12 | `adafruit_wiznet5k` 13 | ================================================================================ 14 | 15 | Pure-Python interface for WIZNET 5k ethernet modules. 16 | 17 | * Author(s): WIZnet, Arduino LLC, Bjoern Hartmann, Paul Stoffregen, Brent Rubell, 18 | Patrick Van Oosterwijck, Martin Stephens 19 | 20 | Implementation Notes 21 | -------------------- 22 | 23 | **Software and Dependencies:** 24 | 25 | * Adafruit CircuitPython firmware for the supported boards: 26 | https://github.com/adafruit/circuitpython/releases 27 | 28 | * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice 29 | """ 30 | 31 | from __future__ import annotations 32 | 33 | try: 34 | from typing import TYPE_CHECKING, Optional, Tuple, Union 35 | 36 | if TYPE_CHECKING: 37 | import busio 38 | import digitalio 39 | from circuitpython_typing import WriteableBuffer 40 | 41 | IpAddress4Raw = Union[bytes, Tuple[int, int, int, int]] 42 | MacAddressRaw = Union[bytes, Tuple[int, int, int, int, int, int]] 43 | except ImportError: 44 | pass 45 | 46 | __version__ = "0.0.0+auto.0" 47 | __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Wiznet5k.git" 48 | 49 | import gc 50 | import time 51 | from random import randint 52 | 53 | from adafruit_bus_device.spi_device import SPIDevice 54 | from adafruit_ticks import ticks_diff, ticks_ms 55 | from micropython import const 56 | 57 | import adafruit_wiznet5k.adafruit_wiznet5k_dhcp as dhcp 58 | import adafruit_wiznet5k.adafruit_wiznet5k_dns as dns 59 | from adafruit_wiznet5k.adafruit_wiznet5k_debug import debug_msg 60 | 61 | # *** Wiznet Common Registers *** 62 | _REG_MR = {"w5100s": const(0x0000), "w5500": const(0x0000)} 63 | # Gateway IPv4 Address. 64 | _REG_GAR = {"w5100s": const(0x0001), "w5500": const(0x0001), "w6100": const(0x4130)} 65 | # Subnet Mask Address 66 | _REG_SUBR = {"w5100s": const(0x0005), "w5500": const(0x0005), "w6100": const(0x4134)} 67 | # Chip version. 68 | _REG_VERSIONR = { 69 | "w5100s": const(0x0080), 70 | "w5500": const(0x0039), 71 | "w6100": const(0x0000), 72 | } 73 | # Source Hardware Address 74 | _REG_SHAR = {"w5100s": const(0x0009), "w5500": const(0x0009), "w6100": const(0x4120)} 75 | # Source IP Address 76 | _REG_SIPR = {"w5100s": const(0x000F), "w5500": const(0x000F), "w6100": const(0x4138)} 77 | # Register with link status flag (PHYCFGR for 5xxxx, PHYSR for 6100). 78 | _REG_LINK_FLAG = { 79 | "w5100s": const(0x003C), 80 | "w5500": const(0x002E), 81 | "w6100": const(0x3000), 82 | } 83 | _REG_RCR = {"w5100s": const(0x0019), "w5500": const(0x001B), "w6100": const(0x4204)} 84 | _REG_RTR = {"w5100s": const(0x0017), "w5500": const(0x0019), "w6100": const(0x4200)} 85 | 86 | # *** Wiznet Socket Registers *** 87 | # Socket n Mode. 88 | _REG_SNMR = const(0x0000) 89 | # Socket n Command. 90 | _REG_SNCR = {"w5100s": const(0x0001), "w5500": const(0x0001), "w6100": const(0x0010)} 91 | # Socket n Interrupt. 92 | _REG_SNIR = {"w5100s": const(0x0002), "w5500": const(0x0002), "w6100": const(0x0020)} 93 | # Socket n Status. 94 | _REG_SNSR = {"w5100s": const(0x0003), "w5500": const(0x0003), "w6100": const(0x0030)} 95 | # Socket n Source Port. 96 | _REG_SNPORT = {"w5100s": const(0x0004), "w5500": const(0x0004), "w6100": const(0x0114)} 97 | # Destination IPv4 Address. 98 | _REG_SNDIPR = {"w5100s": const(0x000C), "w5500": const(0x000C), "w6100": const(0x0120)} 99 | # Destination Port. 100 | _REG_SNDPORT = {"w5100s": const(0x0010), "w5500": const(0x0010), "w6100": const(0x0140)} 101 | # RX Free Size. 102 | _REG_SNRX_RSR = { 103 | "w5100s": const(0x0026), 104 | "w5500": const(0x0026), 105 | "w6100": const(0x0224), 106 | } 107 | # Read Size Pointer. 108 | _REG_SNRX_RD = {"w5100s": const(0x0028), "w5500": const(0x0028), "w6100": const(0x0228)} 109 | # Socket n TX Free Size. 110 | _REG_SNTX_FSR = { 111 | "w5100s": const(0x0020), 112 | "w5500": const(0x0020), 113 | "w6100": const(0x0204), 114 | } 115 | # TX Write Pointer. 116 | _REG_SNTX_WR = {"w5100s": const(0x0024), "w5500": const(0x0024), "w6100": const(0x020C)} 117 | 118 | # SNSR Commands 119 | SNSR_SOCK_CLOSED = const(0x00) 120 | _SNSR_SOCK_INIT = const(0x13) 121 | SNSR_SOCK_LISTEN = const(0x14) 122 | _SNSR_SOCK_SYNSENT = const(0x15) 123 | SNSR_SOCK_SYNRECV = const(0x16) 124 | SNSR_SOCK_ESTABLISHED = const(0x17) 125 | SNSR_SOCK_FIN_WAIT = const(0x18) 126 | _SNSR_SOCK_CLOSING = const(0x1A) 127 | SNSR_SOCK_TIME_WAIT = const(0x1B) 128 | SNSR_SOCK_CLOSE_WAIT = const(0x1C) 129 | _SNSR_SOCK_LAST_ACK = const(0x1D) 130 | _SNSR_SOCK_UDP = const(0x22) 131 | _SNSR_SOCK_IPRAW = const(0x32) 132 | _SNSR_SOCK_MACRAW = const(0x42) 133 | _SNSR_SOCK_PPPOE = const(0x5F) 134 | 135 | # Sock Commands (CMD) 136 | _CMD_SOCK_OPEN = const(0x01) 137 | _CMD_SOCK_LISTEN = const(0x02) 138 | _CMD_SOCK_CONNECT = const(0x04) 139 | _CMD_SOCK_DISCON = const(0x08) 140 | _CMD_SOCK_CLOSE = const(0x10) 141 | _CMD_SOCK_SEND = const(0x20) 142 | _CMD_SOCK_SEND_MAC = const(0x21) 143 | _CMD_SOCK_SEND_KEEP = const(0x22) 144 | _CMD_SOCK_RECV = const(0x40) 145 | 146 | # Socket n Interrupt Register 147 | _SNIR_SEND_OK = const(0x10) 148 | SNIR_TIMEOUT = const(0x08) 149 | _SNIR_RECV = const(0x04) 150 | SNIR_DISCON = const(0x02) 151 | _SNIR_CON = const(0x01) 152 | 153 | _CH_SIZE = const(0x100) 154 | _SOCK_SIZE = const(0x800) # MAX W5k socket size 155 | _SOCK_MASK = const(0x7FF) 156 | # Register commands 157 | _MR_RST = const(0x80) # Mode Register RST 158 | # Socket mode register 159 | _SNMR_CLOSE = const(0x00) 160 | _SNMR_TCP = const(0x21) 161 | SNMR_UDP = const(0x02) 162 | _SNMR_IPRAW = const(0x03) 163 | _SNMR_MACRAW = const(0x04) 164 | _SNMR_PPPOE = const(0x05) 165 | 166 | _MAX_PACKET = const(4000) 167 | _LOCAL_PORT = const(0x400) 168 | # Default hardware MAC address 169 | _DEFAULT_MAC = "DE:AD:BE:EF:FE:ED" 170 | 171 | # Maximum number of sockets to support, differs between chip versions. 172 | _MAX_SOCK_NUM = {"w5100s": const(0x04), "w5500": const(0x08), "w6100": const(0x08)} 173 | _SOCKET_INVALID = const(0xFF) 174 | 175 | 176 | def _unprettyfy(data: str, seperator: str, correct_length: int) -> bytes: 177 | """Helper for converting . or : delimited strings to bytes objects.""" 178 | data = bytes(int(x) for x in data.split(seperator)) 179 | if len(data) == correct_length: 180 | return data 181 | raise ValueError("Invalid IP or MAC address.") 182 | 183 | 184 | class WIZNET5K: 185 | """Interface for WIZNET5K module.""" 186 | 187 | _sockets_reserved = [] 188 | 189 | def __init__( 190 | self, 191 | spi_bus: busio.SPI, 192 | cs: digitalio.DigitalInOut, 193 | reset: Optional[digitalio.DigitalInOut] = None, 194 | is_dhcp: bool = True, 195 | mac: Union[MacAddressRaw, str] = _DEFAULT_MAC, 196 | hostname: Optional[str] = None, 197 | debug: bool = False, 198 | spi_baudrate: int = 8000000, 199 | ) -> None: 200 | """ 201 | :param busio.SPI spi_bus: The SPI bus the Wiznet module is connected to. 202 | :param digitalio.DigitalInOut cs: Chip select pin. 203 | :param digitalio.DigitalInOut reset: Optional reset pin, defaults to None. 204 | :param bool is_dhcp: Whether to start DHCP automatically or not, defaults to True. 205 | :param Union[MacAddressRaw, str] mac: The Wiznet's MAC Address, defaults to 206 | (0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED). 207 | :param str hostname: The desired hostname, with optional {} to fill in the MAC 208 | address, defaults to None. 209 | :param bool debug: Enable debugging output, defaults to False. 210 | :param int spi_baudrate: The SPI clock frequency, defaults to 8MHz. 211 | Might not be the actual baudrate, dependent on the hardware. 212 | """ 213 | self._debug = debug 214 | self._chip_type = None 215 | self._device = SPIDevice(spi_bus, cs, baudrate=spi_baudrate, polarity=0, phase=0) 216 | # init c.s. 217 | self._cs = cs 218 | 219 | # Reset wiznet module prior to initialization. 220 | if reset: 221 | debug_msg("* Resetting WIZnet chip", self._debug) 222 | reset.switch_to_output() 223 | reset.value = False 224 | time.sleep(0.1) 225 | reset.value = True 226 | time.sleep(5) 227 | 228 | # Setup chip_select pin. 229 | time.sleep(1) 230 | self._cs.switch_to_output() 231 | self._cs.value = 1 232 | 233 | # Buffer for reading params from module 234 | self._pbuff = bytearray(8) 235 | self._rxbuf = bytearray(_MAX_PACKET) 236 | 237 | # attempt to initialize the module 238 | self._ch_base_msb = 0 239 | self._src_ports_in_use = [] 240 | self._wiznet_chip_init() 241 | 242 | # Set MAC address 243 | self.mac_address = mac 244 | self.src_port = 0 245 | self._dns = b"\x00\x00\x00\x00" 246 | # udp related 247 | self.udp_from_ip = [b"\x00\x00\x00\x00"] * self.max_sockets 248 | self.udp_from_port = [0] * self.max_sockets 249 | 250 | # Wait to give the Ethernet link to initialise. 251 | start_time = ticks_ms() 252 | timeout = 5000 253 | while ticks_diff(ticks_ms(), start_time) < timeout: 254 | if self.link_status: 255 | break 256 | debug_msg("Ethernet link is down…", self._debug) 257 | time.sleep(0.5) 258 | self._dhcp_client = None 259 | 260 | # Set DHCP 261 | if is_dhcp: 262 | self.set_dhcp(hostname) 263 | 264 | def set_dhcp(self, hostname: Optional[str] = None) -> None: 265 | """ 266 | Initialize the DHCP client and attempt to retrieve and set network 267 | configuration from the DHCP server. 268 | 269 | :param Optional[str] hostname: The desired hostname for the DHCP server with 270 | optional {} to fill in the MAC address, defaults to None. 271 | 272 | :raises RuntimeError: If DHCP lease cannot be established. 273 | """ 274 | debug_msg("* Initializing DHCP", self._debug) 275 | self._dhcp_client = dhcp.DHCP(self, self.mac_address, hostname, self._debug) 276 | if self._dhcp_client.request_dhcp_lease(): 277 | debug_msg( 278 | "Found DHCP Server:\nIP: {}\n Subnet Mask: {}\n GW Addr: {}" 279 | "\n DNS Server: {}".format(*self.ifconfig), 280 | self._debug, 281 | ) 282 | else: 283 | self._dhcp_client = None 284 | raise RuntimeError("Failed to configure DHCP Server!") 285 | 286 | def maintain_dhcp_lease(self) -> None: 287 | """Maintain the DHCP lease.""" 288 | if self._dhcp_client: 289 | self._dhcp_client.maintain_dhcp_lease() 290 | 291 | def get_host_by_name(self, hostname: str) -> bytes: 292 | """ 293 | Convert a hostname to a packed 4-byte IP Address. 294 | 295 | :param str hostname: The host name to be converted. 296 | 297 | :return bytes: The IPv4 address as a 4 byte array. 298 | 299 | :raises RuntimeError: If the DNS lookup fails. 300 | """ 301 | debug_msg("Get host by name", self._debug) 302 | if isinstance(hostname, str): 303 | hostname = bytes(hostname, "utf-8") 304 | # Return IP assigned by DHCP 305 | _dns_client = dns.DNS(self, self.pretty_ip(bytearray(self._dns)), debug=self._debug) 306 | ipv4 = _dns_client.gethostbyname(hostname) 307 | debug_msg(f"* Resolved IP: {ipv4}", self._debug) 308 | if ipv4 == -1: 309 | raise RuntimeError("Failed to resolve hostname!") 310 | return ipv4 311 | 312 | @property 313 | def max_sockets(self) -> int: 314 | """ 315 | Maximum number of sockets supported by chip. 316 | 317 | :return int: Maximum supported sockets. 318 | """ 319 | return _MAX_SOCK_NUM[self._chip_type] 320 | 321 | @property 322 | def chip(self) -> str: 323 | """ 324 | Ethernet controller chip type. 325 | 326 | :return str: The chip type. 327 | """ 328 | return self._chip_type 329 | 330 | @property 331 | def ip_address(self) -> bytes: 332 | """ 333 | Configured IP address for the WIZnet Ethernet hardware. 334 | 335 | :return bytes: IP address as four bytes. 336 | """ 337 | return self._read(_REG_SIPR[self._chip_type], 0x00, 4) 338 | 339 | @property 340 | def ipv4_address(self) -> str: 341 | """ 342 | Configured IP address for the WIZnet Ethernet hardware. 343 | 344 | :return str: The IP address (a string of the form '255.255.255.255') 345 | """ 346 | return self.pretty_ip(self.ip_address) 347 | 348 | @staticmethod 349 | def pretty_ip(ipv4: bytes) -> str: 350 | """ 351 | Convert a 4 byte IP address to a dotted-quad string for printing. 352 | 353 | :param bytearray ipv4: A four byte IP address. 354 | 355 | :return str: The IP address (a string of the form '255.255.255.255'). 356 | 357 | :raises ValueError: If IP address is not 4 bytes. 358 | """ 359 | if len(ipv4) != 4: 360 | raise ValueError("Wrong length for IPv4 address.") 361 | return ".".join(f"{byte}" for byte in ipv4) 362 | 363 | @staticmethod 364 | def unpretty_ip(ipv4: str) -> bytes: 365 | """ 366 | Convert a dotted-quad string to a four byte IP address. 367 | 368 | :param str ipv4: IPv4 address (a string of the form '255.255.255.255') to be converted. 369 | 370 | :return bytes: IPv4 address in four bytes. 371 | 372 | :raises ValueError: If IPv4 address is not 4 bytes. 373 | """ 374 | return _unprettyfy(ipv4, ".", 4) 375 | 376 | @property 377 | def mac_address(self) -> bytes: 378 | """ 379 | The WIZnet Ethernet hardware MAC address. 380 | 381 | :return bytes: Six byte MAC address. 382 | """ 383 | return self._read(_REG_SHAR[self._chip_type], 0x00, 6) 384 | 385 | @mac_address.setter 386 | def mac_address(self, address: Union[MacAddressRaw, str]) -> None: 387 | """ 388 | Set the WIZnet hardware MAC address. 389 | 390 | :param Union[MacAddressRaw, str] address: A hardware MAC address. 391 | 392 | :raises ValueError: If the MAC address is invalid 393 | """ 394 | try: 395 | address = [int(x, 16) for x in address.split(":")] 396 | except TypeError: 397 | pass 398 | try: 399 | if len(address) != 6: 400 | raise ValueError() 401 | # Bytes conversion will raise ValueError if values are not 0-255 402 | self._write(_REG_SHAR[self._chip_type], 0x04, bytes(address)) 403 | except ValueError: 404 | raise ValueError("Invalid MAC address.") 405 | 406 | @staticmethod 407 | def pretty_mac(mac: bytes) -> str: 408 | """ 409 | Convert a bytes MAC address to a ':' seperated string for display. 410 | 411 | :param bytes mac: The MAC address. 412 | 413 | :return str: Mac Address in the form 00:00:00:00:00:00 414 | 415 | :raises ValueError: If MAC address is not 6 bytes. 416 | """ 417 | if len(mac) != 6: 418 | raise ValueError("Incorrect length for MAC address.") 419 | return ":".join(f"{byte:02x}" for byte in mac) 420 | 421 | def remote_ip(self, socket_num: int) -> str: 422 | """ 423 | IP address of the host which sent the current incoming packet. 424 | 425 | :param int socket_num: ID number of the socket to check. 426 | 427 | :return str: The IPv4 address. 428 | 429 | :raises ValueError: If the socket number is out of range. 430 | """ 431 | self._sock_num_in_range(socket_num) 432 | for octet in range(4): 433 | self._pbuff[octet] = self._read_socket_register( 434 | socket_num, _REG_SNDIPR[self._chip_type] + octet 435 | ) 436 | return self.pretty_ip(self._pbuff[:4]) 437 | 438 | def remote_port(self, socket_num: int) -> int: 439 | """ 440 | Port number of the host which sent the current incoming packet. 441 | 442 | :param int socket_num: ID number of the socket to check. 443 | 444 | :return int: The incoming port number of the socket connection. 445 | 446 | :raises ValueError: If the socket number is out of range. 447 | """ 448 | self._sock_num_in_range(socket_num) 449 | return self._read_two_byte_sock_reg(socket_num, _REG_SNDPORT[self._chip_type]) 450 | 451 | @property 452 | def link_status(self) -> bool: 453 | """ 454 | Physical hardware (PHY) connection status. 455 | 456 | Whether the WIZnet hardware is physically connected to an Ethernet network. 457 | 458 | :return bool: True if the link is up, False if the link is down. 459 | """ 460 | return bool(int.from_bytes(self._read(_REG_LINK_FLAG[self._chip_type], 0x00), "big") & 0x01) 461 | 462 | @property 463 | def ifconfig(self) -> Tuple[bytes, bytes, bytes, bytes]: 464 | """ 465 | Network configuration information. 466 | 467 | :return Tuple[bytes, bytes, bytes, bytes]: The IP address, subnet mask, gateway 468 | address and DNS server address. 469 | """ 470 | return ( 471 | self.ip_address, 472 | self._read(_REG_SUBR[self._chip_type], 0x00, 4), 473 | self._read(_REG_GAR[self._chip_type], 0x00, 4), 474 | self._dns, 475 | ) 476 | 477 | @ifconfig.setter 478 | def ifconfig( 479 | self, params: Tuple[IpAddress4Raw, IpAddress4Raw, IpAddress4Raw, IpAddress4Raw] 480 | ) -> None: 481 | """ 482 | Set network configuration. 483 | 484 | :param Tuple[Address4Bytes, Address4Bytes, Address4Bytes, Address4Bytes]: Configuration 485 | settings - (ip_address, subnet_mask, gateway_address, dns_server). 486 | """ 487 | for param in params: 488 | if len(param) != 4: 489 | raise ValueError("IPv4 address must be 4 bytes.") 490 | ip_address, subnet_mask, gateway_address, dns_server = params 491 | 492 | self._write(_REG_SIPR[self._chip_type], 0x04, bytes(ip_address)) 493 | self._write(_REG_SUBR[self._chip_type], 0x04, bytes(subnet_mask)) 494 | self._write(_REG_GAR[self._chip_type], 0x04, bytes(gateway_address)) 495 | 496 | self._dns = bytes(dns_server) 497 | 498 | # *** Public Socket Methods *** 499 | 500 | def socket_available(self, socket_num: int, sock_type: int = _SNMR_TCP) -> int: 501 | """ 502 | Number of bytes available to be read from the socket. 503 | 504 | :param int socket_num: Socket to check for available bytes. 505 | :param int sock_type: Socket type. Use SNMR_TCP for TCP or SNMR_UDP for UDP, 506 | defaults to SNMR_TCP. 507 | 508 | :return int: Number of bytes available to read. 509 | 510 | :raises ValueError: If the socket number is out of range. 511 | :raises ValueError: If the number of bytes on a UDP socket is negative. 512 | """ 513 | debug_msg( 514 | f"socket_available called on socket {socket_num}, protocol {sock_type}", 515 | self._debug, 516 | ) 517 | self._sock_num_in_range(socket_num) 518 | 519 | number_of_bytes = self._get_rx_rcv_size(socket_num) 520 | if self._read_snsr(socket_num) == SNMR_UDP: 521 | number_of_bytes -= 8 # Subtract UDP header from packet size. 522 | if number_of_bytes < 0: 523 | raise ValueError("Negative number of bytes found on socket.") 524 | return number_of_bytes 525 | 526 | def socket_status(self, socket_num: int) -> int: 527 | """ 528 | Socket connection status. 529 | 530 | Can be: SNSR_SOCK_CLOSED, SNSR_SOCK_INIT, SNSR_SOCK_LISTEN, SNSR_SOCK_SYNSENT, 531 | SNSR_SOCK_SYNRECV, SNSR_SYN_SOCK_ESTABLISHED, SNSR_SOCK_FIN_WAIT, 532 | SNSR_SOCK_CLOSING, SNSR_SOCK_TIME_WAIT, SNSR_SOCK_CLOSE_WAIT, SNSR_LAST_ACK, 533 | SNSR_SOCK_UDP, SNSR_SOCK_IPRAW, SNSR_SOCK_MACRAW, SNSR_SOCK_PPOE. 534 | 535 | :param int socket_num: ID of socket to check. 536 | 537 | :return int: The connection status. 538 | """ 539 | return self._read_snsr(socket_num) 540 | 541 | def socket_connect( 542 | self, 543 | socket_num: int, 544 | dest: IpAddress4Raw, 545 | port: int, 546 | conn_mode: int = _SNMR_TCP, 547 | ) -> int: 548 | """ 549 | Open and verify a connection from a socket to a destination IPv4 address 550 | or hostname. A TCP connection is made by default. A UDP connection can also 551 | be made. 552 | 553 | :param int socket_num: ID of the socket to be connected. 554 | :param IpAddress4Raw dest: The destination as a host name or IP address. 555 | :param int port: Port to connect to (0 - 65,535). 556 | :param int conn_mode: The connection mode. Use SNMR_TCP for TCP or SNMR_UDP for UDP, 557 | defaults to SNMR_TCP. 558 | 559 | :raises ValueError: if the socket number is out of range. 560 | :raises ConnectionError: If the connection to the socket cannot be established. 561 | """ 562 | self._sock_num_in_range(socket_num) 563 | self._check_link_status() 564 | debug_msg( 565 | f"W5K socket connect, protocol={conn_mode}, port={port}, ip={self.pretty_ip(dest)}", 566 | self._debug, 567 | ) 568 | # initialize a socket and set the mode 569 | self.socket_open(socket_num, conn_mode=conn_mode) 570 | # set socket destination IP and port 571 | self._write_sndipr(socket_num, dest) 572 | self._write_sndport(socket_num, port) 573 | self._write_sncr(socket_num, _CMD_SOCK_CONNECT) 574 | 575 | if conn_mode == _SNMR_TCP: 576 | # wait for tcp connection establishment 577 | while self.socket_status(socket_num) != SNSR_SOCK_ESTABLISHED: 578 | time.sleep(0.001) 579 | debug_msg(f"SNSR: {self.socket_status(socket_num)}", self._debug) 580 | if self.socket_status(socket_num) == SNSR_SOCK_CLOSED: 581 | raise ConnectionError("Failed to establish connection.") 582 | return 1 583 | 584 | def get_socket(self, *, reserve_socket=False) -> int: 585 | """ 586 | Request, allocate and return a socket from the WIZnet 5k chip. 587 | 588 | Cycle through the sockets to find the first available one. If the called with 589 | reserve_socket=True, update the list of reserved sockets (intended to be used with 590 | socket.socket()). Note that reserved sockets must be released by calling 591 | release_socket() once they are no longer needed. 592 | 593 | If all sockets are reserved, no sockets are available for DNS calls, etc. Therefore, 594 | one socket cannot be reserved. Since socket 0 is the only socket that is capable of 595 | operating in MacRAW mode, it is the non-reservable socket. 596 | 597 | :param bool reserve_socket: Whether to reserve the socket. 598 | 599 | :returns int: The first available socket. 600 | 601 | :raises RuntimeError: If no socket is available. 602 | """ 603 | debug_msg("*** Get socket.", self._debug) 604 | # Prefer socket zero for none reserved calls as it cannot be reserved. 605 | if not reserve_socket and self.socket_status(0) == SNSR_SOCK_CLOSED: 606 | debug_msg("Allocated socket # 0", self._debug) 607 | return 0 608 | # Then check the other sockets. 609 | 610 | # Call garbage collection to encourage socket.__del__() be called to on any 611 | # destroyed instances. Not at all guaranteed to work! 612 | gc.collect() 613 | debug_msg(f"Reserved sockets: {WIZNET5K._sockets_reserved}", self._debug) 614 | 615 | for socket_number, reserved in enumerate(WIZNET5K._sockets_reserved, start=1): 616 | if not reserved and self.socket_status(socket_number) == SNSR_SOCK_CLOSED: 617 | if reserve_socket: 618 | WIZNET5K._sockets_reserved[socket_number - 1] = True 619 | debug_msg( 620 | f"Allocated socket # {socket_number}.", 621 | self._debug, 622 | ) 623 | return socket_number 624 | raise RuntimeError("All sockets in use.") 625 | 626 | def release_socket(self, socket_number): 627 | """ 628 | Update the socket reservation list when a socket is no longer reserved. 629 | 630 | :param int socket_number: The socket to release. 631 | 632 | :raises ValueError: If the socket number is out of range. 633 | """ 634 | self._sock_num_in_range(socket_number) 635 | WIZNET5K._sockets_reserved[socket_number - 1] = False 636 | 637 | def socket_listen(self, socket_num: int, port: int, conn_mode: int = _SNMR_TCP) -> None: 638 | """ 639 | Listen on a socket's port. 640 | 641 | :param int socket_num: ID of socket to listen on. 642 | :param int port: Port to listen on (0 - 65,535). 643 | :param int conn_mode: Connection mode SNMR_TCP for TCP or SNMR_UDP for 644 | UDP, defaults to SNMR_TCP. 645 | 646 | :raises ValueError: If the socket number is out of range. 647 | :raises ConnectionError: If the Ethernet link is down. 648 | :raises RuntimeError: If unable to connect to a hardware socket. 649 | """ 650 | self._sock_num_in_range(socket_num) 651 | self._check_link_status() 652 | debug_msg( 653 | f"* Listening on port={port}, ip={self.pretty_ip(self.ip_address)}", 654 | self._debug, 655 | ) 656 | # Initialize a socket and set the mode 657 | self.src_port = port 658 | self.socket_open(socket_num, conn_mode=conn_mode) 659 | self.src_port = 0 660 | # Send listen command 661 | self._write_sncr(socket_num, _CMD_SOCK_LISTEN) 662 | # Wait until ready 663 | status = SNSR_SOCK_CLOSED 664 | while status not in { 665 | SNSR_SOCK_LISTEN, 666 | SNSR_SOCK_ESTABLISHED, 667 | _SNSR_SOCK_UDP, 668 | }: 669 | status = self._read_snsr(socket_num) 670 | if status == SNSR_SOCK_CLOSED: 671 | raise RuntimeError("Listening socket closed.") 672 | 673 | def socket_accept(self, socket_num: int) -> Tuple[int, Tuple[str, int]]: 674 | """ 675 | Destination IPv4 address and port from an incoming connection. 676 | 677 | Return the next socket number so listening can continue, along with 678 | the IP address and port of the incoming connection. 679 | 680 | :param int socket_num: Socket number with connection to check. 681 | 682 | :return Tuple[int, Tuple[Union[str, bytearray], Union[int, bytearray]]]: 683 | If successful, the next (socket number, (destination IP address, destination port)). 684 | 685 | :raises ValueError: If the socket number is out of range. 686 | """ 687 | self._sock_num_in_range(socket_num) 688 | dest_ip = self.remote_ip(socket_num) 689 | dest_port = self.remote_port(socket_num) 690 | next_socknum = self.get_socket() 691 | debug_msg( 692 | f"Dest is ({dest_ip}, {dest_port}), Next listen socknum is #{next_socknum}", 693 | self._debug, 694 | ) 695 | return next_socknum, (dest_ip, dest_port) 696 | 697 | def socket_open(self, socket_num: int, conn_mode: int = _SNMR_TCP) -> None: 698 | """ 699 | Open an IP socket. 700 | 701 | The socket may connect via TCP or UDP protocols. 702 | 703 | :param int socket_num: The socket number to open. 704 | :param int conn_mode: The protocol to use. Use SNMR_TCP for TCP or SNMR_UDP for \ 705 | UDP, defaults to SNMR_TCP. 706 | 707 | :raises ValueError: If the socket number is out of range. 708 | :raises ConnectionError: If the Ethernet link is down or no connection to socket. 709 | :raises RuntimeError: If unable to open a socket in UDP or TCP mode. 710 | """ 711 | self._sock_num_in_range(socket_num) 712 | self._check_link_status() 713 | debug_msg(f"*** Opening socket {socket_num}", self._debug) 714 | if self._read_snsr(socket_num) not in { 715 | SNSR_SOCK_CLOSED, 716 | SNSR_SOCK_TIME_WAIT, 717 | SNSR_SOCK_FIN_WAIT, 718 | SNSR_SOCK_CLOSE_WAIT, 719 | _SNSR_SOCK_CLOSING, 720 | _SNSR_SOCK_UDP, 721 | }: 722 | raise ConnectionError("Failed to initialize a connection with the socket.") 723 | debug_msg(f"* Opening W5k Socket, protocol={conn_mode}", self._debug) 724 | time.sleep(0.00025) 725 | 726 | self._write_snmr(socket_num, conn_mode) 727 | self.write_snir(socket_num, 0xFF) 728 | 729 | if self.src_port > 0: 730 | # write to socket source port 731 | self._write_sock_port(socket_num, self.src_port) 732 | else: 733 | s_port = randint(49152, 65535) 734 | while s_port in self._src_ports_in_use: 735 | s_port = randint(49152, 65535) 736 | self._write_sock_port(socket_num, s_port) 737 | self._src_ports_in_use[socket_num] = s_port 738 | 739 | # open socket 740 | self._write_sncr(socket_num, _CMD_SOCK_OPEN) 741 | if self._read_snsr(socket_num) not in {_SNSR_SOCK_INIT, _SNSR_SOCK_UDP}: 742 | raise RuntimeError("Could not open socket in TCP or UDP mode.") 743 | 744 | def socket_close(self, socket_num: int) -> None: 745 | """ 746 | Close a socket. 747 | 748 | :param int socket_num: The socket to close. 749 | 750 | :raises ValueError: If the socket number is out of range. 751 | """ 752 | debug_msg(f"*** Closing socket {socket_num}", self._debug) 753 | self._sock_num_in_range(socket_num) 754 | self._write_sncr(socket_num, _CMD_SOCK_CLOSE) 755 | debug_msg(" Waiting for socket to close…", self._debug) 756 | start = ticks_ms() 757 | timeout = 5000 758 | while self._read_snsr(socket_num) != SNSR_SOCK_CLOSED: 759 | if ticks_diff(ticks_ms(), start) > timeout: 760 | raise RuntimeError( 761 | f"Wiznet5k failed to close socket, status = {self._read_snsr(socket_num)}." 762 | ) 763 | time.sleep(0.0001) 764 | debug_msg(" Socket has closed.", self._debug) 765 | 766 | def socket_disconnect(self, socket_num: int) -> None: 767 | """ 768 | Disconnect a TCP or UDP connection. 769 | 770 | :param int socket_num: The socket to close. 771 | 772 | :raises ValueError: If the socket number is out of range. 773 | """ 774 | debug_msg(f"*** Disconnecting socket {socket_num}", self._debug) 775 | self._sock_num_in_range(socket_num) 776 | self._write_sncr(socket_num, _CMD_SOCK_DISCON) 777 | 778 | def socket_read(self, socket_num: int, length: int) -> Tuple[int, bytes]: 779 | """ 780 | Read data from a hardware socket. Called directly by TCP socket objects and via 781 | read_udp() for UDP socket objects. 782 | 783 | :param int socket_num: The socket to read data from. 784 | :param int length: The number of bytes to read from the socket. 785 | 786 | :returns Tuple[int, bytes]: If the read was successful then the first 787 | item of the tuple is the length of the data and the second is the data. 788 | If the read was unsuccessful then 0, b"" is returned. 789 | 790 | :raises ValueError: If the socket number is out of range. 791 | :raises ConnectionError: If the Ethernet link is down. 792 | :raises RuntimeError: If the socket connection has been lost. 793 | """ 794 | self._sock_num_in_range(socket_num) 795 | self._check_link_status() 796 | 797 | # Check if there is data available on the socket 798 | bytes_on_socket = self._get_rx_rcv_size(socket_num) 799 | debug_msg(f"Bytes avail. on sock: {bytes_on_socket}", self._debug) 800 | if bytes_on_socket: 801 | bytes_on_socket = length if bytes_on_socket > length else bytes_on_socket 802 | debug_msg(f"* Processing {bytes_on_socket} bytes of data", self._debug) 803 | # Read the starting save address of the received data. 804 | pointer = self._read_snrx_rd(socket_num) 805 | # Read data from the hardware socket. 806 | bytes_read = self._chip_socket_read(socket_num, pointer, bytes_on_socket) 807 | # After reading the received data, update Sn_RX_RD register. 808 | pointer = (pointer + bytes_on_socket) & 0xFFFF 809 | self._write_snrx_rd(socket_num, pointer) 810 | self._write_sncr(socket_num, _CMD_SOCK_RECV) 811 | else: 812 | # no data on socket 813 | if self._read_snmr(socket_num) in { 814 | SNSR_SOCK_LISTEN, 815 | SNSR_SOCK_CLOSED, 816 | SNSR_SOCK_CLOSE_WAIT, 817 | }: 818 | raise RuntimeError("Lost connection to peer.") 819 | bytes_read = b"" 820 | return bytes_on_socket, bytes_read 821 | 822 | def read_udp(self, socket_num: int, length: int) -> Tuple[int, bytes]: 823 | """ 824 | Read UDP socket's current message bytes. 825 | 826 | :param int socket_num: The socket to read data from. 827 | :param int length: The number of bytes to read from the socket. 828 | 829 | :return Tuple[int, bytes]: If the read was successful then the first 830 | item of the tuple is the length of the data and the second is the data. 831 | If the read was unsuccessful then (0, b"") is returned. 832 | 833 | :raises ValueError: If the socket number is out of range. 834 | """ 835 | self._sock_num_in_range(socket_num) 836 | bytes_on_socket, bytes_read = 0, b"" 837 | # Parse the UDP packet header. 838 | header_length, self._pbuff[:8] = self.socket_read(socket_num, 8) 839 | if header_length: 840 | if header_length != 8: 841 | raise ValueError("Invalid UDP header.") 842 | data_length = self._chip_parse_udp_header(socket_num) 843 | # Read the UDP packet data. 844 | if data_length: 845 | if data_length <= length: 846 | bytes_on_socket, bytes_read = self.socket_read(socket_num, data_length) 847 | else: 848 | bytes_on_socket, bytes_read = self.socket_read(socket_num, length) 849 | # just consume the rest, it is lost to the higher layers 850 | self.socket_read(socket_num, data_length - length) 851 | return bytes_on_socket, bytes_read 852 | 853 | def socket_write(self, socket_num: int, buffer: bytearray, timeout: float = 0.0) -> int: 854 | """ 855 | Write data to a socket. 856 | 857 | :param int socket_num: The socket to write to. 858 | :param bytearray buffer: The data to write to the socket. 859 | :param float timeout: Write data timeout in seconds, defaults to 0.0 which waits 860 | indefinitely. 861 | 862 | :return int: The number of bytes written to the socket. 863 | 864 | :raises ConnectionError: If the Ethernet link is down. 865 | :raises ValueError: If the socket number is out of range. 866 | :raises RuntimeError: If the data cannot be sent. 867 | """ 868 | self._sock_num_in_range(socket_num) 869 | self._check_link_status() 870 | if len(buffer) > _SOCK_SIZE: 871 | bytes_to_write = _SOCK_SIZE 872 | else: 873 | bytes_to_write = len(buffer) 874 | start_time = ticks_ms() 875 | 876 | # If buffer is available, start the transfer 877 | free_size = self._get_tx_free_size(socket_num) 878 | while free_size < bytes_to_write: 879 | free_size = self._get_tx_free_size(socket_num) 880 | status = self.socket_status(socket_num) 881 | if status not in {SNSR_SOCK_ESTABLISHED, SNSR_SOCK_CLOSE_WAIT} or ( 882 | timeout and ticks_diff(ticks_ms(), start_time) / 1000 > timeout 883 | ): 884 | raise RuntimeError("Unable to write data to the socket.") 885 | 886 | # Read the starting address for saving the transmitting data. 887 | pointer = self._read_sntx_wr(socket_num) 888 | offset = pointer & _SOCK_MASK 889 | self._chip_socket_write(socket_num, offset, bytes_to_write, buffer) 890 | # update sn_tx_wr to the value + data size 891 | pointer = (pointer + bytes_to_write) & 0xFFFF 892 | self._write_sntx_wr(socket_num, pointer) 893 | self._write_sncr(socket_num, _CMD_SOCK_SEND) 894 | 895 | # check data was transferred correctly 896 | while not self.read_snir(socket_num) & _SNIR_SEND_OK: 897 | if self.socket_status(socket_num) in { 898 | SNSR_SOCK_CLOSED, 899 | SNSR_SOCK_TIME_WAIT, 900 | SNSR_SOCK_FIN_WAIT, 901 | SNSR_SOCK_CLOSE_WAIT, 902 | _SNSR_SOCK_CLOSING, 903 | }: 904 | raise RuntimeError("No data was sent, socket was closed.") 905 | if timeout and ticks_diff(ticks_ms(), start_time) / 1000 > timeout: 906 | raise RuntimeError("Operation timed out. No data sent.") 907 | if self.read_snir(socket_num) & SNIR_TIMEOUT: 908 | self.write_snir(socket_num, SNIR_TIMEOUT) 909 | # TCP sockets are closed by the hardware timeout 910 | # so that will be caught at the while statement. 911 | # UDP sockets are 1:many so not closed thus return 0. 912 | if self._read_snmr(socket_num) == SNMR_UDP: 913 | return 0 914 | time.sleep(0.001) 915 | self.write_snir(socket_num, _SNIR_SEND_OK) 916 | return bytes_to_write 917 | 918 | def sw_reset(self) -> None: 919 | """ 920 | Soft reset and reinitialize the WIZnet chip. 921 | 922 | :raises RuntimeError: If reset fails. 923 | """ 924 | self._wiznet_chip_init() 925 | 926 | def _sw_reset_5x00(self) -> bool: 927 | """ 928 | Perform a soft reset on the WIZnet 5100s and 5500 chips. 929 | 930 | :returns bool: True if reset was success 931 | """ 932 | self._write_mr(_MR_RST) 933 | time.sleep(0.05) 934 | return self._read_mr() == {"w5500": 0x00, "w5100s": 0x03}[self._chip_type] 935 | 936 | def _wiznet_chip_init(self) -> None: 937 | """ 938 | Detect and initialize a WIZnet 5k Ethernet module. 939 | 940 | :raises RuntimeError: If no WIZnet chip is detected. 941 | """ 942 | 943 | def _setup_sockets() -> None: 944 | """Initialise sockets for w5500 and w6100 chips.""" 945 | for sock_num in range(_MAX_SOCK_NUM[self._chip_type]): 946 | ctrl_byte = 0x0C + (sock_num << 5) 947 | self._write(0x1E, ctrl_byte, 2) 948 | self._write(0x1F, ctrl_byte, 2) 949 | self._ch_base_msb = 0x00 950 | WIZNET5K._sockets_reserved = [False] * (_MAX_SOCK_NUM[self._chip_type] - 1) 951 | self._src_ports_in_use = [0] * _MAX_SOCK_NUM[self._chip_type] 952 | 953 | def _detect_and_reset_w6100() -> bool: 954 | """ 955 | Detect and reset a W6100 chip. Called at startup to initialize the 956 | interface hardware. 957 | 958 | :return bool: True if a W6100 chip is detected, False if not. 959 | """ 960 | self._chip_type = "w6100" 961 | 962 | # Reset w6100 963 | self._write(0x41F4, 0x04, 0xCE) # Unlock chip settings. 964 | time.sleep(0.05) # Wait for unlock. 965 | self._write(0x2004, 0x04, 0x00) # Reset chip. 966 | time.sleep(0.05) # Wait for reset. 967 | 968 | if self._read(_REG_VERSIONR[self._chip_type], 0x00)[0] != 0x61: 969 | return False 970 | # Initialize w6100. 971 | self._write(0x41F5, 0x04, 0x3A) # Unlock network settings. 972 | _setup_sockets() 973 | return True 974 | 975 | def _detect_and_reset_w5500() -> bool: 976 | """ 977 | Detect and reset a W5500 chip. Called at startup to initialize the 978 | interface hardware. 979 | 980 | :return bool: True if a W5500 chip is detected, False if not. 981 | """ 982 | self._chip_type = "w5500" 983 | if not self._sw_reset_5x00(): 984 | return False 985 | 986 | self._write_mr(0x08) 987 | if self._read_mr() != 0x08: 988 | return False 989 | 990 | self._write_mr(0x10) 991 | if self._read_mr() != 0x10: 992 | return False 993 | 994 | self._write_mr(0x00) 995 | if self._read_mr() != 0x00: 996 | return False 997 | 998 | if self._read(_REG_VERSIONR[self._chip_type], 0x00)[0] != 0x04: 999 | return False 1000 | # Initialize w5500 1001 | _setup_sockets() 1002 | return True 1003 | 1004 | def _detect_and_reset_w5100s() -> bool: 1005 | """ 1006 | Detect and reset a W5100S chip. Called at startup to initialize the 1007 | interface hardware. 1008 | 1009 | :return bool: True if a W5100 chip is detected, False if not. 1010 | """ 1011 | self._chip_type = "w5100s" 1012 | if not self._sw_reset_5x00(): 1013 | return False 1014 | 1015 | if self._read(_REG_VERSIONR[self._chip_type], 0x00)[0] != 0x51: 1016 | return False 1017 | 1018 | # Initialise w5100s 1019 | self._ch_base_msb = 0x0400 1020 | WIZNET5K._sockets_reserved = [False] * (_MAX_SOCK_NUM[self._chip_type] - 1) 1021 | self._src_ports_in_use = [0] * _MAX_SOCK_NUM[self._chip_type] 1022 | return True 1023 | 1024 | for func in [ 1025 | _detect_and_reset_w5100s, 1026 | _detect_and_reset_w5500, 1027 | _detect_and_reset_w6100, 1028 | ]: 1029 | if func(): 1030 | return 1031 | self._chip_type = None 1032 | raise RuntimeError("Failed to initialize WIZnet module.") 1033 | 1034 | def _sock_num_in_range(self, sock: int) -> None: 1035 | """Check that the socket number is in the range 0 - maximum sockets.""" 1036 | if not 0 <= sock < self.max_sockets: 1037 | raise ValueError("Socket number out of range.") 1038 | 1039 | def _check_link_status(self): 1040 | """Raise an exception if the link is down.""" 1041 | if not self.link_status: 1042 | raise ConnectionError("The Ethernet connection is down.") 1043 | 1044 | @staticmethod 1045 | def _read_socket_reservations() -> list[int]: 1046 | """Return the list of reserved sockets.""" 1047 | return WIZNET5K._sockets_reserved 1048 | 1049 | def _read_mr(self) -> int: 1050 | """Read from the Mode Register (MR).""" 1051 | return int.from_bytes(self._read(_REG_MR[self._chip_type], 0x00), "big") 1052 | 1053 | def _write_mr(self, data: int) -> None: 1054 | """Write to the mode register (MR).""" 1055 | self._write(_REG_MR[self._chip_type], 0x04, data) 1056 | 1057 | # *** Low Level Methods *** 1058 | 1059 | def _read( 1060 | self, 1061 | addr: int, 1062 | callback: int, 1063 | length: int = 1, 1064 | ) -> bytes: 1065 | """ 1066 | Read data from a register address. 1067 | 1068 | :param int addr: Register address to read. 1069 | :param int callback: Callback reference. 1070 | :param int length: Number of bytes to read from the register, defaults to 1. 1071 | 1072 | :return bytes: Data read from the chip. 1073 | """ 1074 | with self._device as bus_device: 1075 | self._chip_read(bus_device, addr, callback) 1076 | self._rxbuf = bytearray(length) 1077 | bus_device.readinto(self._rxbuf) 1078 | return bytes(self._rxbuf) 1079 | 1080 | def _write(self, addr: int, callback: int, data: Union[int, bytes]) -> None: 1081 | """ 1082 | Write data to a register address. 1083 | 1084 | :param int addr: Destination address. 1085 | :param int callback: Callback reference. 1086 | :param Union[int, bytes] data: Data to write to the register address, if data 1087 | is an integer, it must be 1 or 2 bytes. 1088 | 1089 | :raises OverflowError: if integer data is more than 2 bytes. 1090 | """ 1091 | with self._device as bus_device: 1092 | self._chip_write(bus_device, addr, callback) 1093 | try: 1094 | data = data.to_bytes(1, "big") 1095 | except OverflowError: 1096 | data = data.to_bytes(2, "big") 1097 | except AttributeError: 1098 | pass 1099 | bus_device.write(data) 1100 | 1101 | def _read_two_byte_sock_reg(self, sock: int, reg_address: int) -> int: 1102 | """Read a two byte socket register.""" 1103 | register = self._read_socket_register(sock, reg_address) << 8 1104 | register += self._read_socket_register(sock, reg_address + 1) 1105 | return register 1106 | 1107 | def _write_two_byte_sock_reg(self, sock: int, reg_address: int, data: int) -> None: 1108 | """Write to a two byte socket register.""" 1109 | self._write_socket_register(sock, reg_address, data >> 8 & 0xFF) 1110 | self._write_socket_register(sock, reg_address + 1, data & 0xFF) 1111 | 1112 | # *** Socket Register Methods *** 1113 | 1114 | def _get_rx_rcv_size(self, sock: int) -> int: 1115 | """Size of received and saved in socket buffer.""" 1116 | val = 0 1117 | val_1 = self._read_snrx_rsr(sock) 1118 | while val != val_1: 1119 | val_1 = self._read_snrx_rsr(sock) 1120 | if val_1 != 0: 1121 | val = self._read_snrx_rsr(sock) 1122 | return val 1123 | 1124 | def _get_tx_free_size(self, sock: int) -> int: 1125 | """Free size of socket's tx buffer block.""" 1126 | val = 0 1127 | val_1 = self._read_sntx_fsr(sock) 1128 | while val != val_1: 1129 | val_1 = self._read_sntx_fsr(sock) 1130 | if val_1 != 0: 1131 | val = self._read_sntx_fsr(sock) 1132 | return val 1133 | 1134 | def _read_snrx_rd(self, sock: int) -> int: 1135 | """Read socket n RX Read Data Pointer Register.""" 1136 | return self._read_two_byte_sock_reg(sock, _REG_SNRX_RD[self._chip_type]) 1137 | 1138 | def _write_snrx_rd(self, sock: int, data: int) -> None: 1139 | """Write socket n RX Read Data Pointer Register.""" 1140 | self._write_two_byte_sock_reg(sock, _REG_SNRX_RD[self._chip_type], data) 1141 | 1142 | def _read_sntx_wr(self, sock: int) -> int: 1143 | """Read the socket write buffer pointer for socket `sock`.""" 1144 | return self._read_two_byte_sock_reg(sock, _REG_SNTX_WR[self._chip_type]) 1145 | 1146 | def _write_sntx_wr(self, sock: int, data: int) -> None: 1147 | """Write the socket write buffer pointer for socket `sock`.""" 1148 | self._write_two_byte_sock_reg(sock, _REG_SNTX_WR[self._chip_type], data) 1149 | 1150 | def _read_sntx_fsr(self, sock: int) -> int: 1151 | """Read socket n TX Free Size Register""" 1152 | return self._read_two_byte_sock_reg(sock, _REG_SNTX_FSR[self._chip_type]) 1153 | 1154 | def _read_snrx_rsr(self, sock: int) -> int: 1155 | """Read socket n Received Size Register""" 1156 | return self._read_two_byte_sock_reg(sock, _REG_SNRX_RSR[self._chip_type]) 1157 | 1158 | def _read_sndipr(self, sock) -> bytes: 1159 | """Read socket destination IP address.""" 1160 | data = [] 1161 | for offset in range(4): 1162 | data.append(self._read_socket_register(sock, _REG_SNDIPR[self._chip_type] + offset)) 1163 | return bytes(data) 1164 | 1165 | def _write_sndipr(self, sock: int, ip_addr: bytes) -> None: 1166 | """Write to socket destination IP Address.""" 1167 | for offset, value in enumerate(ip_addr): 1168 | self._write_socket_register(sock, _REG_SNDIPR[self._chip_type] + offset, value) 1169 | 1170 | def _read_sndport(self, sock: int) -> int: 1171 | """Read socket destination port.""" 1172 | return self._read_two_byte_sock_reg(sock, _REG_SNDPORT[self._chip_type]) 1173 | 1174 | def _write_sndport(self, sock: int, port: int) -> None: 1175 | """Write to socket destination port.""" 1176 | self._write_two_byte_sock_reg(sock, _REG_SNDPORT[self._chip_type], port) 1177 | 1178 | def _read_snsr(self, sock: int) -> int: 1179 | """Read Socket n Status Register.""" 1180 | return self._read_socket_register(sock, _REG_SNSR[self._chip_type]) 1181 | 1182 | def read_snir(self, sock: int) -> int: 1183 | """Read Socket n Interrupt Register.""" 1184 | return self._read_socket_register(sock, _REG_SNIR[self._chip_type]) 1185 | 1186 | def write_snir(self, sock: int, data: int) -> None: 1187 | """Write to Socket n Interrupt Register.""" 1188 | self._write_socket_register(sock, _REG_SNIR[self._chip_type], data) 1189 | 1190 | def _read_snmr(self, sock: int) -> int: 1191 | """Read the socket MR register.""" 1192 | return self._read_socket_register(sock, _REG_SNMR) 1193 | 1194 | def _write_snmr(self, sock: int, protocol: int) -> None: 1195 | """Write to Socket n Mode Register.""" 1196 | self._write_socket_register(sock, _REG_SNMR, protocol) 1197 | 1198 | def _write_sock_port(self, sock: int, port: int) -> None: 1199 | """Write to the socket port number.""" 1200 | self._write_two_byte_sock_reg(sock, _REG_SNPORT[self._chip_type], port) 1201 | 1202 | def _write_sncr(self, sock: int, data: int) -> None: 1203 | """Write to socket command register.""" 1204 | self._write_socket_register(sock, _REG_SNCR[self._chip_type], data) 1205 | # Wait for command to complete before continuing. 1206 | while self._read_socket_register(sock, _REG_SNCR[self._chip_type]): 1207 | pass 1208 | 1209 | @property 1210 | def rcr(self) -> int: 1211 | """Retry count register.""" 1212 | return int.from_bytes(self._read(_REG_RCR[self._chip_type], 0x00), "big") 1213 | 1214 | @rcr.setter 1215 | def rcr(self, retry_count: int) -> None: 1216 | """Retry count register.""" 1217 | if 0 > retry_count > 255: 1218 | raise ValueError("Retries must be from 0 to 255.") 1219 | self._write(_REG_RCR[self._chip_type], 0x04, retry_count) 1220 | 1221 | @property 1222 | def rtr(self) -> int: 1223 | """Retry time register.""" 1224 | return int.from_bytes(self._read(_REG_RTR[self._chip_type], 0x00, 2), "big") 1225 | 1226 | @rtr.setter 1227 | def rtr(self, retry_time: int) -> None: 1228 | """Retry time register.""" 1229 | if 0 > retry_time >= 2**16: 1230 | raise ValueError("Retry time must be from 0 to 65535") 1231 | self._write(_REG_RTR[self._chip_type], 0x04, retry_time) 1232 | 1233 | # *** Chip Specific Methods *** 1234 | 1235 | def _chip_read(self, device: busio.SPI, address: int, call_back: int) -> None: 1236 | """Chip specific calls for _read method.""" 1237 | if self._chip_type in {"w5500", "w6100"}: 1238 | device.write((address >> 8).to_bytes(1, "big")) 1239 | device.write((address & 0xFF).to_bytes(1, "big")) 1240 | device.write(call_back.to_bytes(1, "big")) 1241 | elif self._chip_type == "w5100s": 1242 | device.write((0x0F).to_bytes(1, "big")) 1243 | device.write((address >> 8).to_bytes(1, "big")) 1244 | device.write((address & 0xFF).to_bytes(1, "big")) 1245 | 1246 | def _chip_write(self, device: busio.SPI, address: int, call_back: int) -> None: 1247 | """Chip specific calls for _write.""" 1248 | if self._chip_type in {"w5500", "w6100"}: 1249 | device.write((address >> 8).to_bytes(1, "big")) 1250 | device.write((address & 0xFF).to_bytes(1, "big")) 1251 | device.write(call_back.to_bytes(1, "big")) 1252 | elif self._chip_type == "w5100s": 1253 | device.write((0xF0).to_bytes(1, "big")) 1254 | device.write((address >> 8).to_bytes(1, "big")) 1255 | device.write((address & 0xFF).to_bytes(1, "big")) 1256 | 1257 | def _chip_socket_read(self, socket_number, pointer, bytes_to_read): 1258 | """Chip specific calls for socket_read.""" 1259 | if self._chip_type in {"w5500", "w6100"}: 1260 | # Read data from the starting address of snrx_rd 1261 | ctrl_byte = 0x18 + (socket_number << 5) 1262 | bytes_read = self._read(pointer, ctrl_byte, bytes_to_read) 1263 | elif self._chip_type == "w5100s": 1264 | offset = pointer & _SOCK_MASK 1265 | src_addr = offset + (socket_number * _SOCK_SIZE + 0x6000) 1266 | if offset + bytes_to_read > _SOCK_SIZE: 1267 | split_point = _SOCK_SIZE - offset 1268 | bytes_read = self._read(src_addr, 0x00, split_point) 1269 | split_point = bytes_to_read - split_point 1270 | src_addr = socket_number * _SOCK_SIZE + 0x6000 1271 | bytes_read += self._read(src_addr, 0x00, split_point) 1272 | else: 1273 | bytes_read = self._read(src_addr, 0x00, bytes_to_read) 1274 | return bytes_read 1275 | 1276 | def _chip_socket_write( 1277 | self, socket_number: int, offset: int, bytes_to_write: int, buffer: bytes 1278 | ): 1279 | """Chip specific calls for socket_write.""" 1280 | if self._chip_type in {"w5500", "w6100"}: 1281 | dst_addr = offset + (socket_number * _SOCK_SIZE + 0x8000) 1282 | cntl_byte = 0x14 + (socket_number << 5) 1283 | self._write(dst_addr, cntl_byte, buffer[:bytes_to_write]) 1284 | 1285 | elif self._chip_type == "w5100s": 1286 | dst_addr = offset + (socket_number * _SOCK_SIZE + 0x4000) 1287 | 1288 | if offset + bytes_to_write > _SOCK_SIZE: 1289 | split_point = _SOCK_SIZE - offset 1290 | self._write(dst_addr, 0x00, buffer[:split_point]) 1291 | dst_addr = socket_number * _SOCK_SIZE + 0x4000 1292 | self._write(dst_addr, 0x00, buffer[split_point:bytes_to_write]) 1293 | else: 1294 | self._write(dst_addr, 0x00, buffer[:bytes_to_write]) 1295 | 1296 | def _chip_parse_udp_header(self, socket_num) -> int: 1297 | """ 1298 | Parse chip specific UDP header data for IPv4 packets. 1299 | 1300 | Sets the source IPv4 address and port number and returns the UDP data length. 1301 | 1302 | :return int: The UDP data length. 1303 | """ 1304 | if self._chip_type in {"w5100s", "w5500"}: 1305 | self.udp_from_ip[socket_num] = self._pbuff[:4] 1306 | self.udp_from_port[socket_num] = int.from_bytes(self._pbuff[4:6], "big") 1307 | return int.from_bytes(self._pbuff[6:], "big") 1308 | if self._chip_type == "w6100": 1309 | self.udp_from_ip[socket_num] = self._pbuff[3:7] 1310 | self.udp_from_port[socket_num] = int.from_bytes(self._pbuff[6:], "big") 1311 | return int.from_bytes(self._pbuff[:2], "big") & 0x07FF 1312 | raise ValueError("Unsupported chip type.") 1313 | 1314 | def _write_socket_register(self, sock: int, address: int, data: int) -> None: 1315 | """Write to a WIZnet 5k socket register.""" 1316 | if self._chip_type in {"w5500", "w6100"}: 1317 | cntl_byte = (sock << 5) + 0x0C 1318 | self._write(address, cntl_byte, data) 1319 | elif self._chip_type == "w5100s": 1320 | cntl_byte = 0 1321 | self._write(self._ch_base_msb + sock * _CH_SIZE + address, cntl_byte, data) 1322 | 1323 | def _read_socket_register(self, sock: int, address: int) -> int: 1324 | """Read a WIZnet 5k socket register.""" 1325 | if self._chip_type in {"w5500", "w6100"}: 1326 | cntl_byte = (sock << 5) + 0x08 1327 | register = self._read(address, cntl_byte) 1328 | elif self._chip_type == "w5100s": 1329 | cntl_byte = 0 1330 | register = self._read(self._ch_base_msb + sock * _CH_SIZE + address, cntl_byte) 1331 | return int.from_bytes(register, "big") 1332 | -------------------------------------------------------------------------------- /adafruit_wiznet5k/adafruit_wiznet5k_debug.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Martin Stephens 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | """Makes a debug message function available to all modules.""" 6 | 7 | try: 8 | from typing import TYPE_CHECKING, Union 9 | 10 | if TYPE_CHECKING: 11 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 12 | except ImportError: 13 | pass 14 | 15 | import gc 16 | 17 | 18 | def debug_msg(message: Union[Exception, str, bytes, bytearray], debugging: bool) -> None: 19 | """ 20 | Helper function to print debugging messages. If the message is a bytes type 21 | object, create a hexdump. 22 | 23 | :param Union[Exception, str, bytes, bytearray] message: The message to print. 24 | :param bool debugging: Only print if debugging is True. 25 | """ 26 | if debugging: 27 | if isinstance(message, (bytes, bytearray)): 28 | message = _hexdump(message) 29 | print(message) 30 | del message 31 | gc.collect() 32 | 33 | 34 | def _hexdump(src: bytes): 35 | """ 36 | Create a 16 column hexdump of a bytes object. 37 | 38 | :param bytes src: The bytes object to hexdump. 39 | 40 | :returns str: The hexdump. 41 | """ 42 | result = [] 43 | for i in range(0, len(src), 16): 44 | chunk = src[i : i + 16] 45 | hexa = " ".join(f"{x:02x}" for x in chunk) 46 | text = "".join(chr(x) if 0x20 <= x < 0x7F else "." for x in chunk) 47 | result.append(f"{i:04x} {hexa:<48} {text}") 48 | return "\n".join(result) 49 | -------------------------------------------------------------------------------- /adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2009 Jordan Terrell (blog.jordanterrell.com) 2 | # SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries 3 | # SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck @ Silicognition LLC 4 | # SPDX-FileCopyrightText: 2022 Martin Stephens 5 | # 6 | # SPDX-License-Identifier: MIT 7 | 8 | """ 9 | `adafruit_wiznet5k_dhcp` 10 | ================================================================================ 11 | 12 | Pure-Python implementation of Jordan Terrell's DHCP library v0.3 13 | 14 | * Author(s): Jordan Terrell, Brent Rubell, Martin Stephens 15 | 16 | """ 17 | 18 | from __future__ import annotations 19 | 20 | try: 21 | from typing import TYPE_CHECKING, Optional, Tuple, Union 22 | 23 | if TYPE_CHECKING: 24 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 25 | except ImportError: 26 | pass 27 | 28 | 29 | import gc 30 | from random import randint 31 | 32 | from adafruit_ticks import ticks_add, ticks_diff, ticks_less, ticks_ms 33 | from micropython import const 34 | 35 | from adafruit_wiznet5k.adafruit_wiznet5k_debug import debug_msg 36 | 37 | # DHCP State Machine 38 | _STATE_INIT = const(0x01) 39 | _STATE_SELECTING = const(0x02) 40 | _STATE_REQUESTING = const(0x03) 41 | _STATE_BOUND = const(0x04) 42 | _STATE_RENEWING = const(0x05) 43 | _STATE_REBINDING = const(0x06) 44 | 45 | # DHCP Message Types 46 | _DHCP_DISCOVER = const(1) 47 | _DHCP_OFFER = const(2) 48 | _DHCP_REQUEST = const(3) 49 | _DHCP_DECLINE = const(4) 50 | _DHCP_ACK = const(5) 51 | _DHCP_NAK = const(6) 52 | _DHCP_RELEASE = const(7) 53 | _DHCP_INFORM = const(8) 54 | 55 | # DHCP Message OP Codes 56 | _DHCP_BOOT_REQUEST = const(0x01) 57 | _DHCP_BOOT_REPLY = const(0x02) 58 | 59 | _DHCP_HTYPE10MB = const(0x01) 60 | _DHCP_HTYPE100MB = const(0x02) 61 | 62 | _DHCP_HLENETHERNET = const(0x06) 63 | _DHCP_HOPS = const(0x00) 64 | 65 | _MAGIC_COOKIE = b"c\x82Sc" # Four bytes 99.130.83.99 66 | _MAX_DHCP_OPT = const(0x10) 67 | 68 | _SNMR_UDP = const(0x02) 69 | 70 | # Default DHCP Server port 71 | _DHCP_SERVER_PORT = const(67) 72 | # DHCP Lease Time, in seconds 73 | _BROADCAST_SERVER_ADDR = b"\xff\xff\xff\xff" # (255.255.255.255) 74 | _UNASSIGNED_IP_ADDR = b"\x00\x00\x00\x00" # (0.0.0.0) 75 | 76 | # DHCP Response Options 77 | _MSG_TYPE = const(53) 78 | _SUBNET_MASK = const(1) 79 | _ROUTERS_ON_SUBNET = const(3) 80 | _DNS_SERVERS = const(6) 81 | _DHCP_SERVER_ID = const(54) 82 | _T1_VAL = const(58) 83 | _T2_VAL = const(59) 84 | _LEASE_TIME = const(51) 85 | _OPT_END = const(255) 86 | 87 | # Packet buffer 88 | _BUFF_LENGTH = 512 89 | _BUFF = bytearray(_BUFF_LENGTH) 90 | 91 | 92 | class DHCP: 93 | """Wiznet5k DHCP Client. 94 | 95 | Implements a DHCP client using a finite state machine (FSM). This allows the DHCP client 96 | to run in a non-blocking mode suitable for CircuitPython. 97 | 98 | The DHCP client obtains a lease and maintains it. The process of obtaining the initial 99 | lease is run in a blocking mode, as several messages must be exchanged with the DHCP 100 | server. Once the lease has been allocated, lease maintenance can be performed in 101 | non-blocking mode as nothing needs to be done until it is time to reallocate the 102 | lease. Renewing or rebinding is a simpler process which may be repeated periodically 103 | until successful. If the lease expires, the client attempts to obtain a new lease in 104 | blocking mode when the maintenance routine is run. 105 | 106 | These class methods are not designed to be called directly. They should be called via 107 | methods in the WIZNET5K class. 108 | 109 | Since DHCP uses UDP, messages may be lost. The DHCP protocol uses exponential backoff 110 | for retrying. Retries occur after 4, 8, and 16 +/- 1 seconds (the final retry is followed 111 | by a wait of 32 seconds) so it takes about a minute to decide that no DHCP server 112 | is available. 113 | 114 | The DHCP client cannot check whether the allocated IP address is already in use because 115 | the ARP protocol is not available. Therefore, it is possible that the IP address 116 | allocated by the server has been manually assigned to another device. In most cases, 117 | the DHCP server will make this check before allocating an address, but some do not. 118 | 119 | The DHCPRELEASE message is not implemented. The DHCP protocol does not require it and 120 | DHCP servers can handle disappearing clients and clients that ask for 'replacement' 121 | IP addresses. 122 | """ 123 | 124 | def __init__( 125 | self, 126 | eth: WIZNET5K, 127 | mac_address: bytes, 128 | hostname: Optional[str] = None, 129 | debug: bool = False, 130 | ) -> None: 131 | """ 132 | :param adafruit_wiznet5k.WIZNET5K eth: Wiznet 5k object 133 | :param bytes mac_address: Hardware MAC address. 134 | :param Optional[str] hostname: The desired hostname, with optional {} to fill 135 | in the MAC address, defaults to None. 136 | :param bool debug: Enable debugging output. 137 | """ 138 | self._debug = debug 139 | debug_msg("Initialising DHCP client instance.", self._debug) 140 | 141 | if not isinstance(mac_address, bytes): 142 | raise TypeError("MAC address must be a bytes object.") 143 | # Prevent buffer overrun in send_dhcp_message() 144 | if len(mac_address) != 6: 145 | raise ValueError("MAC address must be 6 bytes.") 146 | self._mac_address = mac_address 147 | 148 | # Set socket interface 149 | self._eth = eth 150 | 151 | # DHCP state machine 152 | self._dhcp_state = _STATE_INIT 153 | self._transaction_id = randint(1, 0x7FFFFFFF) 154 | self._start_ticks = 0 155 | self._blocking = False 156 | self._renew = None 157 | 158 | # DHCP binding configuration 159 | self.dhcp_server_ip = _BROADCAST_SERVER_ADDR 160 | self.local_ip = _UNASSIGNED_IP_ADDR 161 | self.gateway_ip = _UNASSIGNED_IP_ADDR 162 | self.subnet_mask = _UNASSIGNED_IP_ADDR 163 | self.dns_server_ip = _UNASSIGNED_IP_ADDR 164 | 165 | # Lease expiry times 166 | self._t1_timeout = 0 167 | self._t2_timeout = 0 168 | self._lease_timeout = 0 169 | self._t1 = None 170 | self._t2 = None 171 | self._lease = None 172 | 173 | # Host name 174 | mac_string = "".join(f"{o:02X}" for o in mac_address) 175 | self._hostname = bytes( 176 | (hostname or "WIZnet{}").split(".")[0].format(mac_string)[:42], "utf-8" 177 | ) 178 | 179 | def request_dhcp_lease(self) -> bool: 180 | """ 181 | Request acquire a DHCP lease. 182 | 183 | :returns bool: A lease has been acquired. 184 | """ 185 | debug_msg("Requesting DHCP lease.", self._debug) 186 | self._dhcp_state_machine(blocking=True) 187 | return self._dhcp_state == _STATE_BOUND 188 | 189 | def maintain_dhcp_lease(self, blocking: bool = False) -> None: 190 | """ 191 | Maintain a DHCP lease. 192 | :param bool blocking: Run the DHCP FSM in non-blocking mode. 193 | """ 194 | debug_msg(f"Maintaining lease with blocking = {blocking}", self._debug) 195 | self._dhcp_state_machine(blocking=blocking) 196 | 197 | def _dsm_reset(self) -> None: 198 | """Close the socket and set attributes to default values used by the 199 | state machine INIT state.""" 200 | debug_msg("Resetting DHCP state machine.", self._debug) 201 | self.dhcp_server_ip = _BROADCAST_SERVER_ADDR 202 | self._eth.ifconfig = ( 203 | _UNASSIGNED_IP_ADDR, 204 | _UNASSIGNED_IP_ADDR, 205 | _UNASSIGNED_IP_ADDR, 206 | _UNASSIGNED_IP_ADDR, 207 | ) 208 | self.gateway_ip = _UNASSIGNED_IP_ADDR 209 | self.local_ip = _UNASSIGNED_IP_ADDR 210 | self.subnet_mask = _UNASSIGNED_IP_ADDR 211 | self.dns_server_ip = _UNASSIGNED_IP_ADDR 212 | self._renew = None 213 | self._increment_transaction_id() 214 | self._start_ticks = ticks_ms() 215 | self._lease = None 216 | self._t1 = None 217 | self._t2 = None 218 | 219 | def _increment_transaction_id(self) -> None: 220 | """Increment the transaction ID and roll over from 0x7fffffff to 0.""" 221 | debug_msg("Incrementing transaction ID", self._debug) 222 | self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF 223 | 224 | def _next_retry_time(self, *, attempt: int, interval: int = 4) -> int: 225 | """Calculate a retry stop time. 226 | 227 | The interval is calculated as an exponential fallback with a random variation to 228 | prevent DHCP packet collisions. This timeout is intended to be compared with 229 | time.monotonic(). 230 | 231 | :param int attempt: The current attempt, used as the exponent for calculating the 232 | timeout. 233 | :param int interval: The base retry interval in seconds. Defaults to 4 as per the 234 | DHCP standard for Ethernet connections. Minimum value 2, defaults to 4. 235 | 236 | :returns int: The timeout in ticks_ms milliseconds. 237 | 238 | :raises ValueError: If the interval is not > 1 second as this could return a zero or 239 | negative delay. 240 | """ 241 | debug_msg("Calculating next retry time and incrementing retries.", self._debug) 242 | if interval <= 1: 243 | raise ValueError("Retry interval must be > 1 second.") 244 | delay = (2**attempt * interval + randint(-1, 1)) * 1000 245 | return delay 246 | 247 | def _receive_dhcp_response(self, socket_num: int, timeout: int) -> int: 248 | """ 249 | Receive data from the socket in response to a DHCP query. 250 | 251 | Reads data from the buffer until a viable minimum packet size has been 252 | received or the operation times out. If a viable packet is received, it is 253 | stored in the global buffer and the number of bytes received is returned. 254 | If the packet is too short, it is discarded and zero is returned. The 255 | maximum packet size is limited by the size of the global buffer. 256 | 257 | :param int socket_num: Socket to read from. 258 | :param int timeout: ticks_ms interval at which attempt should time out. 259 | 260 | :returns int: The number of bytes stored in the global buffer. 261 | """ 262 | debug_msg("Receiving a DHCP response.", self._debug) 263 | start_time = ticks_ms() 264 | while ticks_diff(ticks_ms(), start_time) < timeout: 265 | # DHCP returns the query plus additional data. The query length is 236 bytes. 266 | if self._eth.socket_available(socket_num, _SNMR_UDP) > 236: 267 | bytes_count, bytes_read = self._eth.read_udp(socket_num, _BUFF_LENGTH) 268 | _BUFF[:bytes_count] = bytes_read 269 | debug_msg(f"Received {bytes_count} bytes", self._debug) 270 | del bytes_read 271 | gc.collect() 272 | return bytes_count 273 | return 0 # No bytes received. 274 | 275 | def _process_messaging_states(self, *, message_type: int): 276 | """ 277 | Process a message while the FSM is in SELECTING or REQUESTING state. 278 | 279 | Check the message and update the FSM state if it is a valid type. 280 | 281 | :param int message_type: The type of message received from the DHCP server. 282 | 283 | :returns bool: True if the message was valid for the current state. 284 | """ 285 | if self._dhcp_state == _STATE_SELECTING and message_type == _DHCP_OFFER: 286 | debug_msg("FSM state is SELECTING with valid OFFER.", self._debug) 287 | self._dhcp_state = _STATE_REQUESTING 288 | elif self._dhcp_state == _STATE_REQUESTING: 289 | debug_msg("FSM state is REQUESTING.", self._debug) 290 | if message_type == _DHCP_NAK: 291 | debug_msg("Message is NAK, setting FSM state to INIT.", self._debug) 292 | self._dhcp_state = _STATE_INIT 293 | elif message_type == _DHCP_ACK: 294 | debug_msg("Message is ACK, setting FSM state to BOUND.", self._debug) 295 | lease = self._lease or 60 296 | self._lease_timeout = ticks_add(self._start_ticks, lease * 1000) 297 | self._t1_timeout = ticks_add(self._start_ticks, (self._t1 or (lease // 2)) * 1000) 298 | self._t2_timeout = ticks_add( 299 | self._start_ticks, (self._t2 or (lease - lease // 8)) * 1000 300 | ) 301 | self._increment_transaction_id() 302 | if not self._renew: 303 | self._eth.ifconfig = ( 304 | self.local_ip, 305 | self.subnet_mask, 306 | self.gateway_ip, 307 | self.dns_server_ip, 308 | ) 309 | self._renew = None 310 | self._dhcp_state = _STATE_BOUND 311 | 312 | def _handle_dhcp_message(self) -> int: 313 | """Send, receive and process DHCP message. Update the finite state machine (FSM). 314 | 315 | Send a message and wait for a response from the DHCP server, resending on an 316 | exponential fallback schedule matching the DHCP standard if no response is received. 317 | Only called when the FSM is in SELECTING or REQUESTING states. 318 | 319 | :returns int: The DHCP message type, or 0 if no message received in non-blocking 320 | or renewing states. 321 | 322 | :raises ValueError: If the function is not called from SELECTING or BLOCKING FSM 323 | states. 324 | :raises TimeoutError: If the FSM is in blocking mode and no valid response has 325 | been received before the timeout expires. 326 | """ 327 | debug_msg("Processing SELECTING or REQUESTING state.", self._debug) 328 | if self._dhcp_state == _STATE_SELECTING: 329 | msg_type_out = _DHCP_DISCOVER 330 | elif self._dhcp_state == _STATE_REQUESTING: 331 | msg_type_out = _DHCP_REQUEST 332 | else: 333 | raise ValueError("FSM can only send messages while in SELECTING or REQUESTING states.") 334 | debug_msg("Setting up connection for DHCP.", self._debug) 335 | if self._renew: 336 | dhcp_server = self.dhcp_server_ip 337 | else: 338 | dhcp_server = _BROADCAST_SERVER_ADDR 339 | sock_num = None 340 | deadline = 5000 341 | start_time = ticks_ms() 342 | try: 343 | while sock_num is None: 344 | sock_num = self._eth.get_socket() 345 | if sock_num == 0xFF: 346 | sock_num = None 347 | if ticks_diff(ticks_ms(), start_time) > deadline: 348 | raise RuntimeError("Unable to initialize UDP socket.") 349 | 350 | self._eth.src_port = 68 351 | self._eth.socket_connect(sock_num, dhcp_server, _DHCP_SERVER_PORT, conn_mode=0x02) 352 | self._eth.src_port = 0 353 | 354 | message_length = self._generate_dhcp_message(message_type=msg_type_out) 355 | for attempt in range(4): # Initial attempt plus 3 retries. 356 | self._eth.socket_write(sock_num, _BUFF[:message_length]) 357 | next_resend = self._next_retry_time(attempt=attempt) 358 | start_time = ticks_ms() 359 | while ticks_diff(ticks_ms(), start_time) < next_resend: 360 | if self._receive_dhcp_response(sock_num, next_resend): 361 | try: 362 | msg_type_in = self._parse_dhcp_response() 363 | debug_msg( 364 | f"Received message type {msg_type_in}", 365 | self._debug, 366 | ) 367 | return msg_type_in 368 | except ValueError as error: 369 | debug_msg(error, self._debug) 370 | if not self._blocking or self._renew: 371 | debug_msg( 372 | "No message, FSM is nonblocking or renewing, exiting loop.", 373 | self._debug, 374 | ) 375 | return 0 # Did not receive a response in a single attempt. 376 | raise TimeoutError(f"No response from DHCP server after {attempt} retries.") 377 | finally: 378 | self._eth.socket_close(sock_num) # Close the socket whatever happens. 379 | 380 | def _dhcp_state_machine(self, *, blocking: bool = False) -> None: 381 | """ 382 | A finite state machine to allow the DHCP lease to be managed without blocking 383 | the main program. The initial lease... 384 | """ 385 | debug_msg(f"DHCP FSM called with blocking = {blocking}", self._debug) 386 | debug_msg(f"FSM initial state is {self._dhcp_state}", self._debug) 387 | self._blocking = blocking 388 | while True: 389 | if self._dhcp_state == _STATE_BOUND: 390 | now = ticks_ms() 391 | if ticks_less(now, self._t1_timeout): 392 | debug_msg("No timers have expired. Exiting FSM.", self._debug) 393 | return 394 | if ticks_less(self._lease_timeout, now): 395 | debug_msg("Lease has expired, switching state to INIT.", self._debug) 396 | self._blocking = True 397 | self._dhcp_state = _STATE_INIT 398 | elif ticks_less(self._t2_timeout, now): 399 | debug_msg("T2 has expired, switching state to REBINDING.", self._debug) 400 | self._dhcp_state = _STATE_REBINDING 401 | else: 402 | debug_msg("T1 has expired, switching state to RENEWING.", self._debug) 403 | self._dhcp_state = _STATE_RENEWING 404 | 405 | if self._dhcp_state == _STATE_RENEWING: 406 | debug_msg("FSM state is RENEWING.", self._debug) 407 | self._renew = "renew" 408 | self._start_ticks = ticks_ms() 409 | self._lease = None 410 | self._t1 = None 411 | self._t2 = None 412 | self._dhcp_state = _STATE_REQUESTING 413 | 414 | if self._dhcp_state == _STATE_REBINDING: 415 | debug_msg("FSM state is REBINDING.", self._debug) 416 | self._renew = "rebind" 417 | self.dhcp_server_ip = _BROADCAST_SERVER_ADDR 418 | self._start_ticks = ticks_ms() 419 | self._lease = None 420 | self._t1 = None 421 | self._t2 = None 422 | self._dhcp_state = _STATE_REQUESTING 423 | 424 | if self._dhcp_state == _STATE_INIT: 425 | debug_msg("FSM state is INIT.", self._debug) 426 | self._dsm_reset() 427 | self._dhcp_state = _STATE_SELECTING 428 | 429 | if self._dhcp_state == _STATE_SELECTING: 430 | debug_msg("FSM state is SELECTING.", self._debug) 431 | self._process_messaging_states(message_type=self._handle_dhcp_message()) 432 | 433 | if self._dhcp_state == _STATE_REQUESTING: 434 | debug_msg("FSM state is REQUESTING.", self._debug) 435 | self._process_messaging_states(message_type=self._handle_dhcp_message()) 436 | 437 | if self._renew: 438 | debug_msg( 439 | "Lease has not expired, resetting state to BOUND and exiting FSM.", 440 | self._debug, 441 | ) 442 | self._dhcp_state = _STATE_BOUND 443 | return 444 | gc.collect() 445 | 446 | def _generate_dhcp_message( 447 | self, 448 | *, 449 | message_type: int, 450 | broadcast: bool = False, 451 | ) -> int: 452 | """ 453 | Assemble a DHCP message. The content will vary depending on which type of 454 | message is being sent and whether the lease is new or being renewed. 455 | 456 | :param int message_type: Type of message to generate. 457 | :param bool broadcast: Used to set the flag requiring a broadcast reply from the 458 | DHCP server. Defaults to False which matches the DHCP standard. 459 | 460 | :returns int: The length of the DHCP message. 461 | """ 462 | 463 | def option_writer( 464 | offset: int, option_code: int, option_data: Union[Tuple[int, ...], bytes] 465 | ) -> int: 466 | """Helper function to set DHCP option data for a DHCP 467 | message. 468 | 469 | :param int offset: Pointer to start of a DHCP option. 470 | :param int option_code: Type of option to add. 471 | :param Tuple[int] option_data: The data for the option. 472 | 473 | :returns int: Pointer to start of next option. 474 | """ 475 | _BUFF[offset] = option_code 476 | data_length = len(option_data) 477 | offset += 1 478 | _BUFF[offset] = data_length 479 | offset += 1 480 | data_end = offset + data_length 481 | _BUFF[offset:data_end] = bytes(option_data) 482 | return data_end 483 | 484 | debug_msg(f"Generating DHCP message type {message_type}", self._debug) 485 | _BUFF[:] = bytearray(_BUFF_LENGTH) 486 | # OP.HTYPE.HLEN.HOPS 487 | _BUFF[0:4] = bytes([_DHCP_BOOT_REQUEST, _DHCP_HTYPE10MB, _DHCP_HLENETHERNET, _DHCP_HOPS]) 488 | # Transaction ID (xid) 489 | _BUFF[4:8] = self._transaction_id.to_bytes(4, "big") 490 | # Seconds elapsed 491 | _BUFF[8:10] = int(ticks_diff(ticks_ms(), self._start_ticks) / 1000).to_bytes(2, "big") 492 | # Flags (only bit 0 is used, all other bits must be 0) 493 | if broadcast: 494 | _BUFF[10] = 0b10000000 495 | else: 496 | _BUFF[10] = 0b00000000 497 | if self._renew: 498 | _BUFF[12:16] = bytes(self.local_ip) 499 | # chaddr 500 | _BUFF[28:34] = self._mac_address 501 | # Magic Cookie 502 | _BUFF[236:240] = _MAGIC_COOKIE 503 | 504 | # Set DHCP options. 505 | pointer = 240 506 | 507 | # Option - DHCP Message Type 508 | pointer = option_writer(offset=pointer, option_code=53, option_data=(message_type,)) 509 | # Option - Host Name 510 | pointer = option_writer(offset=pointer, option_code=12, option_data=self._hostname) 511 | 512 | # Option - Client ID 513 | pointer = option_writer( 514 | offset=pointer, 515 | option_code=61, 516 | option_data=b"\x01" + self._mac_address, 517 | ) 518 | 519 | # Request subnet mask, router and DNS server. 520 | pointer = option_writer(offset=pointer, option_code=55, option_data=(1, 3, 6)) 521 | 522 | # Request a 90 day lease. 523 | pointer = option_writer(offset=pointer, option_code=51, option_data=b"\x00\x76\xa7\x00") 524 | 525 | if message_type == _DHCP_REQUEST: 526 | # Set Requested IP Address to offered IP address. 527 | pointer = option_writer(offset=pointer, option_code=50, option_data=self.local_ip) 528 | # Set Server ID to chosen DHCP server IP address. 529 | if self._renew != "rebind": 530 | pointer = option_writer( 531 | offset=pointer, option_code=54, option_data=self.dhcp_server_ip 532 | ) 533 | 534 | _BUFF[pointer] = 0xFF 535 | pointer += 1 536 | if pointer > _BUFF_LENGTH: 537 | raise ValueError("DHCP message too long.") 538 | debug_msg(_BUFF[:pointer], self._debug) 539 | return pointer 540 | 541 | def _parse_dhcp_response( 542 | self, 543 | ) -> int: 544 | """Parse DHCP response from DHCP server. 545 | 546 | Check that the message is for this client. Extract data from the fixed positions 547 | in the first 236 bytes of the message, then cycle through the options for 548 | additional data. 549 | 550 | :returns Tuple[int, bytearray]: DHCP packet type and ID. 551 | 552 | :raises ValueError: Checks that the message is a reply, the transaction ID 553 | matches, a client ID exists and the 'magic cookie' is set. If any of these tests 554 | fail or no message type is found in the options, raises a ValueError. 555 | """ 556 | 557 | def option_reader(pointer: int) -> Tuple[int, int, bytes]: 558 | """Helper function to extract DHCP option data from a 559 | response. 560 | 561 | :param int pointer: Pointer to start of a DHCP option. 562 | 563 | :returns Tuple[int, int, bytes]: Pointer to next option, 564 | option type, and option data. 565 | """ 566 | # debug_msg("initial pointer = {}".format(pointer), self._debug) 567 | option_type = _BUFF[pointer] 568 | # debug_msg("option type = {}".format(option_type), self._debug) 569 | pointer += 1 570 | data_length = _BUFF[pointer] 571 | # debug_msg("data length = {}".format(data_length), self._debug) 572 | pointer += 1 573 | data_end = pointer + data_length 574 | # debug_msg("data end = {}".format(data_end), self._debug) 575 | option_data = bytes(_BUFF[pointer:data_end]) 576 | # debug_msg(option_data, self._debug) 577 | # debug_msg("Final pointer = {}".format(pointer), self._debug) 578 | return data_end, option_type, option_data 579 | 580 | debug_msg("Parsing DHCP message.", self._debug) 581 | # Validate OP 582 | if _BUFF[0] != _DHCP_BOOT_REPLY: 583 | raise ValueError("DHCP message is not the expected DHCP Reply.") 584 | # Confirm transaction IDs match. 585 | xid = _BUFF[4:8] 586 | if xid != self._transaction_id.to_bytes(4, "big"): 587 | raise ValueError("DHCP response ID mismatch.") 588 | # Check that there is a client ID. 589 | if _BUFF[28:34] == b"\x00\x00\x00\x00\x00\x00": 590 | raise ValueError("No client hardware MAC address in the response.") 591 | # Check for the magic cookie. 592 | if _BUFF[236:240] != _MAGIC_COOKIE: 593 | raise ValueError("No DHCP Magic Cookie in the response.") 594 | # Set the IP address to Claddr 595 | self.local_ip = bytes(_BUFF[16:20]) 596 | 597 | # Parse options 598 | msg_type = None 599 | ptr = 240 600 | while _BUFF[ptr] != _OPT_END: 601 | ptr, data_type, data = option_reader(ptr) 602 | if data_type == _MSG_TYPE: 603 | msg_type = data[0] 604 | elif data_type == _SUBNET_MASK: 605 | self.subnet_mask = data 606 | elif data_type == _DHCP_SERVER_ID: 607 | self.dhcp_server_ip = data 608 | elif data_type == _LEASE_TIME: 609 | self._lease = int.from_bytes(data, "big") 610 | elif data_type == _ROUTERS_ON_SUBNET: 611 | self.gateway_ip = data[:4] 612 | elif data_type == _DNS_SERVERS: 613 | self.dns_server_ip = data[:4] 614 | elif data_type == _T1_VAL: 615 | self._t1 = int.from_bytes(data, "big") 616 | elif data_type == _T2_VAL: 617 | self._t2 = int.from_bytes(data, "big") 618 | elif data_type == 0: 619 | break 620 | 621 | debug_msg( 622 | f"Msg Type: {msg_type}\nSubnet Mask: {self.subnet_mask}\nDHCP Server IP: {self.dhcp_server_ip}\nDNS Server IP: {self.dns_server_ip}\ 623 | \nGateway IP: {self.gateway_ip}\nLocal IP: {self.local_ip}\nT1: {self._t1}\nT2: {self._t2}\nLease Time: {self._lease}", 624 | self._debug, 625 | ) 626 | if msg_type is None: 627 | raise ValueError("No valid message type in response.") 628 | return msg_type 629 | -------------------------------------------------------------------------------- /adafruit_wiznet5k/adafruit_wiznet5k_dns.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2009-2010 MCQN Ltd 2 | # SPDX-FileCopyrightText: Brent Rubell for Adafruit Industries 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | """ 7 | `adafruit_wiznet5k_dns` 8 | ================================================================================ 9 | 10 | Pure-Python implementation of the Arduino DNS client for WIZnet 5k-based 11 | ethernet modules. 12 | 13 | * Author(s): MCQN Ltd, Brent Rubell, Martin Stephens 14 | 15 | """ 16 | 17 | from __future__ import annotations 18 | 19 | try: 20 | from typing import TYPE_CHECKING, Tuple, Union 21 | 22 | if TYPE_CHECKING: 23 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 24 | except ImportError: 25 | pass 26 | 27 | import time 28 | from random import getrandbits 29 | 30 | from adafruit_ticks import ticks_diff, ticks_ms 31 | from micropython import const 32 | 33 | _QUERY_FLAG = const(0x00) 34 | _OPCODE_STANDARD_QUERY = const(0x00) 35 | _RECURSION_DESIRED_FLAG = 1 << 8 36 | 37 | _TYPE_A = const(0x0001) 38 | _CLASS_IN = const(0x0001) 39 | _DATA_LEN = const(0x0004) 40 | 41 | # Return codes for gethostbyname 42 | _SUCCESS = const(1) 43 | _TIMED_OUT = const(-1) 44 | _INVALID_SERVER = const(-2) 45 | _TRUNCATED = const(-3) 46 | _INVALID_RESPONSE = const(-4) 47 | 48 | _DNS_PORT = const(0x35) # port used for DNS request 49 | 50 | 51 | def _debug_print(*, debug: bool, message: str) -> None: 52 | """Helper function to improve code readability.""" 53 | if debug: 54 | print(message) 55 | 56 | 57 | def _build_dns_query(domain: bytes) -> Tuple[int, int, bytearray]: 58 | """Builds DNS header.""" 59 | # generate a random, 16-bit, request identifier 60 | query_id = getrandbits(16) 61 | # Hard code everything except the ID, it never changes in this implementation. 62 | query = bytearray( 63 | [ 64 | query_id >> 8, # Query MSB. 65 | query_id & 0xFF, # Query LSB. 66 | 0x01, # Flags MSB: QR=0, 4 bit Opcode=0, AA=0, TC=0, RD=1 (recursion is desired). 67 | 0x00, # Flags LSB: RA=0, 3 bit Z=0, 4 bit RCode=0. 68 | 0x00, # QDcount MSB: 69 | 0x01, # QDcount LSB: Question count, always 1 in this implementation. 70 | 0x00, # ANcount MSB: 71 | 0x00, # ANcount LSB: Answer Record Count, 0 in queries. 72 | 0x00, # NScount MSB: 73 | 0x00, # NScount LSB: Authority Record Count, 0 in queries. 74 | 0x00, # ARcount MSB: 75 | 0x00, # ARcount LSB: Additional Record Count, 0 in queries. 76 | ] 77 | ) 78 | host = domain.decode("utf-8").split(".") 79 | # Write out each label of question name. 80 | for label in host: 81 | # Append the length of the label 82 | query.append(len(label)) 83 | # Append the label 84 | query += bytes(label, "utf-8") 85 | # Hard code null, question type and class as they never vary. 86 | query += bytearray( 87 | [ 88 | 0x00, # Null, indicates end of question name 89 | 0x00, # Question Type MSB: 90 | 0x01, # Question Type LSB: Always 1 (Type A) in this implementation. 91 | 0x00, # Question Class MSB: 92 | 0x01, # Question Class LSB: Always 1 (Class IN) in this implementation. 93 | ] 94 | ) 95 | return query_id, len(query), query 96 | 97 | 98 | def _parse_dns_response( 99 | *, response: bytearray, query_id: int, query_length: int, debug: bool 100 | ) -> bytearray: 101 | """ 102 | Parses a DNS query response. 103 | 104 | :param bytearray response: Data returned as a DNS query response. 105 | :param int query_id: The ID of the query that generated the response, used to validate 106 | the response. 107 | :param int query_length: The number of bytes in the DNS query that generated the response. 108 | :param bool debug: Whether to output debugging messsages. 109 | 110 | :returns bytearray: Four byte IPv4 address. 111 | 112 | :raises ValueError: If the response does not yield a valid IPv4 address from a type A, 113 | class IN answer. 114 | """ 115 | # Validate request identifier 116 | response_id = int.from_bytes(response[0:2], "big") 117 | _debug_print(debug=debug, message=f"Parsing packet with ID {response_id:#x}") 118 | if response_id != query_id: 119 | raise ValueError(f"Response ID 0x{response_id:x} does not match query ID 0x{query_id:x}") 120 | # Validate flags 121 | flags = int.from_bytes(response[2:4], "big") 122 | # Mask out authenticated, truncated and recursion bits, unimportant to parsing. 123 | flags &= 0xF87F 124 | # Check that the response bit is set, the query is standard and no error occurred. 125 | if flags != 0x8000: 126 | # noinspection PyStringFormat 127 | raise ValueError(f"Invalid flags {flags:#04x}, {flags:#016b}.") 128 | # Number of questions 129 | question_count = int.from_bytes(response[4:6], "big") 130 | # Never more than one question per DNS query in this implementation. 131 | if question_count != 1: 132 | raise ValueError(f"Question count should be 1, is {question_count}.") 133 | # Number of answers 134 | answer_count = int.from_bytes(response[6:8], "big") 135 | _debug_print(debug=debug, message=f"* DNS Answer Count: {answer_count}.") 136 | if answer_count < 1: 137 | raise ValueError(f"Answer count should be > 0, is {answer_count}.") 138 | 139 | # Parse answers 140 | pointer = query_length # Response header is the same length as the query header. 141 | try: 142 | for answer in range(answer_count): 143 | # Move the pointer past the name. 144 | label_length = response[pointer] 145 | while True: 146 | if label_length >= 0xC0: 147 | # Pointer to a section of domain name, skip over it. 148 | pointer += 2 149 | label_length = response[pointer] 150 | if label_length == 0: 151 | # One byte past the end of the name. 152 | break 153 | else: 154 | # Section of the domain name, skip through it. 155 | pointer += label_length 156 | label_length = response[pointer] 157 | if label_length == 0: 158 | # On the null byte at the end of the name. Increment the pointer. 159 | pointer += 1 160 | break 161 | # Check for a type A answer. 162 | if int.from_bytes(response[pointer : pointer + 2], "big") == _TYPE_A: 163 | # Check for an IN class answer. 164 | if int.from_bytes(response[pointer + 2 : pointer + 4], "big") == _CLASS_IN: 165 | _debug_print( 166 | debug=debug, 167 | message=f"Type A, class IN found in answer {answer + 1} of {answer_count}.", 168 | ) 169 | # Set pointer to start of resource record. 170 | pointer += 8 171 | # Confirm that the resource record is 4 bytes (an IPv4 address). 172 | if int.from_bytes(response[pointer : pointer + 2], "big") == _DATA_LEN: 173 | ipv4 = response[pointer + 2 : pointer + 6] 174 | # Low probability that the response was truncated inside the 4 byte address. 175 | if len(ipv4) != _DATA_LEN: 176 | raise ValueError("IPv4 address is not 4 bytes.") 177 | _debug_print( 178 | debug=debug, 179 | message="IPv4 address found : 0x{:x}.".format( 180 | int.from_bytes(ipv4, "big") 181 | ), 182 | ) 183 | return ipv4 184 | # Set pointer to start of next answer 185 | pointer += 10 + int.from_bytes(response[pointer + 8 : pointer + 10], "big") 186 | _debug_print( 187 | debug=debug, 188 | message=f"Answer {answer + 1} of {answer_count} was not type A, class IN.", 189 | ) 190 | # No IPv4 address in any answer. 191 | raise ValueError() 192 | except (IndexError, ValueError) as error: 193 | # IndexError means we ran out of data in an answer, maybe truncated. 194 | # ValueError means we ran out of answers. 195 | raise ValueError("No type A, class IN answers found in the DNS response.") from error 196 | 197 | 198 | class DNS: 199 | """W5K DNS implementation.""" 200 | 201 | def __init__( 202 | self, 203 | iface: WIZNET5K, 204 | dns_address: Union[str, Tuple[int, int, int, int]], 205 | debug: bool = False, 206 | ) -> None: 207 | """ 208 | :param adafruit_wiznet5k.WIZNET5K: Ethernet network connection. 209 | :param Union[str, Tuple[int, int, int, int]]: IP address of the DNS server. 210 | :param bool debug: Enable debugging messages, defaults to False. 211 | """ 212 | self._debug = debug 213 | self._iface = iface 214 | self._dns_server = ( 215 | self._iface.unpretty_ip(dns_address) if isinstance(dns_address, str) else dns_address 216 | ) 217 | self._query_id = 0 # Request ID. 218 | self._query_length = 0 # Length of last query. 219 | 220 | def gethostbyname(self, hostname: bytes) -> Union[int, bytes]: 221 | """ 222 | DNS look up of a host name. 223 | 224 | :param bytes hostname: Host name to connect to. 225 | 226 | :return Union[int, bytes] The IPv4 address if successful, -1 otherwise. 227 | """ 228 | if self._dns_server is None: 229 | return _INVALID_SERVER 230 | # build DNS request packet 231 | self._query_id, self._query_length, buffer = _build_dns_query(hostname) 232 | 233 | # Send DNS request packet 234 | dns_socket = self._iface.get_socket() 235 | self._iface.socket_connect(dns_socket, bytes(self._dns_server), _DNS_PORT, conn_mode=0x02) 236 | _debug_print(debug=self._debug, message="* DNS: Sending request packet...") 237 | self._iface.socket_write(dns_socket, buffer) 238 | 239 | # Read and parse the DNS response 240 | ipaddress = -1 241 | for _ in range(5): 242 | # wait for a response 243 | socket_timeout = 5000 244 | start_time = ticks_ms() 245 | while not self._iface.socket_available(dns_socket, 0x02): 246 | if ticks_diff(ticks_ms(), start_time) > socket_timeout: 247 | _debug_print( 248 | debug=self._debug, 249 | message="* DNS ERROR: Did not receive DNS response (socket timeout).", 250 | ) 251 | self._iface.socket_close(dns_socket) 252 | raise RuntimeError("Failed to resolve hostname!") 253 | time.sleep(0.05) 254 | # recv packet into buf 255 | _, buffer = self._iface.read_udp(dns_socket, 512) 256 | _debug_print( 257 | debug=self._debug, 258 | message=f"DNS Packet Received: {buffer}", 259 | ) 260 | try: 261 | ipaddress = _parse_dns_response( 262 | response=buffer, 263 | query_id=self._query_id, 264 | query_length=self._query_length, 265 | debug=self._debug, 266 | ) 267 | break 268 | except ValueError as error: 269 | _debug_print( 270 | debug=self._debug, 271 | message="* DNS ERROR: Failed to resolve DNS response, retrying…\n" 272 | f" ({error.args[0]}).", 273 | ) 274 | self._iface.socket_close(dns_socket) 275 | return ipaddress 276 | -------------------------------------------------------------------------------- /adafruit_wiznet5k/adafruit_wiznet5k_socketpool.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 ladyada for Adafruit Industries 2 | # SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries 3 | # 4 | # SPDX-License-Identifier: MIT 5 | """ 6 | `adafruit_wiznet5k_socketpool` 7 | ================================================================================ 8 | 9 | A socket compatible interface with the Wiznet5k module. 10 | 11 | * Author(s): ladyada, Brent Rubell, Patrick Van Oosterwijck, Adam Cummick, Martin Stephens 12 | 13 | """ 14 | 15 | from __future__ import annotations 16 | 17 | try: 18 | from typing import TYPE_CHECKING, List, Optional, Tuple, Union 19 | 20 | if TYPE_CHECKING: 21 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 22 | except ImportError: 23 | pass 24 | 25 | import errno 26 | import gc 27 | from sys import byteorder 28 | 29 | from adafruit_ticks import ticks_diff, ticks_ms 30 | from micropython import const 31 | 32 | import adafruit_wiznet5k as wiznet5k 33 | 34 | _SOCKET_TYPE_TO_WIZNET = b"\0\x21\2" 35 | _SOCKET_INVALID = const(255) 36 | 37 | _global_socketpool = {} 38 | 39 | 40 | class SocketPool: 41 | """WIZNET5K SocketPool library""" 42 | 43 | # These must match circuitpython "socketpool" values. However, we cannot 44 | # depend on socketpool being importable, so hard-code them here. 45 | SOCK_STREAM = 1 46 | SOCK_DGRAM = 2 47 | 48 | SOL_SOCKET = 0xFFF 49 | SO_REUSEADDR = 0x0004 50 | 51 | AF_INET = const(3) 52 | 53 | def __new__(cls, iface: WIZNET5K): 54 | # We want to make sure to return the same pool for the same interface 55 | if iface not in _global_socketpool: 56 | _global_socketpool[iface] = super().__new__(cls) 57 | return _global_socketpool[iface] 58 | 59 | def __init__(self, iface: WIZNET5K): 60 | self._interface = iface 61 | self._default_socket_timeout = None 62 | 63 | @staticmethod 64 | def _is_ipv4_string(ipv4_address: str) -> bool: 65 | """Check for a valid IPv4 address in dotted-quad string format 66 | (for example, "123.45.67.89"). 67 | 68 | :param: str ipv4_address: The string to test. 69 | 70 | :return bool: True if a valid IPv4 address, False otherwise. 71 | """ 72 | octets = ipv4_address.split(".", 3) 73 | if len(octets) == 4 and "".join(octets).isdigit(): 74 | if all(0 <= int(octet) <= 255 for octet in octets): 75 | return True 76 | return False 77 | 78 | def getdefaulttimeout(self) -> Optional[float]: 79 | """ 80 | Return the default timeout in seconds for new socket objects. A value of 81 | None indicates that new socket objects have no timeout. When the socket module is 82 | first imported, the default is None. 83 | """ 84 | return self._default_socket_timeout 85 | 86 | def setdefaulttimeout(self, _timeout: Optional[float]) -> None: 87 | """ 88 | Set the default timeout in seconds (float) for new socket objects. When the socket 89 | module is first imported, the default is None. See settimeout() for possible values 90 | and their respective meanings. 91 | 92 | :param Optional[float] _timeout: The default timeout in seconds or None. 93 | """ 94 | if _timeout is None or _timeout >= 0: 95 | self._default_socket_timeout = _timeout 96 | else: 97 | raise ValueError("Timeout must be None, 0.0 or a positive numeric value.") 98 | 99 | @staticmethod 100 | def htonl(x: int) -> int: 101 | """ 102 | Convert 32-bit positive integer from host to network byte order. 103 | 104 | :param int x: 32-bit positive integer from host. 105 | 106 | :return int: 32-bit positive integer in network byte order. 107 | """ 108 | if byteorder == "big": 109 | return x 110 | return int.from_bytes(x.to_bytes(4, "little"), "big") 111 | 112 | @staticmethod 113 | def htons(x: int) -> int: 114 | """ 115 | Convert 16-bit positive integer from host to network byte order. 116 | 117 | :param int x: 16-bit positive integer from host. 118 | 119 | :return int: 16-bit positive integer in network byte order. 120 | """ 121 | if byteorder == "big": 122 | return x 123 | return ((x << 8) & 0xFF00) | ((x >> 8) & 0xFF) 124 | 125 | def inet_aton(self, ip_address: str) -> bytes: 126 | """ 127 | Convert an IPv4 address from dotted-quad string format (for example, "123.45.67.89") 128 | to 32-bit packed binary format, as a bytes object four characters in length. This is 129 | useful when conversing with a program that uses the standard C library and needs 130 | objects of type struct in_addr, which is the C type for the 32-bit packed binary this 131 | function returns. 132 | 133 | :param str ip_address: The IPv4 address to convert. 134 | 135 | :return bytes: The converted IPv4 address. 136 | """ 137 | if not self._is_ipv4_string(ip_address): 138 | raise ValueError("The IPv4 address must be a dotted-quad string.") 139 | return self._interface.unpretty_ip(ip_address) 140 | 141 | def inet_ntoa(self, ip_address: Union[bytes, bytearray]) -> str: 142 | """ 143 | Convert a 32-bit packed IPv4 address (a bytes-like object four bytes in length) to 144 | its standard dotted-quad string representation (for example, "123.45.67.89"). This is 145 | useful when conversing with a program that uses the standard C library and needs 146 | objects of type struct in_addr, which is the C type for the 32-bit packed binary data 147 | this function takes as an argument. 148 | 149 | :param Union[bytes, bytearray ip_address: The IPv4 address to convert. 150 | 151 | :return str: The converted ip_address: 152 | """ 153 | if len(ip_address) != 4: 154 | raise ValueError("The IPv4 address must be 4 bytes.") 155 | return self._interface.pretty_ip(ip_address) 156 | 157 | def getaddrinfo( 158 | self, 159 | host: str, 160 | port: int, 161 | family: int = 0, 162 | type: int = 0, 163 | proto: int = 0, 164 | flags: int = 0, 165 | ) -> List[Tuple[int, int, int, str, Tuple[str, int]]]: 166 | """ 167 | Translate the host/port argument into a sequence of 5-tuples that contain all the necessary 168 | arguments for creating a socket connected to that service. 169 | 170 | :param str host: a domain name, a string representation of an IPv4 address or 171 | None. 172 | :param int port: Port number to connect to (0 - 65536). 173 | :param int family: Ignored and hardcoded as 0x03 (the only family implemented) by the 174 | function. 175 | :param int type: The type of socket, either SOCK_STREAM (0x21) for TCP or SOCK_DGRAM (0x02) 176 | for UDP, defaults to 0. 177 | :param int proto: Unused in this implementation of socket. 178 | :param int flags: Unused in this implementation of socket. 179 | 180 | :return List[Tuple[int, int, int, str, Tuple[str, int]]]: Address info entries in the form 181 | (family, type, proto, canonname, sockaddr). In these tuples, family, type, proto are 182 | meant to be passed to the socket() function. canonname will always be an empty string, 183 | sockaddr is a tuple describing a socket address, whose format is (address, port), and 184 | is meant to be passed to the socket.connect() method. 185 | """ 186 | if not isinstance(port, int): 187 | raise ValueError("Port must be an integer") 188 | if not self._is_ipv4_string(host): 189 | host = self.gethostbyname(host) 190 | return [(SocketPool.AF_INET, type, proto, "", (host, port))] 191 | 192 | def gethostbyname(self, hostname: str) -> str: 193 | """ 194 | Translate a host name to IPv4 address format. The IPv4 address is returned as a string, such 195 | as '100.50.200.5'. If the host name is an IPv4 address itself it is returned unchanged. 196 | 197 | :param str hostname: Hostname to lookup. 198 | 199 | :return str: IPv4 address (a string of the form '0.0.0.0'). 200 | """ 201 | if self._is_ipv4_string(hostname): 202 | return hostname 203 | address = self._interface.get_host_by_name(hostname) 204 | address = f"{address[0]}.{address[1]}.{address[2]}.{address[3]}" 205 | return address 206 | 207 | def socket( 208 | self, 209 | family: int = AF_INET, 210 | type: int = SOCK_STREAM, 211 | proto: int = 0, 212 | fileno: Optional[int] = None, 213 | ): 214 | """Create a new socket and return it""" 215 | return Socket(self, family, type, proto, fileno) 216 | 217 | 218 | class Socket: 219 | """ 220 | A simplified implementation of the Python 'socket' class for connecting 221 | to a Wiznet5k module. 222 | """ 223 | 224 | def __init__( 225 | self, 226 | socket_pool: SocketPool, 227 | family: int = SocketPool.AF_INET, 228 | type: int = SocketPool.SOCK_STREAM, 229 | proto: int = 0, 230 | fileno: Optional[int] = None, 231 | ) -> None: 232 | """ 233 | :param int family: Socket address (and protocol) family, defaults to AF_INET. 234 | :param int type: Socket type, use SOCK_STREAM for TCP and SOCK_DGRAM for UDP, 235 | defaults to SOCK_STREAM. 236 | :param int proto: Unused, retained for compatibility. 237 | :param Optional[int] fileno: Unused, retained for compatibility. 238 | """ 239 | if family != SocketPool.AF_INET: 240 | raise RuntimeError("Only AF_INET family supported by W5K modules.") 241 | self._socket_pool = socket_pool 242 | self._interface = self._socket_pool._interface 243 | self._socket_closed = False 244 | self._sock_type = type 245 | self._buffer = b"" 246 | self._timeout = self._socket_pool._default_socket_timeout 247 | self._listen_port = None 248 | 249 | self._socknum = self._interface.get_socket(reserve_socket=True) 250 | if self._socknum == _SOCKET_INVALID: 251 | raise RuntimeError("Failed to allocate socket.") 252 | 253 | def __del__(self): 254 | self._interface.release_socket(self._socknum) 255 | 256 | def __enter__(self): 257 | return self 258 | 259 | def __exit__(self, exc_type, exc_val, exc_tb) -> None: 260 | self._interface.release_socket(self._socknum) 261 | if self._sock_type == SocketPool.SOCK_STREAM: 262 | self._interface.write_snir( 263 | self._socknum, 0xFF & (~wiznet5k.adafruit_wiznet5k.SNIR_DISCON) 264 | ) # Reset socket interrupt register. 265 | self._interface.socket_disconnect(self._socknum) 266 | mask = wiznet5k.adafruit_wiznet5k.SNIR_TIMEOUT | wiznet5k.adafruit_wiznet5k.SNIR_DISCON 267 | while not self._interface.read_snir(self._socknum) & mask: 268 | pass 269 | self._interface.write_snir(self._socknum, 0xFF) # Reset socket interrupt register. 270 | self._interface.socket_close(self._socknum) 271 | while ( 272 | self._interface.socket_status(self._socknum) 273 | != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED 274 | ): 275 | pass 276 | 277 | # This works around problems with using a class method as a decorator. 278 | def _check_socket_closed(func): 279 | """Decorator to check whether the socket object has been closed.""" 280 | 281 | def wrapper(self, *args, **kwargs): 282 | if self._socket_closed: 283 | raise RuntimeError("The socket has been closed.") 284 | return func(self, *args, **kwargs) 285 | 286 | return wrapper 287 | 288 | @property 289 | def _status(self) -> int: 290 | """ 291 | Return the status of the socket. 292 | 293 | :return int: Status of the socket. 294 | """ 295 | return self._interface.socket_status(self._socknum) 296 | 297 | @property 298 | def _connected(self) -> bool: 299 | """ 300 | Return whether connected to the socket. 301 | 302 | :return bool: Whether connected. 303 | """ 304 | 305 | if self._socknum >= self._interface.max_sockets: 306 | return False 307 | status = self._interface.socket_status(self._socknum) 308 | if status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSE_WAIT and self._available() == 0: 309 | result = False 310 | else: 311 | result = status not in { 312 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED, 313 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN, 314 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_TIME_WAIT, 315 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_FIN_WAIT, 316 | } 317 | if not result and status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN: 318 | self.close() 319 | return result 320 | 321 | @_check_socket_closed 322 | def getpeername(self) -> Tuple[str, int]: 323 | """ 324 | Return the remote address to which the socket is connected. 325 | 326 | :return Tuple[str, int]: IPv4 address and port the socket is connected to. 327 | """ 328 | return self._interface.remote_ip(self._socknum), self._interface.remote_port(self._socknum) 329 | 330 | @_check_socket_closed 331 | def bind(self, address: Tuple[Optional[str], int]) -> None: 332 | """ 333 | Bind the socket to address. The socket must not already be bound. 334 | 335 | The hardware sockets on WIZNET5K systems all share the same IPv4 address. The 336 | address is assigned at startup. Ports can only be bound to this address. 337 | 338 | :param Tuple[Optional[str], int] address: Address as a (host, port) tuple. 339 | 340 | :raises ValueError: If the IPv4 address specified is not the address 341 | assigned to the WIZNET5K interface. 342 | """ 343 | # Check to see if the socket is bound. 344 | if self._listen_port: 345 | raise ConnectionError("The socket is already bound.") 346 | self._bind(address) 347 | 348 | def _bind(self, address: Tuple[Optional[str], int]) -> None: 349 | """ 350 | Helper function to allow bind() to check for an existing connection and for 351 | accept() to generate a new socket connection. 352 | 353 | :param Tuple[Optional[str], int] address: Address as a (host, port) tuple. 354 | """ 355 | if address[0]: 356 | if self._socket_pool.gethostbyname(address[0]) != self._interface.pretty_ip( 357 | self._interface.ip_address 358 | ): 359 | raise ValueError( 360 | f"The IPv4 address requested must match {self._interface.pretty_ip(self._interface.ip_address)}, " 361 | "the one assigned to the WIZNET5K interface." 362 | ) 363 | self._listen_port = address[1] 364 | # For UDP servers we need to open the socket here because we won't call 365 | # listen 366 | if self._sock_type == SocketPool.SOCK_DGRAM: 367 | self._interface.socket_listen( 368 | self._socknum, 369 | self._listen_port, 370 | wiznet5k.adafruit_wiznet5k.SNMR_UDP, 371 | ) 372 | self._buffer = b"" 373 | 374 | @_check_socket_closed 375 | def listen(self, backlog: int = 0) -> None: 376 | """ 377 | Enable a server to accept connections. 378 | 379 | :param int backlog: Included for compatibility but ignored. 380 | """ 381 | if self._listen_port is None: 382 | raise RuntimeError("Use bind to set the port before listen!") 383 | self._interface.socket_listen(self._socknum, self._listen_port) 384 | self._buffer = b"" 385 | 386 | @_check_socket_closed 387 | def accept( 388 | self, 389 | ) -> Tuple[Socket, Tuple[str, int]]: 390 | """ 391 | Accept a connection. The socket must be bound to an address and listening for connections. 392 | 393 | :return Tuple[socket, Tuple[str, int]]: The return value is a pair 394 | (conn, address) where conn is a new socket object to send and receive data on 395 | the connection, and address is the address bound to the socket on the other 396 | end of the connection. 397 | """ 398 | stamp = ticks_ms() 399 | while self._status not in { 400 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_SYNRECV, 401 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_ESTABLISHED, 402 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN, 403 | }: 404 | if self._timeout and 0 < self._timeout < ticks_diff(ticks_ms(), stamp) / 1000: 405 | raise TimeoutError("Failed to accept connection.") 406 | if self._status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSE_WAIT: 407 | self._disconnect() 408 | self.listen() 409 | if self._status == wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED: 410 | self.close() 411 | self.listen() 412 | 413 | _, addr = self._interface.socket_accept(self._socknum) 414 | current_socknum = self._socknum 415 | # Create a new socket object and swap socket nums, so we can continue listening 416 | client_sock = Socket(self._socket_pool) 417 | self._socknum = client_sock._socknum 418 | client_sock._socknum = current_socknum 419 | self._bind((None, self._listen_port)) 420 | self.listen() 421 | if self._status != wiznet5k.adafruit_wiznet5k.SNSR_SOCK_LISTEN: 422 | raise RuntimeError("Failed to open new listening socket") 423 | return client_sock, addr 424 | 425 | @_check_socket_closed 426 | def connect(self, address: Tuple[str, int]) -> None: 427 | """ 428 | Connect to a remote socket at address. 429 | 430 | :param Tuple[str, int] address: Remote socket as a (host, port) tuple. 431 | """ 432 | if self._listen_port is not None: 433 | self._interface.src_port = self._listen_port 434 | result = self._interface.socket_connect( 435 | self._socknum, 436 | self._interface.unpretty_ip(self._socket_pool.gethostbyname(address[0])), 437 | address[1], 438 | _SOCKET_TYPE_TO_WIZNET[self._sock_type], 439 | ) 440 | self._interface.src_port = 0 441 | if not result: 442 | raise RuntimeError("Failed to connect to host ", address[0]) 443 | self._buffer = b"" 444 | 445 | @_check_socket_closed 446 | def send(self, data: Union[bytes, bytearray]) -> int: 447 | """ 448 | Send data to the socket. The socket must be connected to a remote socket. 449 | Applications are responsible for checking that all data has been sent; if 450 | only some of the data was transmitted, the application needs to attempt 451 | delivery of the remaining data. 452 | 453 | :param bytearray data: Data to send to the socket. 454 | 455 | :return int: Number of bytes sent. 456 | """ 457 | _timeout = 0 if self._timeout is None else self._timeout 458 | bytes_sent = self._interface.socket_write(self._socknum, data, _timeout) 459 | gc.collect() 460 | return bytes_sent 461 | 462 | @_check_socket_closed 463 | def sendto(self, data: bytearray, *flags_and_or_address: any) -> int: 464 | """ 465 | Send data to the socket. The socket should not be connected to a remote socket, since the 466 | destination socket is specified by address. Return the number of bytes sent.. 467 | 468 | Either: 469 | :param bytearray data: Data to send to the socket. 470 | :param [Tuple[str, int]] address: Remote socket as a (host, port) tuple. 471 | 472 | Or: 473 | :param bytearray data: Data to send to the socket. 474 | :param int flags: Not implemented, kept for compatibility. 475 | :param Tuple[int, Tuple(str, int)] address: Remote socket as a (host, port) tuple 476 | """ 477 | # May be called with (data, address) or (data, flags, address) 478 | other_args = list(flags_and_or_address) 479 | if len(other_args) in {1, 2}: 480 | address = other_args[-1] 481 | else: 482 | raise ValueError("Incorrect number of arguments, should be 2 or 3.") 483 | self.connect(address) 484 | return self.send(data) 485 | 486 | @_check_socket_closed 487 | def recv( 488 | self, 489 | bufsize: int, 490 | flags: int = 0, 491 | ) -> bytes: 492 | """ 493 | Receive data from the socket. The return value is a bytes object representing the data 494 | received. The maximum amount of data to be received at once is specified by bufsize. 495 | 496 | :param int bufsize: Maximum number of bytes to receive. 497 | :param int flags: ignored, present for compatibility. 498 | 499 | :return bytes: Data from the socket. 500 | """ 501 | buf = bytearray(bufsize) 502 | nread = self.recv_into(buf, bufsize) 503 | if nread == 0: 504 | return b"" 505 | if nread < bufsize: 506 | return bytes(buf[:nread]) 507 | return bytes(buf) 508 | 509 | def _embed_recv(self, bufsize: int = 0, flags: int = 0) -> bytes: 510 | """ 511 | Read from the connected remote address. 512 | 513 | :param int bufsize: Maximum number of bytes to receive, ignored by the 514 | function, defaults to 0. 515 | :param int flags: ignored, present for compatibility. 516 | 517 | :return bytes: All data available from the connection. 518 | """ 519 | avail = self._available() 520 | if avail: 521 | if self._sock_type == SocketPool.SOCK_STREAM: 522 | self._buffer += self._interface.socket_read(self._socknum, avail)[1] 523 | elif self._sock_type == SocketPool.SOCK_DGRAM: 524 | self._buffer += self._interface.read_udp(self._socknum, avail)[1] 525 | gc.collect() 526 | ret = self._buffer 527 | self._buffer = b"" 528 | gc.collect() 529 | return ret 530 | 531 | @_check_socket_closed 532 | def recvfrom(self, bufsize: int, flags: int = 0) -> Tuple[bytes, Tuple[str, int]]: 533 | """ 534 | Receive data from the socket. The return value is a pair (bytes, address) where bytes is 535 | a bytes object representing the data received and address is the address of the socket 536 | sending the data. 537 | 538 | :param int bufsize: Maximum number of bytes to receive. 539 | :param int flags: Ignored, present for compatibility. 540 | 541 | :return Tuple[bytes, Tuple[str, int]]: a tuple (bytes, address) 542 | where address is a tuple (address, port) 543 | """ 544 | return ( 545 | self.recv(bufsize), 546 | ( 547 | self._interface.pretty_ip(self._interface.udp_from_ip[self._socknum]), 548 | self._interface.udp_from_port[self._socknum], 549 | ), 550 | ) 551 | 552 | @_check_socket_closed 553 | def recv_into(self, buffer: bytearray, nbytes: int = 0, flags: int = 0) -> int: 554 | """ 555 | Receive up to nbytes bytes from the socket, storing the data into a buffer 556 | rather than creating a new bytestring. 557 | 558 | :param bytearray buffer: Data buffer to read into. 559 | :param nbytes: Maximum number of bytes to receive (if 0, use length of buffer). 560 | :param int flags: ignored, present for compatibility. 561 | 562 | :return int: the number of bytes received 563 | """ 564 | if not 0 <= nbytes <= len(buffer): 565 | raise ValueError("nbytes must be 0 to len(buffer)") 566 | 567 | last_read_time = ticks_ms() 568 | num_to_read = len(buffer) if nbytes == 0 else nbytes 569 | num_read = 0 570 | while num_to_read > 0: 571 | # we might have read socket data into the self._buffer with: 572 | # _readline 573 | if len(self._buffer) > 0: 574 | bytes_to_read = min(num_to_read, len(self._buffer)) 575 | buffer[num_read : num_read + bytes_to_read] = self._buffer[:bytes_to_read] 576 | num_read += bytes_to_read 577 | num_to_read -= bytes_to_read 578 | self._buffer = self._buffer[bytes_to_read:] 579 | # explicitly recheck num_to_read to avoid extra checks 580 | continue 581 | 582 | num_avail = self._available() 583 | if num_avail > 0: 584 | last_read_time = ticks_ms() 585 | bytes_to_read = min(num_to_read, num_avail) 586 | if self._sock_type == SocketPool.SOCK_STREAM: 587 | bytes_read = self._interface.socket_read(self._socknum, bytes_to_read)[1] 588 | else: 589 | bytes_read = self._interface.read_udp(self._socknum, bytes_to_read)[1] 590 | buffer[num_read : num_read + len(bytes_read)] = bytes_read 591 | num_read += len(bytes_read) 592 | num_to_read -= len(bytes_read) 593 | elif num_read > 0: 594 | # We got a message, but there are no more bytes to read, so we can stop. 595 | break 596 | elif self._status in { 597 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSED, 598 | wiznet5k.adafruit_wiznet5k.SNSR_SOCK_CLOSE_WAIT, 599 | }: 600 | # No bytes to read and we will not get more, stop. 601 | break 602 | # No bytes yet, or more bytes requested. 603 | if self._timeout is None: 604 | # blocking mode 605 | continue 606 | if self._timeout == 0: 607 | # non-blocking mode 608 | break 609 | if ticks_diff(ticks_ms(), last_read_time) / 1000 > self._timeout: 610 | raise OSError(errno.ETIMEDOUT) 611 | return num_read 612 | 613 | @_check_socket_closed 614 | def recvfrom_into( 615 | self, buffer: bytearray, nbytes: int = 0, flags: int = 0 616 | ) -> Tuple[int, Tuple[str, int]]: 617 | """ 618 | Receive data from the socket, writing it into buffer instead of creating a new bytestring. 619 | The return value is a pair (nbytes, address) where nbytes is the number of bytes received 620 | and address is the address of the socket sending the data. 621 | 622 | :param bytearray buffer: Data buffer. 623 | :param int nbytes: Maximum number of bytes to receive. 624 | :param int flags: Unused, present for compatibility. 625 | 626 | :return Tuple[int, Tuple[str, int]]: The number of bytes and address. 627 | """ 628 | return ( 629 | self.recv_into(buffer, nbytes), 630 | ( 631 | self._interface.pretty_ip(self._interface.udp_from_ip[self._socknum]), 632 | self._interface.udp_from_port[self._socknum], 633 | ), 634 | ) 635 | 636 | def _readline(self) -> bytes: 637 | """ 638 | Read a line from the socket. 639 | 640 | Deprecated, will be removed in the future. 641 | 642 | Attempt to return as many bytes as we can up to but not including a carriage return and 643 | linefeed character pair. 644 | 645 | :return bytes: The data read from the socket. 646 | """ 647 | stamp = ticks_ms() 648 | while b"\r\n" not in self._buffer: 649 | avail = self._available() 650 | if avail: 651 | if self._sock_type == SocketPool.SOCK_STREAM: 652 | self._buffer += self._interface.socket_read(self._socknum, avail)[1] 653 | elif self._sock_type == SocketPool.SOCK_DGRAM: 654 | self._buffer += self._interface.read_udp(self._socknum, avail)[1] 655 | if ( 656 | self._timeout 657 | and not avail 658 | and 0 < self._timeout < ticks_diff(ticks_ms(), stamp) / 1000 659 | ): 660 | self.close() 661 | raise RuntimeError("Didn't receive response, failing out...") 662 | firstline, self._buffer = self._buffer.split(b"\r\n", 1) 663 | gc.collect() 664 | return firstline 665 | 666 | def _disconnect(self) -> None: 667 | """Disconnect a TCP socket.""" 668 | if self._sock_type != SocketPool.SOCK_STREAM: 669 | raise RuntimeError("Socket must be a TCP socket.") 670 | self._interface.socket_disconnect(self._socknum) 671 | 672 | @_check_socket_closed 673 | def close(self) -> None: 674 | """ 675 | Mark the socket closed. Once that happens, all future operations on the socket object 676 | will fail. The remote end will receive no more data. 677 | """ 678 | self._interface.release_socket(self._socknum) 679 | self._interface.socket_close(self._socknum) 680 | self._socket_closed = True 681 | 682 | def _available(self) -> int: 683 | """ 684 | Return how many bytes of data are available to be read from the socket. 685 | 686 | :return int: Number of bytes available. 687 | """ 688 | return self._interface.socket_available( 689 | self._socknum, 690 | _SOCKET_TYPE_TO_WIZNET[self._sock_type], 691 | ) 692 | 693 | @_check_socket_closed 694 | def setsockopt(self, level: int, opt: int, value: any) -> None: 695 | """ 696 | Set a socket option. 697 | 698 | Only SOL_SOCKET SO_REUSEADDR is accepted (and the value is ignored). 699 | 700 | Other calls result in OSError.""" 701 | if level == SocketPool.SOL_SOCKET and opt == SocketPool.SO_REUSEADDR: 702 | return 703 | raise OSError 704 | 705 | @_check_socket_closed 706 | def settimeout(self, value: Optional[float]) -> None: 707 | """ 708 | Set a timeout on blocking socket operations. The value argument can be a 709 | non-negative floating point number expressing seconds, or None. If a non-zero 710 | value is given, subsequent socket operations will raise a timeout exception 711 | if the timeout period value has elapsed before the operation has completed. 712 | If zero is given, the socket is put in non-blocking mode. If None is given, 713 | the socket is put in blocking mode.. 714 | 715 | :param Optional[float] value: Socket read timeout in seconds. 716 | """ 717 | if value is None or value >= 0: 718 | self._timeout = value 719 | else: 720 | raise ValueError("Timeout must be None, 0.0 or a positive numeric value.") 721 | 722 | @_check_socket_closed 723 | def gettimeout(self) -> Optional[float]: 724 | """ 725 | Return the timeout in seconds (float) associated with socket operations, or None if no 726 | timeout is set. This reflects the last call to setblocking() or settimeout(). 727 | 728 | :return Optional[float]: Timeout in seconds, or None if no timeout is set. 729 | """ 730 | return self._timeout 731 | 732 | @_check_socket_closed 733 | def setblocking(self, flag: bool) -> None: 734 | """ 735 | Set blocking or non-blocking mode of the socket: if flag is false, the socket is set 736 | to non-blocking, else to blocking mode. 737 | 738 | This method is a shorthand for certain settimeout() calls: 739 | 740 | sock.setblocking(True) is equivalent to sock.settimeout(None) 741 | sock.setblocking(False) is equivalent to sock.settimeout(0.0) 742 | 743 | :param bool flag: The blocking mode of the socket. 744 | 745 | :raises TypeError: If flag is not a bool. 746 | 747 | """ 748 | if flag is True: 749 | self.settimeout(None) 750 | elif flag is False: 751 | self.settimeout(0.0) 752 | else: 753 | raise TypeError("Flag must be a boolean.") 754 | 755 | @_check_socket_closed 756 | def getblocking(self) -> bool: 757 | """ 758 | Return True if socket is in blocking mode, False if in non-blocking. 759 | 760 | This is equivalent to checking socket.gettimeout() == 0. 761 | 762 | :return bool: Blocking mode of the socket. 763 | """ 764 | return self.gettimeout() == 0 765 | 766 | @property 767 | @_check_socket_closed 768 | def family(self) -> int: 769 | """Socket family (always 0x03 in this implementation).""" 770 | return 3 771 | 772 | @property 773 | @_check_socket_closed 774 | def type(self): 775 | """Socket type.""" 776 | return self._sock_type 777 | 778 | @property 779 | @_check_socket_closed 780 | def proto(self): 781 | """Socket protocol (always 0x00 in this implementation).""" 782 | return 0 783 | -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Wiznet5k/1cab7a21682c508b0d8a46e875ecffdfa5736c24/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/favicon.ico.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries 2 | 3 | SPDX-License-Identifier: CC-BY-4.0 4 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | .. If you created a package, create one automodule per module in the package. 3 | 4 | .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) 5 | .. use this format as the module name: "adafruit_foo.foo" 6 | 7 | API Reference 8 | ############# 9 | 10 | .. automodule:: adafruit_wiznet5k.adafruit_wiznet5k 11 | :members: 12 | 13 | .. automodule:: adafruit_wiznet5k.adafruit_wiznet5k_socketpool 14 | :members: 15 | 16 | .. automodule:: adafruit_wiznet5k.adafruit_wiznet5k_dhcp 17 | :members: 18 | 19 | .. automodule:: adafruit_wiznet5k.adafruit_wiznet5k_dns 20 | :members: 21 | -------------------------------------------------------------------------------- /docs/api.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | import datetime 6 | import os 7 | import sys 8 | 9 | sys.path.insert(0, os.path.abspath("..")) 10 | 11 | # -- General configuration ------------------------------------------------ 12 | 13 | # Add any Sphinx extension module names here, as strings. They can be 14 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 15 | # ones. 16 | extensions = [ 17 | "sphinx.ext.autodoc", 18 | "sphinxcontrib.jquery", 19 | "sphinx.ext.intersphinx", 20 | "sphinx.ext.napoleon", 21 | "sphinx.ext.todo", 22 | ] 23 | 24 | # TODO: Please Read! 25 | # Uncomment the below if you use native CircuitPython modules such as 26 | # digitalio, micropython and busio. List the modules you use. Without it, the 27 | # autodoc module docs will fail to generate with a warning. 28 | autodoc_mock_imports = ["digitalio", "busio", "adafruit_bus_device"] 29 | 30 | 31 | intersphinx_mapping = { 32 | "python": ("https://docs.python.org/3", None), 33 | "BusDevice": ( 34 | "https://docs.circuitpython.org/projects/busdevice/en/latest/", 35 | None, 36 | ), 37 | "CircuitPython": ("https://docs.circuitpython.org/en/latest/", None), 38 | } 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | source_suffix = ".rst" 44 | 45 | # The master toctree document. 46 | master_doc = "index" 47 | 48 | # General information about the project. 49 | project = "Adafruit Wiznet5k Library" 50 | creation_year = "2020" 51 | current_year = str(datetime.datetime.now().year) 52 | year_duration = ( 53 | current_year if current_year == creation_year else creation_year + " - " + current_year 54 | ) 55 | copyright = year_duration + " Brent Rubell" 56 | author = "Brent Rubell" 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = "1.0" 64 | # The full version, including alpha/beta/rc tags. 65 | release = "1.0" 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = "en" 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This patterns also effect to html_static_path and html_extra_path 77 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".env", "CODE_OF_CONDUCT.md"] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all 80 | # documents. 81 | # 82 | default_role = "any" 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | # 86 | add_function_parentheses = True 87 | 88 | # The name of the Pygments (syntax highlighting) style to use. 89 | pygments_style = "sphinx" 90 | 91 | # If true, `todo` and `todoList` produce output, else they produce nothing. 92 | todo_include_todos = False 93 | 94 | # If this is True, todo emits a warning for each TODO entries. The default is False. 95 | todo_emit_warnings = True 96 | 97 | napoleon_numpy_docstring = False 98 | 99 | # -- Options for HTML output ---------------------------------------------- 100 | 101 | # The theme to use for HTML and HTML Help pages. See the documentation for 102 | # a list of builtin themes. 103 | # 104 | import sphinx_rtd_theme 105 | 106 | html_theme = "sphinx_rtd_theme" 107 | 108 | # Add any paths that contain custom static files (such as style sheets) here, 109 | # relative to this directory. They are copied after the builtin static files, 110 | # so a file named "default.css" will overwrite the builtin "default.css". 111 | html_static_path = ["_static"] 112 | 113 | # The name of an image file (relative to this directory) to use as a favicon of 114 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 115 | # pixels large. 116 | # 117 | html_favicon = "_static/favicon.ico" 118 | 119 | # Output file base name for HTML help builder. 120 | htmlhelp_basename = "AdafruitWiznet5kLibrarydoc" 121 | 122 | # -- Options for LaTeX output --------------------------------------------- 123 | 124 | latex_elements = { 125 | # The paper size ('letterpaper' or 'a4paper'). 126 | # 127 | # 'papersize': 'letterpaper', 128 | # The font size ('10pt', '11pt' or '12pt'). 129 | # 130 | # 'pointsize': '10pt', 131 | # Additional stuff for the LaTeX preamble. 132 | # 133 | # 'preamble': '', 134 | # Latex figure (float) alignment 135 | # 136 | # 'figure_align': 'htbp', 137 | } 138 | 139 | # Grouping the document tree into LaTeX files. List of tuples 140 | # (source start file, target name, title, 141 | # author, documentclass [howto, manual, or own class]). 142 | latex_documents = [ 143 | ( 144 | master_doc, 145 | "AdafruitWiznet5kLibrary.tex", 146 | "AdafruitWiznet5k Library Documentation", 147 | author, 148 | "manual", 149 | ) 150 | ] 151 | 152 | # -- Options for manual page output --------------------------------------- 153 | 154 | # One entry per manual page. List of tuples 155 | # (source start file, name, description, authors, manual section). 156 | man_pages = [ 157 | ( 158 | master_doc, 159 | "AdafruitWiznet5klibrary", 160 | "Adafruit Wiznet5k Library Documentation", 161 | [author], 162 | 1, 163 | ) 164 | ] 165 | 166 | # -- Options for Texinfo output ------------------------------------------- 167 | 168 | # Grouping the document tree into Texinfo files. List of tuples 169 | # (source start file, target name, title, author, 170 | # dir menu entry, description, category) 171 | texinfo_documents = [ 172 | ( 173 | master_doc, 174 | "AdafruitWiznet5kLibrary", 175 | "Adafruit Wiznet5k Library Documentation", 176 | author, 177 | "AdafruitWiznet5kLibrary", 178 | "One line description of project.", 179 | "Miscellaneous", 180 | ) 181 | ] 182 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Simple test 2 | ------------ 3 | 4 | Ensure your device works with this simple test. 5 | 6 | .. literalinclude:: ../examples/wiznet5k_simpletest.py 7 | :caption: examples/wiznet5k_simpletest.py 8 | :linenos: 9 | -------------------------------------------------------------------------------- /docs/examples.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | Table of Contents 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 4 8 | :hidden: 9 | 10 | self 11 | 12 | .. toctree:: 13 | :caption: Examples 14 | 15 | examples 16 | 17 | .. toctree:: 18 | :caption: API Reference 19 | :maxdepth: 3 20 | 21 | api 22 | 23 | .. toctree:: 24 | :caption: Tutorials 25 | 26 | 27 | .. toctree:: 28 | :caption: Related Products 29 | 30 | 31 | .. toctree:: 32 | :caption: Other Links 33 | 34 | Download from GitHub 35 | Download Library Bundle 36 | CircuitPython Reference Documentation 37 | CircuitPython Support Forum 38 | Discord Chat 39 | Adafruit Learning System 40 | Adafruit Blog 41 | Adafruit Store 42 | 43 | Indices and tables 44 | ================== 45 | 46 | * :ref:`genindex` 47 | * :ref:`modindex` 48 | * :ref:`search` 49 | -------------------------------------------------------------------------------- /docs/index.rst.license: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 ladyada for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | sphinx 6 | sphinxcontrib-jquery 7 | sphinx-rtd-theme 8 | -------------------------------------------------------------------------------- /examples/wiznet5k_aio_post.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | from os import getenv 6 | 7 | import adafruit_connection_manager 8 | import adafruit_requests 9 | import board 10 | import busio 11 | from digitalio import DigitalInOut 12 | 13 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 14 | 15 | # Get Adafruit IO keys, ensure these are setup in settings.toml 16 | # (visit io.adafruit.com if you need to create an account, or if you need your Adafruit IO key.) 17 | aio_username = getenv("ADAFRUIT_AIO_USERNAME") 18 | aio_key = getenv("ADAFRUIT_AIO_KEY") 19 | 20 | cs = DigitalInOut(board.D10) 21 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 22 | 23 | # Initialize ethernet interface with DHCP 24 | eth = WIZNET5K(spi_bus, cs) 25 | # Initialize a requests session 26 | pool = adafruit_connection_manager.get_radio_socketpool(eth) 27 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(eth) 28 | requests = adafruit_requests.Session(pool, ssl_context) 29 | 30 | counter = 0 31 | 32 | while True: 33 | print("Posting data...", end="") 34 | data = counter 35 | feed = "test" 36 | payload = {"value": data} 37 | response = requests.post( 38 | f"http://io.adafruit.com/api/v2/{aio_username}/feeds/{feed}/data", 39 | json=payload, 40 | headers={"X-AIO-KEY": aio_key}, 41 | ) 42 | print(response.json()) 43 | response.close() 44 | counter = counter + 1 45 | print("OK") 46 | response = None 47 | time.sleep(15) 48 | -------------------------------------------------------------------------------- /examples/wiznet5k_cheerlights.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import time 5 | 6 | import adafruit_connection_manager 7 | import adafruit_fancyled.adafruit_fancyled as fancy 8 | import adafruit_requests 9 | import board 10 | import busio 11 | import neopixel 12 | from digitalio import DigitalInOut 13 | 14 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 15 | 16 | cs = DigitalInOut(board.D10) 17 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 18 | 19 | # Initialize ethernet interface with DHCP 20 | eth = WIZNET5K(spi_bus, cs) 21 | 22 | # Initialize a requests session 23 | pool = adafruit_connection_manager.get_radio_socketpool(eth) 24 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(eth) 25 | requests = adafruit_requests.Session(pool, ssl_context) 26 | 27 | DATA_SOURCE = "http://api.thingspeak.com/channels/1417/feeds.json?results=1" 28 | DATA_LOCATION = ["feeds", 0, "field2"] 29 | 30 | # neopixels 31 | pixels = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3) 32 | pixels.fill(0) 33 | 34 | attempts = 3 # Number of attempts to retry each request 35 | failure_count = 0 36 | response = None 37 | 38 | # we'll save the value in question 39 | last_value = value = None 40 | 41 | while True: 42 | try: 43 | print("Fetching json from", DATA_SOURCE) 44 | response = requests.get(DATA_SOURCE) 45 | print(response.json()) 46 | value = response.json() 47 | for key in DATA_LOCATION: 48 | value = value[key] 49 | print(value) 50 | response.close() 51 | failure_count = 0 52 | except AssertionError as error: 53 | print("Request failed, retrying...\n", error) 54 | failure_count += 1 55 | if failure_count >= attempts: 56 | raise AssertionError( 57 | "Failed to resolve hostname, \ 58 | please check your router's DNS configuration." 59 | ) from error 60 | continue 61 | if not value: 62 | continue 63 | if last_value != value: 64 | color = int(value[1:], 16) 65 | red = color >> 16 & 0xFF 66 | green = color >> 8 & 0xFF 67 | blue = color & 0xFF 68 | gamma_corrected = fancy.gamma_adjust(fancy.CRGB(red, green, blue)).pack() 69 | 70 | pixels.fill(gamma_corrected) 71 | last_value = value 72 | response = None 73 | time.sleep(60) 74 | -------------------------------------------------------------------------------- /examples/wiznet5k_cpython_client_for_simpleserver.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 ladyada 2 | # 3 | # SPDX-License-Identifier: MIT 4 | #!/usr/bin/env python3 5 | 6 | """ 7 | This example client runs on CPython and connects to / sends data to the 8 | simpleserver example. 9 | """ 10 | 11 | import socket 12 | import time 13 | 14 | print("A simple client for the wiznet5k_simpleserver.py example in this directory") 15 | print( 16 | "Run this on any device connected to the same network as the server, after " 17 | "editing this script with the correct HOST & PORT\n" 18 | ) 19 | # Or, use any TCP-based client that can easily send 1024 bytes. For example: 20 | # python -c 'print("1234"*256)' | nc 192.168.10.1 50007 21 | 22 | 23 | # edit host and port to match server 24 | HOST = "192.168.10.1" 25 | PORT = 50007 26 | TIMEOUT = 10 27 | INTERVAL = 5 28 | MAXBUF = 1024 29 | 30 | while True: 31 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 32 | s.settimeout(TIMEOUT) 33 | print(f"Connecting to {HOST}:{PORT}") 34 | s.connect((HOST, PORT)) 35 | # wiznet5k_simpleserver.py wants exactly 1024 bytes 36 | size = s.send(b"A5" * 512) 37 | print("Sent", size, "bytes") 38 | buf = s.recv(MAXBUF) 39 | print("Received", buf) 40 | s.close() 41 | time.sleep(INTERVAL) 42 | -------------------------------------------------------------------------------- /examples/wiznet5k_httpserver.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Tim C for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import board 5 | import digitalio 6 | from adafruit_httpserver import Request, Response, Server 7 | 8 | import adafruit_wiznet5k.adafruit_wiznet5k_socketpool as socketpool 9 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 10 | 11 | print("Wiznet5k HTTPServer Test") 12 | 13 | # For Adafruit Ethernet FeatherWing 14 | cs = digitalio.DigitalInOut(board.D10) 15 | # For Particle Ethernet FeatherWing 16 | # cs = digitalio.DigitalInOut(board.D5) 17 | spi_bus = board.SPI() 18 | 19 | # Initialize ethernet interface with DHCP 20 | eth = WIZNET5K(spi_bus, cs) 21 | 22 | # Create a socket pool 23 | pool = socketpool.SocketPool(eth) 24 | 25 | # initialize the server 26 | server = Server(pool, "/static", debug=True) 27 | 28 | 29 | @server.route("/") 30 | def base(request: Request): 31 | """ 32 | Serve a default static plain text message. 33 | """ 34 | return Response(request, "Hello from the CircuitPython HTTP Server!") 35 | 36 | 37 | server.serve_forever(str(eth.pretty_ip(eth.ip_address))) 38 | -------------------------------------------------------------------------------- /examples/wiznet5k_simpleserver.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-FileCopyrightText: 2021 Adam Cummick 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | import board 7 | import busio 8 | import digitalio 9 | 10 | import adafruit_wiznet5k.adafruit_wiznet5k_socketpool as socketpool 11 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 12 | 13 | print("Wiznet5k SimpleServer Test") 14 | 15 | # For Adafruit Ethernet FeatherWing 16 | cs = digitalio.DigitalInOut(board.D10) 17 | # For Particle Ethernet FeatherWing 18 | # cs = digitalio.DigitalInOut(board.D5) 19 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 20 | 21 | # Initialize ethernet interface 22 | eth = WIZNET5K(spi_bus, cs, is_dhcp=True) 23 | 24 | # Initialize a socket for our server 25 | pool = socketpool.SocketPool(eth) 26 | server = pool.socket() # Allocate socket for the server 27 | server_ip = eth.pretty_ip(eth.ip_address) # IP address of server 28 | server_port = 50007 # Port to listen on 29 | server.bind((server_ip, server_port)) # Bind to IP and Port 30 | server.listen() # Begin listening for incoming clients 31 | 32 | while True: 33 | print(f"Accepting connections on {server_ip}:{server_port}") 34 | conn, addr = server.accept() # Wait for a connection from a client. 35 | print(f"Connection accepted from {addr}, reading exactly 1024 bytes from client") 36 | with conn: 37 | data = conn.recv(1024) 38 | if data: # Wait for receiving data 39 | print(data) 40 | conn.send(data) # Echo message back to client 41 | print("Connection closed") 42 | -------------------------------------------------------------------------------- /examples/wiznet5k_simpletest.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import adafruit_connection_manager 5 | import adafruit_requests 6 | import board 7 | import busio 8 | import digitalio 9 | 10 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 11 | 12 | print("Wiznet5k WebClient Test") 13 | 14 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 15 | JSON_URL = "http://api.coindesk.com/v1/bpi/currentprice/USD.json" 16 | 17 | # For Adafruit Ethernet FeatherWing 18 | cs = digitalio.DigitalInOut(board.D10) 19 | # For Particle Ethernet FeatherWing 20 | # cs = digitalio.DigitalInOut(board.D5) 21 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 22 | 23 | # Initialize ethernet interface with DHCP 24 | eth = WIZNET5K(spi_bus, cs) 25 | 26 | # Initialize a requests session 27 | pool = adafruit_connection_manager.get_radio_socketpool(eth) 28 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(eth) 29 | requests = adafruit_requests.Session(pool, ssl_context) 30 | 31 | print("Chip Version:", eth.chip) 32 | print("MAC Address:", [hex(i) for i in eth.mac_address]) 33 | print("My IP address is:", eth.pretty_ip(eth.ip_address)) 34 | print("IP lookup adafruit.com: %s" % eth.pretty_ip(eth.get_host_by_name("adafruit.com"))) 35 | 36 | 37 | # eth._debug = True 38 | print("Fetching text from", TEXT_URL) 39 | r = requests.get(TEXT_URL) 40 | print("-" * 40) 41 | print(r.text) 42 | print("-" * 40) 43 | r.close() 44 | 45 | print() 46 | print("Fetching json from", JSON_URL) 47 | r = requests.get(JSON_URL) 48 | print("-" * 40) 49 | print(r.json()) 50 | print("-" * 40) 51 | r.close() 52 | 53 | print("Done!") 54 | -------------------------------------------------------------------------------- /examples/wiznet5k_simpletest_manual_network.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries 2 | # SPDX-License-Identifier: MIT 3 | 4 | import adafruit_connection_manager 5 | import adafruit_requests 6 | import board 7 | import busio 8 | import digitalio 9 | 10 | from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K 11 | 12 | TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html" 13 | 14 | # Setup your network configuration below 15 | IP_ADDRESS = (192, 168, 10, 1) 16 | SUBNET_MASK = (255, 255, 0, 0) 17 | GATEWAY_ADDRESS = (192, 168, 0, 1) 18 | DNS_SERVER = (8, 8, 8, 8) 19 | 20 | print("Wiznet5k WebClient Test (no DHCP)") 21 | 22 | cs = digitalio.DigitalInOut(board.D10) 23 | spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) 24 | 25 | # Initialize ethernet interface without DHCP 26 | eth = WIZNET5K(spi_bus, cs, is_dhcp=False) 27 | 28 | # Set network configuration 29 | eth.ifconfig = (IP_ADDRESS, SUBNET_MASK, GATEWAY_ADDRESS, DNS_SERVER) 30 | 31 | # Initialize a requests session 32 | pool = adafruit_connection_manager.get_radio_socketpool(eth) 33 | ssl_context = adafruit_connection_manager.get_radio_ssl_context(eth) 34 | requests = adafruit_requests.Session(pool, ssl_context) 35 | 36 | print("Chip Version:", eth.chip) 37 | print("MAC Address:", [hex(i) for i in eth.mac_address]) 38 | print("My IP address is:", eth.pretty_ip(eth.ip_address)) 39 | print("IP lookup adafruit.com: %s" % eth.pretty_ip(eth.get_host_by_name("adafruit.com"))) 40 | 41 | # eth._debug = True 42 | print("Fetching text from", TEXT_URL) 43 | r = requests.get(TEXT_URL) 44 | print("-" * 40) 45 | print(r.text) 46 | print("-" * 40) 47 | r.close() 48 | 49 | print() 50 | -------------------------------------------------------------------------------- /optional_requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | [build-system] 6 | requires = [ 7 | "setuptools", 8 | "wheel", 9 | "setuptools-scm", 10 | ] 11 | 12 | [project] 13 | name = "adafruit-circuitpython-wiznet5k" 14 | description = "Pure-Python interface for WIZNET 5k ethernet modules." 15 | version = "0.0.0+auto.0" 16 | readme = "README.rst" 17 | authors = [ 18 | {name = "Adafruit Industries", email = "circuitpython@adafruit.com"} 19 | ] 20 | urls = {Homepage = "https://github.com/adafruit/Adafruit_CircuitPython_Wiznet5k"} 21 | keywords = [ 22 | "adafruit", 23 | "blinka", 24 | "circuitpython", 25 | "micropython", 26 | "wiznet5k", 27 | "ethernet,", 28 | "wiznet,", 29 | "w5500,", 30 | "w5200,", 31 | "internet,", 32 | "iot", 33 | ] 34 | license = {text = "MIT"} 35 | classifiers = [ 36 | "Intended Audience :: Developers", 37 | "Topic :: Software Development :: Libraries", 38 | "Topic :: Software Development :: Embedded Systems", 39 | "Topic :: System :: Hardware", 40 | "License :: OSI Approved :: MIT License", 41 | "Programming Language :: Python :: 3", 42 | ] 43 | dynamic = ["dependencies", "optional-dependencies"] 44 | 45 | [tool.setuptools] 46 | packages = ["adafruit_wiznet5k"] 47 | 48 | [tool.setuptools.dynamic] 49 | dependencies = {file = ["requirements.txt"]} 50 | optional-dependencies = {optional = {file = ["optional_requirements.txt"]}} 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | Adafruit-Blinka 6 | adafruit-circuitpython-busdevice 7 | adafruit-circuitpython-ticks 8 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | target-version = "py38" 6 | line-length = 100 7 | 8 | [lint] 9 | preview = true 10 | select = ["I", "PL", "UP"] 11 | 12 | extend-select = [ 13 | "D419", # empty-docstring 14 | "E501", # line-too-long 15 | "W291", # trailing-whitespace 16 | "PLC0414", # useless-import-alias 17 | "PLC2401", # non-ascii-name 18 | "PLC2801", # unnecessary-dunder-call 19 | "PLC3002", # unnecessary-direct-lambda-call 20 | "E999", # syntax-error 21 | "PLE0101", # return-in-init 22 | "F706", # return-outside-function 23 | "F704", # yield-outside-function 24 | "PLE0116", # continue-in-finally 25 | "PLE0117", # nonlocal-without-binding 26 | "PLE0241", # duplicate-bases 27 | "PLE0302", # unexpected-special-method-signature 28 | "PLE0604", # invalid-all-object 29 | "PLE0605", # invalid-all-format 30 | "PLE0643", # potential-index-error 31 | "PLE0704", # misplaced-bare-raise 32 | "PLE1141", # dict-iter-missing-items 33 | "PLE1142", # await-outside-async 34 | "PLE1205", # logging-too-many-args 35 | "PLE1206", # logging-too-few-args 36 | "PLE1307", # bad-string-format-type 37 | "PLE1310", # bad-str-strip-call 38 | "PLE1507", # invalid-envvar-value 39 | "PLE2502", # bidirectional-unicode 40 | "PLE2510", # invalid-character-backspace 41 | "PLE2512", # invalid-character-sub 42 | "PLE2513", # invalid-character-esc 43 | "PLE2514", # invalid-character-nul 44 | "PLE2515", # invalid-character-zero-width-space 45 | "PLR0124", # comparison-with-itself 46 | "PLR0202", # no-classmethod-decorator 47 | "PLR0203", # no-staticmethod-decorator 48 | "UP004", # useless-object-inheritance 49 | "PLR0206", # property-with-parameters 50 | "PLR0904", # too-many-public-methods 51 | "PLR0911", # too-many-return-statements 52 | "PLR0912", # too-many-branches 53 | "PLR0913", # too-many-arguments 54 | "PLR0914", # too-many-locals 55 | "PLR0915", # too-many-statements 56 | "PLR0916", # too-many-boolean-expressions 57 | "PLR1702", # too-many-nested-blocks 58 | "PLR1704", # redefined-argument-from-local 59 | "PLR1711", # useless-return 60 | "C416", # unnecessary-comprehension 61 | "PLR1733", # unnecessary-dict-index-lookup 62 | "PLR1736", # unnecessary-list-index-lookup 63 | 64 | # ruff reports this rule is unstable 65 | #"PLR6301", # no-self-use 66 | 67 | "PLW0108", # unnecessary-lambda 68 | "PLW0120", # useless-else-on-loop 69 | "PLW0127", # self-assigning-variable 70 | "PLW0129", # assert-on-string-literal 71 | "B033", # duplicate-value 72 | "PLW0131", # named-expr-without-context 73 | "PLW0245", # super-without-brackets 74 | "PLW0406", # import-self 75 | "PLW0602", # global-variable-not-assigned 76 | "PLW0603", # global-statement 77 | "PLW0604", # global-at-module-level 78 | 79 | # fails on the try: import typing used by libraries 80 | #"F401", # unused-import 81 | 82 | "F841", # unused-variable 83 | "E722", # bare-except 84 | "PLW0711", # binary-op-exception 85 | "PLW1501", # bad-open-mode 86 | "PLW1508", # invalid-envvar-default 87 | "PLW1509", # subprocess-popen-preexec-fn 88 | "PLW2101", # useless-with-lock 89 | "PLW3301", # nested-min-max 90 | ] 91 | 92 | ignore = [ 93 | "PLR2004", # magic-value-comparison 94 | "UP030", # format literals 95 | "PLW1514", # unspecified-encoding 96 | "PLR0913", # too-many-arguments 97 | "PLR0915", # too-many-statements 98 | "PLR0917", # too-many-positional-arguments 99 | "PLR0904", # too-many-public-methods 100 | "PLR0912", # too-many-branches 101 | "PLR0916", # too-many-boolean-expressions 102 | "PLR6301", # could-be-static no-self-use 103 | "PLC0415", # import outside toplevel 104 | "UP007", # type or instead of union 105 | "UP006", # type built-in instead of typing class 106 | "PLR1702", # too many nested blocks 107 | "E501", # line too long 108 | ] 109 | 110 | [format] 111 | line-ending = "lf" 112 | --------------------------------------------------------------------------------