diff --git a/examples/projects/product/README.md b/examples/projects/product/README.md index e69de29bb..c9c22bd55 100644 --- a/examples/projects/product/README.md +++ b/examples/projects/product/README.md @@ -0,0 +1,39 @@ +Luos logo + +![](https://github.com/Luos-io/luos_engine/actions/workflows/build.yml/badge.svg) +[![](https://img.shields.io/github/license/Luos-io/luos_engine)](https://github.com/Luos-io/luos_engine/blob/master/LICENSE) + +[![](https://img.shields.io/badge/Luos-Documentation-34A3B4)](https://www.luos.io) +[![PlatformIO Registry](https://badges.registry.platformio.org/packages/luos/library/luos_engine.svg)](https://registry.platformio.org/libraries/luos_engine/luos_engine) + +[![](https://img.shields.io/discord/902486791658041364?label=Discord&logo=discord&style=social)](https://discord.gg/luos) +[![](https://img.shields.io/badge/LinkedIn-Share-0077B5?style=social&logo=linkedin)](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fgithub.com%2Fluos-io) + +# The complete product example :bulb: + +This project demonstrate how to deal with a real life project using Luos_engine. This project deal with custom project type, custom message commands, and demonstrate how to adapt your gate and pyluos to properly handle it. + +This project is a laser Galvo controller that can be use in an engraving machine, a small surface laser cutter or a laser show device. +This have been tested with the great [interface board made by the opengalvo OPAL project](https://github.com/leswright1977/OPAL_PCB) + +This product is composed of multiple nodes: + +- A gate node called `custom_gate` running on your computer +- A laser Galvo node called `laser_galvo` running on a microcontroller (tested on nucleo_l476rg but should work on most of the STM32 family) + +## How to use :computer: + +1. Download and install [Platformio](https://platformio.org/platformio-ide) +2. Open the `custom_gate` folder into Platformio +3. Build (Platformio will do the rest) +4. Open the `laser_galvo` folder into Platformio +5. Build and flash the board (Platformio will do the rest) (of course your board with the galvo should be connected to your computer) +6. Run the `custom_gate` program in `custom_gate/.pio/build/native_serial/program` (or `custom_gate/.pio/build/native_serial/program.exe` if you use windows) +7. Install python requirements with `pip install -r requirements.txt` +8. Then you can play with the Ipython notebook `laser_galvo_control.ipynb` to control the laser Galvo + +## Don't hesitate to read [our documentation](https://www.luos.io/docs/luos-technology), or to post your questions/issues on the [Luos' community](https://discord.gg/luos). :books: + +[![](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fcommunity.luos.io&logo=Discourse)](https://discord.gg/luos) +[![](https://img.shields.io/badge/Luos-Documentation-34A3B4)](https://www.luos.io) +[![](https://img.shields.io/badge/LinkedIn-Follow%20us-0077B5?style=flat&logo=linkedin)](https://www.linkedin.com/company/luos) diff --git a/examples/projects/product/laser_galvo_control.ipynb b/examples/projects/product/laser_galvo_control.ipynb new file mode 100644 index 000000000..75fac2d5a --- /dev/null +++ b/examples/projects/product/laser_galvo_control.ipynb @@ -0,0 +1,437 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ae88017d-aec6-46fa-b594-970fe927b7df", + "metadata": {}, + "source": [ + "# Galvo test notebook\n", + "In this notebook, you will find different use cases to use the Luos_engine Galvo example.\n", + "To make it work you need to have the Galvo controlling board connected as well as the custom gate contained in the [Product example code](https://github.com/Luos-io/luos_engine/tree/main/examples/projects/product).\n", + "By executing the next cell you should see your Galvo and Gate board, then you will be ready to use it.\n", + "If you have any questions about it please contact the [Luos_engine community on discord](http://bit.ly/JoinLuosDiscord)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16be06ee-7ccd-4623-a719-011ab94518f5", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import sys\n", + "from pyluos import Device, map_custom_service\n", + "from IPython.display import clear_output\n", + "\n", + "# Import custom service and map it\n", + "sys.path.append('/Users/nicolasrabault/Projects/luos/luos_engine/examples/projects/product')\n", + "from point_2D import Point_2D\n", + "map_custom_service(\"point_2D\", Point_2D)\n", + "\n", + "device = Device('localhost') #/dev/cu.usbserial-D308N897\n", + "#device = Device('localhost', port=8000)\n", + "print(device.nodes)" + ] + }, + { + "cell_type": "markdown", + "id": "1acb7884-d67a-4ccf-b5d1-4504a045be71", + "metadata": {}, + "source": [ + "## Send a simple point to reach" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c96b1e1-7aaa-48c0-9bb7-28367ad6e6e5", + "metadata": {}, + "outputs": [], + "source": [ + "device.galvo.position = (4000, 4000)\n", + "device.galvo.play()" + ] + }, + { + "cell_type": "markdown", + "id": "93f100ea-5a2a-499b-8248-bc537313be2c", + "metadata": {}, + "source": [ + "## Send multiple points to reach" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d52ba92b-898c-4b0a-934c-605622fa4f0e", + "metadata": {}, + "outputs": [], + "source": [ + "# we reduce the sampling frequency to make it easy to see\n", + "device.galvo.sampling_freq = 100\n", + "device.galvo.position = [(0, 0), (20000,20000)]\n", + "device.galvo.play()" + ] + }, + { + "cell_type": "markdown", + "id": "02a99e99-bad5-4b14-9345-feaad9819b60", + "metadata": {}, + "source": [ + "By default the Galvo is in single mode. This mean that your trajectory wil be played only one time.\n", + "Now your small trajectory is loaded into the memory so you can play it again by calling `device.galvo.play()`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4fdcba8-b805-493b-b8a7-12370ac9fbfe", + "metadata": {}, + "outputs": [], + "source": [ + "device.galvo.play()" + ] + }, + { + "cell_type": "markdown", + "id": "ceb74118-7647-4e96-9070-8ea62c7d3106", + "metadata": {}, + "source": [ + "Alternatively you can choose to switch in continuous mode to play your trajectory in loop." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c2e8a65-2123-4f57-8e9f-a45a107dd776", + "metadata": {}, + "outputs": [], + "source": [ + "device.galvo.continuous()\n", + "device.galvo.play()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4cc48e4-1d3d-4ac3-a817-63929cb4dce4", + "metadata": {}, + "outputs": [], + "source": [ + "device.galvo.stop()" + ] + }, + { + "cell_type": "markdown", + "id": "3fba6baa-cd63-4fb0-9e22-3d5bfcc22e12", + "metadata": {}, + "source": [ + "# SVG demo 🎨\n", + "In this demo, we use an SVG input and display it.\n", + "First, execute the next cell to load the function then you will be able to play with it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2566fa9-9daf-4dc8-9cfe-b57d8cd6dc9a", + "metadata": {}, + "outputs": [], + "source": [ + "from svg.path import parse_path\n", + "from xml.dom import minidom\n", + "import pygame\n", + "import numpy as np\n", + "import requests\n", + "\n", + "# Create some functions to convert svg into trajectory\n", + "def get_point_at(path, distance, scale, offset):\n", + " pos = path.point(distance)\n", + " pos += offset\n", + " pos *= scale\n", + " return pos.real, pos.imag\n", + "\n", + "def points_from_path(path, density, scale, offset):\n", + " step = int(path.length() * density)\n", + " last_step = step - 1\n", + "\n", + " if last_step <= 0:\n", + " yield get_point_at(path, 0, scale, offset)\n", + " return\n", + "\n", + " for distance in range(step):\n", + " yield get_point_at(\n", + " path, distance / last_step, scale, offset)\n", + "\n", + "def points_from_svg(url, density=1, scale=1, offset=0):\n", + " response = requests.get(url)\n", + " if response.status_code == 200:\n", + " svg_content = response.text\n", + " doc = minidom.parseString(svg_content)\n", + " else:\n", + " print(\"Can't reach \" + url)\n", + " return\n", + "\n", + " clear_output(wait=True)\n", + " start_time = time.time()\n", + " offset = offset[0] + offset[1] * 1j\n", + " points = []\n", + " offsets = []\n", + " i = 0\n", + " for element in doc.getElementsByTagName(\"path\"):\n", + " for path in parse_path(element.getAttribute(\"d\")):\n", + " points.extend(points_from_path(\n", + " path, density, scale, offset))\n", + " i = i+1\n", + "\n", + " end_time = time.time()\n", + " elapsed_time = end_time - start_time\n", + " #print(f\"Execution time: {elapsed_time} seconds\")\n", + " return points\n", + "\n", + "def animated_svg_translation(url, density, scale, move_from, move_to, frame_nb):\n", + " frames = []\n", + " point1 = np.array(move_from)\n", + " point2 = np.array(move_to)\n", + " linear_points = np.linspace(point1, point2, frame_nb)\n", + " for offset in linear_points:\n", + " frames.append(points_from_svg(url, density=density, scale=scale, offset=(offset[0], offset[1])))\n", + " print(\"Generating animation : \" + str(offset[0]*100.0/move_to[0]) + \"%\")\n", + " return frames\n", + "\n", + "def play_animation(frames, scale):\n", + " pygame.init()\n", + " \n", + " framerate = 15\n", + " screen = pygame.display.set_mode([500, 500])\n", + " screen.fill((255, 255, 255))\n", + " running = True\n", + " frame_id = 0\n", + " device.galvo.continuous()\n", + " device.galvo.position = frames[frame_id]\n", + " time.sleep(0.1)\n", + " device.galvo.play()\n", + " while running:\n", + " frame_id = frame_id + 1\n", + " if (frame_id >= len(frames)):\n", + " frame_id = 0\n", + " screen.fill((255, 255, 255))\n", + " for point in frames[frame_id]:\n", + " point = tuple(ti/scale for ti in point)\n", + " pygame.draw.circle(screen, (0, 0, 0), point, 1)\n", + " pygame.display.update()\n", + " for event in pygame.event.get():\n", + " if event.type == pygame.QUIT:\n", + " running = False\n", + " device.galvo.position = frames[frame_id]\n", + " time.sleep(1/framerate)\n", + " pygame.quit()\n", + " device.galvo.stop()" + ] + }, + { + "cell_type": "markdown", + "id": "03331b4b-767b-4d97-8a26-7eca1a8363ff", + "metadata": {}, + "source": [ + "## Simply display a svg" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c8d5761-7bdc-4dce-9988-4c841243c737", + "metadata": {}, + "outputs": [], + "source": [ + "# set the sample freq up to properly display it\n", + "device.galvo.sampling_freq = 4000\n", + "# load an svg image\n", + "points = points_from_svg(\"https://mirrors.creativecommons.org/presskit/icons/cc.svg\", density=0.3, scale=400, offset=(1, 100))\n", + "# we want to continuously display it\n", + "device.galvo.continuous()\n", + "device.galvo.position = points\n", + "# wait for the points to be sent before playing it\n", + "time.sleep(0.1)\n", + "device.galvo.play()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "872ef615-cd36-46cd-981d-c981d5ffe5ca", + "metadata": {}, + "outputs": [], + "source": [ + "device.galvo.stop()" + ] + }, + { + "cell_type": "markdown", + "id": "8a60b64a-4607-403c-b244-aa4004d0e380", + "metadata": {}, + "source": [ + "## Generate an animation out of the svg and play it" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d70efed7-5b49-48ea-abdc-373dcda679b7", + "metadata": {}, + "outputs": [], + "source": [ + "# compute a simple translation animation of an svg\n", + "frames = animated_svg_translation(\"https://mirrors.creativecommons.org/presskit/icons/cc.svg\", density=0.3, scale=400, move_from=(0, 0), move_to=(90, 90), frame_nb=30)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f180167-2a74-4429-a624-fc406c4bb162", + "metadata": {}, + "outputs": [], + "source": [ + "# play the previously generated animation\n", + "# you can close the pygame window to stop it\n", + "play_animation(frames, 100)" + ] + }, + { + "cell_type": "markdown", + "id": "76e723df-d485-436f-8b90-61fc9baf5e91", + "metadata": {}, + "source": [ + "# Live audio FFT demo 🔊\n", + "In this demo, the function downloads a sound file plays it, and displays the FFT of the sound on the galvo.\n", + "First, execute the next cell to load the function then you will be able to play with it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c5a22a3-3176-4b5f-96ee-cdc0799808f9", + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import Audio\n", + "from IPython.display import clear_output\n", + "import time\n", + "import numpy as np\n", + "\n", + "# Load an audio file play it and display the fft\n", + "# default file is:\n", + "#'Ignis' by Scott Buckley - released under CC-BY 4.0. www.scottbuckley.com.au\n", + "def live_display_fft(audio_file_link = 'https://www.scottbuckley.com.au/library/wp-content/uploads/2024/01/Ignis.mp3'):\n", + " rate = 44100 #I don't know how to automatically get it from URL\n", + "\n", + " # Slow down the galvo speed \n", + " device.galvo.sampling_freq = 2000\n", + " \n", + " # Create an Audio object\n", + " print(\"downloading the audio file ...\")\n", + " clear_output(wait=True)\n", + " audio_stream = Audio(url=audio_file_link, embed=True, autoplay=True)\n", + " \n", + " # Enable the galvo\n", + " fft_size = 64\n", + " X_scale = 1000\n", + " init_frame = []\n", + " Y_offset = 500\n", + " Y_scale = 1600\n", + " FPS = 20\n", + " for i in range(fft_size):\n", + " init_frame.append((i*X_scale, Y_offset))\n", + " \n", + " device.galvo.continuous()\n", + " device.galvo.position = init_frame\n", + " time.sleep(0.1)\n", + " device.galvo.play()\n", + " \n", + " # Display the audio\n", + " display(audio_stream)\n", + " \n", + " # Live animation\n", + " def chunker(seq, size):\n", + " return (seq[pos:pos + size] for pos in range(0, len(seq), size))\n", + " \n", + " sample_per_frame = int(rate/FPS)\n", + " for chunk in chunker(audio_stream.data, sample_per_frame):\n", + " chunk = np.frombuffer(chunk, dtype=np.uint8)\n", + " fft = abs(np.fft.fft(chunk, n=fft_size).real)\n", + " # Create points out of it\n", + " frame = []\n", + " freq=0\n", + " for ampl in fft:\n", + " frame.append((freq, Y_offset+(ampl*Y_scale)))\n", + " freq = freq + X_scale\n", + " device.galvo.position = frame\n", + " time.sleep(1/FPS)\n", + " device.galvo.stop()\n", + " # set the default sampling freq\n", + " device.galvo.sampling_freq = 10000" + ] + }, + { + "cell_type": "markdown", + "id": "25551a38-1a14-496c-8709-e4242c2352f0", + "metadata": {}, + "source": [ + "## Live fft\n", + "By executing this function you will play a song and live display the fft on the galvo.\n", + "You can hit pause on the reader and stop the notebook to stop the function.\n", + "You will have to manually stop the galvo if you stop the function." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c19af357-e75a-4ab4-9a34-28192d54d393", + "metadata": {}, + "outputs": [], + "source": [ + "live_display_fft()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "346f7f93-7fea-4d46-b323-c776ae552ae0", + "metadata": {}, + "outputs": [], + "source": [ + "device.galvo.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02759dee-b360-4af1-9fc0-13ecba8facbb", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/projects/product/requirements.txt b/examples/projects/product/requirements.txt new file mode 100644 index 000000000..d39f2efa9 --- /dev/null +++ b/examples/projects/product/requirements.txt @@ -0,0 +1,4 @@ +pyluos +svg.path +xml.dom +pygame