├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ ├── feature-request.md │ └── porting-request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── workflows │ ├── pythonpublish.yml │ └── tests.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── IMU.ipynb └── tools │ └── Motor_PID.ipynb ├── pyluos ├── __init__.py ├── device.py ├── integration │ ├── __init__.py │ └── freedomRobotics.py ├── io │ ├── __init__.py │ ├── serial_io.py │ └── ws.py ├── logging_conf.json ├── services │ ├── __init__.py │ ├── angle.py │ ├── color.py │ ├── distance.py │ ├── gate.py │ ├── imu.py │ ├── light.py │ ├── load.py │ ├── motor.py │ ├── pipe.py │ ├── pressure.py │ ├── service.py │ ├── servoMotor.py │ ├── state.py │ ├── unknown.py │ ├── void.py │ └── voltage.py ├── tools │ ├── __init__.py │ ├── bootloader.py │ ├── discover.py │ ├── shell.py │ ├── usb2ws.py │ ├── usb_gate.py │ └── wifi_gate.py ├── utils.py └── version.py ├── setup.cfg ├── setup.py ├── tests ├── fakerobot.py ├── test_import.py ├── test_lifecycle.py ├── test_rename.py ├── test_serial.py ├── test_servo.py └── test_ws.py ├── tools └── fake_robot.py └── tox.ini /.gitattributes: -------------------------------------------------------------------------------- 1 | 2 | *.ipynb diff=jupyternotebook 3 | 4 | *.ipynb merge=jupyternotebook 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: nicolas-rabault 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Network configuration** 14 | - Add the routing table or list the nodes and their services 15 | - Power input configuration 16 | 17 | **How to reproduce the bug** 18 | Describe thoroughly the steps to reproduce the bug. 19 | 20 | **Additional context** 21 | Add any other context about the problem here. 22 | Don't hesitate to add screenshots or to link an entry from the forum. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Luos Community Support 4 | url: https://community.luos.io 5 | about: Please ask and answer questions about Luos here. 6 | - name: Luos Documentation 7 | url: https://docs.luos.io/ 8 | about: Please read Luos documentation here. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature to be working in Luos 4 | title: "[NEW FEATURE] " 5 | labels: feature 6 | assignees: nicolas-rabault 7 | 8 | --- 9 | 10 | **What feature would you like to be included in Luos?** 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/porting-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Porting request 3 | about: Suggest a new MCU to be compatible with Luos 4 | title: "[MCU PORTING] " 5 | labels: porting 6 | assignees: nicolas-rabault 7 | 8 | --- 9 | 10 | **What MCU would you like Luos to be compatible with?** 11 | 12 | **Link the datasheet of this MCU:** 13 | 14 | **Describe your project:** 15 | 16 | **Describe the electronic board hosting the MCU:** 17 | Add details about the network interface, the pins used, the device, etc. 18 | Don't hesitate to post a schematic. 19 | 20 | **Ensure that every box bellow is checked:** 21 | - [ ] The MCU is not in the [compatible list](https://docs.luos.io/pages/low/electronic-design.html#compatible-mcus) in the documentation. 22 | - [ ] The MCU is not already in an existing [porting issue](https://github.com/Luos-io/Luos/issues). 23 | - [x] The issue has the label `porting`. 24 | - [ ] The issue is added to the [Porting](https://github.com/orgs/Luos-io/projects/3) project. 25 | 26 | **Additional context** 27 | Add any other context about the porting request here. 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Status 2 | **READY/IN DEVELOPMENT/HOLD** 3 | 4 | ## Migrations 5 | YES | NO 6 | 7 | ## Description 8 | A few sentences describing the overall goals of the pull request's commits. 9 | 10 | ## Related PRs 11 | List related PRs against other branches: 12 | 13 | branch | PR 14 | ------ | ------ 15 | other_pr_production | [link]() 16 | other_pr_master | [link]() 17 | 18 | 19 | ## Todos 20 | - [ ] Tests 21 | - [ ] Documentation 22 | 23 | 24 | ## Deploy Notes 25 | Notes regarding deployment the contained body of work. These should note any 26 | db migrations, etc. 27 | 28 | ## Steps to Test or Reproduce 29 | Outline the steps to test or reproduce the PR here. 30 | 31 | ```sh 32 | git pull --prune 33 | git checkout 34 | bundle; script/server 35 | ``` 36 | 37 | 1. 38 | 39 | ## Impacted Areas in Application 40 | List general components of the application that this PR will affect: 41 | 42 | * 43 | -------------------------------------------------------------------------------- /.github/workflows/pythonpublish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | 9 | jobs: 10 | build: 11 | name: Build distribution 📦 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.x" 20 | - name: Install pypa/build 21 | run: >- 22 | python3 -m 23 | pip install 24 | build 25 | --user 26 | - name: Build a binary wheel and a source tarball 27 | run: python3 -m build 28 | - name: Store the distribution packages 29 | uses: actions/upload-artifact@v3 30 | with: 31 | name: python-package-distributions 32 | path: dist/ 33 | 34 | publish-to-pypi: 35 | name: upload release to PyPI 36 | needs: 37 | - build 38 | runs-on: ubuntu-latest 39 | environment: 40 | name: pypi 41 | url: https://pypi.org/p/ # Replace with your PyPI project name 42 | permissions: 43 | id-token: write # IMPORTANT: mandatory for trusted publishing 44 | 45 | steps: 46 | - name: Download all the dists 47 | uses: actions/download-artifact@v3 48 | with: 49 | name: python-package-distributions 50 | path: dist/ 51 | - name: Publish distribution 📦 to PyPI 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test pyluos 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: [ '3.x' ] 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools 25 | - name: test 26 | run: | 27 | pip install -e ./ 28 | pip install -e ./[tests] 29 | pytest 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # mac 92 | .DS_Store 93 | **/.DS_Store 94 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Luos logo 2 | 3 | # Contributor Covenant Code of Conduct 4 | 5 | ## Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to making participation in our project and 9 | our community a harassment-free experience for everyone, regardless of age, body 10 | size, disability, ethnicity, sex characteristics, gender identity and expression, 11 | level of experience, education, socio-economic status, nationality, personal 12 | appearance, race, religion, or sexual identity and orientation. 13 | 14 | ## Our Standards 15 | 16 | Examples of behavior that contributes to creating a positive environment 17 | include: 18 | 19 | * Using welcoming and inclusive language 20 | * Being respectful of differing viewpoints and experiences 21 | * Gracefully accepting constructive criticism 22 | * Focusing on what is best for the community 23 | * Showing empathy towards other community members 24 | 25 | Examples of unacceptable behavior by participants include: 26 | 27 | * The use of sexualized language or imagery and unwelcome sexual attention or 28 | advances 29 | * Trolling, insulting/derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or electronic 32 | address, without explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ## Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at hello@luos.io. All 61 | complaints will be reviewed and investigated and will result in a response that 62 | is deemed necessary and appropriate to the circumstances. The project team is 63 | obligated to maintain confidentiality with regard to the reporter of an incident. 64 | Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ## Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 73 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 74 | 75 | [homepage]: https://www.contributor-covenant.org 76 | 77 | For answers to common questions about this code of conduct, see 78 | https://www.contributor-covenant.org/faq 79 | 80 | ## Don't hesitate to read [our documentation](https://docs.luos.io), or to post your questions/issues on the [Luos' Forum](https://community.luos.io). :books: 81 | 82 | [![](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fcommunity.luos.io&logo=Discourse)](https://community.luos.io) 83 | [![](https://img.shields.io/badge/Luos-Documentation-34A3B4)](https://docs.luos.io) 84 | [![](https://img.shields.io/badge/LinkedIn-Follow%20us-0077B5?style=flat&logo=linkedin)](https://www.linkedin.com/company/luos) 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Luos logo 2 | 3 | # Contributing to Luos 4 | 5 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 6 | 7 | * Reporting a bug 8 | * Discussing the current state of the code 9 | * Submitting a fix 10 | * Proposing new features 11 | * Becoming a maintainer 12 | 13 | ## We Develop with Github 14 | We use github to host code, to track issues and feature requests, as well as accept pull requests. 15 | 16 | ## Report bugs and requests using Github's issues 17 | We use GitHub issues to track public bugs and requests. 18 | Report a bug or a request by opening a new issue; it's that easy! 19 | 20 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html), So All Code Changes Happen Through Pull Requests 21 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 22 | 23 | 1. Fork the repo and create your branch from `master`. 24 | 2. If you've added code that should be tested, add tests. 25 | 3. If you've changed APIs, update the documentation. 26 | 4. Ensure the test suite passes. 27 | 5. Make sure your code lints. 28 | 6. Issue that pull request! 29 | 30 | ## Any contributions to a driver and/or an application you make will be under the [MIT Software License](http://choosealicense.com/licenses/mit/) 31 | In short, when you submit code changes to drivers and apps, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 32 | 33 | ## Any contributions to Luos core technology you make will be under the [Luos License](https://github.com/Luos-io/Luos/blob/master/LICENSE.md) 34 | In short, when you submit code changes to Luos core technology, your submissions are understood to be under the [Luos License](https://github.com/Luos-io/Luos/blob/master/LICENSE.md) that covers the project. Feel free to contact the maintainers if that's a concern. 35 | 36 | ## Write bug reports with detail, background, and sample code 37 | Great Bug Reports tend to have: 38 | 39 | * A quick summary and/or background 40 | * Steps to reproduce 41 | * Be specific! 42 | * Give sample code if you can 43 | * What you expected would happen 44 | * What actually happens 45 | * Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 46 | 47 | People love thorough bug reports. I'm not even kidding. 48 | 49 | ## Don't hesitate to read [our documentation](https://docs.luos.io), or to post your questions/issues on the [Luos' Forum](https://community.luos.io). :books: 50 | 51 | [![](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fcommunity.luos.io&logo=Discourse)](https://community.luos.io) 52 | [![](https://img.shields.io/badge/Luos-Documentation-34A3B4)](https://docs.luos.io) 53 | [![](https://img.shields.io/badge/LinkedIn-Follow%20us-0077B5?style=flat&logo=linkedin)](https://www.linkedin.com/company/luos) 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luos Robotics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyluos/logging_conf.json 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Luos logo 2 | 3 | ![](https://github.com/Luos-io/luos_engine/actions/workflows/build.yml/badge.svg) 4 | [![](https://img.shields.io/github/license/Luos-io/Pyluos)](https://github.com/Luos-io/luos_engine/blob/master/LICENSE) 5 | [![](https://img.shields.io/badge/Luos-Documentation-34A3B4)](https://www.luos.io/docs) 6 | [![](http://certified.luos.io)](https://www.luos.io) 7 | [![PlatformIO Registry](https://badges.registry.platformio.org/packages/luos/library/luos_engine.svg)](https://registry.platformio.org/libraries/luos/luos_engine) 8 | 9 | [![](https://img.shields.io/discord/902486791658041364?label=Discord&logo=discord&style=social)](http://bit.ly/JoinLuosDiscord) 10 | [![](https://img.shields.io/reddit/subreddit-subscribers/Luos?style=social)](https://www.reddit.com/r/Luos) 11 | 12 | # Pyluos 13 | ## The most for the developer​ 14 | Luos provides a simple way to think your hardware products as a group of independant features. You can easily manage and share your hardware products' features with your team, external developers, or with the community. Luos is an open-source lightweight library that can be used on any MCU, leading to free and fast multi-electronic-boards products development. Choosing Luos to design a product will help you to develop, debug, validate, monitor, and manage it from the cloud. 15 | 16 | ## The most for the community​ 17 | Most of the embedded developments are made from scratch. By using Luos, you will be able to capitalize on the development you, your company, or the Luos community already did. The re-usability of features encapsulated in Luos services will fasten the time your products reach the market and reassure the robustness and the universality of your applications. 18 | 19 | * → Join the [Luos Discord server](http://discord.gg/luos) 20 | * → Join the [Luos subreddit](https://www.reddit.com/r/Luos/) 21 | 22 | ## Good practices with Luos​ 23 | Luos proposes organized and effective development practices, guaranteeing development flexibility and evolutivity of your hardware product, from the idea to the maintenance of the industrialized product fleet. 24 | 25 | ## Let's do this​ 26 | 27 | * → Try on your own with the [get started](https://www.luos.io/tutorials/get-started) 28 | * → Consult the full [documentation](https://www.luos.io/docs) 29 | -------------------------------------------------------------------------------- /examples/IMU.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# IMU Test" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "scrolled": false 15 | }, 16 | "outputs": [ 17 | { 18 | "data": { 19 | "application/vnd.jupyter.widget-view+json": { 20 | "model_id": "2c0a026014ce47b889b84af4994a6318", 21 | "version_major": 2, 22 | "version_minor": 0 23 | }, 24 | "text/plain": [ 25 | "Renderer(camera=PerspectiveCamera(aspect=2.0, position=(4.0, 12.0, 10.0), quaternion=(0.0, 0.0, 0.0, 1.0), sca…" 26 | ] 27 | }, 28 | "metadata": {}, 29 | "output_type": "display_data" 30 | }, 31 | { 32 | "name": "stdout", 33 | "output_type": "stream", 34 | "text": [ 35 | "Connected to \"/dev/cu.usbserial-DN2JWZ3D\".\n", 36 | "Sending detection signal.\n", 37 | "b'{\"detection\": {}}\\r'\n", 38 | "Waiting for routing table...\n", 39 | "Device setup.\n", 40 | "-------------------------------------------------\n", 41 | "Type Alias ID \n", 42 | "-------------------------------------------------\n", 43 | "Gate gate 1 \n", 44 | "Imu Imu_mod 2 \n", 45 | "\n" 46 | ] 47 | } 48 | ], 49 | "source": [ 50 | "from pyluos import Device\n", 51 | "import time\n", 52 | "from pythreejs import *\n", 53 | "\n", 54 | "# Create a cube to move following our sensor\n", 55 | "cube = Mesh(\n", 56 | " BoxBufferGeometry(3, 3, 3),\n", 57 | " MeshPhysicalMaterial(color='green'),\n", 58 | " position=[0, 0, 0],\n", 59 | " castShadow = True\n", 60 | ")\n", 61 | "\n", 62 | "# Create a floor\n", 63 | "plane = Mesh(\n", 64 | " PlaneBufferGeometry(100, 100),\n", 65 | " MeshPhysicalMaterial(color='gray'),\n", 66 | " position=[0, -1.5, 0], receiveShadow = True)\n", 67 | "plane.rotation = (-3.14/2, 0, 0, 'XYZ')\n", 68 | "\n", 69 | "# Create a directional ligt folowing our cube\n", 70 | "key_light = SpotLight(position=[0, 10, 10], angle = 0.3, penumbra = 0.1, target = cube, castShadow = True)\n", 71 | "key_light.shadow.mapSize = (2048, 2048)\n", 72 | "\n", 73 | "# Create a camera\n", 74 | "c = PerspectiveCamera(position=[4, 12, 10], up=[0, 1, 0],\n", 75 | " aspect=800/400)\n", 76 | "\n", 77 | "# Create a scene\n", 78 | "scene = Scene(children=[plane, cube, c, key_light, AmbientLight()])\n", 79 | "\n", 80 | "# Display the scene with shadow and everything.\n", 81 | "renderer = Renderer(camera=c, \n", 82 | " scene=scene, \n", 83 | " controls=[OrbitControls(controlling=c)],\n", 84 | " width=800, height=400,\n", 85 | " )\n", 86 | "renderer.shadowMap.enabled = True\n", 87 | "renderer.shadowMap.type = 'PCFSoftShadowMap'\n", 88 | "display(renderer)\n", 89 | "\n", 90 | "# Connect your Luos network (here using an USB service)\n", 91 | "r = Device('/dev/cu.usbserial-DN2JWZ3D')\n", 92 | "print(r.services)\n", 93 | "\n", 94 | "# Control the rotation of the cube with the rotation of the Imu sensor\n", 95 | "while(True):\n", 96 | " cube.quaternion = r.Imu_mod.quaternion\n", 97 | " time.sleep(0.05)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 1, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "ename": "ValueError", 107 | "evalue": "No corresponding IO found (among ).", 108 | "output_type": "error", 109 | "traceback": [ 110 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 111 | "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", 112 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mIPython\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdisplay\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mdisplay\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0mr\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mRobot\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'/dev/cu.usbserial-DN38OIYT'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8\u001b[0m \u001b[0mr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mservices\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 113 | "\u001b[0;32m~/Documents/luos/pyluos/pyluos/robot.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, host, IO, log_conf, test_mode, *args, **kwargs)\u001b[0m\n\u001b[1;32m 58\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 59\u001b[0m self._io = io_from_host(host=host,\n\u001b[0;32m---> 60\u001b[0;31m *args, **kwargs)\n\u001b[0m\u001b[1;32m 61\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 62\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mos\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mexists\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlog_conf\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 114 | "\u001b[0;32m~/Documents/luos/pyluos/pyluos/io/__init__.py\u001b[0m in \u001b[0;36mio_from_host\u001b[0;34m(host, *args, **kwargs)\u001b[0m\n\u001b[1;32m 55\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mcls\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mhost\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mhost\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 56\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 57\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'No corresponding IO found (among {}).'\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdiscover_hosts\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 58\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 59\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", 115 | "\u001b[0;31mValueError\u001b[0m: No corresponding IO found (among )." 116 | ] 117 | } 118 | ], 119 | "source": [ 120 | "from pyluos import Robot\n", 121 | "import time\n", 122 | "from pythreejs import *\n", 123 | "import ipywidgets\n", 124 | "from IPython.display import display\n", 125 | "\n", 126 | "r = Robot('/dev/cu.usbserial-DN38OIYT')\n", 127 | "r.services\n", 128 | "\n", 129 | "cube = Mesh(\n", 130 | " BoxBufferGeometry(3, 3, 3),\n", 131 | " MeshPhysicalMaterial(color='green'),\n", 132 | " position=[0, 0, 0],\n", 133 | " castShadow = True\n", 134 | ")\n", 135 | "\n", 136 | "plane = Mesh(\n", 137 | " PlaneBufferGeometry(100, 100),\n", 138 | " MeshPhysicalMaterial(color='gray'),\n", 139 | " position=[0, -1.5, 0], receiveShadow = True)\n", 140 | "plane.rotation = (-3.14/2, 0, 0, 'XYZ')\n", 141 | "\n", 142 | "\n", 143 | "key_light = SpotLight(position=[0, 10, 10], angle = 0.3, penumbra = 0.1, target = cube, castShadow = True)\n", 144 | "key_light.shadow.mapSize = (2048, 2048)\n", 145 | "\n", 146 | "c = PerspectiveCamera(position=[4, 12, 10], up=[0, 1, 0],\n", 147 | " aspect=800/400)\n", 148 | "\n", 149 | "scene = Scene(children=[plane, cube, c, key_light, AmbientLight()])\n", 150 | "\n", 151 | "renderer = Renderer(camera=c, \n", 152 | " scene=scene, \n", 153 | " controls=[OrbitControls(controlling=c)],\n", 154 | " width=800, height=400,\n", 155 | " )\n", 156 | "renderer.shadowMap.enabled = True\n", 157 | "renderer.shadowMap.type = 'PCFSoftShadowMap'\n", 158 | "display(renderer)" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": 2, 164 | "metadata": {}, 165 | "outputs": [ 166 | { 167 | "data": { 168 | "application/vnd.jupyter.widget-view+json": { 169 | "model_id": "31e3fef3eb03429fbe06bf1c8ff38bb1", 170 | "version_major": 2, 171 | "version_minor": 0 172 | }, 173 | "text/plain": [ 174 | "interactive(children=(Checkbox(value=False, description='accel'), Checkbox(value=False, description='gyro'), C…" 175 | ] 176 | }, 177 | "metadata": {}, 178 | "output_type": "display_data" 179 | }, 180 | { 181 | "data": { 182 | "text/plain": [ 183 | ".change_config(accel, gyro, quat, compass, euler, rot_mat, pedo, linear_accel, gravity_vector, heading)>" 184 | ] 185 | }, 186 | "execution_count": 2, 187 | "metadata": {}, 188 | "output_type": "execute_result" 189 | }, 190 | { 191 | "name": "stdout", 192 | "output_type": "stream", 193 | "text": [ 194 | "{'services': defaultdict(. at 0x11949f2f0>, {'Imu_mod': defaultdict(... at 0x1122a5158>, {'imu_enable': 4})})}\n" 195 | ] 196 | }, 197 | { 198 | "name": "stderr", 199 | "output_type": "stream", 200 | "text": [ 201 | "Exception in thread Thread-4:\n", 202 | "Traceback (most recent call last):\n", 203 | " File \"/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py\", line 916, in _bootstrap_inner\n", 204 | " self.run()\n", 205 | " File \"/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py\", line 864, in run\n", 206 | " self._target(*self._args, **self._kwargs)\n", 207 | " File \"/Users/nicolasrabault/Documents/luos/pyluos/pyluos/io/serial_io.py\", line 100, in _poll\n", 208 | " to_read = self._serial.in_waiting\n", 209 | " File \"/Users/nicolasrabault/.virtualenvs/luos/lib/python3.6/site-packages/serial/serialposix.py\", line 467, in in_waiting\n", 210 | " s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str)\n", 211 | "OSError: [Errno 6] Device not configured\n", 212 | "\n" 213 | ] 214 | } 215 | ], 216 | "source": [ 217 | "r.Imu_mod.control()" 218 | ] 219 | }, 220 | { 221 | "cell_type": "markdown", 222 | "metadata": {}, 223 | "source": [ 224 | "## Quaternion Test" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": {}, 231 | "outputs": [ 232 | { 233 | "name": "stdout", 234 | "output_type": "stream", 235 | "text": [ 236 | "{'services': defaultdict(. at 0x10daaff28>, {'Imu_mod': defaultdict(... at 0x10b22f598>, {'imu_enable': 4})})}\n" 237 | ] 238 | }, 239 | { 240 | "name": "stderr", 241 | "output_type": "stream", 242 | "text": [ 243 | "Exception in thread Thread-4:\n", 244 | "Traceback (most recent call last):\n", 245 | " File \"/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py\", line 916, in _bootstrap_inner\n", 246 | " self.run()\n", 247 | " File \"/usr/local/Cellar/python/3.6.5_1/Frameworks/Python.framework/Versions/3.6/lib/python3.6/threading.py\", line 864, in run\n", 248 | " self._target(*self._args, **self._kwargs)\n", 249 | " File \"/Users/nicolasrabault/Documents/luos/pyluos/pyluos/io/serial_io.py\", line 100, in _poll\n", 250 | " to_read = self._serial.in_waiting\n", 251 | " File \"/Users/nicolasrabault/.virtualenvs/luos/lib/python3.6/site-packages/serial/serialposix.py\", line 467, in in_waiting\n", 252 | " s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str)\n", 253 | "OSError: [Errno 6] Device not configured\n", 254 | "\n" 255 | ] 256 | } 257 | ], 258 | "source": [ 259 | "while(True):\n", 260 | " cube.quaternion = r.Imu_mod.quaternion\n", 261 | " time.sleep(0.05)" 262 | ] 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "metadata": {}, 267 | "source": [ 268 | "## Heading Test" 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": null, 274 | "metadata": {}, 275 | "outputs": [ 276 | { 277 | "name": "stdout", 278 | "output_type": "stream", 279 | "text": [ 280 | "[False, False, False, False, False, False, False, True, False, False]\n", 281 | "[True, False, False, False, False, False, False, True, False, False]\n", 282 | "{'services': defaultdict(. at 0x116670268>, {'Imu_mod': defaultdict(... at 0x1166706a8>, {'imu_enable': 516})})}\n" 283 | ] 284 | } 285 | ], 286 | "source": [ 287 | "r.Imu_mod.heading = True\n", 288 | "time.sleep(0.1)\n", 289 | "lastheading = r.Imu_mod.heading\n", 290 | "while(True):\n", 291 | " cube.rotateZ(((lastheading - r.Imu_mod.heading) * 2.0 * 3.14)/360.0)\n", 292 | " lastheading = r.Imu_mod.heading\n", 293 | " time.sleep(0.05)" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "metadata": {}, 299 | "source": [ 300 | "## acceleration test" 301 | ] 302 | }, 303 | { 304 | "cell_type": "code", 305 | "execution_count": null, 306 | "metadata": {}, 307 | "outputs": [], 308 | "source": [ 309 | "r.Imu_mod.acceleration = True\n", 310 | "time.sleep(0.1)\n", 311 | "while(True):\n", 312 | " cube.scale = r.Imu_mod.acceleration\n", 313 | " time.sleep(0.05)" 314 | ] 315 | }, 316 | { 317 | "cell_type": "markdown", 318 | "metadata": {}, 319 | "source": [ 320 | "## Gravity Test" 321 | ] 322 | }, 323 | { 324 | "cell_type": "code", 325 | "execution_count": null, 326 | "metadata": {}, 327 | "outputs": [], 328 | "source": [ 329 | "r.Imu_mod.gravity_vector = True\n", 330 | "time.sleep(0.1)\n", 331 | "while(True):\n", 332 | " cube.scale = [i/9.81 for i in r.Imu_mod.gravity_vector]\n", 333 | " time.sleep(0.05)" 334 | ] 335 | }, 336 | { 337 | "cell_type": "markdown", 338 | "metadata": {}, 339 | "source": [ 340 | "## Translation test" 341 | ] 342 | }, 343 | { 344 | "cell_type": "code", 345 | "execution_count": null, 346 | "metadata": {}, 347 | "outputs": [], 348 | "source": [ 349 | "r.Imu_mod.linear_acceleration = True\n", 350 | "speed = i * 5 for i in my_list]\n", 351 | "time.sleep(0.1)\n", 352 | "while(True):\n", 353 | " cube.scale = [i/9.81 for i in r.Imu_mod.gravity_vector]\n", 354 | " time.sleep(0.05)" 355 | ] 356 | }, 357 | { 358 | "cell_type": "code", 359 | "execution_count": 2, 360 | "metadata": {}, 361 | "outputs": [ 362 | { 363 | "name": "stdout", 364 | "output_type": "stream", 365 | "text": [ 366 | "{'services': defaultdict(. at 0x113732048>, {'Imu_mod': defaultdict(... at 0x10c537510>, {'imu_enable': 132})})}\n" 367 | ] 368 | }, 369 | { 370 | "data": { 371 | "text/plain": [ 372 | "[0.0521267440000001, -0.01810001200000005, 0.0489912150000001]" 373 | ] 374 | }, 375 | "execution_count": 2, 376 | "metadata": {}, 377 | "output_type": "execute_result" 378 | } 379 | ], 380 | "source": [ 381 | "#bias computing\n", 382 | "r.Imu_mod.linear_acceleration = True\n", 383 | "time.sleep(0.1)\n", 384 | "bias = [0,0,0]\n", 385 | "for i in range(1000):\n", 386 | " bias = [i + y for (i, y) in zip(r.Imu_mod.linear_acceleration, bias)]\n", 387 | " time.sleep(0.01)\n", 388 | "\n", 389 | "bias = [i / 1000 for i in bias]\n", 390 | "bias" 391 | ] 392 | } 393 | ], 394 | "metadata": { 395 | "kernelspec": { 396 | "display_name": "Python 2", 397 | "language": "python", 398 | "name": "python2" 399 | }, 400 | "language_info": { 401 | "codemirror_mode": { 402 | "name": "ipython", 403 | "version": 3 404 | }, 405 | "file_extension": ".py", 406 | "mimetype": "text/x-python", 407 | "name": "python", 408 | "nbconvert_exporter": "python", 409 | "pygments_lexer": "ipython3", 410 | "version": "3.6.5" 411 | } 412 | }, 413 | "nbformat": 4, 414 | "nbformat_minor": 2 415 | } 416 | -------------------------------------------------------------------------------- /examples/tools/Motor_PID.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%matplotlib notebook\n", 10 | "from pyluos import Device\n", 11 | "from IPython.display import clear_output\n", 12 | "import time\n", 13 | "import matplotlib\n", 14 | "import matplotlib.pyplot as plt\n", 15 | "import numpy as np\n", 16 | "from scipy.interpolate import interp1d\n", 17 | "\n", 18 | "# 1. Connect your Luos network (here using an USB service for example)\n", 19 | "r = Device('/dev/cu.usbserial-DN05NM1N')\n", 20 | "print(r.services)\n", 21 | "\n", 22 | "# 2. Select the service of your network you need to configure\n", 23 | "service = r.controlled_moto\n", 24 | "\n", 25 | "# 3. Setup service basic settings\n", 26 | "service.encoder_res = 3\n", 27 | "service.reduction = 210.59\n", 28 | "\n", 29 | "SAMPLERATE = 1.0/service.sampling_freq\n", 30 | "\n", 31 | "def run_speed_test(velocity_target):\n", 32 | " service.rot_position = False\n", 33 | " service.rot_speed = True\n", 34 | " service.current = True\n", 35 | " service.rot_position_mode = False\n", 36 | " service.rot_speed_mode = True\n", 37 | " service.target_rot_speed = 0.0\n", 38 | " service.compliant = False\n", 39 | " target = []\n", 40 | " real = []\n", 41 | " current = []\n", 42 | " test_time_vector = []\n", 43 | " test_start_time = time.time()\n", 44 | " target.append(service.target_rot_speed)\n", 45 | " real.append(service.rot_speed)\n", 46 | " current.append(service.current)\n", 47 | " test_time = time.time()\n", 48 | " test_time_vector.append(0.0)\n", 49 | " while (test_time < test_start_time + 0.5):\n", 50 | " time.sleep(SAMPLERATE)\n", 51 | " target.append(service.target_rot_speed)\n", 52 | " real.append(service.rot_speed)\n", 53 | " current.append(service.current)\n", 54 | " test_time_vector.append(test_time - test_start_time)\n", 55 | " test_time = time.time()\n", 56 | " service.target_rot_speed = velocity_target\n", 57 | " while (test_time < test_start_time + 2.5):\n", 58 | " time.sleep(SAMPLERATE)\n", 59 | " target.append(service.target_rot_speed)\n", 60 | " real.append(service.rot_speed)\n", 61 | " current.append(service.current)\n", 62 | " test_time_vector.append(test_time - test_start_time)\n", 63 | " test_time = time.time()\n", 64 | " service.compliant = True\n", 65 | " plot_test(test_time_vector, target, real, current)\n", 66 | "\n", 67 | "def run_pos_test(pos_target):\n", 68 | " service.rot_speed = False\n", 69 | " service.rot_position = True\n", 70 | " service.current = True\n", 71 | " service.rot_speed_mode = False\n", 72 | " service.rot_position_mode = True\n", 73 | " service.target_rot_position = 0.0\n", 74 | " service.compliant = False\n", 75 | " target = []\n", 76 | " real = []\n", 77 | " current = []\n", 78 | " test_time_vector = []\n", 79 | " test_start_time = time.time()\n", 80 | " target.append(service.target_rot_position)\n", 81 | " real.append(service.rot_position)\n", 82 | " current.append(service.current)\n", 83 | " test_time = time.time()\n", 84 | " test_time_vector.append(0.0)\n", 85 | " while (test_time < test_start_time + 1):\n", 86 | " time.sleep(SAMPLERATE)\n", 87 | " target.append(service.target_rot_position)\n", 88 | " real.append(service.rot_position)\n", 89 | " current.append(service.current)\n", 90 | " test_time_vector.append(test_time - test_start_time)\n", 91 | " test_time = time.time()\n", 92 | " service.target_rot_position = pos_target\n", 93 | " while (test_time < test_start_time + 2.5):\n", 94 | " time.sleep(SAMPLERATE)\n", 95 | " target.append(service.target_rot_position)\n", 96 | " real.append(service.rot_position)\n", 97 | " current.append(service.current)\n", 98 | " test_time_vector.append(test_time - test_start_time)\n", 99 | " test_time = time.time()\n", 100 | " \n", 101 | " # create a smooth trajectory\n", 102 | " moveduration = 2\n", 103 | " keypoints = np.array([90, 4, -10, -33, -87, -87, 10, -80, 0])\n", 104 | " x = np.linspace(0, 1, keypoints.shape[-1], endpoint=True)\n", 105 | " traj = interp1d(x, keypoints, 'cubic')(np.linspace(0, 1, int(moveduration*service.sampling_freq)))\n", 106 | " #send traj to motor\n", 107 | " service.target_rot_position = traj\n", 108 | " # wait a bit for the motor to start\n", 109 | " time.sleep(0.03)\n", 110 | " traj_start_time = time.time()\n", 111 | " for i, sample in enumerate(traj):\n", 112 | " target.append(sample)\n", 113 | " real.append(service.rot_position)\n", 114 | " current.append(service.current)\n", 115 | " test_time_vector.append(test_time - test_start_time)\n", 116 | " #time.sleep(1.0/service.sampling_freq)\n", 117 | " while(time.time() < traj_start_time + SAMPLERATE*(i+1)):\n", 118 | " time.sleep(0.004)\n", 119 | " test_time = time.time()\n", 120 | " traj_start_time = time.time()\n", 121 | " test_time = time.time()\n", 122 | " while (test_time < traj_start_time + 0.5):\n", 123 | " time.sleep(SAMPLERATE)\n", 124 | " target.append(traj[len(traj)-1])\n", 125 | " real.append(service.rot_position)\n", 126 | " current.append(service.current)\n", 127 | " test_time_vector.append(test_time - test_start_time)\n", 128 | " test_time = time.time()\n", 129 | " service.compliant = True\n", 130 | " plot_test(test_time_vector, target, real, current)\n", 131 | "\n", 132 | "def plot_test(test_time_vector, target, real, current):\n", 133 | " fig = plt.figure()\n", 134 | " ax = plt.subplot(111)\n", 135 | " ax.set_xlabel('Time (s)')\n", 136 | " ax.plot(test_time_vector,target,'r', label='Target')\n", 137 | " ax.plot(test_time_vector,real,'b', label='Real')\n", 138 | " ax.legend(loc='upper left')\n", 139 | " ax1 = ax.twinx()\n", 140 | " ax1.set_ylabel('Current (A)')\n", 141 | " ax1.plot(test_time_vector,current,'g', label='Current')\n", 142 | " ax1.tick_params(axis='y', labelcolor='g')\n", 143 | " ax1.legend(loc='upper right')\n", 144 | " plt.show()\n", 145 | " \n", 146 | " #fig2 = plt.figure()\n", 147 | " #ax = plt.subplot(111)\n", 148 | " #plt.show()\n", 149 | " \n", 150 | "#motor wiring test\n", 151 | "def wiring_test():\n", 152 | " service.setToZero()\n", 153 | " service.power_mode = True\n", 154 | " service.compliant = False\n", 155 | " service.power_ratio = 100.0\n", 156 | " time.sleep(0.5)\n", 157 | " service.power_ratio = 0\n", 158 | " service.compliant = True\n", 159 | " if (service.rot_position > 1):\n", 160 | " print(\"Connection OK\")\n", 161 | " service.encoder_res = 3\n", 162 | " service.reduction = 150.0\n", 163 | " service.positionPid = [4.0,0.02,100] # position PID [P, I, D]\n", 164 | " service.setToZero()\n", 165 | " time.sleep(0.1)\n", 166 | " service.rot_position_mode = True\n", 167 | " service.compliant = False\n", 168 | " service.target_rot_position = 90\n", 169 | " time.sleep(1)\n", 170 | " service.compliant = True\n", 171 | " if (service.rot_position > 80) :\n", 172 | " print (\"Sensor direction OK\")\n", 173 | " print (\"Motor OK\")\n", 174 | " else : \n", 175 | " print(\"Sensor direction not ok. Try to inverse your A and B signal of your encoder.\")\n", 176 | " else :\n", 177 | " print(\"Connection not OK. If the motor moved plese check your sensor connection.\")" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "# test motor connections\n", 187 | "wiring_test()" 188 | ] 189 | }, 190 | { 191 | "cell_type": "code", 192 | "execution_count": null, 193 | "metadata": {}, 194 | "outputs": [], 195 | "source": [ 196 | "# Speed settings\n", 197 | "service.speedPid = [0.1,0.1,1.0] # speed PID [P, I, D]\n", 198 | "run_speed_test(200.0)" 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": null, 204 | "metadata": { 205 | "scrolled": false 206 | }, 207 | "outputs": [], 208 | "source": [ 209 | "# position settings\n", 210 | "service.positionPid = [3.0, 0.02, 90] # position PID [P, I, D]\n", 211 | "run_pos_test(90.0)" 212 | ] 213 | } 214 | ], 215 | "metadata": { 216 | "kernelspec": { 217 | "display_name": "Python 3", 218 | "language": "python", 219 | "name": "python3" 220 | }, 221 | "language_info": { 222 | "codemirror_mode": { 223 | "name": "ipython", 224 | "version": 3 225 | }, 226 | "file_extension": ".py", 227 | "mimetype": "text/x-python", 228 | "name": "python", 229 | "nbconvert_exporter": "python", 230 | "pygments_lexer": "ipython3", 231 | "version": "3.6.5" 232 | } 233 | }, 234 | "nbformat": 4, 235 | "nbformat_minor": 2 236 | } 237 | -------------------------------------------------------------------------------- /pyluos/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .device import Device 4 | from .services import * 5 | 6 | nh = logging.NullHandler() 7 | logging.getLogger(__name__).addHandler(nh) 8 | -------------------------------------------------------------------------------- /pyluos/device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import json 5 | import time 6 | import uuid 7 | import logging 8 | import requests 9 | import threading 10 | import logging.config 11 | import numpy as np 12 | 13 | from datetime import datetime 14 | from collections import defaultdict 15 | 16 | from .io import discover_hosts, io_from_host, Ws 17 | from .services import name2mod 18 | 19 | from anytree import AnyNode, RenderTree, DoubleStyle 20 | 21 | 22 | def run_from_unittest(): 23 | return 'unittest' in sys.services 24 | 25 | 26 | class contList(list): 27 | def __repr__(self): 28 | s = '-------------------------------------------------\n' 29 | s += '{:<20s}{:<20s}{:<5s}\n'.format("Type", "Alias", "ID") 30 | s += '-------------------------------------------------\n' 31 | for elem in self: 32 | s += '{:<20s}{:<20s}{:<5d}\n'.format(elem.type, elem.alias, elem.id) 33 | return s 34 | 35 | 36 | class nodeList(list): 37 | def __repr__(self): 38 | # Display the topology 39 | s = '' 40 | prefill = '' 41 | prechild = False 42 | for pre, fill, node in RenderTree(self[0], style=DoubleStyle()): 43 | # Draw the input part 44 | if (node.parent == None): 45 | branch = " ┃ " 46 | else: 47 | branch = "═■┫ " 48 | 49 | # Draw the node body 50 | prefill = (prefill[:len(fill)]) if len(prefill) > len(fill) else prefill 51 | s += '{:<{fillsize}s}'.format(prefill, fillsize=len(fill)) 52 | if (prechild == True): 53 | s = s[:-4] + '║' + s[-4 + 1:] 54 | s += '{:<54s}'.format(" ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n") 55 | tmpstr = '{:<52s}'.format("%s╭────────────────── Node %s ──────────────────" % (branch, node.id)) 56 | 57 | if (len(pre) > 0): 58 | pre = pre[:-1] + "═" 59 | s += pre + tmpstr + '{:>3s}'.format("┃\n") 60 | s += fill + " ┃ │ " + '{:<20s}{:<20s}{:<4s}'.format("Type", "Alias", "ID") + '{:>3s}'.format("┃\n") 61 | for y, elem in enumerate(node.services): 62 | if (y == (len(node.services) - 1)): 63 | s += fill + " ┃ ╰> " + '{:<20s}{:<20s}{:<4d}'.format(elem.type, elem.alias, elem.id) + '{:>3s}'.format("┃\n") 64 | else: 65 | s += fill + " ┃ ├> " + '{:<20s}{:<20s}{:<4d}'.format(elem.type, elem.alias, elem.id) + '{:>3s}'.format("┃\n") 66 | 67 | # Draw the output part 68 | if (node.children): 69 | s += fill + "╔■┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n" 70 | prechild = True 71 | else: 72 | s += fill + " ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n" 73 | prechild = False 74 | prefill = fill 75 | return s 76 | 77 | 78 | class Device(object): 79 | _heartbeat_timeout = 5 # in sec. 80 | _max_alias_length = 15 81 | _base_log_conf = os.path.join(os.path.dirname(__file__), 82 | 'logging_conf.json') 83 | _freedomLink = None 84 | 85 | def __init__(self, host, 86 | IO=None, 87 | log_conf=_base_log_conf, 88 | test_mode=False, 89 | background_task=True, 90 | *args, **kwargs): 91 | if IO is not None: 92 | self._io = IO(host=host, *args, **kwargs) 93 | else: 94 | self._io = io_from_host(host=host, 95 | *args, **kwargs) 96 | 97 | if os.path.exists(log_conf): 98 | with open(log_conf) as f: 99 | config = json.load(f) 100 | logging.config.dictConfig(config) 101 | 102 | self.logger = logging.getLogger(__name__) 103 | self.logger.info('Connected to "{}".'.format(host)) 104 | 105 | self._send_lock = threading.Lock() 106 | self._cmd_lock = threading.Lock() 107 | 108 | # We force a first poll to setup our model. 109 | self._setup() 110 | self.logger.info('Device setup.') 111 | 112 | self._last_update = time.time() 113 | self._running = True 114 | self._pause = False 115 | 116 | if (background_task == True): 117 | # Setup both poll/push synchronization loops. 118 | self._poll_bg = threading.Thread(target=self._poll_and_up) 119 | self._poll_bg.daemon = True 120 | self._poll_bg.start() 121 | 122 | def close(self): 123 | self._running = False 124 | 125 | if hasattr(self, "_poll_bg"): 126 | self._poll_bg.join(timeout=2.0) 127 | 128 | if self._poll_bg.is_alive(): 129 | # _poll_bg didn't terminate within the timeout 130 | print("Warning: device closed on timeout, background thread is still running.") 131 | self._io.close() 132 | 133 | def link_to_freedomrobotics(self): 134 | from .integration.freedomRobotics import FreedomLink 135 | self._freedomLink = FreedomLink(self) 136 | 137 | def pause(self): 138 | self._pause = True 139 | time.sleep(1) 140 | 141 | def play(self): 142 | self._pause = False 143 | 144 | def _setup(self): 145 | self.logger.info('Sending detection signal.') 146 | self._send({}) 147 | time.sleep(0.01) 148 | self._send({'detection': {}}) 149 | self.logger.info('Waiting for routing table...') 150 | startTime = time.time() 151 | state = self._poll_once() 152 | retry = 0 153 | while ('routing_table' not in state): 154 | if ('route_table' in state): 155 | self.logger.info("Watch out the Luos revision you are using on your board is too old to work with this revision of pyluos.\n Please consider updating Luos on your boards") 156 | return 157 | state = self._poll_once() 158 | if (time.time() - startTime > 5): 159 | retry = retry + 1 160 | if retry > 5: 161 | # detection is not working 162 | sys.exit("Detection failed.") 163 | self._send({'detection': {}}) 164 | startTime = time.time() 165 | # Save routing table data 166 | self._routing_table = state 167 | # Create nodes 168 | self._services = [] 169 | self._nodes = [] 170 | for i, node in enumerate(state['routing_table']): 171 | if ('node_id' not in node): 172 | self.logger.info("Watch out the Luos revision you are using on your board is too old to work with this revision of pyluos.\n Please consider updating Luos on your boards") 173 | parent_elem = None 174 | # find a parent and create the link 175 | if (node["con"]["parent"][0] != 0): 176 | parent_id = node["con"]["parent"][0] 177 | for elem in self._nodes: 178 | if (elem.id == parent_id): 179 | parent_elem = elem 180 | break 181 | # create the node 182 | self._nodes.append(AnyNode(id=node["node_id"], parent=parent_elem, connection=node["con"])) 183 | 184 | filtered_services = contList([mod for mod in node["services"] 185 | if 'type' in mod and mod['type'] in name2mod.keys()]) 186 | # Create a list of services in the node 187 | self._nodes[i].services = [ 188 | name2mod[mod['type']](id=mod['id'], 189 | alias=mod['alias'], 190 | device=self) 191 | for mod in filtered_services 192 | if 'type' in mod and 'id' in mod and 'alias' in mod 193 | ] 194 | # Create a list of services of the entire device 195 | self._services = self._services + self._nodes[i].services 196 | for mod in self._nodes[i].services: 197 | setattr(self, mod.alias, mod) 198 | 199 | self._cmd = defaultdict(lambda: defaultdict(lambda: None)) 200 | self._cmd_data = [] 201 | self._binary = [] 202 | 203 | # We push our current state to make sure that 204 | # both our model and the hardware are synced. 205 | self._push_once() 206 | 207 | @property 208 | def services(self): 209 | return contList(self._services) 210 | 211 | @property 212 | def nodes(self): 213 | return nodeList(self._nodes) 214 | 215 | # Poll state from hardware. 216 | def _poll_once(self): 217 | self._state = self._io.read() 218 | if self._state != []: 219 | self._state['timestamp'] = time.time() 220 | return self._state 221 | return [] 222 | 223 | def _poll_and_up(self): 224 | while self._running: 225 | if not self._pause: 226 | state = self._poll_once() 227 | if self._state != []: 228 | self._update(state) 229 | self._push_once() 230 | else: 231 | time.sleep(0.1) 232 | 233 | # Update our model with the new state. 234 | def _update(self, new_state): 235 | if 'dead_service' in new_state.keys(): 236 | # We have lost a service put a flag on this service 237 | service_id = new_state['dead_service'] 238 | # Find the service. 239 | for service in self._services: 240 | if (service.id == service_id): 241 | s = "************************* EXCLUSION *************************\n" 242 | s += "* Service " + str(service.alias) + " have been excluded from the network due to no responses." 243 | s += "\n*************************************************************" 244 | print(s) 245 | if (self._freedomLink != None): 246 | self._freedomLink._kill(service.alias) 247 | service._kill() 248 | break 249 | 250 | if 'dead_node' in new_state.keys(): 251 | # We have lost a node put a flag on all node services 252 | node_id = new_state['dead_node'] 253 | for node in self._nodes: 254 | if (node.id == node_id): 255 | s = "************************* EXCLUSION *************************\n" 256 | s += "* Node " + str(service.alias) + "have been excluded from the network due to no responses." 257 | s += "\nThis exclude all services from this node :" 258 | for service in node.services: 259 | if (self._freedomLink != None): 260 | self._freedomLink._kill(service.alias) 261 | service._kill() 262 | s += "\n* Service " + str(service.alias) + " have been excluded from the network due to no responses." 263 | 264 | s += "\n*************************************************************" 265 | print(s) 266 | break 267 | 268 | if 'assert' in new_state.keys(): 269 | # A node assert, print assert informations 270 | if (('node_id' in new_state['assert']) and ('file' in new_state['assert']) and ('line' in new_state['assert'])): 271 | s = "************************* ASSERT *************************\n" 272 | s += "* Node " + str(new_state['assert']['node_id']) + " assert in file " + new_state['assert']['file'] + " line " + str(new_state['assert']['line']) 273 | s += "\n**********************************************************" 274 | print(s) 275 | # Consider this service as dead. 276 | # Find the service from it's node id. 277 | for node in self._nodes: 278 | if (node.id == new_state['assert']['node_id']): 279 | for service in node.services: 280 | service._kill() 281 | break 282 | if (self._freedomLink != None): 283 | self._freedomLink._assert(alias) 284 | if 'services' not in new_state.keys(): 285 | return 286 | 287 | for alias, mod in new_state['services'].items(): 288 | if hasattr(self, alias): 289 | getattr(self, alias)._update(mod) 290 | if (self._freedomLink != None): 291 | self._freedomLink._update(alias, mod) 292 | 293 | self._last_update = time.time() 294 | 295 | def update_cmd(self, alias, key, val): 296 | with self._cmd_lock: 297 | self._cmd[alias][key] = val 298 | 299 | def update_data(self, alias, key, val, data): 300 | with self._cmd_lock: 301 | self._cmd_data.append({alias: {key: val}}) 302 | self._binary.append(data.tobytes()) 303 | 304 | def _push_once(self): 305 | with self._cmd_lock: 306 | if self._cmd: 307 | self._write(json.dumps({'services': self._cmd}).encode()) 308 | self._cmd = defaultdict(lambda: defaultdict(lambda: None)) 309 | for cmd, binary in zip(self._cmd_data, self._binary): 310 | time.sleep(0.01) 311 | self._write(json.dumps({'services': cmd}).encode() + '\n'.encode() + binary) 312 | 313 | self._cmd_data = [] 314 | self._binary = [] 315 | 316 | def _send(self, msg): 317 | with self._send_lock: 318 | self._io.send(msg) 319 | 320 | def _write(self, data): 321 | with self._send_lock: 322 | self._io.write(data) 323 | -------------------------------------------------------------------------------- /pyluos/integration/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pyluos/integration/freedomRobotics.py: -------------------------------------------------------------------------------- 1 | from freedomrobotics.link import Link 2 | 3 | import logging 4 | import time 5 | 6 | class FreedomLink(object): 7 | def __init__(self, device): 8 | self._link = Link("core", command_callback=self.callback) 9 | self._delegate = device 10 | 11 | def callback(msg): 12 | print("I receive:" + str(msg) ) 13 | self._link.log("info", "I heard " + str(msg) ) 14 | # Topic represent services. 15 | if hasattr(self._delegate, msg["topic"][1:]): 16 | service = getattr(self._delegate, msg["topic"][1:]) 17 | print ("we have this service") 18 | # We have this service. 19 | if hasattr(service, msg["message"]): 20 | service_data = getattr(service, msg["message"]) 21 | service_data = msg["message"][msg["message"]] 22 | 23 | def _update(self, alias, new_state): 24 | if 'io_state' in new_state: 25 | self._link.message("/" + alias + "/io_state", \ 26 | "sensor_msgs/Joy", \ 27 | {"io_state": new_state['io_state']}) 28 | if 'temperature' in new_state: 29 | self._link.message("/" + alias + "/temperature", \ 30 | "sensor_msgs/Temperature", \ 31 | {"temperature": new_state['temperature']}) 32 | if 'lux' in new_state: 33 | self._link.message("/" + alias + "/lux", \ 34 | "sensor_msgs/Illuminance", \ 35 | {"illuminance": new_state['lux']}) 36 | if 'rot_position' in new_state: 37 | self._link.message("/" + alias + "/rot_position", \ 38 | "sensor_msgs/JointState", \ 39 | {"position": new_state['rot_position'], "name":alias}) 40 | if 'trans_position' in new_state: 41 | self._link.message("/" + alias + "/trans_position", \ 42 | "sensor_msgs/Range", \ 43 | {"range": new_state['trans_position']}) 44 | if 'rot_speed' in new_state: 45 | self._link.message("/" + alias + "/rot_speed", \ 46 | "sensor_msgs/JointState", \ 47 | {"velocity": new_state['rot_speed'], "name":alias}) 48 | if 'trans_speed' in new_state: 49 | self._link.message("/" + alias + "/trans_speed", \ 50 | "sensor_msgs/JointState", \ 51 | {"velocity": new_state['trans_speed'], "name":alias}) 52 | if 'force' in new_state: 53 | self._link.message("/" + alias + "/force", \ 54 | "sensor_msgs/JointState", \ 55 | {"effort": new_state['force'], "name":alias}) 56 | if 'current' in new_state: 57 | self._link.message("/" + alias + "/current", \ 58 | "sensor_msgs/BatteryState", \ 59 | {"current": new_state['current']}) 60 | if 'volt' in new_state: 61 | self._link.message("/" + alias + "/volt", \ 62 | "sensor_msgs/BatteryState", \ 63 | {"voltage": new_state['volt']}) 64 | if 'quaternion' in new_state: 65 | self._link.message("/" + alias + "/quaternion", \ 66 | "geometry_msgs/Quaternion", \ 67 | {"y": new_state['quaternion'][0],"x": new_state['quaternion'][1],"z": new_state['quaternion'][2],"w": new_state['quaternion'][3]}) 68 | if 'linear_accel' in new_state: 69 | self._link.message("/" + alias + "/linear_accel", \ 70 | "geometry_msgs/Accel", \ 71 | {"linear": {"y": new_state['linear_accel'][0],"x": new_state['linear_accel'][1],"z": new_state['linear_accel'][2]}}) 72 | if 'accel' in new_state: 73 | self._link.message("/" + alias + "/accel", \ 74 | "geometry_msgs/Accel", \ 75 | {"angular": {"y": new_state['accel'][0],"x": new_state['accel'][1],"z": new_state['accel'][2]}}) 76 | if 'gyro' in new_state: 77 | self._link.message("/" + alias + "/gyro", \ 78 | "geometry_msgs/Vector3", \ 79 | {"y": new_state['gyro'][0],"x": new_state['gyro'][1],"z": new_state['gyro'][2]}) 80 | if 'euler' in new_state: 81 | self._link.message("/" + alias + "/euler", \ 82 | "geometry_msgs/Vector3", \ 83 | {"y": new_state['euler'][0],"x": new_state['euler'][1],"z": new_state['euler'][2]}) 84 | if 'compass' in new_state: 85 | self._link.message("/" + alias + "/compass", \ 86 | "geometry_msgs/Vector3", \ 87 | {"y": new_state['compass'][0],"x": new_state['compass'][1],"z": new_state['compass'][2]}) 88 | if 'gravity_vector' in new_state: 89 | self._link.message("/" + alias + "/gravity", \ 90 | "geometry_msgs/Vector3", \ 91 | {"y": new_state['gravity_vector'][0],"x": new_state['gravity_vector'][1],"z": new_state['gravity_vector'][2]}) 92 | 93 | # I don't know what to do with those ones : 94 | # if 'rotational_matrix' in new_state: 95 | # self._rotational_matrix = new_state['rotational_matrix'] 96 | # if 'pedometer' in new_state: 97 | # self._pedometer = new_state['pedometer'] 98 | # if 'walk_time' in new_state: 99 | # self._walk_time = new_state['walk_time'] 100 | 101 | # if 'heading' in new_state: 102 | # self._heading = new_state['heading'] 103 | # if 'revision' in new_state: 104 | # self._firmware_revision = new_state['revision'] 105 | # if 'luos_revision' in new_state: 106 | # self._luos_revision = new_state['luos_revision'] 107 | # if 'luos_statistics' in new_state: 108 | # self._luos_statistics = new_state['luos_statistics'] 109 | 110 | 111 | def _kill(self, alias): 112 | print ("service", alias, "have been excluded from the network due to no responses.") 113 | 114 | def _assert(self, alias): 115 | print ("service", alias, "assert.") 116 | -------------------------------------------------------------------------------- /pyluos/io/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | 5 | class IOHandler(object): 6 | @classmethod 7 | def is_host_compatible(cls, host): 8 | return False 9 | 10 | def __init__(self, host): 11 | raise NotImplementedError 12 | 13 | def is_ready(self): 14 | raise NotImplementedError 15 | 16 | def read(self, trials=5): 17 | try: 18 | data = self.recv() 19 | return self.loads(data) 20 | except Exception as e: 21 | logging.getLogger(__name__).debug('Msg read failed: {}'.format(str(e))) 22 | if trials == 0: 23 | raise e 24 | 25 | return self.read(trials - 1) 26 | 27 | def recv(self): 28 | raise NotImplementedError 29 | 30 | def send(self, msg): 31 | self.write(self.dumps(msg)) 32 | 33 | def write(self, data): 34 | self.write(data) 35 | 36 | def loads(self, data): 37 | if type(data) == bytes: 38 | data = data.decode() 39 | return json.loads(data) 40 | return [] 41 | 42 | def dumps(self, msg): 43 | return json.dumps(msg).encode() 44 | 45 | 46 | from .ws import Ws 47 | from .serial_io import Serial 48 | 49 | IOs = [Serial, Ws] 50 | 51 | 52 | def io_from_host(host, *args, **kwargs): 53 | for cls in IOs: 54 | if cls.is_host_compatible(host): 55 | return cls(host=host, **kwargs) 56 | 57 | raise ValueError('No corresponding IO found (among {}).'.format(discover_hosts)) 58 | 59 | 60 | def discover_hosts(): 61 | return sum([io.available_hosts() for io in IOs], []) 62 | -------------------------------------------------------------------------------- /pyluos/io/serial_io.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import os 4 | import json 5 | import time 6 | import serial as _serial 7 | import platform 8 | import sys 9 | import struct 10 | if sys.version_info >= (3, 0): 11 | import queue 12 | else: 13 | import Queue as queue 14 | 15 | 16 | from threading import Event, Thread 17 | 18 | from serial.tools.list_ports import comports 19 | 20 | from . import IOHandler 21 | 22 | try: 23 | JSONDecodeError = json.decoder.JSONDecodeError 24 | except AttributeError: 25 | JSONDecodeError = ValueError 26 | 27 | class Serial(IOHandler): 28 | poll_frequency = 200 29 | period = 1 / poll_frequency 30 | message_rate = 12000 # Max messages per seconds 31 | 32 | @classmethod 33 | def available_hosts(cls): 34 | devices = comports(include_links=True) 35 | 36 | return [d.device for d in devices] 37 | 38 | @classmethod 39 | def is_host_compatible(cls, host): 40 | return host in cls.available_hosts() 41 | 42 | def __init__(self, host, baudrate=None): 43 | if baudrate is None: 44 | baudrate = os.getenv('LUOS_BAUDRATE', 1000000) 45 | 46 | self._serial = _serial.Serial(host, baudrate) 47 | self._serial.flush() 48 | 49 | self._msg = queue.Queue(int((self.message_rate / self.period) / 1000)) 50 | self._running = True 51 | 52 | self._poll_loop = Thread(target=self._poll) 53 | self._poll_loop.daemon = True 54 | self._poll_loop.start() 55 | 56 | def is_ready(self): 57 | if self._serial.in_waiting == 0: 58 | return False 59 | 60 | try: 61 | self.read() 62 | return True 63 | except (UnicodeDecodeError, JSONDecodeError): 64 | return False 65 | 66 | def recv(self): 67 | try: 68 | data = self._msg.get(True, 1) 69 | except queue.Empty: 70 | data = None 71 | return data 72 | 73 | def write(self, data): 74 | self._serial.write(b'\x7E' + struct.pack(' 20000): 97 | # Bad header 98 | # Remove the header and do it again 99 | return extract_line(s[H+1:]) 100 | else: 101 | # Size seems ok 102 | # Check if we receive the entire data 103 | if len(s[H+3:]) < size+1: 104 | # We don't have the entire data 105 | return b'', s 106 | else: 107 | # We have the complete data 108 | # Check the footer 109 | data_start = H+3 110 | data_end = data_start + size 111 | if (s[data_end] != ord(b'\x81')): 112 | # The footer is not ok, this mean we don't have a good header 113 | # Remove the header and do it again 114 | return extract_line(s[H+1:]) 115 | else: 116 | # Footer is ok 117 | return s[data_start:data_end], s[data_end + 1:] 118 | 119 | buff = b'' 120 | 121 | while self._running: 122 | to_read = self._serial.in_waiting 123 | 124 | if to_read == 0: 125 | time.sleep(self.period) 126 | continue 127 | 128 | s = self._serial.read(to_read) 129 | buff = buff + s 130 | 131 | while self._running: 132 | line, buff = extract_line(buff) 133 | if not len(line): 134 | break 135 | if self._msg.full(): 136 | print("Warning: Serial message queue is full. Some datas could be lost") 137 | self._msg.get() 138 | self._msg.put(line) 139 | -------------------------------------------------------------------------------- /pyluos/io/ws.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import websocket 4 | import struct 5 | 6 | import sys 7 | if sys.version_info >= (3, 0): 8 | import queue 9 | else: 10 | import Queue as queue 11 | 12 | from threading import Event, Thread 13 | from . import IOHandler 14 | 15 | 16 | def resolve_hostname(hostname, port): 17 | # We do our own mDNS resolution 18 | # to enforce we only search for IPV4 address 19 | # and avoid a 5s timeout in the websocket on the ESP 20 | # See https://github.com/esp8266/Arduino/issues/2110 21 | addrinfo = socket.getaddrinfo(hostname, port, 22 | socket.AF_INET, 0, 23 | socket.SOL_TCP) 24 | addr = addrinfo[0][4][0] 25 | return addr 26 | 27 | 28 | class Ws(IOHandler): 29 | 30 | @classmethod 31 | def is_host_compatible(cls, host): 32 | try: 33 | socket.inet_pton(socket.AF_INET, host) 34 | return True 35 | except socket.error: 36 | return host.endswith('.local') or (host == "localhost") 37 | 38 | @classmethod 39 | def available_hosts(cls): 40 | hosts = ['pi-gate.local'] 41 | 42 | return [ 43 | ip 44 | for ip in hosts 45 | if os.system('ping -c 1 -W1 -t1 {} > /dev/null 2>&1'.format(ip)) == 0 46 | ] 47 | 48 | def __init__(self, host, port=9342, baudrate=None): 49 | host = resolve_hostname(host, port) 50 | 51 | self._ws = websocket.WebSocket() 52 | self._ws.connect("ws://" + str(host) + ":" + str(port) + "/ws") 53 | 54 | self._msg = queue.Queue(4096) 55 | self._running = True 56 | 57 | self._poll_loop = Thread(target=self._poll) 58 | self._poll_loop.daemon = True 59 | self._poll_loop.start() 60 | 61 | def is_ready(self): 62 | return True 63 | 64 | def recv(self): 65 | try: 66 | data = self._msg.get(True, 1) 67 | except queue.Empty: 68 | data = None 69 | return data 70 | 71 | def write(self, data): 72 | self._ws.send(data) 73 | 74 | def close(self): 75 | self._running = False 76 | self._poll_loop.join() 77 | self._ws.close() 78 | 79 | def _poll(self): 80 | def extract_line(s): 81 | j = s.find(b'\n') 82 | if j == -1: 83 | return b'', s 84 | # Sometimes the begin of serial data can be wrong remove it 85 | # Find the first '{' 86 | 87 | x = s.find(b'{') 88 | if x == -1: 89 | return b'', s[j + 1:] 90 | 91 | return s[x:j], s[j + 1:] 92 | 93 | buff = b'' 94 | 95 | while self._running: 96 | s = self._ws.recv() 97 | buff = buff + s 98 | while self._running: 99 | line, buff = extract_line(buff) 100 | if not len(line): 101 | break 102 | if self._msg.full(): 103 | print("Warning: Web socket message queue is full. Some datas could be lost") 104 | self._msg.get() 105 | self._msg.put(line) 106 | -------------------------------------------------------------------------------- /pyluos/logging_conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "disable_existing_loggers": false, 4 | "formatters": { 5 | "simple": { 6 | "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 7 | } 8 | }, 9 | 10 | "handlers": { 11 | "console": { 12 | "class": "logging.StreamHandler", 13 | "level": "DEBUG", 14 | "stream": "ext://sys.stdout" 15 | }, 16 | 17 | "info_file_handler": { 18 | "class": "logging.handlers.RotatingFileHandler", 19 | "level": "INFO", 20 | "formatter": "simple", 21 | "filename": "info.log", 22 | "maxBytes": 10485760, 23 | "backupCount": 20, 24 | "encoding": "utf8" 25 | } 26 | }, 27 | 28 | "loggers": { 29 | "pyluos": { 30 | "level": "DEBUG", 31 | "handlers": ["console", "info_file_handler"] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pyluos/services/__init__.py: -------------------------------------------------------------------------------- 1 | from .state import State 2 | from .color import Color 3 | from .motor import Motor 4 | from .servoMotor import ServoMotor 5 | from .angle import Angle 6 | from .distance import Distance 7 | from .gate import Gate 8 | from .imu import Imu 9 | from .light import Light 10 | from .void import Void 11 | from .load import Load 12 | from .voltage import Voltage 13 | from .pipe import Pipe 14 | from .pressure import Pressure 15 | from .unknown import Unknown 16 | 17 | 18 | __all__ = [ 19 | 'name2mod', 20 | 'State', 21 | 'Color', 22 | 'Motor', 23 | 'ServoMotor', 24 | 'Angle', 25 | 'Distance', 26 | 'Gate', 27 | 'Imu' , 28 | 'Light', 29 | 'Void', 30 | 'Load', 31 | 'Voltage', 32 | 'Pipe', 33 | 'Pressure', 34 | 'Unknown' 35 | ] 36 | 37 | name2mod = { 38 | 'State': State, 39 | 'Color': Color, 40 | 'Motor': Motor, 41 | 'ServoMotor': ServoMotor, 42 | 'Angle': Angle, 43 | 'Distance': Distance, 44 | 'Gate': Gate, 45 | 'Imu': Imu, 46 | 'Light' : Light, 47 | 'Void' : Void, 48 | 'Load' : Load, 49 | 'Voltage' : Voltage, 50 | 'Pipe' : Pipe, 51 | 'Pressure': Pressure, 52 | 'Unknown' : Unknown 53 | } 54 | -------------------------------------------------------------------------------- /pyluos/services/angle.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Angle(Service): 5 | possible_events = {'changed', 'filter_changed'} 6 | threshold = 10 7 | 8 | def __init__(self, id, alias, device): 9 | Service.__init__(self, 'Angle', id, alias, device) 10 | self._value = 0 11 | 12 | @property 13 | def rot_position(self): 14 | """ Position in degrees. """ 15 | return self._value 16 | 17 | @rot_position.setter 18 | def rot_position(self, new_val): 19 | self._value = new_val 20 | self._push_value('target_rot_position', new_val) 21 | 22 | def _update(self, new_state): 23 | Service._update(self, new_state) 24 | if 'rot_position' in new_state.keys(): 25 | new_val = new_state['rot_position'] 26 | if new_val != self._value: 27 | self._pub_event('changed', self._value, new_val) 28 | 29 | if abs(new_val - self._value) > self.threshold: 30 | self._pub_event('filter_changed', 31 | self._value, new_val) 32 | 33 | self._value = new_val 34 | -------------------------------------------------------------------------------- /pyluos/services/color.py: -------------------------------------------------------------------------------- 1 | from .service import Service, interact 2 | import numpy as np 3 | 4 | 5 | class Color(Service): 6 | def __init__(self, id, alias, device): 7 | Service.__init__(self, 'Color', id, alias, device) 8 | self._time = None 9 | self._size = None 10 | self._color = None 11 | 12 | @property 13 | def color(self): 14 | return self._color 15 | 16 | @color.setter 17 | def color(self, new_color): 18 | new_color = [int(min(max(c, 0), 255)) for c in new_color] 19 | if len(new_color) > 3 : 20 | self._color = new_color 21 | self._push_data('color', [len(new_color)], np.array(new_color, dtype=np.uint8)) 22 | else : 23 | self._color = new_color 24 | self._push_value('color', new_color) 25 | @property 26 | def time(self): 27 | return self._time 28 | 29 | @time.setter 30 | def time(self, new_time): 31 | self._time = new_time 32 | self._push_value('time', new_time) 33 | 34 | @property 35 | def size(self): 36 | return self._size 37 | 38 | @size.setter 39 | def size(self, new_size): 40 | self._size = new_size 41 | self._push_value('parameters', new_size) 42 | 43 | def _update(self, new_state): 44 | Service._update(self, new_state) 45 | 46 | def control(self): 47 | def change_color(red, green, blue): 48 | self.color = (red, green, blue) 49 | 50 | return interact(change_color, 51 | red=(0, 255, 1), 52 | green=(0, 255, 1), 53 | blue=(0, 255, 1)) 54 | -------------------------------------------------------------------------------- /pyluos/services/distance.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Distance(Service): 5 | possible_events = {'changed', 'filter_changed'} 6 | threshold = 10 7 | 8 | def __init__(self, id, alias, device): 9 | Service.__init__(self, 'Distance', id, alias, device) 10 | self._value = 0 11 | 12 | @property 13 | def distance(self): 14 | """ Distance in mm. """ 15 | return self._value 16 | 17 | def _update(self, new_state): 18 | Service._update(self, new_state) 19 | if 'trans_position' in new_state.keys(): 20 | new_dist = new_state['trans_position'] 21 | if new_dist != self._value: 22 | self._pub_event('changed', self._value, new_dist) 23 | 24 | if abs(new_dist - self._value) > self.threshold: 25 | self._pub_event('filter_changed', 26 | self._value, new_dist) 27 | 28 | self._value = new_dist 29 | -------------------------------------------------------------------------------- /pyluos/services/gate.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Gate(Service): 5 | 6 | def __init__(self, id, alias, device): 7 | Service.__init__(self, 'Gate', id, alias, device) 8 | 9 | def _update(self, new_state): 10 | Service._update(self, new_state) 11 | 12 | def control(self): 13 | def delay(delay): 14 | self._value = delay 15 | 16 | return interact(delay, delay=(0, 100, 1)) 17 | -------------------------------------------------------------------------------- /pyluos/services/imu.py: -------------------------------------------------------------------------------- 1 | from .service import Service, interact 2 | import collections 3 | from copy import copy 4 | import time 5 | 6 | compare = lambda x, y: collections.Counter(x) == collections.Counter(y) 7 | 8 | 9 | class Imu(Service): 10 | _ACCELL = 9 11 | _GYRO = 8 12 | _QUAT = 7 13 | _COMPASS = 6 14 | _EULER = 5 15 | _ROT_MAT = 4 16 | _PEDO = 3 17 | _LINEAR_ACCEL = 2 18 | _GRAVITY_VECTOR = 1 19 | _HEADING = 0 20 | 21 | 22 | def __init__(self, id, alias, device): 23 | Service.__init__(self, 'Imu', id, alias, device) 24 | self._config = [False] * (Imu._ACCELL + 1) 25 | self._config[Imu._QUAT] = True # by default enable quaternion 26 | self._quaternion = (0, 0, 0, 0) 27 | self._acceleration = (0, 0, 0) 28 | self._gyro = (0, 0, 0) 29 | self._compass = (0, 0, 0) 30 | self._euler = (0, 0, 0) 31 | self._rotational_matrix = (0, 0, 0, 0, 0, 0, 0, 0, 0) 32 | self._pedometer = 0 33 | self._walk_time = 0 34 | self._linear_acceleration = (0, 0, 0) 35 | self._gravity_vector = (0, 0, 0) 36 | self._heading = 0 37 | 38 | def _convert_config(self): 39 | return int(''.join(['1' if c else '0' for c in self._config]), 2) # Table read reversly 40 | 41 | 42 | def bit(self, i, enable): 43 | self._config = self._config[:i] + () + self._config[i + 1:] 44 | 45 | @property 46 | def quaternion(self): 47 | self.quaternion = True 48 | return self._quaternion 49 | 50 | @quaternion.setter 51 | def quaternion(self, enable): 52 | bak = copy(self._config) 53 | self._config[Imu._QUAT] = True if enable != 0 else False 54 | if bak != self._config: 55 | self._push_value('parameters', self._convert_config()) 56 | time.sleep(0.01) 57 | 58 | @property 59 | def acceleration(self): 60 | self.acceleration = True 61 | return self._acceleration 62 | 63 | @acceleration.setter 64 | def acceleration(self, enable): 65 | bak = copy(self._config) 66 | self._config[Imu._ACCELL] = True if enable != 0 else False 67 | if bak != self._config: 68 | self._push_value('parameters', self._convert_config()) 69 | time.sleep(0.01) 70 | 71 | @property 72 | def gyro(self): 73 | self.gyro = True 74 | return self._gyro 75 | 76 | @gyro.setter 77 | def gyro(self, enable): 78 | bak = copy(self._config) 79 | self._config[Imu._GYRO] = True if enable != 0 else False 80 | if bak != self._config: 81 | self._push_value('parameters', self._convert_config()) 82 | time.sleep(0.01) 83 | 84 | @property 85 | def compass(self): 86 | self.compass = True 87 | return self._compass 88 | 89 | @compass.setter 90 | def compass(self, enable): 91 | bak = copy(self._config) 92 | self._config[Imu._COMPASS] = True if enable != 0 else False 93 | if bak != self._config: 94 | self._push_value('parameters', self._convert_config()) 95 | time.sleep(0.01) 96 | 97 | @property 98 | def euler(self): 99 | self.euler = True 100 | return self._euler 101 | 102 | @euler.setter 103 | def euler(self, enable): 104 | bak = copy(self._config) 105 | self._config[Imu._EULER] = True if enable != 0 else False 106 | if bak != self._config: 107 | self._push_value('parameters', self._convert_config()) 108 | time.sleep(0.01) 109 | 110 | @property 111 | def rotational_matrix(self): 112 | self.rotational_matrix = True 113 | return self._rotational_matrix 114 | 115 | @rotational_matrix.setter 116 | def rotational_matrix(self, enable): 117 | bak = copy(self._config) 118 | self._config[Imu._ROT_MAT] = True if enable != 0 else False 119 | if bak != self._config: 120 | self._push_value('parameters', self._convert_config()) 121 | time.sleep(0.01) 122 | 123 | @property 124 | def pedometer(self): 125 | self.pedometer = True 126 | return self._pedometer 127 | 128 | @pedometer.setter 129 | def pedometer(self, enable): 130 | bak = copy(self._config) 131 | self._config[Imu._PEDO] = True if enable != 0 else False 132 | if bak != self._config: 133 | self._push_value('parameters', self._convert_config()) 134 | time.sleep(0.01) 135 | 136 | @property 137 | def walk_time(self): 138 | self.walk_time = True 139 | return self._walk_time 140 | 141 | @walk_time.setter 142 | def walk_time(self, enable): 143 | self.pedometer = enable 144 | 145 | @property 146 | def linear_acceleration(self): 147 | self.linear_acceleration = True 148 | return self._linear_acceleration 149 | 150 | @linear_acceleration.setter 151 | def linear_acceleration(self, enable): 152 | bak = copy(self._config) 153 | self._config[Imu._LINEAR_ACCEL] = True if enable != 0 else False 154 | if bak != self._config: 155 | self._push_value('parameters', self._convert_config()) 156 | time.sleep(0.01) 157 | 158 | @property 159 | def gravity_vector(self): 160 | self.gravity_vector = True 161 | return self._gravity_vector 162 | 163 | @gravity_vector.setter 164 | def gravity_vector(self, enable): 165 | bak = copy(self._config) 166 | self._config[Imu._GRAVITY_VECTOR] = True if enable != 0 else False 167 | if bak != self._config: 168 | self._push_value('parameters', self._convert_config()) 169 | time.sleep(0.01) 170 | 171 | @property 172 | def heading(self): 173 | self.heading = True 174 | return self._heading 175 | 176 | @heading.setter 177 | def heading(self, enable): 178 | bak = copy(self._config) 179 | self._config[Imu._HEADING] = True if enable != 0 else False 180 | if bak != self._config: 181 | self._push_value('parameters', self._convert_config()) 182 | time.sleep(0.01) 183 | 184 | 185 | def _update(self, new_state): 186 | Service._update(self, new_state) 187 | if 'quaternion' in new_state.keys(): 188 | self._quaternion = new_state['quaternion'] 189 | if 'accel' in new_state.keys(): 190 | self._acceleration = new_state['accel'] 191 | if 'gyro' in new_state.keys(): 192 | self._gyro = new_state['gyro'] 193 | if 'compass' in new_state.keys(): 194 | self._compass = new_state['compass'] 195 | if 'euler' in new_state.keys(): 196 | self._euler = new_state['euler'] 197 | if 'rotational_matrix' in new_state.keys(): 198 | self._rotational_matrix = new_state['rotational_matrix'] 199 | if 'pedometer' in new_state.keys(): 200 | self._pedometer = new_state['pedometer'] 201 | if 'walk_time' in new_state.keys(): 202 | self._walk_time = new_state['walk_time'] 203 | if 'linear_accel' in new_state.keys(): 204 | self._linear_acceleration = new_state['linear_accel'] 205 | if 'gravity_vector' in new_state.keys(): 206 | self._gravity_vector = new_state['gravity_vector'] 207 | if 'heading' in new_state.keys(): 208 | self._heading = new_state['heading'] 209 | 210 | def control(self): 211 | def change_config(accel, gyro, quat, compass, euler, rot_mat, pedo, linear_accel, gravity_vector, heading): 212 | self.acceleration = accel 213 | self.gyro = gyro 214 | self.quaternion = quat 215 | self.compass = compass 216 | self.euler = euler 217 | self.rotational_matrix = rot_mat 218 | self.pedometer = pedo 219 | self.linear_acceleration = linear_accel 220 | self.gravity_vector = gravity_vector 221 | self.heading = heading 222 | self._push_value('parameters', self._convert_config()) 223 | 224 | return interact(change_config, 225 | accel=self._config[Imu._ACCELL], 226 | gyro=self._config[Imu._GYRO], 227 | quat=self._config[Imu._QUAT], 228 | compass=self._config[Imu._COMPASS], 229 | euler=self._config[Imu._EULER], 230 | rot_mat=self._config[Imu._ROT_MAT], 231 | pedo=self._config[Imu._PEDO], 232 | linear_accel=self._config[Imu._LINEAR_ACCEL], 233 | gravity_vector=self._config[Imu._GRAVITY_VECTOR], 234 | heading=self._config[Imu._HEADING]) 235 | -------------------------------------------------------------------------------- /pyluos/services/light.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Light(Service): 5 | possible_events = {'changed', 'filter_changed'} 6 | threshold = 10 7 | 8 | def __init__(self, id, alias, device): 9 | Service.__init__(self, 'Light', id, alias, device) 10 | self._value = 0.0 11 | 12 | @property 13 | def lux(self): 14 | """ Light in lux. """ 15 | return self._value 16 | 17 | def _update(self, new_state): 18 | Service._update(self, new_state) 19 | if 'lux' in new_state.keys(): 20 | new_light = new_state['lux'] 21 | if new_light != self._value: 22 | self._pub_event('changed', self._value, new_light) 23 | 24 | if abs(new_light - self._value) > self.threshold: 25 | self._pub_event('filter_changed', 26 | self._value, new_light) 27 | 28 | self._value = new_light 29 | -------------------------------------------------------------------------------- /pyluos/services/load.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Load(Service): 5 | possible_events = {'changed', 'filter_changed'} 6 | threshold = 10 7 | 8 | def __init__(self, id, alias, device): 9 | Service.__init__(self, 'Load', id, alias, device) 10 | self._load = 0.0 11 | self._offset = 0.0 12 | self._scale = 1.0 13 | 14 | @property 15 | def load(self): 16 | """ force """ 17 | return self._load 18 | 19 | def tare(self): 20 | # measure and offset 21 | self._push_value("reinit", None) 22 | time.sleep(1.0) 23 | 24 | @property 25 | def offset(self): 26 | return self._offset 27 | 28 | @offset.setter 29 | def offset(self, value): 30 | self._offset = value 31 | self._push_value("offset",value) 32 | 33 | @property 34 | def scale(self): 35 | return self._scale 36 | 37 | @scale.setter 38 | def scale(self, value): 39 | self._scale = value 40 | self._push_value("resolution",value) 41 | 42 | def _update(self, new_state): 43 | Service._update(self, new_state) 44 | if 'force' in new_state.keys(): 45 | new_force = new_state['force'] 46 | if new_force != self._value: 47 | self._pub_event('changed', self._value, new_force) 48 | if abs(new_force - self._load) > self.threshold: 49 | self._pub_event('filter_changed', 50 | self._value, new_force) 51 | 52 | self._load = new_force 53 | 54 | def control(self): 55 | def change_config(offset, scale): 56 | # report config 57 | self.offset = offset 58 | self.scale = scale 59 | 60 | w = interact(change_config, 61 | offset = self.offset, 62 | scale = self.scale) 63 | -------------------------------------------------------------------------------- /pyluos/services/motor.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from .service import Service, interact 4 | 5 | 6 | class Motor(Service): 7 | def __init__(self, id, alias, device): 8 | Service.__init__(self, 'Motor', id, alias, device) 9 | 10 | @property 11 | def power_ratio(self): 12 | self._value 13 | 14 | @power_ratio.setter 15 | def power_ratio(self, s): 16 | s = min(max(s, -100.0), 100.0) 17 | self._value = s 18 | self._push_value("power_ratio",s) 19 | 20 | def _update(self, new_state): 21 | Service._update(self, new_state) 22 | 23 | def control(self): 24 | def move(power_ratio): 25 | self.power_ratio = power_ratio 26 | 27 | return interact(move, power_ratio=(-100.0, 100.0, 1.0)) 28 | -------------------------------------------------------------------------------- /pyluos/services/pipe.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Pipe(Service): 5 | 6 | def __init__(self, id, alias, device): 7 | Service.__init__(self, 'Pipe', id, alias, device) 8 | -------------------------------------------------------------------------------- /pyluos/services/pressure.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Pressure(Service): 5 | possible_events = {'changed', 'filter_changed'} 6 | threshold = 10 7 | 8 | def __init__(self, id, alias, device): 9 | Service.__init__(self, 'Pressure', id, alias, device) 10 | self._value = 0 11 | 12 | @property 13 | def pressure(self): 14 | """ Pressure in Pa. """ 15 | return self._value 16 | 17 | @pressure.setter 18 | def pressure(self, new_val): 19 | self._value = new_val 20 | self._push_value('pressure', new_val) 21 | 22 | def _update(self, new_state): 23 | Service._update(self, new_state) 24 | if 'pressure' in new_state.keys(): 25 | new_val = new_state['pressure'] 26 | if new_val != self._value: 27 | self._pub_event('changed', self._value, new_val) 28 | 29 | if abs(new_val - self._value) > self.threshold: 30 | self._pub_event('filter_changed', 31 | self._value, new_val) 32 | 33 | self._value = new_val 34 | -------------------------------------------------------------------------------- /pyluos/services/service.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, namedtuple 2 | 3 | import logging 4 | import time 5 | 6 | try: 7 | from ipywidgets import interact 8 | from ipywidgets import widgets 9 | except ImportError: 10 | def interact(*args, **kwargs): 11 | msg = 'You first have to install ipywidgets to use the control widgets.' 12 | logging.getLogger(__name__).warning(msg) 13 | return None 14 | 15 | def widgets(*args, **kwargs): 16 | msg = 'You first have to install ipywidgets to use the control widgets.' 17 | logging.getLogger(__name__).warning(msg) 18 | return None 19 | 20 | Event = namedtuple('Event', ('name', 'old_value', 'new_value')) 21 | 22 | READ_TIMEOUT = 0.3 23 | 24 | 25 | class Service(object): 26 | possible_events = set() 27 | 28 | def __init__(self, 29 | type, id, alias, 30 | device): 31 | self.id = id 32 | self.type = type 33 | self.alias = alias 34 | self.refresh_freq = 0.0 35 | self.max_refresh_time = 0.0 36 | self._update_time = 0.01 37 | self._delegate = device 38 | self._value = None 39 | self._cb = defaultdict(list) 40 | self._led = False 41 | self._node_temperature = None 42 | self._node_voltage = None 43 | self._firmware_revision = "Unknown" 44 | self._luos_revision = "Unknown" 45 | self._robus_revision = "Unknown" 46 | self._killed = False 47 | self._last_update = [] 48 | self._luos_statistics = {} 49 | 50 | def __repr__(self): 51 | return ('<{self.type} ' 52 | 'alias="{self.alias}" ' 53 | 'id={self.id}>'.format(self=self)) 54 | 55 | def _update(self, new_state): 56 | if not isinstance(new_state, dict): 57 | new_state = {new_state: ""} 58 | 59 | self._last_update.append(time.time()) 60 | if (len(self._last_update) > 1): 61 | self.max_refresh_time = max(self.max_refresh_time, self._last_update[-1] - self._last_update[-2]) 62 | if (self._last_update[0] < time.time() - 1.0): 63 | while (self._last_update[0] < time.time() - 10.0): 64 | self._last_update.pop(0) 65 | self.refresh_freq = (len(self._last_update) / 10.0) * 0.05 + 0.95 * self.refresh_freq 66 | 67 | if 'revision' in new_state.keys(): 68 | self._firmware_revision = new_state['revision'] 69 | if 'luos_revision' in new_state.keys(): 70 | self._luos_revision = new_state['luos_revision'] 71 | if 'luos_statistics' in new_state.keys(): 72 | self._luos_statistics = new_state['luos_statistics'] 73 | self._luos_statistics['alias'] = self.alias 74 | 75 | def _kill(self): 76 | self._killed = True 77 | 78 | def _push_value(self, key, new_val): 79 | if (self._killed): 80 | print("service", self.alias, "have been excluded, you can no longer acess it.") 81 | else: 82 | if isinstance(new_val, float): 83 | self._delegate.update_cmd(self.alias, key, float(str("%.3f" % new_val))) 84 | else: 85 | self._delegate.update_cmd(self.alias, key, new_val) 86 | 87 | def _push_data(self, key, new_val, data): 88 | if (self._killed): 89 | print("service", self.alias, "have been excluded, you can no longer acess it.") 90 | else: 91 | self._delegate.update_data(self.alias, key, new_val, data) 92 | 93 | @property 94 | def firmware_revision(self): 95 | self._firmware_revision = None 96 | self._push_value('revision', "") 97 | 98 | tick_start = time.time() 99 | while time.time() - tick_start < READ_TIMEOUT and self._firmware_revision is None: 100 | time.sleep(0.01) 101 | 102 | return self._firmware_revision 103 | 104 | @property 105 | def luos_revision(self): 106 | self._luos_revision = None 107 | self._push_value('luos_revision', "") 108 | 109 | tick_start = time.time() 110 | while time.time() - tick_start < READ_TIMEOUT and self._luos_revision is None: 111 | time.sleep(0.01) 112 | 113 | return self._luos_revision 114 | 115 | @property 116 | def luos_statistics(self): 117 | """Get service statistics with a timeout of 1 second.""" 118 | 119 | self._luos_statistics = None 120 | self._push_value('luos_statistics', "") 121 | 122 | tick_start = time.time() 123 | while time.time() - tick_start < 1 and self._luos_statistics is None: 124 | time.sleep(0.01) 125 | 126 | try: 127 | max_table = [self._luos_statistics["rx_msg_stack"], self._luos_statistics["luos_stack"], self._luos_statistics["tx_msg_stack"], self._luos_statistics["buffer_occupation"]] 128 | max_val = max(max_table) 129 | s = self.alias + " statistics :" 130 | s = s + "\n.luos allocated RAM occupation \t= " + repr(max_val) 131 | s = s + "%\n\t.RX message stack \t = " + repr(self._luos_statistics["rx_msg_stack"]) 132 | s = s + "%\n\t.TX message stack \t = " + repr(self._luos_statistics["tx_msg_stack"]) 133 | s = s + "%\n\t.Luos stack \t\t = " + repr(self._luos_statistics["luos_stack"]) 134 | s = s + "%\n\t.Buffer occupation \t = " + repr(self._luos_statistics["buffer_occupation"]) 135 | s = s + "%\n.Dropped messages number \t= " + repr(self._luos_statistics["msg_drop"]) 136 | s = s + "\n.Max luos loop delay \t\t= " + repr(self._luos_statistics["loop_ms"]) 137 | s = s + "ms\n.msg max retry number \t\t= " + repr(self._luos_statistics["max_retry"]) 138 | s = s + "\n" 139 | return self._luos_statistics 140 | except: 141 | return None 142 | 143 | def rename(self, name): 144 | # check if the string start with a number before sending 145 | self._push_value('rename', name) 146 | self.alias = name 147 | 148 | @property 149 | def update_time(self): 150 | return self._update_time 151 | 152 | @update_time.setter 153 | def update_time(self, time): 154 | self._push_value('update_time', time) 155 | self._update_time = time 156 | 157 | # Events cb handling 158 | 159 | def add_callback(self, event, cb): 160 | if event not in self.possible_events: 161 | raise ValueError('Unknown callback: {} (poss={})'.format(event, self.possible_events)) 162 | 163 | self._cb[event].append(cb) 164 | 165 | def remove_callback(self, event, cb): 166 | self._cb[event].remove(cb) 167 | 168 | def _pub_event(self, trigger, old_value, new_value): 169 | event = Event(trigger, old_value, new_value) 170 | 171 | for cb in self._cb[trigger]: 172 | cb(event) 173 | -------------------------------------------------------------------------------- /pyluos/services/servoMotor.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from __future__ import division 3 | import collections 4 | from copy import copy 5 | import time 6 | 7 | from .service import Service, interact 8 | import numpy as np 9 | 10 | 11 | class ServoMotor(Service): 12 | # target modes 13 | _MODE_COMPLIANT = 13 14 | _MODE_POWER = 12 15 | _MODE_TORQUE = 11 16 | _MODE_ANGULAR_SPEED = 10 17 | _MODE_ANGULAR_POSITION = 9 18 | _MODE_LINEAR_SPEED = 8 19 | _MODE_LINEAR_POSITION = 7 20 | # report modes 21 | _ANGULAR_POSITION = 6 22 | _ANGULAR_SPEED = 5 23 | _LINEAR_POSITION = 4 24 | _LINEAR_SPEED = 3 25 | _CURRENT = 2 26 | _TEMPERATURE = 1 27 | _TORQUE = 0 28 | 29 | # control modes 30 | _PLAY = 0 31 | _PAUSE = 1 32 | _STOP = 2 33 | _REC = 4 34 | 35 | def __init__(self, id, alias, device): 36 | Service.__init__(self, 'ServoMotor', id, alias, device) 37 | self._config = [False] * (ServoMotor._MODE_COMPLIANT + 1) 38 | # default configs, enable compliant, power_mode, and rotation position report 39 | self._config[ServoMotor._MODE_COMPLIANT] = True 40 | self._config[ServoMotor._MODE_POWER] = True 41 | self._config[ServoMotor._ANGULAR_POSITION] = True 42 | 43 | #configuration 44 | self._positionPid = [0.0, 0.0, 0.0] 45 | self._speedPid = [0.0, 0.0, 0.0] 46 | self._resolution = 16 # encoder resolution 47 | self._reduction = 131 # mechanical reduction after encoder 48 | self._dimension = 100 # Wheel size (mm) 49 | self._limit_rot_position = None 50 | self._limit_trans_position = None 51 | self._limit_rot_speed = None 52 | self._limit_trans_speed = None 53 | self._limit_power = 100.0 54 | self._limit_current = 6.0 55 | self._sampling_freq = 100.0 56 | self._control = 0 57 | 58 | #targets 59 | self._compliant = True 60 | self._target_power = 0.0 61 | self._target_rot_speed = 0.0 62 | self._target_rot_position = 0.0 63 | self._target_trans_speed = 0.0 64 | self._target_trans_position = 0.0 65 | 66 | # report modes 67 | self._rot_position = 0.0 68 | self._rot_speed = 0.0 69 | self._trans_position= 0.0 70 | self._trans_speed = 0.0 71 | self._current = 0.0 72 | self._temperature = 0.0 73 | 74 | def _convert_config(self): 75 | return int(''.join(['1' if c else '0' for c in self._config]), 2) # Table read reversly 76 | 77 | def bit(self, i, enable): 78 | self._config = self._config[:i] + () + self._config[i + 1:] 79 | 80 | #************************** configurations ***************************** 81 | 82 | def play(self): 83 | if (self._control >= self._REC): 84 | self._control = self._PLAY + self._REC 85 | else : 86 | self._control = self._PLAY 87 | self._push_value('control', self._control) 88 | 89 | def pause(self): 90 | if (self._control >= self._REC): 91 | self._control = self._PAUSE + self._REC 92 | else : 93 | self._control = self._PAUSE 94 | self._push_value('control', self._control) 95 | 96 | def stop(self): 97 | # also stop recording 98 | self._control = self._STOP 99 | self._push_value('control', self._control) 100 | 101 | def rec(self, enable): 102 | if (self._control >= self._REC): 103 | if (enable == False): 104 | self._control = self._control - self._REC 105 | else : 106 | if (enable == True): 107 | self._control = self._control + self._REC 108 | self._push_value('control', self._control) 109 | 110 | def setToZero(self): 111 | self._push_value('reinit', None) 112 | 113 | @property 114 | def sampling_freq(self): 115 | return self._sampling_freq 116 | 117 | @sampling_freq.setter 118 | def sampling_freq(self, sampling_freq): 119 | self._sampling_freq = sampling_freq 120 | self._push_value("time", 1.0 / sampling_freq) 121 | 122 | @property 123 | def positionPid(self): 124 | return self._positionPid 125 | 126 | @positionPid.setter 127 | def positionPid(self, new_pid): 128 | bak = copy(self._config) 129 | self.compliant = True 130 | self.rot_position_mode = True 131 | self.rot_speed_mode = False 132 | time.sleep(0.2) 133 | self._positionPid = new_pid 134 | self._push_value('pid', new_pid) 135 | time.sleep(0.2) 136 | self._config = bak 137 | self._push_value('parameters', self._convert_config()) 138 | time.sleep(0.01) 139 | 140 | @property 141 | def speedPid(self): 142 | return self._speedPid 143 | 144 | @speedPid.setter 145 | def speedPid(self, new_pid): 146 | bak = copy(self._config) 147 | self.compliant = True 148 | self.rot_position_mode = False 149 | self.rot_speed_mode = True 150 | time.sleep(0.2) 151 | self._speedPid = new_pid 152 | self._push_value('pid', new_pid) 153 | time.sleep(0.2) 154 | self._config = bak 155 | self._push_value('parameters', self._convert_config()) 156 | time.sleep(0.01) 157 | 158 | @property 159 | def encoder_res(self): 160 | return self._resolution 161 | 162 | @encoder_res.setter 163 | def encoder_res(self, s): 164 | self._resolution = s 165 | self._push_value("resolution", s) 166 | 167 | @property 168 | def reduction(self): 169 | return self._resolution 170 | 171 | @reduction.setter 172 | def reduction(self, s): 173 | self._reduction = s 174 | self._push_value("reduction", s) 175 | 176 | @property 177 | def wheel_size(self): 178 | return self._dimension 179 | 180 | @wheel_size.setter 181 | def wheel_size(self, s): 182 | self._dimension = s 183 | self._push_value("dimension", s) 184 | 185 | @property 186 | def limit_rot_position(self): 187 | return self._limit_rot_position 188 | 189 | @limit_rot_position.setter 190 | def limit_rot_position(self, s): 191 | self._limit_rot_position = s 192 | self._push_value("limit_rot_position", s) 193 | 194 | @property 195 | def limit_trans_position(self): 196 | return self._limit_trans_position 197 | 198 | @limit_trans_position.setter 199 | def limit_trans_position(self, s): 200 | self._limit_trans_position = s 201 | self._push_value("limit_trans_position", s) 202 | 203 | @property 204 | def limit_rot_speed(self): 205 | return self._limit_rot_speed 206 | 207 | @limit_rot_speed.setter 208 | def limit_rot_speed(self, s): 209 | self._limit_rot_speed = s 210 | self._push_value("limit_rot_speed", s) 211 | 212 | @property 213 | def limit_trans_speed(self): 214 | return self._limit_trans_speed 215 | 216 | @limit_trans_speed.setter 217 | def limit_trans_speed(self, s): 218 | self._limit_trans_speed = s 219 | self._push_value("limit_trans_speed", s) 220 | 221 | @property 222 | def limit_power(self): 223 | return self._limit_power 224 | 225 | @limit_power.setter 226 | def limit_power(self, s): 227 | self._limit_power = abs(s) 228 | s = min(s, 100.0) 229 | self._push_value("limit_power", s) 230 | 231 | @property 232 | def limit_current(self): 233 | return self._limit_current 234 | 235 | @limit_current.setter 236 | def limit_current(self, s): 237 | self._limit_current = s 238 | self._push_value("limit_current", s) 239 | 240 | #************************** target modes ***************************** 241 | 242 | # compliant 243 | @property 244 | def compliant(self): 245 | return self._config[ServoMotor._MODE_COMPLIANT] 246 | 247 | @compliant.setter 248 | def compliant(self, enable): 249 | self._config[ServoMotor._MODE_COMPLIANT] = True if enable != 0 else False 250 | self._compliant = enable 251 | self._push_value('parameters', self._convert_config()) 252 | if (enable == False): 253 | self._target_rot_position = self._rot_position 254 | time.sleep(0.01) 255 | 256 | # power 257 | @property 258 | def power_ratio(self): 259 | if (self._config[ServoMotor._MODE_POWER] != True): 260 | print("power mode is not enabled in the service please use 'device.service.power_mode = True' to enable it") 261 | return 262 | return self._target_power 263 | 264 | @power_ratio.setter 265 | def power_ratio(self, s): 266 | if (self._config[ServoMotor._MODE_POWER] != True): 267 | print("power mode is not enabled in the service please use 'device.service.power_mode = True' to enable it") 268 | s = min(max(s, -100.0), 100.0) 269 | #if s != self._target_power: 270 | self._target_power = s 271 | self._push_value("power_ratio",s) 272 | 273 | @property 274 | def power_mode(self): 275 | return self._config[ServoMotor._MODE_POWER] 276 | 277 | @power_mode.setter 278 | def power_mode(self, enable): 279 | self._config[ServoMotor._MODE_POWER] = True if enable != 0 else False 280 | if (enable == True) : 281 | self._config[ServoMotor._MODE_ANGULAR_SPEED] = False 282 | self._config[ServoMotor._MODE_ANGULAR_POSITION] = False 283 | self._config[ServoMotor._MODE_LINEAR_SPEED] = False 284 | self._config[ServoMotor._MODE_LINEAR_POSITION] = False 285 | self._push_value('parameters', self._convert_config()) 286 | time.sleep(0.01) 287 | 288 | # rotation speed 289 | @property 290 | def target_rot_speed(self): 291 | if (self._config[ServoMotor._MODE_ANGULAR_SPEED] != True): 292 | print("rotation speed mode could be not enabled in the service please use 'device.service.rot_speed_mode = True' to enable it") 293 | return self._target_rot_speed 294 | 295 | @target_rot_speed.setter 296 | def target_rot_speed(self, s): 297 | if (self._config[ServoMotor._MODE_ANGULAR_SPEED] != True): 298 | print("rotation speed mode could be not enabled in the service please use 'device.service.rot_speed_mode = True' to enable it") 299 | self._target_rot_speed = s 300 | if hasattr(s, "__len__"): 301 | self._push_data('target_rot_speed', [len(s) * 4], np.array(s, dtype=np.float32)) # multiplying by the size of float32 302 | else : 303 | self._push_value("target_rot_speed", s) 304 | 305 | @property 306 | def rot_speed_mode(self): 307 | return self._config[ServoMotor._MODE_ANGULAR_SPEED] 308 | 309 | @rot_speed_mode.setter 310 | def rot_speed_mode(self, enable): 311 | self._config[ServoMotor._MODE_ANGULAR_SPEED] = True if enable != 0 else False 312 | if (enable == True) : 313 | self._config[ServoMotor._MODE_LINEAR_SPEED] = False 314 | self._config[ServoMotor._MODE_POWER] = False 315 | self._push_value('parameters', self._convert_config()) 316 | time.sleep(0.01) 317 | 318 | # rotation position 319 | @property 320 | def target_rot_position(self): 321 | if (self._config[ServoMotor._MODE_ANGULAR_POSITION] != True): 322 | print("rotation position mode could be not enabled in the service please use 'device.service.rot_position_mode = True' to enable it") 323 | return self._target_rot_position 324 | 325 | @target_rot_position.setter 326 | def target_rot_position(self, s): 327 | if (self._config[ServoMotor._MODE_ANGULAR_POSITION] != True): 328 | print("rotation position mode could be not enabled in the service please use 'device.service.rot_position_mode = True' to enable it") 329 | self._target_rot_position = s 330 | if hasattr(s, "__len__"): 331 | self._push_data('target_rot_position', [len(s) * 4], np.array(s, dtype=np.float32)) # multiplying by the size of float32 332 | else : 333 | self._push_value("target_rot_position", s) 334 | 335 | @property 336 | def rot_position_mode(self): 337 | return self._config[ServoMotor._MODE_ANGULAR_POSITION] 338 | 339 | @rot_position_mode.setter 340 | def rot_position_mode(self, enable): 341 | self._config[ServoMotor._MODE_ANGULAR_POSITION] = True if enable != 0 else False 342 | if (enable == True) : 343 | self._config[ServoMotor._MODE_LINEAR_POSITION] = False 344 | self._config[ServoMotor._MODE_POWER] = False 345 | self._push_value('parameters', self._convert_config()) 346 | time.sleep(0.01) 347 | 348 | # translation speed 349 | @property 350 | def target_trans_speed(self): 351 | if (self._config[ServoMotor._MODE_LINEAR_SPEED] != True): 352 | print("translation speed mode could be not enabled in the service please use 'device.service.trans_speed_mode = True' to enable it") 353 | return self._target_trans_speed 354 | 355 | @target_trans_speed.setter 356 | def target_trans_speed(self, s): 357 | if (self._config[ServoMotor._MODE_LINEAR_SPEED] != True): 358 | print("translation speed mode could be not enabled in the service please use 'device.service.trans_speed_mode = True' to enable it") 359 | self._target_trans_speed = s 360 | self._push_value("target_trans_speed", s) 361 | 362 | @property 363 | def trans_speed_mode(self): 364 | return self._config[ServoMotor._MODE_LINEAR_SPEED] 365 | 366 | @trans_speed_mode.setter 367 | def trans_speed_mode(self, enable): 368 | self._config[ServoMotor._MODE_LINEAR_SPEED] = True if enable != 0 else False 369 | if (enable == True) : 370 | self._config[ServoMotor._MODE_ANGULAR_SPEED] = False 371 | self._config[ServoMotor._MODE_POWER] = False 372 | self._push_value('parameters', self._convert_config()) 373 | time.sleep(0.01) 374 | 375 | # translation position 376 | @property 377 | def target_trans_position(self): 378 | if (self._config[ServoMotor._MODE_LINEAR_POSITION] != True): 379 | print("translation speed mode could be not enabled in the service please use 'device.service.trans_pos_mode = True' to enable it") 380 | return self._target_trans_position 381 | 382 | @target_trans_position.setter 383 | def target_trans_position(self, s): 384 | if (self._config[ServoMotor._MODE_LINEAR_POSITION] != True): 385 | print("translation speed mode could be not enabled in the service please use 'device.service.trans_position_mode = True' to enable it") 386 | self._target_trans_position = s 387 | if hasattr(s, "__len__"): 388 | self._push_value('target_trans_position', [len(s) * 4]) # multiplying by the size of float32 389 | self._push_data(np.array(s, dtype=np.float32)) 390 | else : 391 | self._push_value("target_trans_position", s) 392 | 393 | @property 394 | def trans_position_mode(self): 395 | return self._config[ServoMotor._MODE_LINEAR_POSITION] 396 | 397 | @trans_position_mode.setter 398 | def trans_position_mode(self, enable): 399 | self._config[ServoMotor._MODE_LINEAR_POSITION] = True if enable != 0 else False 400 | if (enable == True) : 401 | self._config[ServoMotor._MODE_ANGULAR_POSITION] = False 402 | self._config[ServoMotor._MODE_POWER] = False 403 | self._push_value('parameters', self._convert_config()) 404 | time.sleep(0.01) 405 | #************************** report modes ***************************** 406 | 407 | # rotation position 408 | @property 409 | def rot_position(self): 410 | if (self._config[ServoMotor._ANGULAR_POSITION] != True): 411 | self.rot_position = True 412 | return self._rot_position 413 | 414 | @rot_position.setter 415 | def rot_position(self, enable): 416 | self._config[ServoMotor._ANGULAR_POSITION] = True if enable != 0 else False 417 | self._push_value('parameters', self._convert_config()) 418 | time.sleep(0.01) 419 | 420 | # rotation speed 421 | @property 422 | def rot_speed(self): 423 | if (self._config[ServoMotor._ANGULAR_SPEED] != True): 424 | self.rot_speed = True 425 | return self._rot_speed 426 | 427 | @rot_speed.setter 428 | def rot_speed(self, enable): 429 | self._config[ServoMotor._ANGULAR_SPEED] = True if enable != 0 else False 430 | self._push_value('parameters', self._convert_config()) 431 | time.sleep(0.01) 432 | 433 | # translation position 434 | @property 435 | def trans_position(self): 436 | if (self._config[ServoMotor._LINEAR_POSITION] != True): 437 | self.trans_position = True 438 | return self._rot_position 439 | 440 | @trans_position.setter 441 | def trans_position(self, enable): 442 | self._config[ServoMotor._LINEAR_POSITION] = True if enable != 0 else False 443 | self._push_value('parameters', self._convert_config()) 444 | time.sleep(0.01) 445 | 446 | # translation speed 447 | @property 448 | def trans_speed(self): 449 | if (self._config[ServoMotor._LINEAR_SPEED] != True): 450 | self.trans_speed = True 451 | return self._rot_speed 452 | 453 | @trans_speed.setter 454 | def trans_speed(self, enable): 455 | self._config[ServoMotor._LINEAR_SPEED] = True if enable != 0 else False 456 | self._push_value('parameters', self._convert_config()) 457 | time.sleep(0.01) 458 | 459 | # current 460 | @property 461 | def current(self): 462 | if (self._config[ServoMotor._CURRENT] != True): 463 | self.current = True 464 | return self._current 465 | 466 | @current.setter 467 | def current(self, enable): 468 | self._config[ServoMotor._CURRENT] = True if enable != 0 else False 469 | self._push_value('parameters', self._convert_config()) 470 | time.sleep(0.01) 471 | 472 | # temperature 473 | @property 474 | def temperature(self): 475 | if (self._config[ServoMotor._TEMPERATURE] != True): 476 | self.temperature = True 477 | return self._temperature 478 | 479 | @temperature.setter 480 | def temperature(self, enable): 481 | self._config[ServoMotor._TEMPERATURE] = True if enable != 0 else False 482 | self._push_value('parameters', self._convert_config()) 483 | time.sleep(0.01) 484 | 485 | #************************** custom motor type commands ***************************** 486 | 487 | def dxl_set_id(self, id): 488 | self._push_value('set_id', id) 489 | 490 | def dxl_detect(self): 491 | self._push_value('reinit', 0) 492 | print ("To get new detected Dxl motors usable on pyluos you should recreate your Pyluos object.") 493 | 494 | def dxl_register(self, register, val): 495 | new_val = [register, val] 496 | self._push_value('register', new_val) 497 | 498 | #************************** controls and updates ***************************** 499 | 500 | def _update(self, new_state): 501 | Service._update(self, new_state) 502 | if 'rot_position' in new_state.keys(): 503 | self._rot_position = new_state['rot_position'] 504 | if 'rot_speed' in new_state.keys(): 505 | self._rot_speed = new_state['rot_speed'] 506 | if 'trans_position' in new_state.keys(): 507 | self._trans_position = new_state['trans_position'] 508 | if 'trans_speed' in new_state.keys(): 509 | self._trans_speed = new_state['trans_speed'] 510 | if 'current' in new_state.keys(): 511 | self._current = new_state['current'] 512 | if 'temperature' in new_state.keys(): 513 | self._temperature = new_state['temperature'] 514 | 515 | def control(self): 516 | def change_config(rot_speed_report, rot_position_report, trans_speed_report, trans_position_report, current_report, compliant_mode, power_mode, power_ratio, rot_speed_mode, rot_speed, rot_position_mode, rot_position, trans_speed_mode, trans_speed, trans_position_mode, trans_position): 517 | # report config 518 | self.rot_speed = rot_speed_report 519 | self.rot_position = rot_position_report 520 | self.trans_speed = trans_speed_report 521 | self.trans_position = trans_position_report 522 | self.current = current_report 523 | # target mode 524 | self.compliant = compliant_mode 525 | self.power_mode = power_mode 526 | if (power_mode) : 527 | self.power_ratio = power_ratio 528 | 529 | self.rot_speed_mode = rot_speed_mode 530 | if (rot_speed_mode) : 531 | self.target_rot_speed = rot_speed 532 | 533 | self.rot_position_mode = rot_position_mode 534 | if (rot_position_mode) : 535 | self.target_rot_position = rot_position 536 | 537 | self.trans_speed_mode = trans_speed_mode 538 | if (trans_speed_mode) : 539 | self.target_trans_speed = trans_speed 540 | 541 | self.trans_position_mode = trans_position_mode 542 | if (trans_position_mode) : 543 | self.target_trans_position = trans_position 544 | 545 | w = interact(change_config, 546 | rot_speed_report = self._config[ServoMotor._ANGULAR_SPEED], 547 | rot_position_report = self._config[ServoMotor._ANGULAR_POSITION], 548 | trans_speed_report = self._config[ServoMotor._LINEAR_SPEED], 549 | trans_position_report = self._config[ServoMotor._LINEAR_POSITION], 550 | current_report = self._config[ServoMotor._CURRENT], 551 | temperature_report = self._config[ServoMotor._TEMPERATURE], 552 | 553 | compliant_mode = self._config[ServoMotor._MODE_COMPLIANT], 554 | power_mode = self._config[ServoMotor._MODE_POWER], 555 | power_ratio=(-100.0, 100.0, 1.0), 556 | rot_speed_mode = self._config[ServoMotor._MODE_ANGULAR_SPEED], 557 | rot_speed = (-300.0, 300.0, 1.0), 558 | rot_position_mode = self._config[ServoMotor._MODE_ANGULAR_POSITION], 559 | rot_position = (-360.0, 360.0, 1.0), 560 | trans_speed_mode = self._config[ServoMotor._MODE_LINEAR_SPEED], 561 | trans_speed = (-1000.0, 1000.0, 1.0), 562 | trans_position_mode = self._config[ServoMotor._MODE_LINEAR_POSITION], 563 | trans_position = (-1000.0, 1000.0, 1.0)) 564 | -------------------------------------------------------------------------------- /pyluos/services/state.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class State(Service): 5 | possible_events = {'changed', 'pressed', 'released'} 6 | 7 | def __init__(self, id, alias, device): 8 | Service.__init__(self, 'State', id, alias, device) 9 | self._value = False 10 | 11 | @property 12 | def state(self): 13 | return self._value == True 14 | 15 | @state.setter 16 | def state(self, new_val): 17 | self._value = new_val 18 | self._push_value('io_state', new_val) 19 | 20 | def _update(self, new_state): 21 | Service._update(self, new_state) 22 | if 'io_state' in new_state.keys(): 23 | new_state = new_state['io_state'] 24 | if new_state != self._value: 25 | self._pub_event('changed', self._value, new_state) 26 | 27 | evt = 'pressed' if new_state == True else 'released' 28 | self._pub_event(evt, self._value, new_state) 29 | 30 | self._value = new_state 31 | 32 | def control(self): 33 | def switch(state): 34 | self.state = state 35 | 36 | return interact(switch, state=self._value) 37 | -------------------------------------------------------------------------------- /pyluos/services/unknown.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Unknown(Service): 5 | possible_events = {'changed', 'pressed', 'released'} 6 | 7 | # control modes 8 | _PLAY = 0 9 | _PAUSE = 1 10 | _STOP = 2 11 | _REC = 4 12 | 13 | def __init__(self, id, alias, device): 14 | Service.__init__(self, 'Unknown', id, alias, device) 15 | self._control = 0 16 | self._state = False 17 | self._angular_position = 0.0 18 | self._angular_speed = 0.0 19 | self._trans_position = 0.0 20 | self._trans_speed = 0.0 21 | self._current = 0.0 22 | self._temperature = 0.0 23 | self._pressure = 0.0 24 | self._color = [0, 0, 0] 25 | self._time = 0.0 26 | self._parameters = 0 27 | self._pid = [0, 0, 0] 28 | self._power_ratio = 0.0 29 | self._lux = 0.0 30 | self._load = 0.0 31 | self._volt = 0.0 32 | 33 | def _update(self, new_state): 34 | Service._update(self, new_state) 35 | if 'io_state' in new_state.keys(): 36 | val = new_state['io_state'] 37 | if val != self._state: 38 | self._pub_event('changed', self._state, val) 39 | 40 | evt = 'pressed' if val == True else 'released' 41 | self._pub_event(evt, self._state, val) 42 | 43 | self._state = val 44 | if 'rot_position' in new_state.keys(): 45 | self._angular_position = new_state['rot_position'] 46 | if 'rot_speed' in new_state.keys(): 47 | self._angular_speed = new_state['rot_speed'] 48 | if 'trans_position' in new_state.keys(): 49 | self._trans_position = new_state['trans_position'] 50 | if 'trans_speed' in new_state.keys(): 51 | self._trans_speed = new_state['trans_speed'] 52 | if 'current' in new_state.keys(): 53 | self._current = new_state['current'] 54 | if 'temperature' in new_state.keys(): 55 | self._temperature = new_state['temperature'] 56 | if 'pressure' in new_state.keys(): 57 | self._pressure = new_state['pressure'] 58 | if 'lux' in new_state.keys(): 59 | self._lux = new_state['lux'] 60 | if 'force' in new_state.keys(): 61 | self._load = new_state['force'] 62 | if 'volt' in new_state.keys(): 63 | self._volt = new_state['volt'] 64 | 65 | 66 | 67 | def play(self): 68 | if (self._control >= self._REC): 69 | self._control = self._PLAY + self._REC 70 | else : 71 | self._control = self._PLAY 72 | self._push_value('control', self._control) 73 | 74 | def pause(self): 75 | if (self._control >= self._REC): 76 | self._control = self._PAUSE + self._REC 77 | else : 78 | self._control = self._PAUSE 79 | self._push_value('control', self._control) 80 | 81 | def stop(self): 82 | # also stop recording 83 | self._control = self._STOP 84 | self._push_value('control', self._control) 85 | 86 | def rec(self, enable): 87 | if (self._control >= self._REC): 88 | if (enable == False): 89 | self._control = self._control - self._REC 90 | else : 91 | if (enable == True): 92 | self._control = self._control + self._REC 93 | self._push_value('control', self._control) 94 | 95 | @property 96 | def state(self): 97 | return self._state == True 98 | 99 | @state.setter 100 | def state(self, new_val): 101 | self._state = new_val 102 | self._push_value('io_state', new_val) 103 | 104 | @property 105 | def angular_position(self): 106 | """ Position in degrees. """ 107 | return self._angular_position 108 | 109 | @angular_position.setter 110 | def angular_position(self, new_val): 111 | self._angular_position == new_val 112 | self._push_value('target_rot_position', new_val) 113 | 114 | @property 115 | def angular_speed(self): 116 | return self._angular_speed 117 | 118 | @angular_speed.setter 119 | def angular_speed(self, s): 120 | self._angular_speed = s 121 | self._push_value("target_rot_speed", s) 122 | 123 | @property 124 | def translation_position(self): 125 | """ Position in degrees. """ 126 | return self._trans_position 127 | 128 | @translation_position.setter 129 | def translation_position(self, new_val): 130 | self._trans_position == new_val 131 | self._push_value('target_trans_position', new_val) 132 | 133 | @property 134 | def translation_speed(self): 135 | return self._angular_speed 136 | 137 | @angular_speed.setter 138 | def translation_speed(self, s): 139 | self._angular_speed = s 140 | self._push_value("target_trans_speed", s) 141 | 142 | @property 143 | def current(self): 144 | return self._current 145 | 146 | # temperature 147 | @property 148 | def temperature(self): 149 | return self._temperature 150 | 151 | @property 152 | def pressure(self): 153 | return self._pressure 154 | 155 | @pressure.setter 156 | def pressure(self, new_pressure): 157 | self._pressure = new_pressure 158 | self._push_value('pressure', new_pressure) 159 | 160 | @property 161 | def color(self): 162 | return self._color 163 | 164 | @color.setter 165 | def color(self, new_color): 166 | new_color = [int(min(max(c, 0), 255)) for c in new_color] 167 | if len(new_color) > 3 : 168 | self._color = new_color 169 | self._push_data('color', [len(new_color)], np.array(new_color, dtype=np.uint8)) 170 | else : 171 | self._color = new_color 172 | self._push_value('color', new_color) 173 | @property 174 | def time(self): 175 | return self._time 176 | 177 | @time.setter 178 | def time(self, new_time): 179 | self._time = new_time 180 | self._push_value('time', new_time) 181 | 182 | 183 | 184 | @property 185 | def parameters(self): 186 | return self._parameters 187 | 188 | @parameters.setter 189 | def parameters(self, new_val): 190 | self._parameters = new_val 191 | self._push_value('parameters', new_val) 192 | 193 | def reinit(self): 194 | self._push_value('reinit', None) 195 | 196 | @property 197 | def pid(self): 198 | return self._pid 199 | 200 | @pid.setter 201 | def pid(self, new_pid): 202 | self._pid = new_pid 203 | self._push_value('pid', new_pid) 204 | 205 | @property 206 | def power_ratio(self): 207 | self._power_ratio 208 | 209 | @power_ratio.setter 210 | def power_ratio(self, s): 211 | s = min(max(s, -100.0), 100.0) 212 | self._power_ratio = s 213 | self._push_value("power_ratio",s) 214 | 215 | @property 216 | def lux(self): 217 | """ Light in lux. """ 218 | return self._lux 219 | 220 | @property 221 | def load(self): 222 | """ force """ 223 | return self._load 224 | 225 | @property 226 | def volt(self): 227 | """ Voltage in volt. """ 228 | return self._volt 229 | 230 | @volt.setter 231 | def volt(self, new_val): 232 | self._volt = new_val 233 | self._push_value('volt', new_val) 234 | -------------------------------------------------------------------------------- /pyluos/services/void.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | import time 3 | 4 | class Void(Service): 5 | 6 | def __init__(self, id, alias, device): 7 | Service.__init__(self, 'Void', id, alias, device) 8 | 9 | def _update(self, new_state): 10 | Service._update(self, new_state) 11 | 12 | def dxl_detect(self): 13 | self._push_value('reinit', 0) 14 | print ("To get new detected Dxl motors usable on pyluos you should recreate your Pyluos object.") 15 | 16 | def _factory_reset(self): 17 | new_val = [0xFF, 0] 18 | self._push_value('register', new_val) 19 | 20 | def factory_reset(self): 21 | self._factory_reset() 22 | print("Motor reseted => baudrate : 1000000, ID : same") 23 | print("you should start 'dxl_detect()' command and recreate your Pyluos object.") 24 | 25 | def retrieve_dxl(self): 26 | time.sleep(0.5) 27 | self.dxl_detect() 28 | print("Motor reseted => baudrate : 1000000, ID : same") 29 | print("recreate your Pyluos object.") 30 | -------------------------------------------------------------------------------- /pyluos/services/voltage.py: -------------------------------------------------------------------------------- 1 | from .service import Service 2 | 3 | 4 | class Voltage(Service): 5 | possible_events = {'changed', 'filter_changed'} 6 | 7 | def __init__(self, id, alias, device): 8 | Service.__init__(self, 'Voltage', id, alias, device) 9 | self._value = 0 10 | self.threshold = 1.0 11 | 12 | @property 13 | def volt(self): 14 | """ Voltage in volt. """ 15 | return self._value 16 | 17 | @volt.setter 18 | def volt(self, new_val): 19 | self._value = new_val 20 | self._push_value('volt', new_val) 21 | 22 | def _update(self, new_state): 23 | Service._update(self, new_state) 24 | if 'volt' in new_state.keys(): 25 | new_val = new_state['volt'] 26 | if new_val != self._value: 27 | self._pub_event('changed', self._value, new_val) 28 | if abs(new_val - self._value) > self.threshold: 29 | self._pub_event('filter_changed', 30 | self._value, new_val) 31 | self._value = new_val 32 | 33 | def control(self): 34 | def move(val): 35 | self._value = val 36 | 37 | return interact(move, val=(0.0, 3.3, 0.1)) 38 | -------------------------------------------------------------------------------- /pyluos/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luos-io/Pyluos/38d2d016d0c4fce76e911d94bcb6676d571e0077/pyluos/tools/__init__.py -------------------------------------------------------------------------------- /pyluos/tools/bootloader.py: -------------------------------------------------------------------------------- 1 | # 2 | # LUOS CLI tool 3 | 4 | # ******************************************************************************* 5 | # Import packages 6 | # ******************************************************************************* 7 | import argparse 8 | import sys 9 | import time 10 | from multiprocessing import Process, Value 11 | import json 12 | from pyluos import Device 13 | from pyluos.tools.discover import serial_discover 14 | import numpy as np 15 | import math 16 | import crc8 17 | import os 18 | from ..io import io_from_host 19 | import struct 20 | 21 | # ******************************************************************************* 22 | # Global Variables 23 | # ******************************************************************************* 24 | # BOOTLOADER_IDLE = 0 25 | BOOTLOADER_START = "start" 26 | BOOTLOADER_STOP = "stop" 27 | BOOTLOADER_READY = "ready" 28 | BOOTLOADER_ERASE = "erase" 29 | BOOTLOADER_BIN_CHUNK = "bin_chunk" 30 | BOOTLOADER_BIN_END = "bin_end" 31 | BOOTLOADER_CRC = "crc" 32 | BOOTLOADER_APP_SAVED = "app_saved" 33 | BOOTLOADER_RESET = "reset" 34 | BOOTLOADER_ERROR_SIZE = "error_size" 35 | 36 | OKGREEN = '\r\033[92m' 37 | FAIL = '\r\033[91m' 38 | ENDC = '\033[0m' 39 | UNDERLINE = '\r\033[4m' 40 | BOLD = '\r\033[1m' 41 | 42 | FILEPATH = None 43 | NB_SAMPLE_BY_FRAME_MAX = 127 44 | 45 | RESP_TIMEOUT = 3 46 | ERASE_TIMEOUT = 10 47 | PROGRAM_TIMEOUT = 2 48 | 49 | BOOTLOADER_SUCCESS = 0 50 | BOOTLOADER_DETECT_ERROR = 1 51 | BOOTLOADER_FLASH_ERROR = 2 52 | BOOTLOADER_FLASH_BINARY_ERROR = 3 53 | BOOTLOADER_FLASH_PORT_ERROR = 4 54 | # ******************************************************************************* 55 | # Function 56 | # ******************************************************************************* 57 | 58 | # ******************************************************************************* 59 | # @brief find nodes to program in the network 60 | # @param target list, routing table 61 | # @return a tuple with 2 lists : nodes to reboot and nodes to program 62 | # ******************************************************************************* 63 | 64 | 65 | def create_target_list(args, state): 66 | bypass_node = False 67 | nodes_to_program = [] 68 | for node in state['routing_table']: 69 | # prevent programmation of node 1 70 | bypass_node = False 71 | if (node['node_id'] == 1): 72 | bypass_node = True 73 | # check if node is in target list 74 | if not (bypass_node): 75 | for target in args.target: 76 | if (int(node['node_id']) == int(target)): 77 | nodes_to_program.append(node['node_id']) 78 | errorList = [] 79 | for target in args.target: 80 | if not (int(target) in nodes_to_program): 81 | errorList.append(target) 82 | 83 | if (len(errorList) > 1): 84 | print(BOLD + FAIL + u"\nNodes " + ' '.join(errorList) + " are not available and will be ignored." + ENDC) 85 | elif (len(errorList) == 1): 86 | print(BOLD + FAIL + u"** Node " + errorList[0] + " is not available and will be ignored. **" + ENDC) 87 | 88 | return (nodes_to_program) 89 | 90 | # ******************************************************************************* 91 | # @brief send commands 92 | # @param command type 93 | # @return None 94 | # ******************************************************************************* 95 | 96 | 97 | def send_topic_command(device, topic, command, size=0): 98 | # create a json file with the list of the nodes to program 99 | bootloader_cmd = { 100 | 'bootloader': { 101 | 'command': { 102 | 'type': command, 103 | 'node': 0, 104 | 'topic': topic, 105 | 'size': size 106 | }, 107 | } 108 | } 109 | # send json command 110 | device._send(bootloader_cmd) 111 | 112 | 113 | def send_node_command(device, node, topic, command, size=0): 114 | # create a json file with the list of the nodes to program 115 | bootloader_cmd = { 116 | 'bootloader': { 117 | 'command': { 118 | 'type': command, 119 | 'node': node, 120 | 'topic': topic, 121 | 'size': size 122 | }, 123 | } 124 | } 125 | # send json command 126 | device._send(bootloader_cmd) 127 | 128 | # ******************************************************************************* 129 | # @brief get binary size 130 | # @param 131 | # @return binary size 132 | # ******************************************************************************* 133 | 134 | 135 | def get_binary_size(): 136 | # get number of bytes in binary file 137 | with open(FILEPATH, mode="rb") as f: 138 | nb_bytes = len(f.read()) 139 | 140 | return nb_bytes 141 | 142 | # ******************************************************************************* 143 | # @brief send erase command 144 | # @param command type 145 | # @return None 146 | # ******************************************************************************* 147 | 148 | 149 | def send_ready_cmd(device, node, topic, verbose): 150 | return_value = True 151 | # send ready command to the node 152 | send_node_command(device, node, topic, BOOTLOADER_READY, get_binary_size()) 153 | # wait ready response 154 | state = device._poll_once() 155 | init_time = time.time() 156 | while (time.time() - init_time <= RESP_TIMEOUT): 157 | if 'bootloader' in state: 158 | for response in state['bootloader']: 159 | if response['response'] == BOOTLOADER_ERROR_SIZE: 160 | print(FAIL + u" ╰> Node n°", response['node'], "has not enough space in flash memory." + ENDC) 161 | # don't load binary if there is not enough place in flash memory 162 | return_value = False 163 | else: 164 | if verbose: 165 | print(OKGREEN + u" ╰> Node n°", response['node'], "is ready." + ENDC) 166 | return_value = True 167 | break 168 | 169 | state = device._poll_once() 170 | return_value = False 171 | 172 | return return_value 173 | 174 | # ******************************************************************************* 175 | # @brief waiting for erase response 176 | # @param 177 | # @return binary size 178 | # ******************************************************************************* 179 | 180 | 181 | def waiting_erase(): 182 | period = 0.1 183 | chars = "/—\|" 184 | while (1): 185 | for char in chars: 186 | time.sleep(period) 187 | print(u"\r ╰> Erase flash " + char , end='') 188 | 189 | 190 | # ******************************************************************************* 191 | # @brief send erase command 192 | # @param command type 193 | # @return None 194 | # ******************************************************************************* 195 | 196 | 197 | def erase_flash(device, topic, nodes_to_program, verbose): 198 | return_value = True 199 | failed_nodes = [] 200 | failed_nodes.extend(nodes_to_program) 201 | timeout = ERASE_TIMEOUT * len(nodes_to_program) 202 | 203 | # send erase command 204 | send_topic_command(device, topic, BOOTLOADER_ERASE) 205 | 206 | # display a progress bar 207 | waiting_bg = Process(target=waiting_erase) 208 | waiting_bg.start() 209 | 210 | # pull serial data 211 | state = device._poll_once() 212 | # initialize the timer that counts until node number * response time 213 | init_time = time.time() 214 | # check if all messages are received 215 | while len(failed_nodes): 216 | # timeout for exiting loop in case of fails 217 | if (time.time() - init_time > timeout): 218 | return_value = False 219 | print(FAIL + u"\r ╰> Erase flash of node", failed_nodes, "FAILED %" + ENDC) 220 | break 221 | # check if it is a response message 222 | if 'bootloader' in state: 223 | for response in state['bootloader']: 224 | if (response['response'] == BOOTLOADER_ERASE): 225 | # this node responded, delete it from the failed nodes list 226 | if response['node'] in failed_nodes: 227 | timeout -= ERASE_TIMEOUT 228 | failed_nodes.remove(response['node']) 229 | if verbose: 230 | print(OKGREEN + u"\r ╰> Flash memory of node", response['node'], "erased." + ENDC) 231 | state = device._poll_once() 232 | 233 | # retry sending failed messages 234 | for node in failed_nodes: 235 | send_node_command(device, node, topic, BOOTLOADER_ERASE) 236 | print(u"\r\n ╰> Retry erase memory of node", node) 237 | init_time = time.time() 238 | state = device._poll_once() 239 | while len(failed_nodes): 240 | if (time.time() - init_time > ERASE_TIMEOUT): 241 | return_value = False 242 | break 243 | 244 | # check if it is a response message 245 | if 'bootloader' in state: 246 | for response in state['bootloader']: 247 | if (response['response'] == BOOTLOADER_ERASE): 248 | # this node responded, delete it from the failed nodes list 249 | if response['node'] in failed_nodes: 250 | failed_nodes.remove(response['node']) 251 | if verbose: 252 | print(OKGREEN + u"\r ╰> Flash memory of node", response['node'], "erased." + ENDC) 253 | state = device._poll_once() 254 | 255 | waiting_bg.terminate() 256 | if not len(failed_nodes): 257 | return_value = True 258 | print(OKGREEN + u"\r ╰> All flash erased" + ENDC) 259 | 260 | return return_value, failed_nodes 261 | 262 | # ******************************************************************************* 263 | # @brief get binary size 264 | # @param 265 | # @return binary size 266 | # ******************************************************************************* 267 | 268 | 269 | def loading_bar(loading_progress): 270 | period = 0.1 271 | chars = "/—\|" 272 | while (1): 273 | for char in chars: 274 | time.sleep(period) 275 | print(u"\r ╰> Loading : " + char + " {:.2f} %".format(loading_progress.value), end='') 276 | 277 | # ******************************************************************************* 278 | # @brief send the binary file to the node 279 | # @param command type 280 | # @return None 281 | # ******************************************************************************* 282 | 283 | 284 | def send_binary_data(device, topic, nodes_to_program): 285 | loading_state = True 286 | failed_nodes = [] 287 | prev_fails = [] 288 | # compute total number of bytes to send 289 | with open(FILEPATH, mode="rb") as f: 290 | nb_bytes = len(f.read()) 291 | # compute total number of frames to send 292 | nb_frames = math.ceil(nb_bytes / NB_SAMPLE_BY_FRAME_MAX) 293 | 294 | # display a progress bar to inform user 295 | loading_progress = Value('f', 0.0) 296 | loading_bar_bg = Process(target=loading_bar, args=(loading_progress,)) 297 | loading_bar_bg.start() 298 | 299 | # send each frame to the network 300 | file_offset = 0 301 | for frame_index in range(nb_frames): 302 | if (frame_index == (nb_frames - 1)): 303 | # last frame, compute size 304 | frame_size = nb_bytes - (nb_frames - 1) * NB_SAMPLE_BY_FRAME_MAX 305 | else: 306 | frame_size = NB_SAMPLE_BY_FRAME_MAX 307 | 308 | # send the current frame 309 | loading_state, failed_nodes = send_frame_from_binary(device, topic, frame_size, file_offset, nodes_to_program) 310 | if not loading_state: 311 | print(FAIL + u"\r ╰> Loading of node", failed_nodes, "FAILED" + ENDC) 312 | for fail in failed_nodes: 313 | nodes_to_program.remove(fail) 314 | prev_fails.extend(failed_nodes) 315 | loading_state = True 316 | if not len(nodes_to_program): 317 | loading_state = False 318 | break 319 | # update cursor position in the binary file 320 | file_offset += frame_size 321 | # update loading progress 322 | loading_progress.value = frame_index / nb_frames * 100 323 | 324 | # kill the progress bar at the end of the loading 325 | loading_bar_bg.terminate() 326 | if loading_state: 327 | print(OKGREEN + u"\r ╰> Loading : 100.0 % " + ENDC) 328 | if len(prev_fails): 329 | loading_state = False 330 | return loading_state, prev_fails 331 | 332 | # ******************************************************************************* 333 | # @brief open binary file and send a frame 334 | # @param 335 | # @return None 336 | # ******************************************************************************* 337 | 338 | 339 | def send_frame_from_binary(device, topic, frame_size, file_offset, nodes_to_program): 340 | return_value = True 341 | failed_nodes = [] 342 | failed_nodes.extend(nodes_to_program) 343 | 344 | with open(FILEPATH, mode="rb") as f: 345 | # put the cursor at the beginning of the file 346 | f.seek(file_offset) 347 | # read binary data 348 | data_bytes = f.read(1) 349 | for sample in range(frame_size - 1): 350 | data_bytes = data_bytes + f.read(1) 351 | send_data(device, topic, BOOTLOADER_BIN_CHUNK, frame_size, data_bytes) 352 | # pull serial data 353 | state = device._poll_once() 354 | 355 | # wait nodes response 356 | init_time = time.time() 357 | while len(failed_nodes): 358 | # check for timeout of nodes 359 | if (time.time() - init_time > PROGRAM_TIMEOUT): 360 | return_value = False 361 | break 362 | # check if it is a response message 363 | if 'bootloader' in state: 364 | for response in state['bootloader']: 365 | if (response['response'] == BOOTLOADER_BIN_CHUNK): 366 | # the node responsed, remove it for fails list 367 | if response['node'] in failed_nodes: 368 | failed_nodes.remove(response['node']) 369 | time.sleep(0.001) 370 | # wait for next message 371 | state = device._poll_once() 372 | 373 | for node in failed_nodes: 374 | # retry sending failed messages 375 | send_data_node(device, node, BOOTLOADER_BIN_CHUNK, frame_size, data_bytes) 376 | state = device._poll_once() 377 | print(u"\r\n ╰> Retry sending binary message to node ", node) 378 | init_time = time.time() 379 | while len(failed_nodes): 380 | # check for timeout of nodes 381 | if (time.time() - init_time > PROGRAM_TIMEOUT): 382 | return_value = False 383 | break 384 | 385 | # check if it is a response message 386 | if 'bootloader' in state: 387 | for response in state['bootloader']: 388 | if (response['response'] == BOOTLOADER_BIN_CHUNK): 389 | # the node responsed, remove it for fails list 390 | if response['node'] in failed_nodes: 391 | failed_nodes.remove(response['node']) 392 | # wait for next message 393 | state = device._poll_once() 394 | 395 | if not len(failed_nodes): 396 | return_value = True 397 | 398 | return return_value, failed_nodes 399 | 400 | # ******************************************************************************* 401 | # @brief send binary data with a header 402 | # @param 403 | # @return None 404 | # ******************************************************************************* 405 | 406 | 407 | def send_data(device, topic, command, size, data): 408 | # create a json file with the list of the nodes to program 409 | bootloader_cmd = { 410 | 'bootloader': { 411 | 'command': { 412 | 'size': [size], 413 | 'type': command, 414 | 'topic': topic, 415 | 'node': 0, 416 | }, 417 | } 418 | } 419 | # send json command 420 | device._write(json.dumps(bootloader_cmd).encode() + '\n'.encode() + data) 421 | 422 | # ******************************************************************************* 423 | # @brief send binary data with a header to a specific node 424 | # @param 425 | # @return None 426 | # ******************************************************************************* 427 | 428 | 429 | def send_data_node(device, node, command, size, data): 430 | # create a json file with the list of the nodes to program 431 | bootloader_cmd = { 432 | 'bootloader': { 433 | 'command': { 434 | 'size': [size], 435 | 'type': command, 436 | 'topic': 1, 437 | 'node': node, 438 | }, 439 | } 440 | } 441 | # send json command 442 | device._write(json.dumps(bootloader_cmd).encode() + '\n'.encode() + data) 443 | 444 | # ******************************************************************************* 445 | # @brief send the binary end command 446 | # @param 447 | # @return 448 | # ******************************************************************************* 449 | 450 | 451 | def send_binary_end(device, topic, nodes_to_program, verbose): 452 | return_value = True 453 | failed_nodes = [] 454 | failed_nodes.extend(nodes_to_program) 455 | timeout = RESP_TIMEOUT * len(nodes_to_program) 456 | # send command 457 | send_topic_command(device, topic, BOOTLOADER_BIN_END) 458 | # poll serial data 459 | state = device._poll_once() 460 | # wait bin_end response 461 | init_time = time.time() 462 | while len(failed_nodes) > 0: 463 | # check if we exit with timeout 464 | if (time.time() - init_time > timeout): 465 | return_value = False 466 | break 467 | if 'bootloader' in state: 468 | for response in state['bootloader']: 469 | # check each node response 470 | if (response['response'] == BOOTLOADER_BIN_END): 471 | if verbose: 472 | print(OKGREEN + u" ╰> Node", response['node'], "acknowledge received, loading is complete." + ENDC) 473 | # remove node from fails list 474 | if response['node'] in failed_nodes: 475 | timeout -= RESP_TIMEOUT 476 | failed_nodes.remove(response['node']) 477 | state = device._poll_once() 478 | 479 | for node in failed_nodes: 480 | # retry sending failed messages 481 | send_node_command(device, node, topic, BOOTLOADER_BIN_END) 482 | if verbose: 483 | print(u"\r\n ╰> Retry sending end message to node ", node) 484 | state = device._poll_once() 485 | init_time = time.time() 486 | while len(failed_nodes): 487 | # check for timeout of nodes 488 | if (time.time() - init_time > RESP_TIMEOUT): 489 | return_value = False 490 | break 491 | 492 | # check if it is a response message 493 | if 'bootloader' in state: 494 | for response in state['bootloader']: 495 | if (response['response'] == BOOTLOADER_BIN_END): 496 | # the node responsed, remove it for fails list 497 | if response['node'] in failed_nodes: 498 | failed_nodes.remove(response['node']) 499 | # wait for next message 500 | state = device._poll_once() 501 | 502 | if not len(failed_nodes): 503 | return_value = True 504 | 505 | return return_value, failed_nodes 506 | 507 | # ******************************************************************************* 508 | # @brief compute binary crc 509 | # @param 510 | # @return None 511 | # ******************************************************************************* 512 | 513 | 514 | def compute_crc(): 515 | # create crc8 function object 516 | hash = crc8.crc8() 517 | # get number of bytes in binary file 518 | with open(FILEPATH, mode="rb") as f: 519 | nb_bytes = len(f.read()) 520 | 521 | with open(FILEPATH, mode="rb") as f: 522 | for bytes in range(nb_bytes): 523 | data = f.read(1) 524 | hash.update(data) 525 | crc = hash.digest() 526 | 527 | return crc 528 | 529 | # ******************************************************************************* 530 | # @brief send the binary end command 531 | # @param 532 | # @return 533 | # ******************************************************************************* 534 | 535 | 536 | def check_crc(device, topic, nodes_to_program, verbose): 537 | return_value = True 538 | failed_nodes = nodes_to_program.copy() 539 | 540 | # send crc command 541 | send_topic_command(device, topic, BOOTLOADER_CRC) 542 | 543 | state = device._poll_once() 544 | # wait bin_end response 545 | init_time = time.time() 546 | while len(failed_nodes): 547 | # check for timeout exit 548 | if (time.time() - init_time > RESP_TIMEOUT): 549 | return_value = False 550 | break 551 | # check the response 552 | if 'bootloader' in state: 553 | for response in state['bootloader']: 554 | if (response['response'] == BOOTLOADER_CRC): 555 | source_crc = int.from_bytes(compute_crc(), byteorder='big') 556 | node_crc = response['crc_value'] 557 | node_id = response['node'] 558 | # crc properly received 559 | if (source_crc == node_crc): 560 | if verbose: 561 | print(OKGREEN + u" ╰> CRC test for node", node_id, ": OK." + ENDC) 562 | if node_id in failed_nodes: 563 | failed_nodes.remove(node_id) 564 | else: 565 | # not a good crc 566 | print(FAIL + u" ╰> CRC test for node", node_id, ": NOK." + ENDC) 567 | print(FAIL + u" ╰> waited :", hex(source_crc), ", received :", hex(node_crc) + ENDC) 568 | return_value = False 569 | state = device._poll_once() 570 | 571 | for node in failed_nodes: 572 | # retry sending failed messages 573 | send_node_command(device, node, topic, BOOTLOADER_CRC) 574 | print(u"\r\n ╰> Retry sending crc request to node ", node) 575 | state = device._poll_once() 576 | init_time = time.time() 577 | while len(failed_nodes): 578 | # check for timeout of nodes 579 | if (time.time() - init_time > RESP_TIMEOUT): 580 | return_value = False 581 | break 582 | 583 | # check if it is a response message 584 | if 'bootloader' in state: 585 | for response in state['bootloader']: 586 | if (response['response'] == BOOTLOADER_CRC): 587 | source_crc = int.from_bytes(compute_crc(), byteorder='big') 588 | node_crc = response['crc_value'] 589 | node_id = response['node'] 590 | # crc properly received 591 | if (source_crc == node_crc): 592 | print(OKGREEN + u" ╰> CRC test for node", node_id, " : OK." + ENDC) 593 | if node_id in failed_nodes: 594 | timeout -= RESP_TIMEOUT 595 | failed_nodes.remove(node_id) 596 | else: 597 | # not a good crc 598 | print(FAIL + u" ╰> CRC test for node", node_id, ": NOK." + ENDC) 599 | print(FAIL + u" ╰> waited :", hex(source_crc), ", received :", hex(node_crc) + ENDC) 600 | return_value = False 601 | state = device._poll_once() 602 | 603 | if not len(failed_nodes): 604 | return_value = True 605 | 606 | return return_value, failed_nodes 607 | 608 | # ******************************************************************************* 609 | # @brief reboot all nodes in application mode 610 | # @param 611 | # @return 612 | # ******************************************************************************* 613 | 614 | 615 | def reboot_network(device, topic, nodes_to_program, verbose): 616 | for node in nodes_to_program: 617 | send_node_command(device, node, topic, BOOTLOADER_STOP) 618 | if verbose: 619 | print(OKGREEN + u" ╰> Node", node, ": rebooted." + ENDC) 620 | # delay to let gate send commands 621 | time.sleep(0.01) 622 | 623 | # ******************************************************************************* 624 | # @brief command used to flash luos nodes 625 | # @param flash function arguments : -g, -t, -b 626 | # @return None 627 | # ******************************************************************************* 628 | 629 | 630 | def luos_flash(args): 631 | topic = 1 632 | begin_date = time.time() 633 | if not (args.port): 634 | try: 635 | args.port = serial_discover(os.getenv('LUOS_BAUDRATE', args.baudrate))[0] 636 | except: 637 | print('Please specify a port to access the network.') 638 | return BOOTLOADER_FLASH_PORT_ERROR 639 | 640 | baudrate = os.getenv('LUOS_BAUDRATE', args.baudrate) 641 | 642 | if (args.verbose): 643 | print("\n" + UNDERLINE + "Luos flash subcommand with parameters:" + ENDC) 644 | print('\t--baudrate : ', baudrate) 645 | print('\t--gate : ', args.gate) 646 | print('\t--target : ', args.target) 647 | print('\t--binary : ', args.binary) 648 | print('\t--port : ', args.port) 649 | 650 | # State used to check each step 651 | machine_state = True 652 | # List of all the nodes that may fail in each step 653 | total_fails = [] 654 | # Update firmware path 655 | global FILEPATH 656 | FILEPATH = args.binary 657 | try: 658 | f = open(FILEPATH, mode="rb") 659 | except IOError: 660 | print(FAIL + "Cannot open :", FILEPATH + ENDC) 661 | return BOOTLOADER_FLASH_BINARY_ERROR 662 | else: 663 | f.close() 664 | 665 | # Init device 666 | if (not args.verbose): 667 | sys.stdout = open(os.devnull, 'w') 668 | device = Device(args.port, baudrate=baudrate, background_task=False) 669 | if (not args.verbose): 670 | sys.stdout = sys.__stdout__ 671 | 672 | # Get routing table JSON 673 | state = device._routing_table 674 | if state is None: 675 | return BOOTLOADER_DETECT_ERROR 676 | 677 | # Searching nodes to program in network 678 | nodes_to_program = create_target_list(args, state) 679 | 680 | # Check if we have available node to program 681 | if not nodes_to_program: 682 | print(FAIL + "No target found :\n" + str(device.nodes) + ENDC) 683 | return BOOTLOADER_DETECT_ERROR 684 | 685 | # Reboot all nodes in bootloader mode 686 | print("\n" + BOLD + "Rebooting all nodes in bootloader mode." + ENDC) 687 | 688 | need_to_redetect = False 689 | for node in device._nodes: 690 | if node.id in nodes_to_program: 691 | 692 | if (args.verbose): 693 | print("─> Check if node", node.id, "is in bootloader mode.") 694 | for service in node.services: 695 | if "boot" in service.alias: 696 | if (args.verbose): 697 | print(OKGREEN + " ╰> Node", node.id, "is in bootloader mode." + ENDC) 698 | else: 699 | need_to_redetect = True 700 | if (args.verbose): 701 | print(OKGREEN + " ╰> Reboot node", node.id, "in bootloader mode." + ENDC) 702 | send_node_command(device, node.id, topic, BOOTLOADER_START) 703 | time.sleep(0.01) 704 | 705 | if need_to_redetect: 706 | # Delay to let the gate send the last command 707 | time.sleep(3) 708 | 709 | # remake a detection to check if all nodes are in bootloader mode 710 | device.close() 711 | 712 | if (not args.verbose): 713 | sys.stdout = open(os.devnull, 'w') 714 | device = Device(args.port, baudrate=baudrate, background_task=False) 715 | if (not args.verbose): 716 | sys.stdout = sys.__stdout__ 717 | state = device._routing_table 718 | if (args.verbose): 719 | print("\n" + BOLD + "Check if all node are in bootloader mode:" + ENDC) 720 | if state is None: 721 | print(FAIL + " ╰> Reboot in bootloader mode failed." + ENDC) 722 | return BOOTLOADER_DETECT_ERROR 723 | else: 724 | # Check if all node of the 'nodes_to_program' list is in bootloader mode 725 | detected_node = nodes_to_program.copy() 726 | for node in device._nodes: 727 | if node.id in nodes_to_program: 728 | detected_node.remove(node.id) 729 | if (args.verbose): 730 | print("─> Check if node", node.id, "is in bootloader mode.") 731 | for service in node.services: 732 | if "boot" in service.alias: 733 | if (args.verbose): 734 | print(OKGREEN + " ╰> Node", node.id, "is in bootloader mode." + ENDC) 735 | else: 736 | total_fails.append(node.id) 737 | if (args.verbose): 738 | print(FAIL + " ╰> Node", node.id, "reboot in bootloader mode failed." + ENDC) 739 | if (len(detected_node) > 0): 740 | total_fails.extend(detected_node) 741 | print(FAIL + " ╰> Nodes", detected_node, "failed to restart in bootloader mode." + ENDC) 742 | 743 | for node in total_fails: 744 | try: 745 | nodes_to_program.remove(node) 746 | except: 747 | pass 748 | if len(nodes_to_program) == 0: 749 | print(FAIL + "Programming failed on all targets." + ENDC) 750 | return BOOTLOADER_FLASH_ERROR 751 | 752 | # Wait before the next step 753 | time.sleep(0.4) 754 | 755 | if (args.verbose): 756 | print("\n" + BOLD + "Programming nodes:" + ENDC) 757 | else: 758 | print(BOLD + "Programming nodes:" + ENDC) 759 | # Go to header state if node is ready 760 | for node in nodes_to_program: 761 | if (args.verbose): 762 | print("─> Check if node", node, "is ready.") 763 | machine_state = send_ready_cmd(device, node, topic, args.verbose) 764 | if not machine_state: 765 | total_fails.append(node) 766 | machine_state = True 767 | print(FAIL + " ╰> Node", node, "programming failed." + ENDC) 768 | time.sleep(0.01) 769 | 770 | for node in total_fails: 771 | try: 772 | nodes_to_program.remove(node) 773 | except: 774 | pass 775 | if len(nodes_to_program) == 0: 776 | print(BOLD + FAIL + "Programming failed on all targets." + ENDC) 777 | return BOOTLOADER_FLASH_ERROR 778 | 779 | if not len(nodes_to_program): 780 | print(BOLD + FAIL + "Programming failed on all targets." + ENDC) 781 | return BOOTLOADER_FLASH_ERROR 782 | 783 | # Erase node flash memory 784 | print("─> Erasing flash memory.") 785 | machine_state, failed_nodes = erase_flash(device, topic, nodes_to_program, args.verbose) 786 | if not machine_state: 787 | for fail in failed_nodes: 788 | nodes_to_program.remove(fail) 789 | total_fails.extend(failed_nodes) 790 | machine_state = True 791 | print(FAIL + " ╰> Node", failed_nodes, "flash erasing failed!" + ENDC) 792 | 793 | if not len(nodes_to_program): 794 | print(BOLD + FAIL + "Programming failed on all targets." + ENDC) 795 | return BOOTLOADER_FLASH_ERROR 796 | 797 | # send binary data 798 | print("─> Sending binary data.") 799 | machine_state, failed_nodes = send_binary_data(device, topic, nodes_to_program) 800 | if not machine_state: 801 | total_fails.extend(failed_nodes) 802 | machine_state = True 803 | print(FAIL + "Node", failed_nodes, "programming failed." + ENDC) 804 | 805 | if not len(nodes_to_program): 806 | print(BOLD + FAIL + "Programming failed on all targets." + ENDC) 807 | return BOOTLOADER_FLASH_ERROR 808 | 809 | # inform the node of the end of the loading 810 | if (args.verbose): 811 | print("─> Programmation finished, waiting for acknowledgements.") 812 | machine_state, failed_nodes = send_binary_end(device, topic, nodes_to_program, args.verbose) 813 | if not machine_state: 814 | for fail in failed_nodes: 815 | nodes_to_program.remove(fail) 816 | total_fails.extend(failed_nodes) 817 | machine_state = True 818 | print(FAIL + "Node", failed_nodes, "application validation failed!" + ENDC) 819 | 820 | # Ask the node to send binary crc 821 | if (args.verbose): 822 | print("─> Checking binary CRC.") 823 | machine_state, failed_nodes = check_crc(device, topic, nodes_to_program, args.verbose) 824 | if not machine_state: 825 | for fail in failed_nodes: 826 | nodes_to_program.remove(fail) 827 | total_fails.extend(failed_nodes) 828 | machine_state = True 829 | print(FAIL + "Node", failed_nodes, "ACK failed!" + ENDC) 830 | 831 | # Say to the bootloader that the integrity of the app saved in flash has been verified 832 | if (args.verbose): 833 | print("─> Valid application.") 834 | send_topic_command(device, topic, BOOTLOADER_APP_SAVED) 835 | 836 | # wait before next step 837 | time.sleep(1) 838 | # reboot all nodes in application mode 839 | if (args.verbose): 840 | print("\n" + BOLD + "Rebooting all nodes in application mode." + ENDC) 841 | else: 842 | print(BOLD + "Rebooting all nodes in application mode." + ENDC) 843 | reboot_network(device, topic, nodes_to_program, args.verbose) 844 | if len(total_fails) == 0: 845 | print(OKGREEN + BOLD + "Programming succeed in {:.3f} s.".format(time.time() - begin_date) + ENDC) 846 | device.close() 847 | return BOOTLOADER_SUCCESS 848 | else: 849 | device.close() 850 | print(BOLD + "Programming in {:.3f} s.".format(time.time() - begin_date) + ENDC) 851 | print(FAIL + "Nodes", total_fails, "programming failed, please reboot and retry." + ENDC) 852 | return BOOTLOADER_FLASH_ERROR 853 | 854 | # ******************************************************************************* 855 | # @brief command used to detect network 856 | # @param detect function arguments : -p 857 | # @return None 858 | # ******************************************************************************* 859 | 860 | 861 | def luos_detect(args): 862 | if not (args.port): 863 | try: 864 | args.port = serial_discover(os.getenv('LUOS_BAUDRATE', args.baudrate))[0] 865 | except: 866 | print('Please specify a port to access the network.') 867 | return BOOTLOADER_DETECT_ERROR 868 | 869 | baudrate = os.getenv('LUOS_BAUDRATE', args.baudrate) 870 | 871 | print('Luos detect subcommand on port : ', args.port) 872 | print('\tLuos detect subcommand at baudrate : ', baudrate) 873 | 874 | # detect network 875 | device = Device(args.port, baudrate=baudrate) 876 | # print network to user 877 | print(device.nodes) 878 | device.close() 879 | 880 | return BOOTLOADER_SUCCESS 881 | 882 | # ******************************************************************************* 883 | # @brief command used to force reset a node in bootloader mode 884 | # @param detect function arguments : -p 885 | # @return None 886 | # ******************************************************************************* 887 | 888 | 889 | def luos_reset(args): 890 | if not (args.port): 891 | try: 892 | args.port = serial_discover(os.getenv('LUOS_BAUDRATE', args.baudrate))[0] 893 | except: 894 | return BOOTLOADER_DETECT_ERROR 895 | 896 | baudrate = os.getenv('LUOS_BAUDRATE', args.baudrate) 897 | 898 | print('Luos discover subcommand on port : ', args.port) 899 | print('\tLuos discover subcommand at baudrate : ', baudrate) 900 | 901 | # send rescue command 902 | print('Send reset command.') 903 | # port = serial.Serial(args.port, baudrate, timeout=0.05) 904 | port = io_from_host(host=args.port, baudrate=baudrate) 905 | rst_cmd = { 906 | 'bootloader': { 907 | 'command': { 908 | 'type': BOOTLOADER_RESET, 909 | 'node': 0, 910 | 'size': 0 911 | }, 912 | } 913 | } 914 | s = json.dumps(rst_cmd).encode() 915 | port.write(b'\x7E' + struct.pack('Ser: {}'.format(message)) 27 | 28 | try: 29 | message = message.encode() 30 | self.serial.write(message) 31 | 32 | except UnicodeDecodeError: 33 | print("Fail") 34 | pass 35 | 36 | 37 | def send(self, message): 38 | if self.verbose: 39 | print('Ser->WS: {}'.format(message)) 40 | self.sendMessage(message) 41 | 42 | def _check_msg(self): 43 | while True: 44 | r = self.serial.recv() 45 | try: 46 | self.send(r) 47 | except UnicodeDecodeError: 48 | print('Fail', r) 49 | 50 | print('LOOP OVER!') 51 | 52 | 53 | def main(): 54 | import argparse 55 | 56 | parser = argparse.ArgumentParser() 57 | parser.add_argument('--serial-port', type=str, required=True) 58 | parser.add_argument('--ws-port', type=int, required=True) 59 | parser.add_argument('--verbose', action='store_true', default=False) 60 | args = parser.parse_args() 61 | 62 | SerialToWs.serial_port = args.serial_port 63 | SerialToWs.verbose = args.verbose 64 | 65 | io_server = SimpleWebSocketServer('', args.ws_port, SerialToWs) 66 | io_server.serveforever() 67 | 68 | 69 | if __name__ == '__main__': 70 | main() 71 | -------------------------------------------------------------------------------- /pyluos/tools/usb_gate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from subprocess import Popen 4 | 5 | from ..io.serial_io import Serial 6 | 7 | 8 | def discover(): 9 | return Serial.available_hosts() 10 | 11 | 12 | def redirect_to_ws(serial_port, ws_port): 13 | base_path = os.path.dirname(__file__) 14 | 15 | return Popen(['python', os.path.join(base_path, 'usb2ws.py'), 16 | '--serial-port', serial_port, 17 | '--ws-port', str(ws_port)]) 18 | 19 | 20 | def main(): 21 | import time 22 | import argparse 23 | 24 | parser = argparse.ArgumentParser() 25 | parser.add_argument('cmd', choices=('discover', )) 26 | args = parser.parse_args() 27 | 28 | if args.cmd == 'discover': 29 | while True: 30 | print(discover()) 31 | time.sleep(1.0) 32 | 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /pyluos/tools/wifi_gate.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from contextlib import closing 4 | 5 | from zeroconf import ServiceBrowser, Zeroconf 6 | 7 | 8 | class MyListener(object): 9 | wifi_gates = set() 10 | 11 | def remove_service(self, zeroconf, type, name): 12 | self.wifi_gates.remove(name) 13 | 14 | def add_service(self, zeroconf, type, name): 15 | self.wifi_gates.add(name) 16 | 17 | 18 | def discover(): 19 | with closing(Zeroconf()) as zeroconf: 20 | listener = MyListener() 21 | listener.wifi_gates.clear() 22 | 23 | browser = ServiceBrowser(zeroconf, "_jsongate._tcp.local.", listener) 24 | 25 | time.sleep(1.0) 26 | 27 | gates = [g.replace('._jsongate._tcp', '')[:-1] 28 | for g in listener.wifi_gates] 29 | 30 | return { 31 | host.replace('.local', ''): (host, 9342) 32 | for host in gates 33 | } 34 | 35 | 36 | def main(): 37 | import argparse 38 | 39 | parser = argparse.ArgumentParser() 40 | parser.add_argument('cmd', choices=('discover', )) 41 | args = parser.parse_args() 42 | 43 | if args.cmd == 'discover': 44 | while True: 45 | print(discover()) 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /pyluos/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from threading import Thread 4 | from time import time, sleep 5 | from math import sin, pi 6 | 7 | 8 | class Sinus(object): 9 | update_frequency = 25.0 10 | 11 | def __init__(self, motor, frequency, amplitude, offset, phase): 12 | self.motor = motor 13 | 14 | self.frequency = frequency 15 | self.amplitude = amplitude 16 | self.offset = offset 17 | self.phase = phase 18 | 19 | self._running = False 20 | self._t = None 21 | 22 | def start(self): 23 | if self._t is not None: 24 | raise EnvironmentError('Sinus already running!') 25 | 26 | self._running = True 27 | self._t = Thread(target=self._run) 28 | self._t.start() 29 | 30 | def stop(self): 31 | self._running = False 32 | if self._t is not None: 33 | self._t.join() 34 | self._t = None 35 | 36 | def _run(self): 37 | t0 = time() 38 | 39 | while self._running: 40 | t = time() - t0 41 | pos = self.amplitude * sin(2 * pi * self.frequency * t + (self.phase * pi / 180)) + self.offset 42 | 43 | self.motor.target_position = pos 44 | sleep(1 / self.update_frequency) 45 | -------------------------------------------------------------------------------- /pyluos/version.py: -------------------------------------------------------------------------------- 1 | version = '3.0.0' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E731,F401 3 | max-line-length = 160 4 | exclude = build,dist,tests,*.egg-info,doc/* 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | import imp 5 | version = imp.load_source('pyluos.version', 'pyluos/version.py') 6 | except ImportError: 7 | from importlib.machinery import SourceFileLoader 8 | version = SourceFileLoader('pyluos.version', 'pyluos/version.py').load_module() 9 | 10 | from setuptools import setup, find_packages 11 | 12 | with open("README.md", "r") as fh: 13 | long_description = fh.read() 14 | 15 | setup(name='pyluos', 16 | version=version.version, 17 | author="Luos", 18 | author_email="hello@luos.io", 19 | url="https://docs.luos.io/pages/high/pyluos.html", 20 | description="Python library to set the high level behavior of your device based on Luos embedded system.", 21 | long_description=open('README.md').read(), 22 | long_description_content_type='text/markdown', 23 | license='MIT', 24 | packages=find_packages(), 25 | install_requires=['future', 26 | 'websocket-client', 27 | 'pyserial>3', 28 | 'SimpleWebSocketServer', 29 | 'zeroconf', 30 | 'numpy', 31 | 'anytree', 32 | 'crc8', 33 | 'ipython', 34 | 'requests' 35 | ], 36 | extras_require={ 37 | 'tests': ['pytest', 'flake8'], 38 | 'jupyter-integration': ['ipywidgets'], 39 | }, 40 | entry_points={ 41 | 'console_scripts': [ 42 | 'pyluos-wifi-gate = pyluos.tools.wifi_gate:main', 43 | 'pyluos-usb-gate = pyluos.tools.usb_gate:main', 44 | 'pyluos-usb2ws = pyluos.tools.usb2ws:main', 45 | 'pyluos-bootloader = pyluos.tools.bootloader:main', 46 | 'pyluos-shell = pyluos.tools.shell:main', 47 | 'pyluos-discover = pyluos.tools.discover:main' 48 | ], 49 | }, 50 | ) 51 | -------------------------------------------------------------------------------- /tests/fakerobot.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import socket 3 | import time 4 | 5 | from subprocess import Popen 6 | from contextlib import closing 7 | 8 | 9 | TIMEOUT = 30 10 | host, port = '127.0.0.1', 9342 11 | 12 | 13 | class TestCase(unittest.TestCase): 14 | def setUp(self): 15 | self._fake_robot = Popen(['python', '../tools/fake_robot.py']) 16 | wait_for_server() 17 | 18 | def tearDown(self): 19 | self._fake_robot.terminate() 20 | self._fake_robot.wait() 21 | 22 | 23 | def wait_for_server(): 24 | start = time.time() 25 | while (time.time() - start) < TIMEOUT: 26 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: 27 | if sock.connect_ex((host, port)) == 0: 28 | break 29 | time.sleep(0.1) 30 | else: 31 | raise EnvironmentError('Could not connect to fake robot!') 32 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestImport(unittest.TestCase): 5 | def test_import_general(self): 6 | import pyluos 7 | 8 | def test_import_device(self): 9 | from pyluos import Device 10 | 11 | 12 | if __name__ == '__main__': 13 | unittest.main() 14 | -------------------------------------------------------------------------------- /tests/test_lifecycle.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from threading import Event 4 | from contextlib import closing 5 | from random import randint, choice 6 | from string import ascii_lowercase 7 | 8 | from pyluos import Device 9 | 10 | import fakerobot 11 | 12 | 13 | # class TestWsRobot(fakerobot.TestCase): 14 | # def test_life_cycle(self): 15 | # robot = Device(fakerobot.host) 16 | # self.assertTrue(robot.alive) 17 | # robot.close() 18 | # self.assertFalse(robot.alive) 19 | 20 | # def test_services(self): 21 | # with closing(Device(fakerobot.host)) as robot: 22 | # for mod in robot.services: 23 | # self.assertTrue(hasattr(robot, mod.alias)) 24 | 25 | # def test_cmd(self): 26 | # with closing(Device(fakerobot.host)) as robot: 27 | # pos = randint(0, 180) 28 | # robot.my_servo.position = pos 29 | # self.assertEqual(robot.my_servo.position, pos) 30 | 31 | # def test_possible_events(self): 32 | # with closing(Device(fakerobot.host)) as robot: 33 | # for mod in robot.services: 34 | # self.assertTrue(isinstance(mod.possible_events, set)) 35 | 36 | # self.assertTrue('pressed' in robot.my_button.possible_events) 37 | 38 | # def test_add_evt_cb(self): 39 | # with closing(Device(fakerobot.host)) as robot: 40 | # robot.cb_trigger = Event() 41 | 42 | # def dummy_cb(evt): 43 | # robot.cb_trigger.set() 44 | 45 | # evt = choice(list(robot.my_button.possible_events)) 46 | # robot.my_button.add_callback(evt, dummy_cb) 47 | # robot.cb_trigger.wait() 48 | # robot.my_button.remove_callback(evt, dummy_cb) 49 | 50 | # def test_unknwon_evt(self): 51 | # def dummy_cb(evt): 52 | # pass 53 | 54 | # with closing(Device(fakerobot.host)) as robot: 55 | # mod = robot.my_potentiometer 56 | # while True: 57 | # evt = ''.join(choice(ascii_lowercase) 58 | # for i in range(8)) 59 | # if evt not in mod.possible_events: 60 | # break 61 | 62 | # with self.assertRaises(ValueError): 63 | # mod.add_callback(evt, dummy_cb) 64 | 65 | # def test_servoing_evt(self): 66 | # with closing(Device(fakerobot.host)) as robot: 67 | # robot._synced = Event() 68 | 69 | # def on_move(evt): 70 | # robot.my_servo.position = evt.new_value 71 | # robot._synced.set() 72 | 73 | # robot.my_potentiometer.add_callback('moved', 74 | # on_move) 75 | 76 | # robot._synced.wait() 77 | # self.assertEqual(robot.my_potentiometer.position, 78 | # robot.my_servo.position) 79 | 80 | 81 | if __name__ == '__main__': 82 | unittest.main() 83 | -------------------------------------------------------------------------------- /tests/test_rename.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import random 3 | import string 4 | 5 | from contextlib import closing 6 | 7 | from pyluos import Device 8 | 9 | import fakerobot 10 | 11 | 12 | # class TestWsRobot(fakerobot.TestCase): 13 | # def test_rename(self): 14 | # with closing(Device(fakerobot.host)) as robot: 15 | # for _ in range(5): 16 | # length = random.randint(1, robot._max_alias_length) 17 | # mod = random.choice(robot.services) 18 | 19 | # old = mod.alias 20 | # new = self.random_name(length) 21 | 22 | # robot.rename_service(old, new) 23 | 24 | # self.assertTrue(hasattr(robot, new)) 25 | # self.assertFalse(hasattr(robot, old)) 26 | 27 | # def test_unexisting_service(self): 28 | # with closing(Device(fakerobot.host)) as robot: 29 | # while True: 30 | # length = random.randint(1, robot._max_alias_length) 31 | # name = self.random_name(length) 32 | 33 | # if not hasattr(robot, name): 34 | # break 35 | 36 | # with self.assertRaises(ValueError): 37 | # robot.rename_service(name, 'oups') 38 | 39 | # def test_loooooong_name(self): 40 | # with closing(Device(fakerobot.host)) as robot: 41 | # mod = random.choice(robot.services) 42 | 43 | # length = random.randint(robot._max_alias_length + 1, 44 | # robot._max_alias_length + 100) 45 | # long_name = self.random_name(length) 46 | 47 | # with self.assertRaises(ValueError): 48 | # robot.rename_service(mod.alias, long_name) 49 | 50 | # def random_name(self, length): 51 | # return ''.join(random.choice(string.ascii_lowercase) 52 | # for _ in range(length)) 53 | 54 | 55 | if __name__ == '__main__': 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /tests/test_serial.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestSerialRobot(unittest.TestCase): 5 | def setUp(self): 6 | pass 7 | 8 | def tearDown(self): 9 | pass 10 | 11 | def test_serial_host(self): 12 | from pyluos.io import Serial 13 | 14 | self.assertFalse(Serial.is_host_compatible('192.168.0.42')) 15 | 16 | 17 | if __name__ == '__main__': 18 | unittest.main() 19 | -------------------------------------------------------------------------------- /tests/test_servo.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from threading import Event 4 | from contextlib import closing 5 | 6 | from pyluos import Device 7 | 8 | import fakerobot 9 | 10 | 11 | # class TestWsRobot(fakerobot.TestCase): 12 | # def test_first_command(self): 13 | # with closing(Device(fakerobot.host)) as robot: 14 | # sent = Event() 15 | 16 | # def my_send(msg): 17 | # sent.set() 18 | 19 | # robot._send = my_send 20 | 21 | # robot.my_servo.target_position = 0 22 | # sent.wait() 23 | 24 | # def test_speed_control(self): 25 | # with closing(Device(fakerobot.host)) as robot: 26 | # # Stop sync to make sure the fake robot 27 | # # does not change the position anymore. 28 | # robot.close() 29 | 30 | # servo = robot.my_servo 31 | 32 | # servo.target_speed = 0 33 | # self.assertEqual(servo.target_position, 90) 34 | 35 | # servo.target_position = 180 36 | # self.assertEqual(servo.target_speed, 100) 37 | 38 | 39 | if __name__ == '__main__': 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/test_ws.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from time import sleep 4 | from contextlib import closing 5 | from random import random 6 | 7 | from pyluos import Device 8 | 9 | import fakerobot 10 | 11 | 12 | # class TestWsRobot(fakerobot.TestCase): 13 | # def test_ws_host(self): 14 | # from pyluos.io import Ws 15 | 16 | # self.assertTrue(Ws.is_host_compatible('127.0.0.1')) 17 | # self.assertTrue(Ws.is_host_compatible('192.168.0.42')) 18 | # self.assertTrue(Ws.is_host_compatible('Mundaka.local')) 19 | # self.assertTrue(Ws.is_host_compatible('10.0.0.12')) 20 | # self.assertFalse(Ws.is_host_compatible('/dev/ttyUSB0')) 21 | 22 | # def test_ws_connection(self): 23 | # with closing(Device(fakerobot.host)): 24 | # pass 25 | 26 | # def test_ws_reception(self): 27 | # with closing(Device(fakerobot.host)) as robot: 28 | # self.assertTrue(robot.services) 29 | # self.assertTrue(robot.name) 30 | 31 | # def test_spamming(self): 32 | # with closing(Device(fakerobot.host)) as robot: 33 | # robot.i = 0 34 | 35 | # def my_send(msg): 36 | # robot._io.send(msg) 37 | # robot.i += 1 38 | 39 | # robot._send = my_send 40 | 41 | # for p in range(180): 42 | # robot.my_servo.position = p 43 | 44 | # sleep(robot._heartbeat_timeout + random()) 45 | # self.assertTrue(robot.i < 10) 46 | # self.assertTrue(robot.alive) 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /tools/fake_robot.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import json 4 | 5 | from time import time 6 | from random import randint, choice 7 | from threading import Timer 8 | 9 | from tornado.ioloop import IOLoop 10 | from tornado.web import Application 11 | from tornado.websocket import WebSocketHandler 12 | 13 | 14 | class RepeatedTimer(object): 15 | def __init__(self, interval, function, *args, **kwargs): 16 | self._timer = None 17 | self.interval = interval 18 | self.function = function 19 | self.args = args 20 | self.kwargs = kwargs 21 | self.is_running = False 22 | self.start() 23 | 24 | def _run(self): 25 | self.is_running = False 26 | self.function(*self.args, **self.kwargs) 27 | self.start() 28 | 29 | def start(self): 30 | if not self.is_running: 31 | self._timer = Timer(self.interval, self._run) 32 | self._timer.start() 33 | self.is_running = True 34 | 35 | def stop(self): 36 | self._timer.cancel() 37 | self.is_running = False 38 | 39 | 40 | class FakeRobot(WebSocketHandler): 41 | period = 1 / 10 42 | verbose = False 43 | 44 | def open(self): 45 | if self.verbose: 46 | print('WebSocket connection open.') 47 | 48 | self.set_nodelay(True) 49 | self.rt = RepeatedTimer(self.period, self.proxy_pub) 50 | 51 | def on_message(self, message): 52 | if self.verbose: 53 | print('{}: Received {}'.format(time(), message)) 54 | 55 | self.handle_command(json.loads(message)) 56 | if (message == '{detection:}'): 57 | self.ioloop.add_callback(self.pub_routing_table) 58 | 59 | def on_close(self): 60 | if self.verbose: 61 | print('WebSocket closed {}.'.format(self.close_reason)) 62 | 63 | self.rt.stop() 64 | 65 | def proxy_pub(self): 66 | self.ioloop.add_callback(self.pub_state) 67 | 68 | def pub_routing_table(self): 69 | state = {'routing_table': [{'uuid': [4456498, 1347571976, 540555569], 'port_table': [65535, 2], 'services': [{'type': 'Gate', 'id': 1, 'alias': 'gate'}]}, {'uuid': [3932192, 1194612503, 540554032], 'port_table': [3, 1], 'services': [{'type': 'Angle', 'id': 2, 'alias': 'potentiometer_m'}]}, {'uuid': [2949157, 1194612501, 540554032], 'port_table': [65535, 2], 'services': [{'type': 'Gate', 'id': 3, 'alias': 'gate1'}]}]} 70 | def pub_state(self): 71 | state = { 72 | 'services': [ 73 | { 74 | 'alias': 'my_gate', 75 | 'id': 1, 76 | 'type': 'Gate', 77 | }, 78 | { 79 | 'alias': 'my_led', 80 | 'id': 2, 81 | 'type': 'Color', 82 | }, 83 | { 84 | 'alias': 'my_servo', 85 | 'id': 3, 86 | 'type': 'Servo', 87 | }, 88 | { 89 | 'alias': 'my_button', 90 | 'id': 4, 91 | 'type': 'State', 92 | 'state': choice((0, 1)), 93 | }, 94 | { 95 | 'alias': 'my_potentiometer', 96 | 'id': 5, 97 | 'type': 'Angle', 98 | 'position': randint(0, 4096), 99 | }, 100 | { 101 | 'alias': 'my_relay', 102 | 'id': 6, 103 | 'type': 'relay', 104 | }, 105 | { 106 | 'alias': 'my_distance', 107 | 'id': 7, 108 | 'type': 'Distance', 109 | 'distance': randint(0, 2000), 110 | }, 111 | { 112 | 'alias': 'my_dxl_1', 113 | 'id': 8, 114 | 'type': 'DynamixelMotor', 115 | 'position': randint(-180, 180), 116 | }, 117 | { 118 | 'alias': 'my_dxl_2', 119 | 'id': 9, 120 | 'type': 'DynamixelMotor', 121 | 'position': randint(-180, 180), 122 | }, 123 | ] 124 | } 125 | 126 | self.write_message(json.dumps(state)) 127 | 128 | def handle_command(self, message): 129 | pass 130 | 131 | def check_origin(self, origin): 132 | return True 133 | 134 | 135 | if __name__ == '__main__': 136 | import argparse 137 | 138 | parser = argparse.ArgumentParser() 139 | parser.add_argument('--port', type=int, default=9342) 140 | parser.add_argument('--verbose', action='store_true', default=False) 141 | args = parser.parse_args() 142 | 143 | loop = IOLoop() 144 | 145 | port = args.port 146 | FakeRobot.verbose = args.verbose 147 | FakeRobot.ioloop = loop 148 | 149 | app = Application([ 150 | (r'/', FakeRobot) 151 | ]) 152 | 153 | app.listen(port) 154 | url = 'ws://{}:{}'.format('127.0.0.1', port) 155 | if args.verbose: 156 | print('Fake robot serving on {}'.format(url)) 157 | loop.start() 158 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py36 3 | 4 | [testenv] 5 | deps = 6 | nose 7 | coverage 8 | tornado 9 | commands = nosetests --with-coverage --cover-erase --cover-package=pyluos -v -w tests/ 10 | --------------------------------------------------------------------------------