From 06ef401a3b69d5d515d62485fbafa262bbc99db3 Mon Sep 17 00:00:00 2001 From: Romain Bazile Date: Mon, 28 Sep 2020 11:05:27 +0200 Subject: [PATCH] Extraction and refactor of the python code from node-red flow The rationale for this rewrite is to improve the readability, the modularity, the reliability and the future-proofing of the main python script. All in all, this is now 124 commits that are going to be squashed and merged together, spanning more than two weeks of development and testing. Please test away this release and break things. An upgrading guide will be published in the coming days, along with a new image for people to use if they don't want to upgrade on their own. Read along if you want to know all the goodies! As a starter, the python script was extracted from the main flow, and now lives in its own files at `PlantonScope/scripts/*`. We set up the auto formatting of the code by using [Black](https://github.com/psf/black). This make the code clearer and uniform. We are using the default settings, so if you just install Black and set your editor to format on save using it, you should be good to go. The code is separated in four main processes, each with a specific set of responsibilities: - The main process controls all the others, starts everything up and cleans up on shutdown - The stepper process manages the stepper movements. It's now possible to have simultaneous movements of both motors (this closes #38 ). - The imager process controls the camera and the streaming server via a state machine. - The segmenter process manages the segmentation and its outputs. The segmentation happens recursively in all folders in `/home/pi/PlanktonScope/img/`. Each folder has its own output archive, and bug #26 is now closed. Those processes communicates together using MQTT and json messages. Each message is adressed to one topic. The high level topic controls which process receives the message. The details of each topic is at the end of this commit message. Every imaging sessions has now its own folder, under the `img` root. Metadata are saved individually for every session, in a JSON file in the same directory as the pictures. The configuration is not parsed from `config.json` anymore and passed directly through MQTT messages to the concerned process. A new configuration file has been created: `hardware.json`. This file contains information related to your specific hardware configuration. You can choose to reverse the connection of the motors for example, but you can also define specific speed limits and steps number for your pump and focus stage. This will make it easier for people who wants to experiment with different kind of hardware. It's not necessary to have this file though. If it doesn't exists, the default configuration will be applied. The code is architectured around 6 modules and about 10 classes. I encourage you to have a look at the files, they're pretty straightforward to understand. There is a lot of work left around the node-red code refactoring, dashboard ui improvements, better and clearer LED messages, OLED screen integration and finer control of the segmentation process, but this is quite good for now. Here is the topic lists for MQTT and the corresponding messages. - actuator : This topic adresses the stepper control thread No publication under this topic should happen from the python process - actuator/pump : Control of the pump The message is a json object {"action":"move", "direction":"FORWARD", "volume":10, "flowrate":1} to move 10mL forward at 1mL/min action can be "move" or "stop" Receive only - actuator/focus : Control of the focus stage The message is a json object, speed is optional {"action":"move", "direction":"UP", "distance":0.26, "speed":1} to move up 10mm action can be "move" or "stop" Receive only - imager/image : This topic adresses the imaging thread Is a json object with {"action":"image","sleep":5,"volume":1,"nb_frame":200} sleep in seconds, volume in mL Can also receive a config update message: {"action":"config","config":[...]} Can also receive a camera settings message: {"action":"settings","iso":100,"shutter_speed":40} Receive only - segmenter/segment : This topic adresses the segmenter process Is a json object with {"action":"segment"} Receive only - status : This topics sends feedback to Node-Red No publication or receive at this level - status/pump : State of the pump Is a json object with {"status":"Start", "time_left":25} Status is one of Started, Ready, Done, Interrupted Publish only - status/focus : State of the focus stage Is a json object with {"status":"Start", "time_left":25} Status is one of Started, Ready, Done, Interrupted Publish only - status/imager : State of the imager Is a json object with {"status":"Start", "time_left":25} Status is one of Started, Ready, Completed or 12_11_15_0.1.jpg has been imaged. Publish only - status/segmenter : Status of the segmentation - status/segmenter/name - status/segmenter/object_id - status/segmenter/metric Here is the original commit history: * Extract python main.py from flow * Fix bug in server addresses These addresses should be the loopback device instead of the network address of the device. Using the loopback address will not necessitate to update the script when the network address changes. * clean up picamera import * changes to main python and flow: update MQTT requests address to localhost (bugfix) update streaming output address to nothing update main flow to remove python script references and location * Automatically initialise imaging led on startup to off state. * Add the ability to invert outputs of the motor We added a key to config.json "hardware_config" with a subkey "stepper_reverse". If this key is present in the config file and set to 1, the output of the motors are inversed (stepper2 becomes the pump motor and stepper1 the focus motor) * move all non main script to a subfolder * add __init__.py to package * light module rewrite * json cleanup and absolute path for config file * light.py forgot to import subprocess #oups * Add command to turn the leds off * Auto formatting of main.py I've used Black with default settings, see https://github.com/psf/black * First commit of stepper.py Pump parameters still needs to be checked and tuned. * addition of hardware details in config.json * Introduce hardware.json to replace the `hardware_config` of config.json * stepper.py: calibration, typos * creates the MQTT_Client class * pump_max_speed is now in ml/min to help readability * forgot to add self to the class def * addition of threading capabilities to stepper.py (UNTESTED) * mqtt: fix topic bug * remove counter * mqtt add doc about topics * stepper.py creates an "actuator/*/state" topic * stepper.py: rename mqtt_client to pump_client * mqtt.py: add details about topics * stepper.py: rename pump_client to actuator_client * topic was not split properly and a part was lost * switch to f-strings for mqtt.py * cosmetic update * stepper.py: folder name will be planktoscope change calls * hardware.json became more straightforward * stepper.py syntax bugs * stepper.py addition of a received stop command * stepper.py: update to max travel distance * stepper.py: several typos here * rename folder * main.py: reword to reflect folder rename * main.py: remove logic that has been moved to stepper.py and mqtt.py * main.py: update to add mqtt imaging client * mqtt.py: make command and args local to class and output more verbose * make stepper.py a class * main.py: instantiate stepper class and call it * main.py: name mqtt client * update to main.json to reflect main.py changes * fix bugs around pump control * update flows to latest version from Thibault * distance can be a small value, and definitevely should be a float. * unify mqtt topics * unify mqtt output in the main flow * first logger implementation, uses loguru * mqtt: add reason to on_connect * mqtt: add on_disconnect handler * stepper: add more logger calls for debug mainly * main: add levels for logger * imager.py: first move of the imager logic * imager: time import cleanup * imager: morphocut import cleanup * imager: skimage import cleanup * imager: finishing import cleanup * imager: Class creation - WIP Also provides a fix for #26 (see line 190). * imager: threading is needed for Condition() * streamer: get the streamer server its own file * imager: creates start_camera and get the server creation out * imager: subclass multiprocessing.Process * imager: get Pipeline creation its own function * imager: cleanup of self calls * main: code removal and corresponding calls to newly created classes * imager: various formatting changes * main: management of signal shutdown * add requirements.txt * mqtt: messages are now json objects Also, addition of a flag on receiving a new message * mqtt: make message private and add logic to synchronise * stepper: creates the stepper class * stepper: use the new class * stepper: uses the new logic * stepper: add the shutdown event * stepper: add shutdown method * main: add shutdown event * imager: graceful shutdown * stepper: nicer way of checking the Eevnt * self is a required first argument for a method in a class Especially if you use said class private members! * python: various typos and small errors in import * stepper: create mqtt client during init * stepper: instanciate the mqtt client inside run Otherwise it's not accessible from inside the loop. It's a PITA, more information at https://stackoverflow.com/questions/17172878/using-pythons-multiprocessing-process-class * stepper: little bugs and typos all around * mqtt: add shutdown method * mqtt: add connect in init * stepper: fix bugs, sanitize inputs * stepper: work on delay prediction improvements * stepper: json is mean, double quote are mandatory inside * mqtt: add details about message exchanged * imager: first implementation of json messages * main.json: add new tab for RPi management + json for payloads * imager: add state_machine class * stepper: publish last will * imager: major refactor * main: make streaming server process a daemon * mqtt: insert debug statement on close * main: reorder imports * imager: make it work! Reinsert the streaming server logic in there, because there is a problem with the synchronisation part otherwise. Also, eventually, StreamingOuput() will have to be made not global Final very critical learning: it's super duper important to make sure the memory split is at least 256Meg for the GPU. Chaos ensues otherwise * main: changes to accomodate the streamer/imager fusino * imager_state_machine: insert states transition description * stepper: cleanup of code * segmenter: creation of the class * python: include segmenter changes * remove unused files * stepper: check existence of hardware.json * main.json: changes to reflect the python script evolution * remove unecessary TODOs and add some others * main: add check for config and directories * imager: update_config is implemented and we have better management of directories now * segmenter: now work recursively in all folders * flow: the configuration is now sent via mqtt * segmenter: better manage pipeline error * segmenter: declaration of archive_fn in init * imager: small bugs and typos * main: add uniqueID output * imager: add the camera settings message We can now update the ISO, shutter speed and resolution from Node-Red * package.json: update dependencies --- config.json | 1 - flows/main.json | 2699 ++++++++++-------- hardware.json | 7 + package.json | 13 +- requirements.txt | 9 + scripts/fan.py | 19 - scripts/focus.py | 28 - scripts/image.py | 55 - scripts/light.py | 26 - scripts/main.py | 150 + scripts/mqtt_pump_focus_image_v2.py | 389 --- scripts/planktoscope/__init__.py | 0 scripts/planktoscope/imager.py | 518 ++++ scripts/planktoscope/imager_state_machine.py | 71 + scripts/planktoscope/light.py | 102 + scripts/planktoscope/mqtt.py | 167 ++ scripts/planktoscope/segmenter.py | 338 +++ scripts/planktoscope/stepper.py | 440 +++ scripts/pump.py | 46 - 19 files changed, 3252 insertions(+), 1826 deletions(-) create mode 100644 hardware.json create mode 100644 requirements.txt delete mode 100644 scripts/fan.py delete mode 100644 scripts/focus.py delete mode 100644 scripts/image.py delete mode 100644 scripts/light.py create mode 100644 scripts/main.py delete mode 100644 scripts/mqtt_pump_focus_image_v2.py create mode 100644 scripts/planktoscope/__init__.py create mode 100644 scripts/planktoscope/imager.py create mode 100644 scripts/planktoscope/imager_state_machine.py create mode 100644 scripts/planktoscope/light.py create mode 100644 scripts/planktoscope/mqtt.py create mode 100644 scripts/planktoscope/segmenter.py create mode 100644 scripts/planktoscope/stepper.py delete mode 100644 scripts/pump.py diff --git a/config.json b/config.json index 02458dd..70d1877 100644 --- a/config.json +++ b/config.json @@ -27,4 +27,3 @@ "process_pixel": 1.19, "process_id": 146 } - diff --git a/flows/main.json b/flows/main.json index fe53f36..36f7ad7 100644 --- a/flows/main.json +++ b/flows/main.json @@ -1,13 +1,13 @@ [ { - "id": "c4414305.176578", + "id": "e8d4d920.35344", "type": "tab", "label": "Main", "disabled": false, "info": "" }, { - "id": "aafb7d9f.f516f", + "id": "21b3da63.2cef2e", "type": "subflow", "name": "RPi Monitoring", "info": "", @@ -19,7 +19,7 @@ "icon": "node-red/status.svg" }, { - "id": "3f0fd072.06e2c8", + "id": "130e0533.4f1813", "type": "subflow", "name": "Acquisition actuation", "info": "", @@ -31,9 +31,9 @@ "icon": "font-awesome/fa-camera" }, { - "id": "435c6174.e0c8b", + "id": "d95291dc.83b0b8", "type": "subflow", - "name": "Python Code Creation", + "name": "Python Startup", "info": "", "category": "", "in": [ @@ -42,10 +42,10 @@ "y": 40, "wires": [ { - "id": "10348484.8ca5b3" + "id": "afa9f4b.2e77988" }, { - "id": "e49710cf.c4831" + "id": "672d89a8.4e6968" } ] } @@ -55,7 +55,7 @@ "color": "#DDAA99" }, { - "id": "4273b9bf.bb07a8", + "id": "1bc3f9f9.1ee996", "type": "subflow", "name": "Acquisition inputs", "info": "", @@ -66,13 +66,13 @@ "y": 40, "wires": [ { - "id": "55d70110.127d38" + "id": "8887f3e7.e79b5" }, { - "id": "4e6a3b73.7472d4" + "id": "14be4afa.2ba67d" }, { - "id": "ec5967a.409eb18" + "id": "600c2b46.83db14" } ] } @@ -83,15 +83,15 @@ "y": 40, "wires": [ { - "id": "a105d7c9.07d71", + "id": "d3caa802.6f22d8", "port": 0 }, { - "id": "816c19e2.f940e", + "id": "3a773a31.a6caee", "port": 0 }, { - "id": "530d8426.5c4bec", + "id": "5d4c755a.800b44", "port": 0 } ] @@ -102,7 +102,7 @@ "icon": "node-red-contrib-camerapi/photo.png" }, { - "id": "4d3786dd.dd79f", + "id": "714bd4fe.163ab4", "type": "subflow", "name": "Process metadata", "info": "", @@ -113,7 +113,7 @@ "y": 80, "wires": [ { - "id": "7228965.6f65ae8" + "id": "706715a3.5ad1c4" } ] } @@ -124,7 +124,7 @@ "y": 80, "wires": [ { - "id": "7bad3862.c22b5", + "id": "7cdadcd1.736f04", "port": 0 } ] @@ -134,10 +134,10 @@ "color": "#DDAA99" }, { - "id": "20bc0424.146724", + "id": "bee3b478.ef4b88", "type": "subflow", "name": "MQTT Receive & Plot", - "info": "", + "info": "# MQTT Topics follows this architecture:\n\n - actuator : This topic adresses the stepper control thread\n No publication under this topic should happen from Python\n - actuator/pump : Control of the pump\n The message is something like \"FORWARD 10 1\"\n to move 10mL forward at 1mL/min\n Receive only\n - actuator/focus : Control of the focus stage\n The message is something like \"UP 10\"\n to move up 10mm\n Receive only\n - imager/image : This topic adresses the imaging thread\n Receive only\n - status : This topics sends feedback to Node-Red\n No publication or receive at this level\n - status/pump : State of the pump\n Is one of Start, Done, Interrupted\n Publish only\n - status/focus : State of the focus stage\n Is one of Start, Done, Interrupted\n Publish only\n - status/imager : State of the imager\n Is one of Start, Completed or 12_11_15_0.1.jpg has been imaged.\n Publish only\n - status/segmentation : Status of the segmentation\n - status/segmentation/name\n - status/segmentation/object_id\n - status/segmentation/metric\n", "category": "", "in": [], "out": [], @@ -146,7 +146,7 @@ "icon": "node-red/bridge.svg" }, { - "id": "7aea7e49.4a3c88", + "id": "9c3e6ad4.7471a8", "type": "subflow", "name": "System Commands", "info": "", @@ -158,24 +158,24 @@ "icon": "node-red-dashboard/ui_button.png" }, { - "id": "672ac548.1a9bac", + "id": "1a447be0.198674", "type": "subflow", "name": "Object metadata", "info": "", "category": "", "in": [ { - "x": 40, - "y": 40, + "x": 60, + "y": 160, "wires": [ { - "id": "46ef98a.3d3e8e8" + "id": "5cdbcd15.17ec94" }, { - "id": "e6b2a290.7593c8" + "id": "c2ccc2e1.a697f8" }, { - "id": "61bc4942.96a3d" + "id": "b3e8d04.7c83d3" } ] } @@ -186,11 +186,11 @@ "y": 159, "wires": [ { - "id": "d7ac6b26.75e388", + "id": "f435f66d.26d73", "port": 0 }, { - "id": "70dcbf49.4b0e6", + "id": "79026dc4.133a7c", "port": 0 } ] @@ -200,11 +200,11 @@ "y": 59, "wires": [ { - "id": "415b9f0c.5f0b48", + "id": "9c08f843.b1e1b", "port": 0 }, { - "id": "3e9ae6e6.35b942", + "id": "157ba5ca.c88a52", "port": 0 } ] @@ -214,7 +214,7 @@ "color": "#DDAA99" }, { - "id": "5163a57b.0008b4", + "id": "758bc08f.c57318", "type": "subflow", "name": "Acquisition metadata", "info": "", @@ -225,31 +225,31 @@ "y": 30, "wires": [ { - "id": "7c09012f.b50098" + "id": "f3658d30.b8448" }, { - "id": "3e3b0646.cf9a1a" + "id": "5acd51d4.4ab13" }, { - "id": "2b4282c2.8752ae" + "id": "de2c90cf.b73b08" }, { - "id": "90439ce.e3524e" + "id": "5e3dec55.881074" }, { - "id": "7f710823.92974" + "id": "d3ca8847.4d1ae" }, { - "id": "9355d580.a2338" + "id": "1f133196.96564e" }, { - "id": "c8be3f91.8686e" + "id": "3414b477.4d711c" }, { - "id": "98bb1c89.99c7b8" + "id": "a52e7caf.6bde48" }, { - "id": "218c020a.2b0566" + "id": "68fa1227.dbdd5c" } ] } @@ -260,35 +260,35 @@ "y": 40, "wires": [ { - "id": "52374b71.714fd4", + "id": "98d1f331.a06938", "port": 0 }, { - "id": "858ed565.0993b8", + "id": "3b9700c4.b93ec", "port": 0 }, { - "id": "2cf276d8.880672", + "id": "42505cbd.f02b1c", "port": 0 }, { - "id": "ebcf7cae.9e21c8", + "id": "2f9ae002.b4c96", "port": 0 }, { - "id": "7291a2a3.c9a974", + "id": "c86d98d4.c56538", "port": 0 }, { - "id": "25294db5.4fe722", + "id": "13b51e1e.1f603a", "port": 0 }, { - "id": "4cfd49e3.3db3c", + "id": "6c391d10.2f9744", "port": 0 }, { - "id": "7dbb773.926b488", + "id": "163df12e.f73f5f", "port": 0 } ] @@ -298,7 +298,7 @@ "y": 360, "wires": [ { - "id": "79923da9.365d7c", + "id": "701120fd.c5516", "port": 0 } ] @@ -308,7 +308,7 @@ "color": "#DDAA99" }, { - "id": "b1edcbe7.366f7", + "id": "977131e7.c2e76", "type": "subflow", "name": "Pump actuation", "info": "", @@ -319,10 +319,10 @@ "y": 40, "wires": [ { - "id": "66f2533e.b3080c" + "id": "cc757614.c8fc58" }, { - "id": "e3472832.6c2cc8" + "id": "8ae06f9a.4b253" } ] } @@ -333,11 +333,11 @@ "y": 40, "wires": [ { - "id": "1f1c2de7.b242f2", + "id": "b8bf2a9.be099d8", "port": 0 }, { - "id": "8cab4571.004668", + "id": "f1b85f22.ac673", "port": 0 } ] @@ -348,7 +348,7 @@ "icon": "font-awesome/fa-recycle" }, { - "id": "3df4e02.36602a", + "id": "a0f9bde.423644", "type": "subflow", "name": "Focus actuation", "info": "", @@ -359,18 +359,18 @@ "y": 40, "wires": [ { - "id": "ba9fc5ee.19aee8" + "id": "411211be.745ef8" } ] } ], "out": [ { - "x": 800, - "y": 200, + "x": 1040, + "y": 40, "wires": [ { - "id": "3df51223.81e336", + "id": "b69e435f.b93558", "port": 0 } ] @@ -381,7 +381,7 @@ "icon": "node-red/sort.svg" }, { - "id": "6bc47c75.93e24c", + "id": "81483277.2521e", "type": "subflow", "name": "Sample metadata", "info": "", @@ -392,19 +392,19 @@ "y": 40, "wires": [ { - "id": "a4b7cb08.270d" + "id": "d027a6bf.7049e8" }, { - "id": "d7cff063.331ff8" + "id": "45911c98.2bd83c" }, { - "id": "acfe2f.33fd31d" + "id": "5a811caf.0f3144" }, { - "id": "cfaa2598.c63ec" + "id": "1e09a4ab.72996b" }, { - "id": "25201379.163e3c" + "id": "a3272681.f271c8" } ] } @@ -415,23 +415,23 @@ "y": 40, "wires": [ { - "id": "412da17d.09c39", + "id": "adaccbb2.320458", "port": 0 }, { - "id": "236eeefd.7d50f2", + "id": "a63c1b66.f9a77", "port": 0 }, { - "id": "ccb4ce9e.4f9108", + "id": "67fc8b3f.96c6cc", "port": 0 }, { - "id": "d76b1790.9ffc2", + "id": "8cc6c6f1.65bf28", "port": 0 }, { - "id": "50431d7c.cc673c", + "id": "91a896c8.7c9fb8", "port": 0 } ] @@ -441,51 +441,75 @@ "color": "#DDAA99" }, { - "id": "7a723961.386be", + "id": "4b508eab.dccae8", + "type": "subflow", + "name": "Segmentation control", + "info": "", + "category": "", + "in": [], + "out": [], + "env": [], + "color": "#D7D7A0", + "icon": "node-red/db.svg" + }, + { + "id": "674362b7.9b6574", + "type": "subflow", + "name": "Camera settings", + "info": "", + "category": "", + "in": [], + "out": [], + "env": [], + "color": "#3FADB5", + "icon": "font-awesome/fa-camera" + }, + { + "id": "779606c9.19d2b8", "type": "ui_group", "z": "", "name": "Monitor RPi", - "tab": "c4349ea0.ea1cc8", - "order": 11, + "tab": "d10d9d99.00f4b8", + "order": 13, "disp": true, "width": 24, "collapse": false }, { - "id": "3bd8d121.499b96", + "id": "75a5ce1f.728d", "type": "ui_group", "z": "", "name": "Acquisition actuation", - "tab": "c4349ea0.ea1cc8", - "order": 9, + "tab": "d10d9d99.00f4b8", + "order": 10, "disp": true, "width": 24, "collapse": false }, { - "id": "6fdfb3ec.fd451c", + "id": "758f41b8.1680c8", "type": "ui_group", "z": "", "name": "Acquisition inputs", - "tab": "c4349ea0.ea1cc8", - "order": 8, + "tab": "d10d9d99.00f4b8", + "order": 9, "disp": true, "width": "24", "collapse": false }, { - "id": "c32bf44c.3b67a8", + "id": "eef6f881.7b3b38", "type": "ui_group", "z": "", "name": "Process metadata", - "tab": "c4349ea0.ea1cc8", - "order": 7, + "tab": "d10d9d99.00f4b8", + "order": 8, "disp": true, "width": 24, "collapse": false }, { - "id": "6e9cdee.ac3c2a", + "id": "9a69f0bc.60dcd", "type": "mqtt-broker", "z": "", "name": "", @@ -507,73 +531,62 @@ "willPayload": "" }, { - "id": "1f183a1c.7d2846", + "id": "6d1af0ab.7b4a18", "type": "ui_group", "z": "", "name": "MQTT Plots", - "tab": "c4349ea0.ea1cc8", - "order": 10, - "disp": true, - "width": "24", - "collapse": false - }, - { - "id": "1f044bf8.be704c", - "type": "ui_group", - "z": "", - "name": "RPi commands", - "tab": "c4349ea0.ea1cc8", + "tab": "d10d9d99.00f4b8", "order": 12, "disp": true, "width": "24", "collapse": false }, { - "id": "2f95b761.bd818", + "id": "ab1d06d6.bf9898", "type": "ui_group", "z": "", "name": "Object metadata", - "tab": "c4349ea0.ea1cc8", + "tab": "d10d9d99.00f4b8", "order": 2, "disp": true, - "width": 24, + "width": "24", "collapse": false }, { - "id": "14742691.56c8c1", + "id": "4361c3b6.e2b7a4", "type": "ui_group", "z": "", "name": "Acquisition metadata", - "tab": "c4349ea0.ea1cc8", + "tab": "d10d9d99.00f4b8", "order": 3, "disp": true, - "width": 24, + "width": "24", "collapse": false }, { - "id": "517b2aa5.93722c", + "id": "34f03c4a.abdf94", "type": "ui_group", "z": "", "name": "Pump actuation", - "tab": "c4349ea0.ea1cc8", + "tab": "d10d9d99.00f4b8", + "order": 7, + "disp": true, + "width": "24", + "collapse": false + }, + { + "id": "de7c8e82.7faa98", + "type": "ui_group", + "z": "", + "name": "Focus actuation", + "tab": "d10d9d99.00f4b8", "order": 6, "disp": true, "width": "24", "collapse": false }, { - "id": "88613aab.984d18", - "type": "ui_group", - "z": "", - "name": "Focus actuation", - "tab": "c4349ea0.ea1cc8", - "order": 5, - "disp": true, - "width": "24", - "collapse": false - }, - { - "id": "e6efd12e.dedae8", + "id": "8dc3722c.06efa8", "type": "mqtt-broker", "z": "", "name": "", @@ -595,39 +608,39 @@ "willPayload": "" }, { - "id": "bfdb5a44.4223", + "id": "71c63dd4.311c44", "type": "ui_group", "z": "", - "name": "Sample metadata", - "tab": "c4349ea0.ea1cc8", + "name": "General samples metadata", + "tab": "d10d9d99.00f4b8", "order": 1, "disp": true, - "width": 24, - "collapse": false + "width": "24", + "collapse": true }, { - "id": "c4349ea0.ea1cc8", + "id": "d10d9d99.00f4b8", "type": "ui_tab", "z": "", "name": "Acquisition", "icon": "fa-eyedropper", - "order": 3, + "order": 1, "disabled": false, "hidden": false }, { - "id": "6b2a8cdd.9f43cc", + "id": "59434d8d.70ed94", "type": "ui_group", "z": "", "name": "Streaming camera", - "tab": "c4349ea0.ea1cc8", + "tab": "d10d9d99.00f4b8", "order": 4, "disp": true, - "width": 24, + "width": "24", "collapse": false }, { - "id": "b6a679c9.da3fa8", + "id": "9b06d49f.aa144", "type": "ui_base", "theme": { "name": "theme-dark", @@ -635,7 +648,7 @@ "default": "#0094CE", "baseColor": "#0094CE", "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif", - "edited": false, + "edited": true, "reset": false }, "darkTheme": { @@ -724,9 +737,79 @@ } }, { - "id": "d4285aee.f326e", + "id": "488f4f9a.e0651", + "type": "ui_spacer", + "name": "spacer", + "group": "ab1d06d6.bf9898", + "order": 7, + "width": 6, + "height": 1 + }, + { + "id": "a3de84cd.c01b6", + "type": "ui_tab", + "z": "", + "name": "Management", + "icon": "dashboard", + "order": 2, + "disabled": false, + "hidden": false + }, + { + "id": "adcd0ef7.81a7f8", + "type": "ui_group", + "z": "", + "name": "RPi commands", + "tab": "a3de84cd.c01b6", + "order": 1, + "disp": true, + "width": 11, + "collapse": false + }, + { + "id": "79482db5.1401ec", + "type": "ui_spacer", + "name": "spacer", + "group": "adcd0ef7.81a7f8", + "order": 2, + "width": 11, + "height": 1 + }, + { + "id": "3b4814af.e30e54", + "type": "ui_spacer", + "name": "spacer", + "group": "adcd0ef7.81a7f8", + "order": 4, + "width": 5, + "height": 1 + }, + { + "id": "566dcd6a.03db74", + "type": "ui_group", + "z": "", + "name": "Segmentation control", + "tab": "d10d9d99.00f4b8", + "order": 11, + "disp": true, + "width": "24", + "collapse": false + }, + { + "id": "e8b67968.dea6", + "type": "ui_group", + "z": "", + "name": "Camera settings", + "tab": "d10d9d99.00f4b8", + "order": 5, + "disp": true, + "width": "24", + "collapse": false + }, + { + "id": "98b8d88c.dc5d", "type": "exec", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "command": "free -m | grep \"Mem\" | awk -F ' ' '{print $3}'", "addpay": false, "append": "", @@ -737,17 +820,17 @@ "y": 100, "wires": [ [ - "5d2fe716.8ad198", - "6326d8e5.292c5" + "ddf0e242.88258", + "f1a7a389.2cc528" ], [], [] ] }, { - "id": "d6e7a1be.f8f8f8", + "id": "34130a77.042d46", "type": "inject", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "", "repeat": "1", "crontab": "", @@ -760,16 +843,16 @@ "y": 100, "wires": [ [ - "d4285aee.f326e" + "98b8d88c.dc5d" ] ] }, { - "id": "5d2fe716.8ad198", + "id": "ddf0e242.88258", "type": "ui_chart", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "Memory Load Chart", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 8, "width": 19, "height": 4, @@ -800,18 +883,18 @@ ], "useOldStyle": false, "outputs": 1, - "x": 660, + "x": 650, "y": 120, "wires": [ [] ] }, { - "id": "6326d8e5.292c5", + "id": "f1a7a389.2cc528", "type": "ui_gauge", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "Memory Load Donut", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 7, "width": 5, "height": 4, @@ -833,11 +916,11 @@ "wires": [] }, { - "id": "b38a018c.21dc18", + "id": "30fcb8f6.12fe", "type": "ui_gauge", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "CPU Temp Donut", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 1, "width": 5, "height": 4, @@ -859,9 +942,9 @@ "wires": [] }, { - "id": "c35a43a5.f0d1e8", + "id": "81105630.ed06e8", "type": "exec", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "command": "vcgencmd measure_temp | tr -d \"temp=\" | tr -d \"'C\" | tr -d \"\\n\"", "addpay": false, "append": "", @@ -872,20 +955,20 @@ "y": 180, "wires": [ [ - "8f6f6c48.fe30c", - "b38a018c.21dc18", - "d2d8b9f6.c82e68" + "59fe3900.da6c8", + "30fcb8f6.12fe", + "d70d3e88.eaca48" ], [], [] ] }, { - "id": "d2d8b9f6.c82e68", + "id": "d70d3e88.eaca48", "type": "ui_chart", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "CPU Temp Chart", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 2, "width": 19, "height": 4, @@ -923,9 +1006,9 @@ ] }, { - "id": "602add74.9aa2a4", + "id": "6e383122.133d5", "type": "python3-function", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "fan.py", "func": "#!/usr/bin/python\nimport smbus\nimport sys\n\nstate = msg[\"payload\"]\n\nbus = smbus.SMBus(1)\n\nDEVICE_ADDRESS = 0x0d\n\nif state == \"off\":\n bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x00)\n bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x00)\nif state == \"on\":\n bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x01)\n bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x01)", "outputs": 1, @@ -936,9 +1019,9 @@ ] }, { - "id": "8f6f6c48.fe30c", + "id": "59fe3900.da6c8", "type": "switch", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "", "property": "payload", "propertyType": "msg", @@ -961,17 +1044,17 @@ "y": 140, "wires": [ [ - "916264f4.1b31f" + "1d11bfc1.ad4788" ], [ - "743c04f5.97e51c" + "5aeccc80.5e9c54" ] ] }, { - "id": "743c04f5.97e51c", + "id": "5aeccc80.5e9c54", "type": "change", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "", "rules": [ { @@ -991,14 +1074,14 @@ "y": 160, "wires": [ [ - "602add74.9aa2a4" + "6e383122.133d5" ] ] }, { - "id": "9f9c97d.42b1768", + "id": "8338294b.8082d", "type": "inject", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "", "repeat": "5", "crontab": "", @@ -1008,19 +1091,19 @@ "payload": "", "payloadType": "date", "x": 190, - "y": 180, + "y": 260, "wires": [ [ - "2d899302.9bc384", - "e0f99dcc.c0257", - "c35a43a5.f0d1e8" + "e7fe62ae.efb4f", + "eba0a98a.e4d56", + "81105630.ed06e8" ] ] }, { - "id": "2d899302.9bc384", + "id": "e7fe62ae.efb4f", "type": "exec", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "command": "top -d 0.5 -b -n2 | grep \"Cpu(s)\"|tail -n 1 | awk '{print $2 + $4}' | tr -d \"\\n\"", "addpay": false, "append": "", @@ -1031,17 +1114,17 @@ "y": 260, "wires": [ [ - "35e3cb5a.debb74", - "84d67d53.6001e" + "aa4065d2.14a66", + "a9aef217.f0aae" ], [], [] ] }, { - "id": "e0f99dcc.c0257", + "id": "eba0a98a.e4d56", "type": "exec", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "command": "free | grep Mem | awk '{print 100*($4+$6+$7)/$2}' | awk -F \".\" '{print $1}' | tr -d \"\\n\"", "addpay": false, "append": "", @@ -1052,19 +1135,19 @@ "y": 340, "wires": [ [ - "f86fc2d5.49c258", - "cf879f92.12d6b" + "539c69d4.618bd8", + "96ceb8d7.0ed66" ], [], [] ] }, { - "id": "35e3cb5a.debb74", + "id": "aa4065d2.14a66", "type": "ui_gauge", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "CPU Load Donut", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 5, "width": 5, "height": 4, @@ -1086,11 +1169,11 @@ "wires": [] }, { - "id": "f86fc2d5.49c258", + "id": "539c69d4.618bd8", "type": "ui_gauge", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "Free memory Donut", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 9, "width": 5, "height": 4, @@ -1107,14 +1190,14 @@ ], "seg1": "", "seg2": "", - "x": 660, + "x": 650, "y": 320, "wires": [] }, { - "id": "91eb9ad.4dbb5e8", + "id": "9031b9ea.483fa8", "type": "exec", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "command": "df -h | grep /dev/root | awk -F ' ' '{print $3}' | tr -d G | tr \"\\n$\" \"\\ \" | sed 's/,/./' | tr -d \" \" ", "addpay": false, "append": "", @@ -1125,19 +1208,19 @@ "y": 420, "wires": [ [ - "7653ee80.f1f07", - "8ae4d547.d3bb98" + "32eded3f.f3b42a", + "49c71440.02a25c" ], [], [] ] }, { - "id": "7653ee80.f1f07", + "id": "32eded3f.f3b42a", "type": "ui_gauge", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "Disk Usage Donut", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 3, "width": 5, "height": 4, @@ -1159,11 +1242,11 @@ "wires": [] }, { - "id": "84d67d53.6001e", + "id": "a9aef217.f0aae", "type": "ui_chart", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "CPU Load Chart", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 6, "width": 19, "height": 4, @@ -1201,11 +1284,11 @@ ] }, { - "id": "cf879f92.12d6b", + "id": "96ceb8d7.0ed66", "type": "ui_chart", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "Free memory Chart", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 10, "width": 19, "height": 4, @@ -1243,9 +1326,9 @@ ] }, { - "id": "22b3544f.b649bc", + "id": "fb614565.7a2108", "type": "inject", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "", "repeat": "60", "crontab": "", @@ -1258,16 +1341,16 @@ "y": 420, "wires": [ [ - "91eb9ad.4dbb5e8" + "9031b9ea.483fa8" ] ] }, { - "id": "8ae4d547.d3bb98", + "id": "49c71440.02a25c", "type": "ui_chart", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "Disk Usage Chart", - "group": "7a723961.386be", + "group": "779606c9.19d2b8", "order": 4, "width": 19, "height": 4, @@ -1305,9 +1388,9 @@ ] }, { - "id": "916264f4.1b31f", + "id": "1d11bfc1.ad4788", "type": "change", - "z": "aafb7d9f.f516f", + "z": "21b3da63.2cef2e", "name": "", "rules": [ { @@ -1327,16 +1410,16 @@ "y": 120, "wires": [ [ - "602add74.9aa2a4" + "6e383122.133d5" ] ] }, { - "id": "a06de215.74a87", + "id": "4b489713.ccde5", "type": "ui_button", - "z": "3f0fd072.06e2c8", + "z": "130e0533.4f1813", "name": "", - "group": "3bd8d121.499b96", + "group": "75a5ce1f.728d", "order": 2, "width": 16, "height": 1, @@ -1348,43 +1431,45 @@ "icon": "", "payload": "", "payloadType": "str", - "topic": "actuator/image", + "topic": "imager/image", "x": 200, "y": 100, "wires": [ [ - "d6238ee1.db955" + "c9f510c0.7d1328" ] ] }, { - "id": "d6238ee1.db955", + "id": "c9f510c0.7d1328", "type": "function", - "z": "3f0fd072.06e2c8", + "z": "130e0533.4f1813", "name": "image.js", - "func": "state = global.get(\"state\");\nglobal.set('img_counter',0)\nglobal.set('obj_counter',0)\nif (state == null){state=\"free\"}\n\nvar sleep_before= global.get(\"custom_sleep_before\");\nvar nb_step= global.get(\"custom_nb_step\");\nvar nb_frame= global.get(\"custom_nb_frame\");\n\nif (sleep_before === undefined || sleep_before === \"\" || sleep_before === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Duration before the acquisition\";\n \n}else if (nb_step === undefined || nb_step === \"\" || nb_step === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Number of step in between two frames\";\n \n}else if (nb_frame === undefined || nb_frame === \"\" || nb_frame === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Number of image to save\";\n \n}else {\n nb_frame=nb_frame-1\n \n msg.payload=sleep_before+' '+nb_step+' '+nb_frame;\n}\nreturn msg;", + "func": "state = global.get(\"state\");\nglobal.set('img_counter',0);\nglobal.set('obj_counter',0);\nif (state === null){state=\"free\"}\n\nvar sleep_before= global.get(\"custom_sleep_before\");\nvar nb_step= global.get(\"custom_nb_step\");\nvar nb_frame= global.get(\"custom_nb_frame\");\n\nif (sleep_before === undefined || sleep_before === \"\" || sleep_before === null) {\n msg.topic = \"Missing entry :\";\n msg.payload = \"Duration before the acquisition\";\n \n}else if (nb_step === undefined || nb_step === \"\" || nb_step === null) {\n msg.topic = \"Missing entry :\";\n msg.payload = \"Number of step in between two frames\";\n \n}else if (nb_frame === undefined || nb_frame === \"\" || nb_frame === null) {\n msg.topic = \"Missing entry :\";\n msg.payload = \"Number of image to save\";\n \n}else {\n nb_frame=nb_frame-1;\n msg.payload={\"action\":\"image\", \n \"sleep\":sleep_before,\n \"volume\":nb_step,\n \"nb_frame\":nb_frame,\n }\n}\n\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 360, + "initialize": "", + "finalize": "", + "x": 380, "y": 100, "wires": [ [ - "40b06bd3.39ae74" + "d6ebaa2.ea21d58" ] ], "info": "### Focusing\n##### focus.py `nb_step` `orientation`\n\n- `nb_step` : **integer** (from 1 to 100000) - number of step to perform by the stage (about 31um/step)\n- `orientation` : **string** - orientation of the focus either `up` or `down`\n\nExample:\n\n python3.7 $HOME/PlanktonScope/scripts/focus.py 650 up\n" }, { - "id": "40b06bd3.39ae74", + "id": "d6ebaa2.ea21d58", "type": "switch", - "z": "3f0fd072.06e2c8", + "z": "130e0533.4f1813", "name": "", "property": "topic", "propertyType": "msg", "rules": [ { "t": "eq", - "v": "actuator/image", + "v": "imager/image", "vt": "str" }, { @@ -1400,17 +1485,17 @@ "y": 100, "wires": [ [ - "9c6b9e61.7436" + "c3e50240.82aa58" ], [ - "f8f02776.e0e178" + "20e0a8c8.edbeb" ] ] }, { - "id": "f8f02776.e0e178", + "id": "20e0a8c8.edbeb", "type": "ui_toast", - "z": "3f0fd072.06e2c8", + "z": "130e0533.4f1813", "position": "dialog", "displayTime": "3", "highlight": "", @@ -1428,24 +1513,24 @@ ] }, { - "id": "9c6b9e61.7436", + "id": "c3e50240.82aa58", "type": "mqtt out", - "z": "3f0fd072.06e2c8", + "z": "130e0533.4f1813", "name": "", "topic": "", "qos": "", "retain": "", - "broker": "e6efd12e.dedae8", + "broker": "8dc3722c.06efa8", "x": 670, "y": 80, "wires": [] }, { - "id": "6ff281cf.69e9e8", + "id": "3a4450b1.4459a8", "type": "ui_button", - "z": "3f0fd072.06e2c8", + "z": "130e0533.4f1813", "name": "Stop Acquisition", - "group": "3bd8d121.499b96", + "group": "75a5ce1f.728d", "order": 1, "width": 8, "height": 1, @@ -1455,43 +1540,43 @@ "color": "", "bgcolor": "#AD1625", "icon": "", - "payload": "off", - "payloadType": "str", - "topic": "actuator/wait", + "payload": "{\"action\":\"stop\"}", + "payloadType": "json", + "topic": "imager/image", "x": 200, "y": 140, "wires": [ [ - "c2451eaf.eedc1" + "d74210ef.edc15" ] ] }, { - "id": "c2451eaf.eedc1", + "id": "d74210ef.edc15", "type": "mqtt out", - "z": "3f0fd072.06e2c8", + "z": "130e0533.4f1813", "name": "", "topic": "", "qos": "", "retain": "", - "broker": "e6efd12e.dedae8", - "x": 350, + "broker": "8dc3722c.06efa8", + "x": 390, "y": 140, "wires": [] }, { - "id": "e972dce3.84be7", + "id": "9998aa86.74bb", "type": "exec", - "z": "435c6174.e0c8b", - "command": "python3.7 /home/pi/PlanktonScope/script/main.py", + "z": "d95291dc.83b0b8", + "command": "python3 /home/pi/PlanktonScope/scripts/main.py", "addpay": false, "append": "", - "useSpawn": "false", + "useSpawn": "true", "timer": "", "oldrc": false, "name": "", - "x": 890, - "y": 120, + "x": 840, + "y": 140, "wires": [ [], [], @@ -1499,75 +1584,34 @@ ] }, { - "id": "fd813497.f44d5", - "type": "file", - "z": "435c6174.e0c8b", - "name": "", - "filename": "/home/pi/PlanktonScope/script/main.py", - "appendNewline": false, - "createDir": true, - "overwriteFile": "true", - "encoding": "none", - "x": 530, - "y": 100, - "wires": [ - [ - "e972dce3.84be7" - ] - ] - }, - { - "id": "a2e43b6f.3b73a", + "id": "35ded403.3d7244", "type": "template", - "z": "435c6174.e0c8b", + "z": "d95291dc.83b0b8", "name": "main.py", "field": "payload", "fieldType": "msg", "format": "python", "syntax": "plain", - "template": "#Library to send command over I2C for the light module on the fan and subprocess to run bash command\nimport smbus, subprocess\n################################################################################\n#LEDs Actuation\n################################################################################\n\n#define the bus used to actuate the light module on the fan\nbus = smbus.SMBus(1)\n\ndef rgb(R,G,B):\n #Update LED n1\n bus.write_byte_data(0x0d, 0x00, 0)\n bus.write_byte_data(0x0d, 0x01, R)\n bus.write_byte_data(0x0d, 0x02, G)\n bus.write_byte_data(0x0d, 0x03, B)\n\n #Update LED n2\n bus.write_byte_data(0x0d, 0x00, 1)\n bus.write_byte_data(0x0d, 0x01, R)\n bus.write_byte_data(0x0d, 0x02, G)\n bus.write_byte_data(0x0d, 0x03, B)\n\n #Update LED n3\n bus.write_byte_data(0x0d, 0x00, 2)\n bus.write_byte_data(0x0d, 0x01, R)\n bus.write_byte_data(0x0d, 0x02, G)\n bus.write_byte_data(0x0d, 0x03, B)\n\n #Update the I2C Bus in order to really update the LEDs new values\n cmd=\"i2cdetect -y 1\"\n subprocess.Popen(cmd.split(),stdout=subprocess.PIPE)\n \n#Present the RED color\nrgb(255,0,0)\n\n################################################################################\n#Actuator Libraries\n################################################################################\n\n#Library for exchaning messages with Node-RED\nimport paho.mqtt.client as mqtt\n\n#Library to control the PiCamera\nfrom picamera import PiCamera\n\n#Libraries to control the steppers for focusing and pumping\nfrom adafruit_motor import stepper\nfrom adafruit_motorkit import MotorKit\n\n################################################################################\n#Practical Libraries\n################################################################################\n\n#Library to get date and time for folder name and filename\nfrom datetime import datetime, timedelta\n\n#Library to be able to sleep for a duration\nfrom time import sleep\n\n#Libraries manipulate json format, execute bash commands\nimport json, shutil, os\n\n################################################################################\n#Morphocut Libraries\n################################################################################\n\nfrom skimage.util import img_as_ubyte\nfrom morphocut import Call\nfrom morphocut.contrib.ecotaxa import EcotaxaWriter\nfrom morphocut.contrib.zooprocess import CalculateZooProcessFeatures\nfrom morphocut.core import Pipeline\nfrom morphocut.file import Find\nfrom morphocut.image import (ExtractROI,\n FindRegions,\n ImageReader,\n ImageWriter,\n RescaleIntensity,\n RGB2Gray\n)\nfrom morphocut.stat import RunningMedian\nfrom morphocut.str import Format\nfrom morphocut.stream import TQDM, Enumerate, FilterVariables\n\n################################################################################\n#Other image processing Libraries\n################################################################################\n\nfrom skimage.feature import canny\nfrom skimage.color import rgb2gray, label2rgb\nfrom skimage.morphology import disk\nfrom skimage.morphology import erosion, dilation, closing\nfrom skimage.measure import label, regionprops\nimport cv2\n\n################################################################################\n#Streaming PiCamera over server\n################################################################################\nimport io\nimport picamera\nimport logging\nimport socketserver\nfrom threading import Condition\nfrom http import server\nimport threading\n\n################################################################################\n#Get possibility to generate random numbers\n################################################################################\n# generate random integer values\nfrom random import seed\nfrom random import randint\n\n\n\n################################################################################\n#Creation of the webpage containing the PiCamera Streaming\n################################################################################\n\nPAGE=\"\"\"\\\n\n\nPlanktonScope v2 | PiCamera Streaming\n\n\n\n\n\n\"\"\"\n\n################################################################################\n#Classes for the PiCamera Streaming\n################################################################################\n\nclass StreamingOutput(object):\n def __init__(self):\n self.frame = None\n self.buffer = io.BytesIO()\n self.condition = Condition()\n\n def write(self, buf):\n if buf.startswith(b'\\xff\\xd8'):\n # New frame, copy the existing buffer's content and notify all\n # clients it's available\n self.buffer.truncate()\n with self.condition:\n self.frame = self.buffer.getvalue()\n self.condition.notify_all()\n self.buffer.seek(0)\n return self.buffer.write(buf)\n\nclass StreamingHandler(server.BaseHTTPRequestHandler):\n def do_GET(self):\n if self.path == '/':\n self.send_response(301)\n self.send_header('Location', '/index.html')\n self.end_headers()\n elif self.path == '/index.html':\n content = PAGE.encode('utf-8')\n self.send_response(200)\n self.send_header('Content-Type', 'text/html')\n self.send_header('Content-Length', len(content))\n self.end_headers()\n self.wfile.write(content)\n elif self.path == '/stream.mjpg':\n self.send_response(200)\n self.send_header('Age', 0)\n self.send_header('Cache-Control', 'no-cache, private')\n self.send_header('Pragma', 'no-cache')\n self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')\n self.end_headers()\n try:\n while True:\n with output.condition:\n output.condition.wait()\n frame = output.frame\n self.wfile.write(b'--FRAME\\r\\n')\n self.send_header('Content-Type', 'image/jpeg')\n self.send_header('Content-Length', len(frame))\n self.end_headers()\n self.wfile.write(frame)\n self.wfile.write(b'\\r\\n')\n except Exception as e:\n logging.warning(\n 'Removed streaming client %s: %s',\n self.client_address, str(e))\n else:\n self.send_error(404)\n self.end_headers()\n\nclass StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):\n allow_reuse_address = True\n daemon_threads = True\n\n################################################################################\n#MQTT core functions\n################################################################################\n\n#Run this function in order to connect to the client (Node-RED)\ndef on_connect(client, userdata, flags, rc):\n #Print when connected\n print(\"Connected! - \" + str(rc))\n #When connected, run subscribe()\n client.subscribe(\"actuator/#\")\n #Turn green the light module\n rgb(0,255,0)\n\n#Run this function in order to subscribe to all the topics begining by actuator\ndef on_subscribe(client, obj, mid, granted_qos):\n #Print when subscribed\n print(\"Subscribed! - \"+str(mid)+\" \"+str(granted_qos))\n\n#Run this command when Node-RED is sending a message on the subscribed topic\ndef on_message(client, userdata, msg):\n #Print the topic and the message\n print(msg.topic+\" \"+str(msg.qos)+\" \"+str(msg.payload))\n #Update the global variables command, args and counter\n global command\n global args\n global counter\n #Parse the topic to find the command. ex : actuator/pump -> pump\n command=msg.topic.split(\"/\")[1]\n #Decode the message to find the arguments\n args=str(msg.payload.decode())\n #Reset the counter to 0\n counter=0\n\n################################################################################\n#Init functions\n################################################################################\n\n#define the names for the 2 exsting steppers\nkit = MotorKit()\npump_stepper = kit.stepper1\nfocus_stepper = kit.stepper2\n#Make sure the steppers are release and do not use any power\npump_stepper.release()\nfocus_stepper.release()\n\n#Precise the settings of the PiCamera\ncamera = PiCamera()\ncamera.resolution = (3280, 2464)\ncamera.iso = 60\ncamera.shutter_speed = 500\ncamera.exposure_mode = 'fixedfps'\n\n#Declare the global variables command, args and counter\ncommand = ''\nargs = ''\ncounter=''\n\n#MQTT Client functions definition\nclient = mqtt.Client()\nclient.connect(\"127.0.0.1\",1883,60)\nclient.on_connect = on_connect\nclient.on_subscribe = on_subscribe\nclient.on_message = on_message\nclient.loop_start()\n\n################################################################################\n#Definition of the few important metadata\n################################################################################\n\nlocal_metadata = {\n \"process_datetime\": datetime.now(),\n \"acq_camera_resolution\" : camera.resolution,\n \"acq_camera_iso\" : camera.iso,\n \"acq_camera_shutter_speed\" : camera.shutter_speed\n}\n\n#Read the content of config.json containing the metadata defined on Node-RED\nconfig_json = open('/home/pi/PlanktonScope/config.json','r')\nnode_red_metadata = json.loads(config_json.read())\n\n#Concat the local metadata and the metadata from Node-RED\nglobal_metadata = {**local_metadata, **node_red_metadata}\n\n#Define the name of the .zip file that will contain the images and the .tsv table for EcoTaxa\narchive_fn = os.path.join(\"/home/pi/PlanktonScope/\",\"export\", \"ecotaxa_export.zip\")\n\n################################################################################\n#MorphoCut Script\n################################################################################\n\n#Define processing pipeline\nwith Pipeline() as p:\n\n #Recursively find .jpg files in import_path.\n #Sort to get consective frames.\n abs_path = Find(\"/home/pi/PlanktonScope/tmp\", [\".jpg\"], sort=True, verbose=True)\n\n #Extract name from abs_path\n name = Call(lambda p: os.path.splitext(os.path.basename(p))[0], abs_path)\n\n #Set the LEDs as Green\n Call(rgb, 0,255,0)\n\n #Read image\n img = ImageReader(abs_path)\n\n #Show progress bar for frames\n TQDM(Format(\"Frame {name}\", name=name))\n\n #Apply running median to approximate the background image\n flat_field = RunningMedian(img, 5)\n\n #Correct image\n img = img / flat_field\n\n #Rescale intensities and convert to uint8 to speed up calculations\n img = RescaleIntensity(img, in_range=(0, 1.1), dtype=\"uint8\")\n\n #Publish the json containing all the metadata to via MQTT to Node-RED\n Call(client.publish, \"receiver/segmentation/name\", name)\n\n #Filter variable to reduce memory load\n FilterVariables(name,img)\n\n #Save cleaned images\n #frame_fn = Format(os.path.join(\"/home/pi/PlanktonScope/tmp\",\"CLEAN\", \"{name}.jpg\"), name=name)\n #ImageWriter(frame_fn, img)\n\n #Convert image to uint8 gray\n img_gray = RGB2Gray(img)\n\n #?\n img_gray = Call(img_as_ubyte, img_gray)\n\n #Canny edge detection using OpenCV\n img_canny = Call(cv2.Canny, img_gray, 50,100)\n\n #Dilate using OpenCV\n kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15))\n img_dilate = Call(cv2.dilate, img_canny, kernel, iterations=2)\n\n #Close using OpenCV\n kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (5, 5))\n img_close = Call(cv2.morphologyEx, img_dilate, cv2.MORPH_CLOSE, kernel, iterations=1)\n\n #Erode using OpenCV\n kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15))\n mask = Call(cv2.erode, img_close, kernel, iterations=2)\n\n #Find objects\n regionprops = FindRegions(\n mask, img_gray, min_area=1000, padding=10, warn_empty=name\n )\n\n #Set the LEDs as Purple\n Call(rgb, 255,0,255)\n\n # For an object, extract a vignette/ROI from the image\n roi_orig = ExtractROI(img, regionprops, bg_color=255)\n\n # Generate an object identifier\n i = Enumerate()\n\n #Call(print,i)\n\n #Define the ID of each object\n object_id = Format(\"{name}_{i:d}\", name=name, i=i)\n\n #Call(print,object_id)\n\n #Define the name of each object\n object_fn = Format(os.path.join(\"/home/pi/PlanktonScope/\",\"OBJECTS\", \"{name}.jpg\"), name=object_id)\n\n #Save the image of the object with its name\n ImageWriter(object_fn, roi_orig)\n\n #Calculate features. The calculated features are added to the global_metadata.\n #Returns a Variable representing a dict for every object in the stream.\n meta = CalculateZooProcessFeatures(\n regionprops, prefix=\"object_\", meta=global_metadata\n )\n\n #Get all the metadata\n json_meta = Call(json.dumps,meta, sort_keys=True, default=str)\n\n #Publish the json containing all the metadata to via MQTT to Node-RED\n Call(client.publish, \"receiver/segmentation/metric\", json_meta)\n\n #Add object_id to the metadata dictionary\n meta[\"object_id\"] = object_id\n\n #Generate object filenames\n orig_fn = Format(\"{object_id}.jpg\", object_id=object_id)\n\n #Write objects to an EcoTaxa archive:\n #roi image in original color, roi image in grayscale, metadata associated with each object\n EcotaxaWriter(archive_fn, (orig_fn, roi_orig), meta)\n\n #Progress bar for objects\n TQDM(Format(\"Object {object_id}\", object_id=object_id))\n\n #Publish the object_id to via MQTT to Node-RED\n Call(client.publish, \"receiver/segmentation/object_id\", object_id)\n\n #Set the LEDs as Green\n Call(rgb, 0,255,0)\n\n################################################################################\n#While loop for capting commands from Node-RED\n################################################################################\n\noutput = StreamingOutput()\naddress = ('', 8000)\nserver = StreamingServer(address, StreamingHandler)\nthreading.Thread(target=server.serve_forever).start()\ncamera.start_recording(output, format='mjpeg', resize=(640, 480))\n\nwhile True:\n\n ############################################################################\n #Pump Event\n ############################################################################\n\n #If the command is \"pump\"\n if (command==\"pump\"):\n\n #Set the LEDs as Blue\n rgb(0,0,255)\n\n #Get direction from the different received arguments\n direction=args.split(\" \")[0]\n\n #Get delay (in between steps) from the different received arguments\n delay=float(args.split(\" \")[1])\n\n #Get number of steps from the different received arguments\n nb_step=int(args.split(\" \")[2])\n\n #Print status\n print(\"The pump has been started.\")\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/pump\", delay);\n\n ########################################################################\n while True:\n\n #Depending on direction, select the right direction for the pump\n if direction == \"BACKWARD\":\n direction=stepper.BACKWARD\n\n if direction == \"FORWARD\":\n direction=stepper.FORWARD\n\n #Actuate the pump for one step in the right direction\n pump_stepper.onestep(direction=direction, style=stepper.DOUBLE)\n\n #Increment the counter\n counter+=1\n\n #Wait during the delay to pump at the right flowrate\n sleep(delay)\n\n ####################################################################\n #If counter reach the number of step, break\n if counter>nb_step:\n\n #Release the pump stepper to stop power draw\n pump_stepper.release()\n\n #Print status\n print(\"The pumping is done.\")\n\n #Change the command to not re-enter in this while loop\n command=\"wait\"\n\n #Publish the status \"Done\" to via MQTT to Node-RED\n client.publish(\"receiver/pump\", \"Done\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ####################################################################\n #If a new received command isn't \"pump\", break this while loop\n if command!=\"pump\":\n\n #Release the pump stepper to stop power draw\n pump_stepper.release()\n\n #Print status\n print(\"The pump has been interrompted.\")\n\n #Publish the status \"Interrompted\" to via MQTT to Node-RED\n client.publish(\"receiver/pump\", \"Interrompted\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ############################################################################\n #Focus Event\n ############################################################################\n\n #If the command is \"focus\"\n elif (command==\"focus\"):\n\n #Set the LEDs as Yellow\n rgb(255,255,0)\n\n #Get direction from the different received arguments\n direction=args.split(\" \")[0]\n\n #Get number of steps from the different received arguments\n nb_step=int(args.split(\" \")[1])\n\n #Print status\n print(\"The focus has been started.\")\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/focus\", \"Start\");\n\n ########################################################################\n while True:\n\n #Depending on direction, select the right direction for the focus\n if direction == \"FORWARD\":\n direction=stepper.FORWARD\n\n if direction == \"BACKWARD\":\n direction=stepper.BACKWARD\n\n #Actuate the focus for one microstep in the right direction\n focus_stepper.onestep(direction=direction, style=stepper.MICROSTEP)\n\n #Increment the counter\n counter+=1\n\n ####################################################################\n #If counter reach the number of step, break\n if counter>nb_step:\n\n #Release the focus steppers to stop power draw\n focus_stepper.release()\n\n #Print status\n print(\"The focusing is done.\")\n\n #Change the command to not re-enter in this while loop\n command=\"wait\"\n\n #Publish the status \"Done\" to via MQTT to Node-RED\n client.publish(\"receiver/focus\", \"Done\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ####################################################################\n #If a new received command isn't \"pump\", break this while loop\n if command!=\"focus\":\n\n #Release the focus steppers to stop power draw\n focus_stepper.release()\n\n #Print status\n print(\"The stage has been interrompted.\")\n\n #Publish the status \"Done\" to via MQTT to Node-RED\n client.publish(\"receiver/focus\", \"Interrompted\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ############################################################################\n #Image Event\n ############################################################################\n\n elif (command==\"image\"):\n\n #Get duration to wait before an image from the different received arguments\n sleep_before=int(args.split(\" \")[0])\n\n #Get number of step in between two images from the different received arguments\n nb_step=int(args.split(\" \")[1])\n\n #Get the number of frames to image from the different received arguments\n nb_frame=int(args.split(\" \")[2])\n\n #Sleep a duration before to start acquisition\n sleep(sleep_before)\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/image\", \"Start\");\n\n #Set the LEDs as Blue\n rgb(0,0,255)\n\n #Pump duing a given number of steps (in between each image)\n for i in range(nb_step):\n\n #If the command is still image - pump a defined nb of steps\n if (command==\"image\"):\n\n #Actuate the pump for one step in the FORWARD direction\n pump_stepper.onestep(direction=stepper.FORWARD, style=stepper.DOUBLE)\n\n #The flowrate is fixed for now.\n sleep(0.01)\n\n #If the command isn't image anymore - break\n else:\n\n break\n\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n while True:\n #Release the pump stepper to stop power draw\n pump_stepper.release()\n\n #Set the LEDs as Cyan\n rgb(0,255,255)\n\n #Increment the counter\n counter+=1\n\n #Get datetime\n datetime_tmp=datetime.now().strftime(\"%H_%M_%S_%f\")\n\n #Print datetime\n print(datetime_tmp)\n\n #Define the filename of the image\n filename = os.path.join(\"/home/pi/PlanktonScope/tmp\",datetime_tmp+\".jpg\")\n\n #Capture an image with the proper filename\n camera.capture(filename)\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Publish the name of the image to via MQTT to Node-RED\n\n client.publish(\"receiver/image\", datetime_tmp+\".jpg has been imaged.\");\n \n #Set the LEDs as Blue\n rgb(0,0,255)\n\n #Pump during a given nb of steps\n for i in range(nb_step):\n\n #Actuate the pump for one step in the FORWARD direction\n pump_stepper.onestep(direction=stepper.FORWARD, style=stepper.DOUBLE)\n\n #The flowrate is fixed for now.\n sleep(0.01)\n\n #Wait a fixed delay which set the framerate as < than 2 imag/sec\n sleep(0.5)\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n ####################################################################\n #If counter reach the number of frame, break\n if(counter>nb_frame):\n\n #Publish the status \"Completed\" to via MQTT to Node-RED\n client.publish(\"receiver/image\", \"Completed\");\n\n #Release the pump steppers to stop power draw\n pump_stepper.release()\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/segmentation\", \"Start\");\n\n #Start the MorphoCut Pipeline\n p.run()\n\n #remove directory\n #shutil.rmtree(import_path)\n\n #Publish the status \"Completed\" to via MQTT to Node-RED\n client.publish(\"receiver/segmentation\", \"Completed\");\n\n #Set the LEDs as White\n rgb(255,255,255)\n\n sample_project=node_red_metadata['sample_project'];\n \n acq_id=node_red_metadata['acq_id'];\n \n export_name = str(sample_project)+\"_\"+str(acq_id)+\".zip\"\n \n \n os.popen(\"mv /home/pi/PlanktonScope/export/ecotaxa_export.zip /home/pi/PlanktonScope/export/\"+export_name)\n \n os.popen(\"rm -rf /home/pi/PlanktonScope/tmp/*.jpg\")\n \n os.popen(\"rm -rf /home/pi/PlanktonScope/OBJECTS/*.jpg\")\n \n\n #Let it happen\n sleep(1)\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n\n #Change the command to not re-enter in this while loop\n command=\"wait\"\n \n #Set the LEDs as Green\n rgb(0,255,255)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ####################################################################\n #If a new received command isn't \"image\", break this while loop\n if command!=\"image\":\n\n #Release the pump steppers to stop power draw\n pump_stepper.release()\n\n #Print status\n print(\"The imaging has been interrompted.\")\n\n #Publish the status \"Interrompted\" to via MQTT to Node-RED\n client.publish(\"receiver/image\", \"Interrompted\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n else:\n #Its just waiting to receive command from Node-RED\n sleep(0.4)\n", + "template": "#Library to send command over I2C for the light module on the fan and subprocess to run bash command\nimport smbus, subprocess\n################################################################################\n#LEDs Actuation\n################################################################################\n\n#define the bus used to actuate the light module on the fan\nbus = smbus.SMBus(1)\n\ndef rgb(R,G,B):\n #Update LED n1\n bus.write_byte_data(0x0d, 0x00, 0)\n bus.write_byte_data(0x0d, 0x01, R)\n bus.write_byte_data(0x0d, 0x02, G)\n bus.write_byte_data(0x0d, 0x03, B)\n\n #Update LED n2\n bus.write_byte_data(0x0d, 0x00, 1)\n bus.write_byte_data(0x0d, 0x01, R)\n bus.write_byte_data(0x0d, 0x02, G)\n bus.write_byte_data(0x0d, 0x03, B)\n\n #Update LED n3\n bus.write_byte_data(0x0d, 0x00, 2)\n bus.write_byte_data(0x0d, 0x01, R)\n bus.write_byte_data(0x0d, 0x02, G)\n bus.write_byte_data(0x0d, 0x03, B)\n\n #Update the I2C Bus in order to really update the LEDs new values\n cmd=\"i2cdetect -y 1\"\n subprocess.Popen(cmd.split(),stdout=subprocess.PIPE)\n \n#Present the RED color\nrgb(255,0,0)\n\n################################################################################\n#Actuator Libraries\n################################################################################\n\n#Library for exchaning messages with Node-RED\nimport paho.mqtt.client as mqtt\n\n#Library to control the PiCamera\nfrom picamera import PiCamera\n\n#Libraries to control the steppers for focusing and pumping\nfrom adafruit_motor import stepper\nfrom adafruit_motorkit import MotorKit\n\n################################################################################\n#Practical Libraries\n################################################################################\n\n#Library to get date and time for folder name and filename\nfrom datetime import datetime, timedelta\n\n#Library to be able to sleep for a duration\nfrom time import sleep\n\n#Libraries manipulate json format, execute bash commands\nimport json, shutil, os\n\n################################################################################\n#Morphocut Libraries\n################################################################################\n\nfrom skimage.util import img_as_ubyte\nfrom morphocut import Call\nfrom morphocut.contrib.ecotaxa import EcotaxaWriter\nfrom morphocut.contrib.zooprocess import CalculateZooProcessFeatures\nfrom morphocut.core import Pipeline\nfrom morphocut.file import Find\nfrom morphocut.image import (ExtractROI,\n FindRegions,\n ImageReader,\n ImageWriter,\n RescaleIntensity,\n RGB2Gray\n)\nfrom morphocut.stat import RunningMedian\nfrom morphocut.str import Format\nfrom morphocut.stream import TQDM, Enumerate, FilterVariables\n\n################################################################################\n#Other image processing Libraries\n################################################################################\n\nfrom skimage.feature import canny\nfrom skimage.color import rgb2gray, label2rgb\nfrom skimage.morphology import disk\nfrom skimage.morphology import erosion, dilation, closing\nfrom skimage.measure import label, regionprops\nimport cv2\n\n################################################################################\n#Streaming PiCamera over server\n################################################################################\nimport io\nimport picamera\nimport logging\nimport socketserver\nfrom threading import Condition\nfrom http import server\nimport threading\n\n################################################################################\n#Get possibility to generate random numbers\n################################################################################\n# generate random integer values\nfrom random import seed\nfrom random import randint\n\n\n\n################################################################################\n#Creation of the webpage containing the PiCamera Streaming\n################################################################################\n\nPAGE=\"\"\"\\\n\n\nPlanktonScope v2 | PiCamera Streaming\n\n\n\n\n\n\"\"\"\n\n################################################################################\n#Classes for the PiCamera Streaming\n################################################################################\n\nclass StreamingOutput(object):\n def __init__(self):\n self.frame = None\n self.buffer = io.BytesIO()\n self.condition = Condition()\n\n def write(self, buf):\n if buf.startswith(b'\\xff\\xd8'):\n # New frame, copy the existing buffer's content and notify all\n # clients it's available\n self.buffer.truncate()\n with self.condition:\n self.frame = self.buffer.getvalue()\n self.condition.notify_all()\n self.buffer.seek(0)\n return self.buffer.write(buf)\n\nclass StreamingHandler(server.BaseHTTPRequestHandler):\n def do_GET(self):\n if self.path == '/':\n self.send_response(301)\n self.send_header('Location', '/index.html')\n self.end_headers()\n elif self.path == '/index.html':\n content = PAGE.encode('utf-8')\n self.send_response(200)\n self.send_header('Content-Type', 'text/html')\n self.send_header('Content-Length', len(content))\n self.end_headers()\n self.wfile.write(content)\n elif self.path == '/stream.mjpg':\n self.send_response(200)\n self.send_header('Age', 0)\n self.send_header('Cache-Control', 'no-cache, private')\n self.send_header('Pragma', 'no-cache')\n self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')\n self.end_headers()\n try:\n while True:\n with output.condition:\n output.condition.wait()\n frame = output.frame\n self.wfile.write(b'--FRAME\\r\\n')\n self.send_header('Content-Type', 'image/jpeg')\n self.send_header('Content-Length', len(frame))\n self.end_headers()\n self.wfile.write(frame)\n self.wfile.write(b'\\r\\n')\n except Exception as e:\n logging.warning(\n 'Removed streaming client %s: %s',\n self.client_address, str(e))\n else:\n self.send_error(404)\n self.end_headers()\n\nclass StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):\n allow_reuse_address = True\n daemon_threads = True\n\n################################################################################\n#MQTT core functions\n################################################################################\n\n#Run this function in order to connect to the client (Node-RED)\ndef on_connect(client, userdata, flags, rc):\n #Print when connected\n print(\"Connected! - \" + str(rc))\n #When connected, run subscribe()\n client.subscribe(\"actuator/#\")\n #Turn green the light module\n rgb(0,255,0)\n\n#Run this function in order to subscribe to all the topics begining by actuator\ndef on_subscribe(client, obj, mid, granted_qos):\n #Print when subscribed\n print(\"Subscribed! - \"+str(mid)+\" \"+str(granted_qos))\n\n#Run this command when Node-RED is sending a message on the subscribed topic\ndef on_message(client, userdata, msg):\n #Print the topic and the message\n print(msg.topic+\" \"+str(msg.qos)+\" \"+str(msg.payload))\n #Update the global variables command, args and counter\n global command\n global args\n global counter\n #Parse the topic to find the command. ex : actuator/pump -> pump\n command=msg.topic.split(\"/\")[1]\n #Decode the message to find the arguments\n args=str(msg.payload.decode())\n #Reset the counter to 0\n counter=0\n\n################################################################################\n#Init functions\n################################################################################\n\n#define the names for the 2 exsting steppers\nkit = MotorKit()\npump_stepper = kit.stepper1\nfocus_stepper = kit.stepper2\n#Make sure the steppers are release and do not use any power\npump_stepper.release()\nfocus_stepper.release()\n\n#Precise the settings of the PiCamera\ncamera = PiCamera()\ncamera.resolution = (3280, 2464)\ncamera.iso = 60\ncamera.shutter_speed = 500\ncamera.exposure_mode = 'fixedfps'\n\n#Declare the global variables command, args and counter\ncommand = ''\nargs = ''\ncounter=''\n\n#MQTT Client functions definition\nclient = mqtt.Client()\nclient.connect(\"192.168.4.1\",1883,60)\nclient.on_connect = on_connect\nclient.on_subscribe = on_subscribe\nclient.on_message = on_message\nclient.loop_start()\n\n################################################################################\n#Definition of the few important metadata\n################################################################################\n\nlocal_metadata = {\n \"process_datetime\": datetime.now(),\n \"acq_camera_resolution\" : camera.resolution,\n \"acq_camera_iso\" : camera.iso,\n \"acq_camera_shutter_speed\" : camera.shutter_speed\n}\n\n#Read the content of config.json containing the metadata defined on Node-RED\nconfig_json = open('/home/pi/PlanktonScope/config.json','r')\nnode_red_metadata = json.loads(config_json.read())\n\n#Concat the local metadata and the metadata from Node-RED\nglobal_metadata = {**local_metadata, **node_red_metadata}\n\n#Define the name of the .zip file that will contain the images and the .tsv table for EcoTaxa\narchive_fn = os.path.join(\"/home/pi/PlanktonScope/\",\"export\", \"ecotaxa_export.zip\")\n\n################################################################################\n#MorphoCut Script\n################################################################################\n\n#Define processing pipeline\nwith Pipeline() as p:\n\n #Recursively find .jpg files in import_path.\n #Sort to get consective frames.\n abs_path = Find(\"/home/pi/PlanktonScope/tmp\", [\".jpg\"], sort=True, verbose=True)\n\n #Extract name from abs_path\n name = Call(lambda p: os.path.splitext(os.path.basename(p))[0], abs_path)\n\n #Set the LEDs as Green\n Call(rgb, 0,255,0)\n\n #Read image\n img = ImageReader(abs_path)\n\n #Show progress bar for frames\n TQDM(Format(\"Frame {name}\", name=name))\n\n #Apply running median to approximate the background image\n flat_field = RunningMedian(img, 5)\n\n #Correct image\n img = img / flat_field\n\n #Rescale intensities and convert to uint8 to speed up calculations\n img = RescaleIntensity(img, in_range=(0, 1.1), dtype=\"uint8\")\n\n #Publish the json containing all the metadata to via MQTT to Node-RED\n Call(client.publish, \"receiver/segmentation/name\", name)\n\n #Filter variable to reduce memory load\n FilterVariables(name,img)\n\n #Save cleaned images\n #frame_fn = Format(os.path.join(\"/home/pi/PlanktonScope/tmp\",\"CLEAN\", \"{name}.jpg\"), name=name)\n #ImageWriter(frame_fn, img)\n\n #Convert image to uint8 gray\n img_gray = RGB2Gray(img)\n\n #?\n img_gray = Call(img_as_ubyte, img_gray)\n\n #Canny edge detection using OpenCV\n img_canny = Call(cv2.Canny, img_gray, 50,100)\n\n #Dilate using OpenCV\n kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15))\n img_dilate = Call(cv2.dilate, img_canny, kernel, iterations=2)\n\n #Close using OpenCV\n kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (5, 5))\n img_close = Call(cv2.morphologyEx, img_dilate, cv2.MORPH_CLOSE, kernel, iterations=1)\n\n #Erode using OpenCV\n kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15))\n mask = Call(cv2.erode, img_close, kernel, iterations=2)\n\n #Find objects\n regionprops = FindRegions(\n mask, img_gray, min_area=1000, padding=10, warn_empty=name\n )\n\n #Set the LEDs as Purple\n Call(rgb, 255,0,255)\n\n # For an object, extract a vignette/ROI from the image\n roi_orig = ExtractROI(img, regionprops, bg_color=255)\n\n # Generate an object identifier\n i = Enumerate()\n\n #Call(print,i)\n\n #Define the ID of each object\n object_id = Format(\"{name}_{i:d}\", name=name, i=i)\n\n #Call(print,object_id)\n\n #Define the name of each object\n object_fn = Format(os.path.join(\"/home/pi/PlanktonScope/\",\"OBJECTS\", \"{name}.jpg\"), name=object_id)\n\n #Save the image of the object with its name\n ImageWriter(object_fn, roi_orig)\n\n #Calculate features. The calculated features are added to the global_metadata.\n #Returns a Variable representing a dict for every object in the stream.\n meta = CalculateZooProcessFeatures(\n regionprops, prefix=\"object_\", meta=global_metadata\n )\n\n #Get all the metadata\n json_meta = Call(json.dumps,meta, sort_keys=True, default=str)\n\n #Publish the json containing all the metadata to via MQTT to Node-RED\n Call(client.publish, \"receiver/segmentation/metric\", json_meta)\n\n #Add object_id to the metadata dictionary\n meta[\"object_id\"] = object_id\n\n #Generate object filenames\n orig_fn = Format(\"{object_id}.jpg\", object_id=object_id)\n\n #Write objects to an EcoTaxa archive:\n #roi image in original color, roi image in grayscale, metadata associated with each object\n EcotaxaWriter(archive_fn, (orig_fn, roi_orig), meta)\n\n #Progress bar for objects\n TQDM(Format(\"Object {object_id}\", object_id=object_id))\n\n #Publish the object_id to via MQTT to Node-RED\n Call(client.publish, \"receiver/segmentation/object_id\", object_id)\n\n #Set the LEDs as Green\n Call(rgb, 0,255,0)\n\n################################################################################\n#While loop for capting commands from Node-RED\n################################################################################\n\noutput = StreamingOutput()\naddress = ('192.168.4.1', 8000)\nserver = StreamingServer(address, StreamingHandler)\nthreading.Thread(target=server.serve_forever).start()\ncamera.start_recording(output, format='mjpeg', resize=(640, 480))\n\nwhile True:\n\n ############################################################################\n #Pump Event\n ############################################################################\n\n #If the command is \"pump\"\n if (command==\"pump\"):\n\n #Set the LEDs as Blue\n rgb(0,0,255)\n\n #Get direction from the different received arguments\n direction=args.split(\" \")[0]\n\n #Get delay (in between steps) from the different received arguments\n delay=float(args.split(\" \")[1])\n\n #Get number of steps from the different received arguments\n nb_step=int(args.split(\" \")[2])\n\n #Print status\n print(\"The pump has been started.\")\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/pump\", delay);\n\n ########################################################################\n while True:\n\n #Depending on direction, select the right direction for the pump\n if direction == \"BACKWARD\":\n direction=stepper.BACKWARD\n\n if direction == \"FORWARD\":\n direction=stepper.FORWARD\n\n #Actuate the pump for one step in the right direction\n pump_stepper.onestep(direction=direction, style=stepper.DOUBLE)\n\n #Increment the counter\n counter+=1\n\n #Wait during the delay to pump at the right flowrate\n sleep(delay)\n\n ####################################################################\n #If counter reach the number of step, break\n if counter>nb_step:\n\n #Release the pump stepper to stop power draw\n pump_stepper.release()\n\n #Print status\n print(\"The pumping is done.\")\n\n #Change the command to not re-enter in this while loop\n command=\"wait\"\n\n #Publish the status \"Done\" to via MQTT to Node-RED\n client.publish(\"receiver/pump\", \"Done\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ####################################################################\n #If a new received command isn't \"pump\", break this while loop\n if command!=\"pump\":\n\n #Release the pump stepper to stop power draw\n pump_stepper.release()\n\n #Print status\n print(\"The pump has been interrompted.\")\n\n #Publish the status \"Interrompted\" to via MQTT to Node-RED\n client.publish(\"receiver/pump\", \"Interrompted\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ############################################################################\n #Focus Event\n ############################################################################\n\n #If the command is \"focus\"\n elif (command==\"focus\"):\n\n #Set the LEDs as Yellow\n rgb(255,255,0)\n\n #Get direction from the different received arguments\n direction=args.split(\" \")[0]\n\n #Get number of steps from the different received arguments\n nb_step=int(args.split(\" \")[1])\n\n #Print status\n print(\"The focus has been started.\")\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/focus\", \"Start\");\n\n ########################################################################\n while True:\n\n #Depending on direction, select the right direction for the focus\n if direction == \"FORWARD\":\n direction=stepper.FORWARD\n\n if direction == \"BACKWARD\":\n direction=stepper.BACKWARD\n\n #Actuate the focus for one microstep in the right direction\n focus_stepper.onestep(direction=direction, style=stepper.MICROSTEP)\n\n #Increment the counter\n counter+=1\n\n ####################################################################\n #If counter reach the number of step, break\n if counter>nb_step:\n\n #Release the focus steppers to stop power draw\n focus_stepper.release()\n\n #Print status\n print(\"The focusing is done.\")\n\n #Change the command to not re-enter in this while loop\n command=\"wait\"\n\n #Publish the status \"Done\" to via MQTT to Node-RED\n client.publish(\"receiver/focus\", \"Done\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ####################################################################\n #If a new received command isn't \"pump\", break this while loop\n if command!=\"focus\":\n\n #Release the focus steppers to stop power draw\n focus_stepper.release()\n\n #Print status\n print(\"The stage has been interrompted.\")\n\n #Publish the status \"Done\" to via MQTT to Node-RED\n client.publish(\"receiver/focus\", \"Interrompted\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ############################################################################\n #Image Event\n ############################################################################\n\n elif (command==\"image\"):\n\n #Get duration to wait before an image from the different received arguments\n sleep_before=int(args.split(\" \")[0])\n\n #Get number of step in between two images from the different received arguments\n nb_step=int(args.split(\" \")[1])\n\n #Get the number of frames to image from the different received arguments\n nb_frame=int(args.split(\" \")[2])\n\n #Sleep a duration before to start acquisition\n sleep(sleep_before)\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/image\", \"Start\");\n\n #Set the LEDs as Blue\n rgb(0,0,255)\n\n #Pump duing a given number of steps (in between each image)\n for i in range(nb_step):\n\n #If the command is still image - pump a defined nb of steps\n if (command==\"image\"):\n\n #Actuate the pump for one step in the FORWARD direction\n pump_stepper.onestep(direction=stepper.FORWARD, style=stepper.DOUBLE)\n\n #The flowrate is fixed for now.\n sleep(0.01)\n\n #If the command isn't image anymore - break\n else:\n\n break\n\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n while True:\n #Release the pump stepper to stop power draw\n pump_stepper.release()\n\n #Set the LEDs as Cyan\n rgb(0,255,255)\n\n #Increment the counter\n counter+=1\n\n #Get datetime\n datetime_tmp=datetime.now().strftime(\"%H_%M_%S_%f\")\n\n #Print datetime\n print(datetime_tmp)\n\n #Define the filename of the image\n filename = os.path.join(\"/home/pi/PlanktonScope/tmp\",datetime_tmp+\".jpg\")\n\n #Capture an image with the proper filename\n camera.capture(filename)\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Publish the name of the image to via MQTT to Node-RED\n\n client.publish(\"receiver/image\", datetime_tmp+\".jpg has been imaged.\");\n \n #Set the LEDs as Blue\n rgb(0,0,255)\n\n #Pump during a given nb of steps\n for i in range(nb_step):\n\n #Actuate the pump for one step in the FORWARD direction\n pump_stepper.onestep(direction=stepper.FORWARD, style=stepper.DOUBLE)\n\n #The flowrate is fixed for now.\n sleep(0.01)\n\n #Wait a fixed delay which set the framerate as < than 2 imag/sec\n sleep(0.5)\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n ####################################################################\n #If counter reach the number of frame, break\n if(counter>nb_frame):\n\n #Publish the status \"Completed\" to via MQTT to Node-RED\n client.publish(\"receiver/image\", \"Completed\");\n\n #Release the pump steppers to stop power draw\n pump_stepper.release()\n\n #Publish the status \"Start\" to via MQTT to Node-RED\n client.publish(\"receiver/segmentation\", \"Start\");\n\n #Start the MorphoCut Pipeline\n p.run()\n\n #remove directory\n #shutil.rmtree(import_path)\n\n #Publish the status \"Completed\" to via MQTT to Node-RED\n client.publish(\"receiver/segmentation\", \"Completed\");\n\n #Set the LEDs as White\n rgb(255,255,255)\n\n sample_project=node_red_metadata['sample_project'];\n \n acq_id=node_red_metadata['acq_id'];\n \n export_name = str(sample_project)+\"_\"+str(acq_id)+\".zip\"\n \n \n os.popen(\"mv /home/pi/PlanktonScope/export/ecotaxa_export.zip /home/pi/PlanktonScope/export/\"+export_name)\n \n os.popen(\"rm -rf /home/pi/PlanktonScope/tmp/*.jpg\")\n \n os.popen(\"rm -rf /home/pi/PlanktonScope/OBJECTS/*.jpg\")\n \n\n #Let it happen\n sleep(1)\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n\n #Change the command to not re-enter in this while loop\n command=\"wait\"\n \n #Set the LEDs as Green\n rgb(0,255,255)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n ####################################################################\n #If a new received command isn't \"image\", break this while loop\n if command!=\"image\":\n\n #Release the pump steppers to stop power draw\n pump_stepper.release()\n\n #Print status\n print(\"The imaging has been interrompted.\")\n\n #Publish the status \"Interrompted\" to via MQTT to Node-RED\n client.publish(\"receiver/image\", \"Interrompted\");\n\n #Set the LEDs as Green\n rgb(0,255,0)\n\n #Reset the counter to 0\n counter=0\n\n break\n\n else:\n #Its just waiting to receive command from Node-RED\n sleep(0.4)\n", "output": "str", - "x": 300, - "y": 100, + "x": 560, + "y": 240, "wires": [ - [ - "fd813497.f44d5" - ] - ] - }, - { - "id": "10348484.8ca5b3", - "type": "exec", - "z": "435c6174.e0c8b", - "command": "ps -ax | grep \"python3.7 /home/pi/PlanktonScope/script/main.py\"| head -1 | awk -F \" \" '{print$1}' ", - "addpay": false, - "append": "", - "useSpawn": "false", - "timer": "", - "oldrc": false, - "name": "", - "x": 430, - "y": 40, - "wires": [ - [ - "b41e729d.a3042" - ], - [], [] ] }, { - "id": "b41e729d.a3042", + "id": "672d89a8.4e6968", "type": "exec", - "z": "435c6174.e0c8b", - "command": "kill", + "z": "d95291dc.83b0b8", + "command": "killall python3", "addpay": true, "append": "", "useSpawn": "false", "timer": "", "oldrc": false, "name": "", - "x": 870, + "x": 730, "y": 40, "wires": [ [], @@ -1576,36 +1620,13 @@ ] }, { - "id": "e49710cf.c4831", - "type": "delay", - "z": "435c6174.e0c8b", - "name": "", - "pauseType": "delay", - "timeout": "1", - "timeoutUnits": "seconds", - "rate": "1", - "nbRateUnits": "1", - "rateUnits": "second", - "randomFirst": "1", - "randomLast": "5", - "randomUnits": "seconds", - "drop": false, - "x": 160, - "y": 100, - "wires": [ - [ - "a2e43b6f.3b73a" - ] - ] - }, - { - "id": "a105d7c9.07d71", + "id": "d3caa802.6f22d8", "type": "ui_text_input", - "z": "4273b9bf.bb07a8", + "z": "1bc3f9f9.1ee996", "name": "custom_nb_step", "label": "Number of steps in between two images", "tooltip": "", - "group": "6fdfb3ec.fd451c", + "group": "758f41b8.1680c8", "order": 2, "width": 0, "height": 0, @@ -1620,13 +1641,13 @@ ] }, { - "id": "530d8426.5c4bec", + "id": "5d4c755a.800b44", "type": "ui_text_input", - "z": "4273b9bf.bb07a8", + "z": "1bc3f9f9.1ee996", "name": "custom_nb_frame", "label": "Number of images per acquisition", "tooltip": "", - "group": "6fdfb3ec.fd451c", + "group": "758f41b8.1680c8", "order": 3, "width": 0, "height": 0, @@ -1641,9 +1662,9 @@ ] }, { - "id": "55d70110.127d38", + "id": "8887f3e7.e79b5", "type": "function", - "z": "4273b9bf.bb07a8", + "z": "1bc3f9f9.1ee996", "name": "get custom_nb_step", "func": "msg.payload = msg.payload.custom_nb_step;\nreturn msg;", "outputs": 1, @@ -1652,14 +1673,14 @@ "y": 80, "wires": [ [ - "a105d7c9.07d71" + "d3caa802.6f22d8" ] ] }, { - "id": "ec5967a.409eb18", + "id": "600c2b46.83db14", "type": "function", - "z": "4273b9bf.bb07a8", + "z": "1bc3f9f9.1ee996", "name": "get custom_nb_frame", "func": "msg.payload = msg.payload.custom_nb_frame;\nreturn msg;", "outputs": 1, @@ -1668,14 +1689,14 @@ "y": 120, "wires": [ [ - "530d8426.5c4bec" + "5d4c755a.800b44" ] ] }, { - "id": "4e6a3b73.7472d4", + "id": "14be4afa.2ba67d", "type": "function", - "z": "4273b9bf.bb07a8", + "z": "1bc3f9f9.1ee996", "name": "get custom_sleep_before", "func": "msg.payload = msg.payload.custom_sleep_before;\nreturn msg;", "outputs": 1, @@ -1684,18 +1705,18 @@ "y": 40, "wires": [ [ - "816c19e2.f940e" + "3a773a31.a6caee" ] ] }, { - "id": "816c19e2.f940e", + "id": "3a773a31.a6caee", "type": "ui_text_input", - "z": "4273b9bf.bb07a8", + "z": "1bc3f9f9.1ee996", "name": "custom_sleep_before", "label": "Duration before the acquisition (s)", "tooltip": "", - "group": "6fdfb3ec.fd451c", + "group": "758f41b8.1680c8", "order": 1, "width": 0, "height": 0, @@ -1710,13 +1731,13 @@ ] }, { - "id": "7bad3862.c22b5", + "id": "7cdadcd1.736f04", "type": "ui_text_input", - "z": "4d3786dd.dd79f", + "z": "714bd4fe.163ab4", "name": "process_id", "label": "Id of the process", "tooltip": "", - "group": "c32bf44c.3b67a8", + "group": "eef6f881.7b3b38", "order": 1, "width": 0, "height": 0, @@ -1731,9 +1752,9 @@ ] }, { - "id": "7228965.6f65ae8", + "id": "706715a3.5ad1c4", "type": "function", - "z": "4d3786dd.dd79f", + "z": "714bd4fe.163ab4", "name": "get process_id", "func": "msg.payload = msg.payload.process_id+1;\nreturn msg;", "outputs": 1, @@ -1742,342 +1763,86 @@ "y": 80, "wires": [ [ - "7bad3862.c22b5" + "7cdadcd1.736f04" ] ] }, { - "id": "3b44178c.9bb6c", + "id": "8cb80d95.13d2e8", "type": "mqtt in", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "", - "topic": "receiver/#", + "topic": "status/#", "qos": "0", - "datatype": "auto", - "broker": "6e9cdee.ac3c2a", - "x": 60, - "y": 40, + "datatype": "json", + "broker": "8dc3722c.06efa8", + "x": 70, + "y": 300, "wires": [ [ - "3e32282.7051d58" + "2aa5b118.d75f2e" ] ] }, { - "id": "3e32282.7051d58", + "id": "ecedbda4.fed15", "type": "switch", - "z": "20bc0424.146724", - "name": "", + "z": "bee3b478.ef4b88", + "name": "topic filter", "property": "topic", "propertyType": "msg", "rules": [ { "t": "eq", - "v": "receiver/pump", + "v": "status/pump", "vt": "str" }, { "t": "eq", - "v": "receiver/focus", + "v": "status/focus", "vt": "str" }, { "t": "eq", - "v": "receiver/image", + "v": "status/imager", "vt": "str" }, { "t": "cont", - "v": "receiver/segmentation", + "v": "status/segmenter", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 4, - "x": 250, - "y": 40, + "x": 360, + "y": 300, "wires": [ [ - "eb8571ec.dd894" + "117aad13.53e11b" ], [ - "f3e7c870.e91ed" + "117aad13.53e11b" ], [ - "49534f95.047038" + "af9a1d81.c21fa8" ], [ - "48e4e2bb.9bb35c" + "30a9de16.d55cda" ] ] }, { - "id": "eb8571ec.dd894", + "id": "af9a1d81.c21fa8", "type": "switch", - "z": "20bc0424.146724", - "name": "", + "z": "bee3b478.ef4b88", + "name": "Imaging state", "property": "payload", "propertyType": "msg", "rules": [ { - "t": "eq", - "v": "Start", - "vt": "str" - }, - { - "t": "eq", - "v": "Done", - "vt": "str" - }, - { - "t": "eq", - "v": "Interrompted", - "vt": "str" - } - ], - "checkall": "true", - "repair": false, - "outputs": 3, - "x": 410, - "y": 40, - "wires": [ - [ - "2981833a.c8f3f4" - ], - [ - "a9bc22d0.246c98" - ], - [ - "13f11422.180f9c" - ] - ] - }, - { - "id": "2981833a.c8f3f4", - "type": "change", - "z": "20bc0424.146724", - "name": "The pump has started", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The pump has started", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 920, - "y": 40, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "a9bc22d0.246c98", - "type": "change", - "z": "20bc0424.146724", - "name": "The pump has finished.", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The pump has finished.", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 910, - "y": 80, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "13f11422.180f9c", - "type": "change", - "z": "20bc0424.146724", - "name": "The pump has been stopped.", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The pump has been stopped.", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 900, - "y": 120, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "f3e7c870.e91ed", - "type": "switch", - "z": "20bc0424.146724", - "name": "", - "property": "payload", - "propertyType": "msg", - "rules": [ - { - "t": "eq", - "v": "Start", - "vt": "str" - }, - { - "t": "eq", - "v": "Done", - "vt": "str" - }, - { - "t": "eq", - "v": "Interrompted", - "vt": "str" - } - ], - "checkall": "true", - "repair": false, - "outputs": 3, - "x": 410, - "y": 100, - "wires": [ - [ - "d070dadb.1463a" - ], - [ - "4c014eb.3f18a3" - ], - [ - "c31116f4.7e8488" - ] - ] - }, - { - "id": "d070dadb.1463a", - "type": "change", - "z": "20bc0424.146724", - "name": "The focus has started", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The focus has started", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 920, - "y": 180, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "4c014eb.3f18a3", - "type": "change", - "z": "20bc0424.146724", - "name": "The focus has finished.", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The focus has finished.", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 910, - "y": 220, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "c31116f4.7e8488", - "type": "change", - "z": "20bc0424.146724", - "name": "The focus has been stopped.", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The focus has been stopped.", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 900, - "y": 260, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "49534f95.047038", - "type": "switch", - "z": "20bc0424.146724", - "name": "", - "property": "payload", - "propertyType": "msg", - "rules": [ - { - "t": "eq", - "v": "Start", - "vt": "str" - }, - { - "t": "eq", - "v": "Done", - "vt": "str" - }, - { - "t": "eq", - "v": "Interrompted", - "vt": "str" + "t": "else" }, { "t": "cont", @@ -2087,176 +1852,56 @@ ], "checkall": "true", "repair": false, - "outputs": 4, - "x": 410, - "y": 160, - "wires": [ - [ - "bf293c7.2c0f14" - ], - [ - "ef3bc36b.edc53" - ], - [ - "8404a654.b21db" - ], - [ - "738a3754.b91708" - ] - ] - }, - { - "id": "a555d3d.d63e83", - "type": "switch", - "z": "20bc0424.146724", - "name": "", - "property": "payload", - "propertyType": "msg", - "rules": [ - { - "t": "eq", - "v": "Start", - "vt": "str" - }, - { - "t": "eq", - "v": "Done", - "vt": "str" - } - ], - "checkall": "true", - "repair": false, "outputs": 2, - "x": 670, - "y": 520, - "wires": [ - [ - "8b4cf492.155ee" - ], - [ - "31fa24f3.9545bc" - ] - ] - }, - { - "id": "ef3bc36b.edc53", - "type": "change", - "z": "20bc0424.146724", - "name": "The acquisition has finished.", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The acquisition has finished.", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 900, - "y": 360, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "8404a654.b21db", - "type": "change", - "z": "20bc0424.146724", - "name": "The acquisition has been stopped.", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The acquisition has been stopped.", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 880, - "y": 400, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "bf293c7.2c0f14", - "type": "change", - "z": "20bc0424.146724", - "name": "The acquisition has started", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The acquisition has started", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 900, + "x": 580, "y": 320, "wires": [ [ - "1b713e4c.48b6ca" + "117aad13.53e11b" + ], + [ + "a636cac5.e28448" ] ] }, { - "id": "738a3754.b91708", + "id": "a636cac5.e28448", "type": "function", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "img_counter.js", "func": "img_counter=global.get('img_counter')\nimg_counter=img_counter+1\nglobal.set('img_counter',img_counter)\nmsg.payload = img_counter\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 780, - "y": 440, + "x": 860, + "y": 360, "wires": [ [ - "cec2bb6c.85ec3" + "abd84e73.aefc9" ] ] }, { - "id": "4aa5af79.20b3c8", + "id": "f71d0cda.0a4f88", "type": "function", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "obj_counter.js", "func": "obj_counter=global.get('obj_counter')\nobj_counter=obj_counter+1\nglobal.set('obj_counter',obj_counter)\nmsg.payload = obj_counter\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 780, - "y": 580, + "x": 900, + "y": 500, "wires": [ [ - "d21603af.53b258" + "d0d14f00.e2ff88" ] ] }, { - "id": "cec2bb6c.85ec3", + "id": "abd84e73.aefc9", "type": "ui_chart", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "img_counter", - "group": "1f183a1c.7d2846", + "group": "6d1af0ab.7b4a18", "order": 1, "width": 24, "height": 2, @@ -2287,121 +1932,67 @@ ], "useOldStyle": false, "outputs": 1, - "x": 950, - "y": 440, + "x": 1070, + "y": 360, "wires": [ [] ] }, { - "id": "48e4e2bb.9bb35c", + "id": "30a9de16.d55cda", "type": "switch", - "z": "20bc0424.146724", - "name": "", + "z": "bee3b478.ef4b88", + "name": "Segmenter", "property": "topic", "propertyType": "msg", "rules": [ { "t": "eq", - "v": "receiver/segmentation", + "v": "status/segmenter", "vt": "str" }, { "t": "eq", - "v": "receiver/segmentation/name", + "v": "status/segmenter/name", "vt": "str" }, { "t": "eq", - "v": "receiver/segmentation/object_id", + "v": "status/segmenter/object_id", "vt": "str" }, { "t": "eq", - "v": "receiver/segmentation/metric", + "v": "status/segmenter/metric", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 4, - "x": 450, - "y": 540, + "x": 570, + "y": 480, "wires": [ [ - "a555d3d.d63e83" + "117aad13.53e11b" ], [ - "dec9dc45.e0709" + "c2d35803.b5024" ], [ - "4aa5af79.20b3c8" + "f71d0cda.0a4f88" ], [ - "d5a84642.dc7618" + "87d402c5.c02008" ] ] }, { - "id": "31fa24f3.9545bc", - "type": "change", - "z": "20bc0424.146724", - "name": "The segmentation has finished.", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The segmentation has finished.", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 890, - "y": 540, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "8b4cf492.155ee", - "type": "change", - "z": "20bc0424.146724", - "name": "The segmentation has started", - "rules": [ - { - "t": "set", - "p": "payload", - "pt": "msg", - "to": "The segmentation has started", - "tot": "str" - } - ], - "action": "", - "property": "", - "from": "", - "to": "", - "reg": false, - "x": 890, - "y": 500, - "wires": [ - [ - "1b713e4c.48b6ca" - ] - ] - }, - { - "id": "d21603af.53b258", + "id": "d0d14f00.e2ff88", "type": "ui_chart", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "obj_counter", - "group": "1f183a1c.7d2846", + "group": "6d1af0ab.7b4a18", "order": 2, "width": 24, "height": 2, @@ -2432,50 +2023,50 @@ ], "useOldStyle": false, "outputs": 1, - "x": 950, - "y": 580, + "x": 1070, + "y": 500, "wires": [ [] ] }, { - "id": "7f712083.ec5c78", + "id": "d7944cf2.1ddb18", "type": "function", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "ex : area", "func": "msg.payload=msg.payload.object_area\nmsg.topic=\"area\"\n\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 820, - "y": 640, + "x": 960, + "y": 540, "wires": [ [ - "65556e24.4e09c" + "6f64625f.243fd4" ] ] }, { - "id": "d5a84642.dc7618", + "id": "87d402c5.c02008", "type": "json", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "", "property": "payload", "action": "", "pretty": false, - "x": 650, - "y": 640, + "x": 830, + "y": 540, "wires": [ [ - "7f712083.ec5c78" + "d7944cf2.1ddb18" ] ] }, { - "id": "1b713e4c.48b6ca", + "id": "1403e1a4.528926", "type": "ui_toast", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "position": "top right", - "displayTime": "3", + "displayTime": "5", "highlight": "", "sendall": true, "outputs": 0, @@ -2484,16 +2075,16 @@ "raw": false, "topic": "", "name": "", - "x": 1260, - "y": 260, + "x": 1380, + "y": 280, "wires": [] }, { - "id": "65556e24.4e09c", + "id": "6f64625f.243fd4", "type": "ui_chart", - "z": "20bc0424.146724", + "z": "bee3b478.ef4b88", "name": "chart area", - "group": "1f183a1c.7d2846", + "group": "6d1af0ab.7b4a18", "order": 3, "width": 24, "height": 7, @@ -2524,35 +2115,37 @@ ], "useOldStyle": false, "outputs": 1, - "x": 960, - "y": 640, + "x": 1100, + "y": 540, "wires": [ [] ] }, { - "id": "dec9dc45.e0709", + "id": "c2d35803.b5024", "type": "debug", - "z": "20bc0424.146724", - "name": "", + "z": "bee3b478.ef4b88", + "name": "segmentation name", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", - "x": 690, - "y": 720, + "statusVal": "", + "statusType": "auto", + "x": 910, + "y": 460, "wires": [] }, { - "id": "2c9233f6.a62024", + "id": "ce12cb65.115838", "type": "ui_button", - "z": "7aea7e49.4a3c88", + "z": "9c3e6ad4.7471a8", "name": "", - "group": "1f044bf8.be704c", - "order": 1, - "width": 12, + "group": "adcd0ef7.81a7f8", + "order": 3, + "width": 3, "height": 1, "passthru": false, "label": "Reboot", @@ -2567,14 +2160,14 @@ "y": 80, "wires": [ [ - "bb7e6003.313188" + "677f762b.43c648" ] ] }, { - "id": "6e86ac0a.07e534", + "id": "690ed112.f50c3", "type": "exec", - "z": "7aea7e49.4a3c88", + "z": "9c3e6ad4.7471a8", "command": "sudo", "addpay": true, "append": "", @@ -2591,13 +2184,13 @@ ] }, { - "id": "2b27e400.5709ac", + "id": "4a608a65.1882ec", "type": "ui_button", - "z": "7aea7e49.4a3c88", + "z": "9c3e6ad4.7471a8", "name": "", - "group": "1f044bf8.be704c", - "order": 2, - "width": 12, + "group": "adcd0ef7.81a7f8", + "order": 5, + "width": 3, "height": 1, "passthru": false, "label": "Shutdown", @@ -2612,14 +2205,14 @@ "y": 140, "wires": [ [ - "bb7e6003.313188" + "677f762b.43c648" ] ] }, { - "id": "bb7e6003.313188", + "id": "677f762b.43c648", "type": "python3-function", - "z": "7aea7e49.4a3c88", + "z": "9c3e6ad4.7471a8", "name": "action", "func": "#!/usr/bin/python\nimport smbus\nimport time\nbus = smbus.SMBus(1)\ntime.sleep(1)\n#turn off fan RGB\nbus.write_byte_data(0x0d, 0x07, 0x00)\nbus.write_byte_data(0x0d, 0x07, 0x00)\n\nmsg[\"payload\"] = str(msg[\"topic\"])+' now'\nreturn msg", "outputs": 1, @@ -2627,15 +2220,15 @@ "y": 100, "wires": [ [ - "91077c4d.e71e48", - "6e86ac0a.07e534" + "50d1becd.7b5f", + "690ed112.f50c3" ] ] }, { - "id": "91077c4d.e71e48", + "id": "50d1becd.7b5f", "type": "exec", - "z": "7aea7e49.4a3c88", + "z": "9c3e6ad4.7471a8", "command": "i2cdetect -y 1", "addpay": false, "append": "", @@ -2652,13 +2245,13 @@ ] }, { - "id": "d7ac6b26.75e388", + "id": "f435f66d.26d73", "type": "ui_numeric", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "object_depth_min", "label": "Minimum depth (m)", "tooltip": "", - "group": "2f95b761.bd818", + "group": "ab1d06d6.bf9898", "order": 3, "width": 0, "height": 0, @@ -2676,13 +2269,13 @@ ] }, { - "id": "70dcbf49.4b0e6", + "id": "79026dc4.133a7c", "type": "ui_numeric", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "object_depth_max", "label": "Maximum depth (m)", "tooltip": "", - "group": "2f95b761.bd818", + "group": "ab1d06d6.bf9898", "order": 2, "width": 0, "height": 0, @@ -2700,41 +2293,41 @@ ] }, { - "id": "46ef98a.3d3e8e8", + "id": "5cdbcd15.17ec94", "type": "function", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "get object_depth_min", "func": "msg.payload = msg.payload.object_depth_min;\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 304, + "x": 360, "y": 120, "wires": [ [ - "d7ac6b26.75e388" + "f435f66d.26d73" ] ] }, { - "id": "e6b2a290.7593c8", + "id": "c2ccc2e1.a697f8", "type": "function", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "get object_depth_max", "func": "msg.payload = msg.payload.object_depth_max;\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 304, + "x": 360, "y": 160, "wires": [ [ - "70dcbf49.4b0e6" + "79026dc4.133a7c" ] ] }, { - "id": "8cfa2316.5a9788", + "id": "cb77803f.357f88", "type": "gpsd", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "", "hostname": "localhost", "port": "2947", @@ -2748,24 +2341,26 @@ "y": 220, "wires": [ [ - "61bc4942.96a3d" + "b3e8d04.7c83d3", + "3f718ceb.5e9f34", + "74554b33.14ab6c" ] ] }, { - "id": "7d5ce5c2.2cb0f4", + "id": "f7ae988c.184598", "type": "ui_worldmap", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "d": true, - "group": "2f95b761.bd818", + "group": "ab1d06d6.bf9898", "order": 1, "width": 0, "height": 0, "name": "", - "lat": "1.5", - "lon": "1.5", - "zoom": "4", - "layer": "OSM grey", + "lat": "", + "lon": "", + "zoom": "7", + "layer": "Nat Geo", "cluster": "1", "maxage": "", "usermenu": "hide", @@ -2782,29 +2377,33 @@ "wires": [] }, { - "id": "61bc4942.96a3d", + "id": "b3e8d04.7c83d3", "type": "function", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "get object_lat & object_lon", - "func": "\nreturn msg;", + "func": "error = Math.sqrt(msg.payload.epx**2+msg.payload.epy**2)\n\nmsg.payload = {\n \"name\":\"sailboat\",\n \"lat\":msg.payload.lat,\n \"lon\":msg.payload.lon,\n \"speed\":msg.payload.speed,\n \"bearing\":msg.payload.track,\n \"icon\":\"ship\",\n \"accuracy\":error,\n \"command\": { \"lat\":msg.payload.lat, \"lon\":msg.payload.lon,\n grid : {showgrid: true,\n opt: { showLabel:true, dashArray:[5, 5], fontColor:\"#900\" }\n }\n }\n};\n\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 320, + "initialize": "", + "finalize": "", + "x": 350, "y": 200, "wires": [ [ - "7d5ce5c2.2cb0f4" + "f7ae988c.184598" ] ] }, { - "id": "415b9f0c.5f0b48", + "id": "9c08f843.b1e1b", "type": "function", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "set object_time", "func": "var time = new Date(msg.payload);\n\nvar hour = time.getUTCHours();\nif (hour<10){hour = \"0\"+hour;}\nvar minute = time.getUTCMinutes();\nif (minute<10){minute = \"0\"+minute;}\n\nvar time_UTC = \"\"+hour+minute;\nglobal.set('object_time',time_UTC);\nreturn msg;", "outputs": 1, "noerr": 0, + "initialize": "", + "finalize": "", "x": 620, "y": 80, "wires": [ @@ -2812,13 +2411,15 @@ ] }, { - "id": "3e9ae6e6.35b942", + "id": "157ba5ca.c88a52", "type": "function", - "z": "672ac548.1a9bac", + "z": "1a447be0.198674", "name": "set object_date", "func": "var date = new Date(msg.payload);\n\nvar year = date.getUTCFullYear();\nvar month = date.getUTCMonth()+1;\nif (month<10){month = \"0\"+month;}\nvar day = date.getUTCDate();\nif (day<10){day = \"0\"+day;}\n\nvar date_UTC = \"\"+year+month+day;\nglobal.set('object_date',date_UTC);\n\nreturn msg;", "outputs": 1, "noerr": 0, + "initialize": "", + "finalize": "", "x": 620, "y": 40, "wires": [ @@ -2826,9 +2427,9 @@ ] }, { - "id": "79923da9.365d7c", + "id": "701120fd.c5516", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "set optical config", "func": "global.set(msg.topic,msg.payload);\nvar acq_fnumber_objective = String(global.get(msg.topic));\n\nswitch(acq_fnumber_objective) {\n case \"25\":\n global.set(\"acq_magnification\",0.6);\n global.set(\"process_pixel\",1.86);\n global.set(\"sug_min\",60);\n global.set(\"sug_max\",670);\n global.set(\"sug_flowrate\",3);\n break;\n case \"16\":\n global.set(\"acq_magnification\",0.94);\n global.set(\"process_pixel\",1.19);\n global.set(\"sug_min\",40);\n global.set(\"sug_max\",430);\n global.set(\"sug_flowrate\",2.4);\n break;\n case \"12\":\n global.set(\"acq_magnification\",1.20);\n global.set(\"process_pixel\",0.94);\n global.set(\"sug_min\",30);\n global.set(\"sug_max\",340);\n global.set(\"sug_flowrate\",1.25);\n break;\n case \"8\":\n global.set(\"acq_magnification\",1.78);\n global.set(\"process_pixel\",0.63);\n global.set(\"sug_min\",20);\n global.set(\"sug_max\",230);\n global.set(\"sug_flowrate\",0.42);\n break;\n case \"6\":\n global.set(\"acq_magnification\",2.36);\n global.set(\"process_pixel\",0.48);\n global.set(\"sug_min\",15);\n global.set(\"sug_max\",170);\n global.set(\"sug_flowrate\",0.32);\n break;\n}\nreturn msg;", "outputs": 1, @@ -2840,14 +2441,14 @@ ] }, { - "id": "cba1919b.aae78", + "id": "2554ff8e.8cf1a8", "type": "ui_dropdown", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_fnumber_objective", "label": "M12 Lens*", "tooltip": "", "place": "Select option", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 3, "width": 0, "height": 0, @@ -2885,18 +2486,18 @@ "y": 360, "wires": [ [ - "79923da9.365d7c" + "701120fd.c5516" ] ] }, { - "id": "ebcf7cae.9e21c8", + "id": "2f9ae002.b4c96", "type": "ui_numeric", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_minimum_mesh", "label": "Min fraction size (μm)", "tooltip": "", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 8, "width": 0, "height": 0, @@ -2914,13 +2515,13 @@ ] }, { - "id": "7291a2a3.c9a974", + "id": "c86d98d4.c56538", "type": "ui_numeric", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_maximum_mesh", "label": "Max fraction size (μm)", "tooltip": "", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 4, "width": 0, "height": 0, @@ -2938,13 +2539,13 @@ ] }, { - "id": "52374b71.714fd4", + "id": "98d1f331.a06938", "type": "ui_text_input", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_id", "label": "Acquisition unique ID*", "tooltip": "", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 1, "width": 0, "height": 0, @@ -2959,14 +2560,14 @@ ] }, { - "id": "2cf276d8.880672", + "id": "42505cbd.f02b1c", "type": "ui_dropdown", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_celltype", "label": "Thickness flowcell*", "tooltip": "", "place": "Select option", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 7, "width": 0, "height": 0, @@ -3002,13 +2603,13 @@ ] }, { - "id": "25294db5.4fe722", + "id": "13b51e1e.1f603a", "type": "ui_text_input", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_volume", "label": "Volume to pass (ml)", "tooltip": "", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 2, "width": 0, "height": 0, @@ -3023,13 +2624,13 @@ ] }, { - "id": "858ed565.0993b8", + "id": "3b9700c4.b93ec", "type": "ui_text_input", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_instrument", "label": "Acquisition instrument", "tooltip": "PlanktonScope V2.1", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 6, "width": 0, "height": 0, @@ -3044,9 +2645,9 @@ ] }, { - "id": "7c09012f.b50098", + "id": "f3658d30.b8448", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_id", "func": "msg.payload = msg.payload.acq_id+1;\nreturn msg;", "outputs": 1, @@ -3055,14 +2656,14 @@ "y": 40, "wires": [ [ - "52374b71.714fd4" + "98d1f331.a06938" ] ] }, { - "id": "3e3b0646.cf9a1a", + "id": "5acd51d4.4ab13", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_instrument", "func": "msg.payload = msg.payload.acq_instrument;\nreturn msg;", "outputs": 1, @@ -3071,14 +2672,14 @@ "y": 80, "wires": [ [ - "858ed565.0993b8" + "3b9700c4.b93ec" ] ] }, { - "id": "2b4282c2.8752ae", + "id": "de2c90cf.b73b08", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_celltype", "func": "msg.payload = msg.payload.acq_celltype;\nreturn msg;", "outputs": 1, @@ -3087,14 +2688,14 @@ "y": 120, "wires": [ [ - "2cf276d8.880672" + "42505cbd.f02b1c" ] ] }, { - "id": "90439ce.e3524e", + "id": "5e3dec55.881074", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_minimum_mesh", "func": "msg.payload = msg.payload.acq_minimum_mesh;\nreturn msg;", "outputs": 1, @@ -3103,14 +2704,14 @@ "y": 160, "wires": [ [ - "ebcf7cae.9e21c8" + "2f9ae002.b4c96" ] ] }, { - "id": "7f710823.92974", + "id": "d3ca8847.4d1ae", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_maximum_mesh", "func": "msg.payload = msg.payload.acq_maximum_mesh;\nreturn msg;", "outputs": 1, @@ -3119,14 +2720,14 @@ "y": 200, "wires": [ [ - "7291a2a3.c9a974" + "c86d98d4.c56538" ] ] }, { - "id": "9355d580.a2338", + "id": "1f133196.96564e", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_volume", "func": "msg.payload = msg.payload.acq_volume;\nreturn msg;", "outputs": 1, @@ -3135,14 +2736,14 @@ "y": 240, "wires": [ [ - "25294db5.4fe722" + "13b51e1e.1f603a" ] ] }, { - "id": "218c020a.2b0566", + "id": "68fa1227.dbdd5c", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_fnumber_objective", "func": "msg.payload = msg.payload.acq_fnumber_objective;\nreturn msg;", "outputs": 1, @@ -3151,18 +2752,18 @@ "y": 360, "wires": [ [ - "cba1919b.aae78" + "2554ff8e.8cf1a8" ] ] }, { - "id": "4cfd49e3.3db3c", + "id": "6c391d10.2f9744", "type": "ui_numeric", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_min_esd", "label": "Minimum size to segment (μm)", "tooltip": "", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 9, "width": 0, "height": 0, @@ -3180,13 +2781,13 @@ ] }, { - "id": "7dbb773.926b488", + "id": "163df12e.f73f5f", "type": "ui_numeric", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "acq_max_esd", "label": "Maximum size to segment (μm)", "tooltip": "", - "group": "14742691.56c8c1", + "group": "4361c3b6.e2b7a4", "order": 5, "width": 0, "height": 0, @@ -3204,9 +2805,9 @@ ] }, { - "id": "c8be3f91.8686e", + "id": "3414b477.4d711c", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_min_esd", "func": "msg.payload = msg.payload.acq_min_esd;\nreturn msg;", "outputs": 1, @@ -3215,14 +2816,14 @@ "y": 280, "wires": [ [ - "4cfd49e3.3db3c" + "6c391d10.2f9744" ] ] }, { - "id": "98bb1c89.99c7b8", + "id": "a52e7caf.6bde48", "type": "function", - "z": "5163a57b.0008b4", + "z": "758bc08f.c57318", "name": "get acq_max_esd", "func": "msg.payload = msg.payload.acq_max_esd;\nreturn msg;", "outputs": 1, @@ -3231,33 +2832,35 @@ "y": 320, "wires": [ [ - "7dbb773.926b488" + "163df12e.f73f5f" ] ] }, { - "id": "1796cf8d.f042", + "id": "3cb96380.e575ec", "type": "function", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "pump.js", - "func": "state = global.get(\"state\");\n\nif (state == null){state=\"free\"}\n\nvar manual_volume= global.get(\"pump_manual_volume\");\nvar flowrate= global.get(\"pump_flowrate\");\n\nif (manual_volume === undefined || manual_volume === \"\" || manual_volume === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Volume to pass\";\n \n}else if (flowrate === undefined || flowrate === \"\" || flowrate === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Flowrate\";\n \n}else {\n volume = global.get(\"pump_manual_volume\");\n nb_step=volume*507\n msg.volume = volume;\n flowrate = global.get(\"pump_flowrate\");\n duration=(volume*60)/flowrate\n delay=(duration/nb_step)-0.005\n msg.topic = \"actuator/pump\";\n \n if(msg.payload === \"FORWARD\" & state===\"free\"){\n msg.payload='FORWARD '+delay+' '+nb_step;\n }\n if(msg.payload === \"BACKWARD\" & state===\"free\"){\n msg.payload='BACKWARD '+delay+' '+nb_step;\n }\n}\nreturn msg;", + "func": "state = global.get(\"state\");\n\nif (state == null){state=\"free\"}\n\nvar manual_volume= global.get(\"pump_manual_volume\");\nvar flowrate= global.get(\"pump_flowrate\");\n\nif (manual_volume === undefined || manual_volume === \"\" || manual_volume === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Volume to pass\";\n \n}else if (flowrate === undefined || flowrate === \"\" || flowrate === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Flowrate\";\n \n}else {\n volume = global.get(\"pump_manual_volume\");\n msg.volume = volume;\n flowrate = global.get(\"pump_flowrate\");\n msg.topic = \"actuator/pump\";\n \n if(state===\"free\"){\n // msg.payload is FORWARD or BACKWARD here\n msg.payload={\"action\":\"move\", \n \"direction\":msg.payload,\n \"volume\":volume,\n \"flowrate\":flowrate};\n }\n}\nreturn msg;", "outputs": 1, "noerr": 0, + "initialize": "", + "finalize": "", "x": 640, "y": 140, "wires": [ [ - "3b3a975.caa1a68" + "43fa762d.35bcb" ] ], "info": "### Focusing\n##### focus.py `nb_step` `orientation`\n\n- `nb_step` : **integer** (from 1 to 100000) - number of step to perform by the stage (about 31um/step)\n- `orientation` : **string** - orientation of the focus either `up` or `down`\n\nExample:\n\n python3.7 $HOME/PlanktonScope/scripts/focus.py 650 up\n" }, { - "id": "3b125ba5.2878ac", + "id": "517efd7f.811f44", "type": "ui_button", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "BACKWARD", - "group": "517b2aa5.93722c", + "group": "34f03c4a.abdf94", "order": 2, "width": 6, "height": 1, @@ -3274,16 +2877,16 @@ "y": 120, "wires": [ [ - "1796cf8d.f042" + "3cb96380.e575ec" ] ] }, { - "id": "daa07581.f5474", + "id": "d5de2fb4.dc9d8", "type": "ui_button", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "FORWARD", - "group": "517b2aa5.93722c", + "group": "34f03c4a.abdf94", "order": 4, "width": 6, "height": 1, @@ -3300,14 +2903,14 @@ "y": 160, "wires": [ [ - "1796cf8d.f042" + "3cb96380.e575ec" ] ] }, { - "id": "3b3a975.caa1a68", + "id": "43fa762d.35bcb", "type": "switch", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "", "property": "topic", "propertyType": "msg", @@ -3330,17 +2933,17 @@ "y": 140, "wires": [ [ - "70b4938.deb706c" + "bdc8ce57.de1f08" ], [ - "67845166.ed2bd8" + "8bcce348.efc1a" ] ] }, { - "id": "67845166.ed2bd8", + "id": "8bcce348.efc1a", "type": "ui_toast", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "position": "dialog", "displayTime": "3", "highlight": "", @@ -3358,11 +2961,11 @@ ] }, { - "id": "3bbe77de.da4bc", + "id": "f162cc1b.985148", "type": "ui_button", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "stop pump", - "group": "517b2aa5.93722c", + "group": "34f03c4a.abdf94", "order": 5, "width": 4, "height": 1, @@ -3372,51 +2975,51 @@ "color": "", "bgcolor": "#AD1625", "icon": "", - "payload": "off", - "payloadType": "str", - "topic": "actuator/wait", + "payload": "{\"action\":\"stop\"}", + "payloadType": "json", + "topic": "actuator/pump", "x": 470, "y": 200, "wires": [ [ - "e441856c.84eb58" + "c38103c0.9aaa18" ] ] }, { - "id": "70b4938.deb706c", + "id": "bdc8ce57.de1f08", "type": "mqtt out", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "", "topic": "", "qos": "", "retain": "", - "broker": "e6efd12e.dedae8", + "broker": "8dc3722c.06efa8", "x": 910, "y": 120, "wires": [] }, { - "id": "e441856c.84eb58", + "id": "c38103c0.9aaa18", "type": "mqtt out", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "", "topic": "", "qos": "", "retain": "", - "broker": "e6efd12e.dedae8", + "broker": "8dc3722c.06efa8", "x": 610, "y": 200, "wires": [] }, { - "id": "8cab4571.004668", + "id": "f1b85f22.ac673", "type": "ui_text_input", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "pump_manual_volume", "label": "Volume to pass (ml)", "tooltip": "", - "group": "517b2aa5.93722c", + "group": "34f03c4a.abdf94", "order": 3, "width": 8, "height": 1, @@ -3431,9 +3034,9 @@ ] }, { - "id": "e3472832.6c2cc8", + "id": "8ae06f9a.4b253", "type": "function", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "get pump_manual_volume", "func": "msg.payload = msg.payload.pump_manual_volume;\nreturn msg;", "outputs": 1, @@ -3442,18 +3045,18 @@ "y": 80, "wires": [ [ - "8cab4571.004668" + "f1b85f22.ac673" ] ] }, { - "id": "1f1c2de7.b242f2", + "id": "b8bf2a9.be099d8", "type": "ui_slider", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "pump_flowrate", "label": "Flowrate (ml/min)*", "tooltip": "", - "group": "517b2aa5.93722c", + "group": "34f03c4a.abdf94", "order": 1, "width": 0, "height": 0, @@ -3470,9 +3073,9 @@ ] }, { - "id": "66f2533e.b3080c", + "id": "cc757614.c8fc58", "type": "function", - "z": "b1edcbe7.366f7", + "z": "977131e7.c2e76", "name": "get pump_flowrate", "func": "msg.payload = msg.payload.pump_flowrate;\nreturn msg;", "outputs": 1, @@ -3481,53 +3084,57 @@ "y": 40, "wires": [ [ - "1f1c2de7.b242f2" + "b8bf2a9.be099d8" ] ] }, { - "id": "3df51223.81e336", + "id": "f49b3397.2599f8", "type": "ui_text_input", - "z": "3df4e02.36602a", - "name": "focus_nb_step", - "label": "Number of step(s)", - "tooltip": "", - "group": "88613aab.984d18", + "z": "a0f9bde.423644", + "name": "focus_distance", + "label": "Distance in µm", + "tooltip": "This will be rounded to the nearest 25µm", + "group": "de7c8e82.7faa98", "order": 2, "width": 8, "height": 1, "passthru": true, "mode": "number", "delay": 300, - "topic": "focus_nb_step", + "topic": "focus_distance", "x": 540, "y": 40, - "wires": [ - [] - ] - }, - { - "id": "ba9fc5ee.19aee8", - "type": "function", - "z": "3df4e02.36602a", - "name": "get focus_nb_step", - "func": "msg.payload = msg.payload.focus_nb_step;\nreturn msg;", - "outputs": 1, - "noerr": 0, - "x": 230, - "y": 40, "wires": [ [ - "3df51223.81e336" + "b69e435f.b93558" ] ] }, { - "id": "eea8d416.05154", + "id": "411211be.745ef8", + "type": "function", + "z": "a0f9bde.423644", + "name": "get focus_distance", + "func": "msg.payload = msg.payload.focus_distance;\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 220, + "y": 40, + "wires": [ + [ + "f49b3397.2599f8" + ] + ] + }, + { + "id": "30718b7.3986b74", "type": "ui_button", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "name": "DOWN", - "group": "88613aab.984d18", + "group": "de7c8e82.7faa98", "order": 3, "width": 6, "height": 1, @@ -3540,20 +3147,20 @@ "payload": "DOWN", "payloadType": "str", "topic": "actuator/focus", - "x": 520, - "y": 120, + "x": 280, + "y": 320, "wires": [ [ - "89b8374.e9624c8" + "ee01103f.f91b28" ] ] }, { - "id": "28579233.6aa0e6", + "id": "e0e433da.64e4e8", "type": "ui_button", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "name": "UP", - "group": "88613aab.984d18", + "group": "de7c8e82.7faa98", "order": 1, "width": 6, "height": 1, @@ -3566,18 +3173,18 @@ "payload": "UP", "payloadType": "str", "topic": "actuator/focus", - "x": 530, - "y": 81, + "x": 290, + "y": 281, "wires": [ [ - "89b8374.e9624c8" + "ee01103f.f91b28" ] ] }, { - "id": "43bc6030.03b3b8", + "id": "c46d3379.54e17", "type": "switch", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "name": "", "property": "topic", "propertyType": "msg", @@ -3596,21 +3203,21 @@ "checkall": "true", "repair": false, "outputs": 2, - "x": 810, - "y": 100, + "x": 570, + "y": 300, "wires": [ [ - "77e14fc9.c1287" + "a8dfcfd1.c5863" ], [ - "735be178.b56c28" + "6e5f2b4e.fcbeac" ] ] }, { - "id": "735be178.b56c28", + "id": "6e5f2b4e.fcbeac", "type": "ui_toast", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "position": "dialog", "displayTime": "3", "highlight": "", @@ -3621,35 +3228,36 @@ "raw": false, "topic": "", "name": "", - "x": 970, - "y": 140, + "x": 730, + "y": 340, "wires": [ [] ] }, { - "id": "89b8374.e9624c8", + "id": "ee01103f.f91b28", "type": "function", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "name": "focus.js", - "func": "state = global.get(\"state\");\n\nif (state == null){state=\"free\"}\n\nvar nb_step= global.get(\"focus_nb_step\");\n\nif (nb_step === undefined || nb_step === \"\" || nb_step === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Number of steps\";\n \n}else {\n nb_step= global.get(\"focus_nb_step\");\n if(msg.payload === \"UP\" & state===\"free\"){\n msg.payload=\"FORWARD \"+nb_step;\n }\n if(msg.payload === \"DOWN\" & state===\"free\"){\n msg.payload=\"BACKWARD \"+nb_step;\n }\n}\nreturn msg;", + "func": "state = global.get(\"state\");\n\nif (state == null){state=\"free\"}\n\nvar distance = global.get(\"focus_distance\");\n\nif (distance === undefined || distance === \"\" || distance === null) {\n msg.topic = \"Missing entry :\"\n msg.payload = \"Distance\";\n}else {\n distance = global.get(\"focus_distance\");\n if(state===\"free\"){\n // msg.payload is UP or DOWN here\n msg.payload={\"action\":\"move\", \n \"direction\":msg.payload,\n \"distance\":(distance/1000)};\n }\n}\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 660, - "y": 100, + "initialize": "", + "finalize": "", + "x": 420, + "y": 300, "wires": [ [ - "43bc6030.03b3b8" + "c46d3379.54e17" ] - ], - "info": "### Focusing\n##### focus.py `nb_step` `orientation`\n\n- `nb_step` : **integer** (from 1 to 100000) - number of step to perform by the stage (about 31um/step)\n- `orientation` : **string** - orientation of the focus either `up` or `down`\n\nExample:\n\n python3.7 $HOME/PlanktonScope/scripts/focus.py 650 up\n" + ] }, { - "id": "c96e9b7a.34961", + "id": "b8a2ecb3.f4f5f", "type": "ui_button", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "name": "stop focus", - "group": "88613aab.984d18", + "group": "de7c8e82.7faa98", "order": 4, "width": 4, "height": 1, @@ -3659,52 +3267,52 @@ "color": "", "bgcolor": "#AD1625", "icon": "", - "payload": "off", - "payloadType": "str", - "topic": "actuator/wait", - "x": 510, - "y": 160, + "payload": "{\"action\":\"stop\"}", + "payloadType": "json", + "topic": "actuator/focus", + "x": 270, + "y": 400, "wires": [ [ - "f22aeb5d.5a0d2" + "2c100d73.ac6fba" ] ] }, { - "id": "77e14fc9.c1287", + "id": "a8dfcfd1.c5863", "type": "mqtt out", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "name": "", "topic": "", "qos": "", "retain": "", - "broker": "e6efd12e.dedae8", - "x": 950, - "y": 80, + "broker": "8dc3722c.06efa8", + "x": 710, + "y": 280, "wires": [] }, { - "id": "f22aeb5d.5a0d2", + "id": "2c100d73.ac6fba", "type": "mqtt out", - "z": "3df4e02.36602a", + "z": "a0f9bde.423644", "name": "", "topic": "", "qos": "", "retain": "", - "broker": "e6efd12e.dedae8", - "x": 650, - "y": 160, + "broker": "8dc3722c.06efa8", + "x": 410, + "y": 400, "wires": [] }, { - "id": "ccb4ce9e.4f9108", + "id": "67fc8b3f.96c6cc", "type": "ui_text_input", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "sample_ship", "label": "Name of the ship", "tooltip": "", - "group": "bfdb5a44.4223", - "order": 2, + "group": "71c63dd4.311c44", + "order": 3, "width": 0, "height": 0, "passthru": true, @@ -3718,18 +3326,19 @@ ] }, { - "id": "50431d7c.cc673c", + "id": "91a896c8.7c9fb8", "type": "ui_dropdown", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "sample_sampling_gear", "label": "Sampling gear", "tooltip": "", "place": "Select", - "group": "bfdb5a44.4223", - "order": 4, + "group": "71c63dd4.311c44", + "order": 5, "width": 0, "height": 0, "passthru": true, + "multiple": true, "options": [ { "label": "Plankton net", @@ -3766,14 +3375,14 @@ ] }, { - "id": "d76b1790.9ffc2", + "id": "8cc6c6f1.65bf28", "type": "ui_text_input", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "sample_operator", "label": "Name of the operator", "tooltip": "", - "group": "bfdb5a44.4223", - "order": 3, + "group": "71c63dd4.311c44", + "order": 4, "width": 0, "height": 0, "passthru": true, @@ -3787,14 +3396,14 @@ ] }, { - "id": "412da17d.09c39", + "id": "adaccbb2.320458", "type": "ui_text_input", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "sample_project", "label": "Name of the project*", "tooltip": "", - "group": "bfdb5a44.4223", - "order": 1, + "group": "71c63dd4.311c44", + "order": 2, "width": 0, "height": 0, "passthru": true, @@ -3808,14 +3417,14 @@ ] }, { - "id": "236eeefd.7d50f2", + "id": "a63c1b66.f9a77", "type": "ui_text_input", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "sample_id", "label": "ID of the station*", "tooltip": "", - "group": "bfdb5a44.4223", - "order": 5, + "group": "71c63dd4.311c44", + "order": 6, "width": 0, "height": 0, "passthru": true, @@ -3829,9 +3438,9 @@ ] }, { - "id": "a4b7cb08.270d", + "id": "d027a6bf.7049e8", "type": "function", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "get sample_projet", "func": "msg.payload = msg.payload.sample_project;\nreturn msg;", "outputs": 1, @@ -3840,14 +3449,14 @@ "y": 80, "wires": [ [ - "412da17d.09c39" + "adaccbb2.320458" ] ] }, { - "id": "acfe2f.33fd31d", + "id": "5a811caf.0f3144", "type": "function", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "get sample_ship", "func": "msg.payload = msg.payload.sample_ship;\nreturn msg;", "outputs": 1, @@ -3856,14 +3465,14 @@ "y": 160, "wires": [ [ - "ccb4ce9e.4f9108" + "67fc8b3f.96c6cc" ] ] }, { - "id": "d7cff063.331ff8", + "id": "45911c98.2bd83c", "type": "function", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "get sample_id", "func": "msg.payload = msg.payload.sample_id+1;\nreturn msg;", "outputs": 1, @@ -3872,14 +3481,14 @@ "y": 120, "wires": [ [ - "236eeefd.7d50f2" + "a63c1b66.f9a77" ] ] }, { - "id": "cfaa2598.c63ec", + "id": "1e09a4ab.72996b", "type": "function", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "get sample_operator", "func": "msg.payload = msg.payload.sample_operator;\nreturn msg;", "outputs": 1, @@ -3888,14 +3497,14 @@ "y": 200, "wires": [ [ - "d76b1790.9ffc2" + "8cc6c6f1.65bf28" ] ] }, { - "id": "25201379.163e3c", + "id": "a3272681.f271c8", "type": "function", - "z": "6bc47c75.93e24c", + "z": "81483277.2521e", "name": "get sample_sampling_gear", "func": "msg.payload = msg.payload.sample_sampling_gear;\nreturn msg;", "outputs": 1, @@ -3904,81 +3513,84 @@ "y": 240, "wires": [ [ - "50431d7c.cc673c" + "91a896c8.7c9fb8" ] ] }, { - "id": "80851464.e5fcf", + "id": "10bfbbf.494c244", "type": "ui_template", - "z": "6bc47c75.93e24c", - "group": "bfdb5a44.4223", + "z": "81483277.2521e", + "group": "71c63dd4.311c44", "name": "", - "order": 5, - "width": "24", - "height": "2", - "format": "
Fill the different inputs concerning the sample you would to image.
", + "order": 1, + "width": 24, + "height": 2, + "format": "
Fill the different inputs concerning the sample you want to image.
", "storeOutMessages": true, "fwdInMessages": true, + "resendOnRefresh": false, "templateScope": "local", - "x": 290, - "y": 40, + "x": 420, + "y": 320, "wires": [ [] ] }, { - "id": "9ee31ce7.88d9c", + "id": "6f6ad84e.d9222", "type": "function", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "get global", "func": "msg.payload={\n \n \"sample_project\":global.get(\"sample_project\"),\n \"sample_id\":global.get(\"sample_id\"),\n \"sample_ship\":global.get(\"sample_ship\"),\n \"sample_operator\":global.get(\"sample_operator\"),\n \"sample_sampling_gear\":global.get(\"sample_sampling_gear\"),\n \n \"acq_id\":global.get(\"acq_id\"),\n \"acq_instrument\":global.get(\"acq_instrument\"),\n //\"acq_instrument_id\":global.get(\"acq_instrument_id\"),\n \"acq_celltype\":global.get(\"acq_celltype\"),\n \"acq_minimum_mesh\":global.get(\"acq_minimum_mesh\"),\n \"acq_maximum_mesh\":global.get(\"acq_maximum_mesh\"),\n \"acq_min_esd\":global.get(\"acq_min_esd\"),\n \"acq_max_esd\":global.get(\"acq_max_esd\"),\n \"acq_volume\":global.get(\"acq_volume\"),\n \"acq_magnification\":global.get(\"magnification\"),\n \"acq_fnumber_objective\":global.get(\"acq_fnumber_objective\"),\n \n \"acq_camera_name\":\"Pi Camera V2.1 - 8MP\",\n \n \"object_date\":global.get(\"object_date\"),\n \"object_time\":global.get(\"object_time\"),\n \"object_lat\":global.get(\"object_lat\"),\n \"object_lon\":global.get(\"object_lon\"),\n \"object_depth_min\":global.get(\"object_depth_min\"),\n \"object_depth_max\":global.get(\"object_depth_max\"),\n \n \"custom_nb_frame\":global.get(\"custom_nb_frame\"),\n \"custom_nb_step\":global.get(\"custom_nb_step\"),\n \"custom_segmentation\":global.get(\"custom_segmentation\"),\n \"custom_sleep_before\":global.get(\"custom_sleep_before\"),\n \"focus_nb_step\":global.get(\"focus_nb_step\"),\n \"pump_flowrate\":global.get(\"pump_flowrate\"),\n \"pump_manual_volume\":global.get(\"pump_manual_volume\"),\n \n \"process_pixel\":global.get(\"process_pixel\"),\n \"process_id\":global.get(\"process_id\")\n \n \n};\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 1240, - "y": 120, + "initialize": "", + "finalize": "", + "x": 1300, + "y": 220, "wires": [ [ - "3e15ea9d.8a1186" + "7cd7b4d0.cfc56c" ] ] }, { - "id": "6b6db57d.0ac41c", + "id": "bb1b1e3.906e7e", "type": "file", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "", "filename": "/home/pi/PlanktonScope/config.json", "appendNewline": true, "createDir": true, "overwriteFile": "true", "encoding": "none", - "x": 1660, - "y": 120, + "x": 1760, + "y": 220, "wires": [ [] ] }, { - "id": "3e15ea9d.8a1186", + "id": "7cd7b4d0.cfc56c", "type": "json", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "config.json", "property": "payload", "action": "str", "pretty": true, - "x": 1410, - "y": 120, + "x": 1510, + "y": 220, "wires": [ [ - "6b6db57d.0ac41c" + "bb1b1e3.906e7e" ] ] }, { - "id": "92f32906.e7cb08", + "id": "83f2e27a.b59688", "type": "file in", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "", "filename": "/home/pi/PlanktonScope/config.json", "format": "utf8", @@ -3989,16 +3601,25 @@ "y": 80, "wires": [ [ - "11e61823.32d9a8" + "3333aa46.5fa3ee" ] ], "info": "# PlanktonScope Help\nThis Node will read the content of the file named **config.txt** containing all the input placeholders.\n" }, { - "id": "233acfba.0fa74", + "id": "9db37b60.5e5488", "type": "inject", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "on_load", + "props": [ + { + "p": "payload" + }, + { + "p": "topic", + "vt": "str" + } + ], "repeat": "", "crontab": "", "once": true, @@ -4010,17 +3631,16 @@ "y": 80, "wires": [ [ - "92f32906.e7cb08", - "8d581456.eb62f8", - "43ea9a81.af94a4" + "83f2e27a.b59688", + "172d187b.ad216" ] ], "info": "# PlanktonScope Help\nWhen the **Pi** is booting, **Node-RED** will be initiated and this node will be activated once and execute the following nodes." }, { - "id": "11e61823.32d9a8", + "id": "3333aa46.5fa3ee", "type": "json", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "config.json", "property": "payload", "action": "", @@ -4029,39 +3649,41 @@ "y": 80, "wires": [ [ - "f5241d95.8c94a8", - "f7d7ea16.bcd9d8", - "7f1ce067.e4ae9", - "5c0870d2.949358", - "3687179c.9ab2e8", - "129419c2.cd531e", - "eb940d39.f6b0d" + "1f2d3f3d.5eeff9", + "64b58fc8.cdc4e", + "c94db91f.7cf2b8", + "70625f12.179948", + "f56b39e5.245b4", + "24a381c3.761f56", + "6a7bbade.f3174c" ] ] }, { - "id": "d00f18b.a005468", + "id": "6725d70.a4aed28", "type": "function", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "set global", "func": "var value = msg.payload;\nvar key = msg.topic;\n\nglobal.set(key,value);\nreturn msg;", "outputs": 1, "noerr": 0, - "x": 1240, - "y": 80, + "initialize": "", + "finalize": "", + "x": 1300, + "y": 140, "wires": [ [ - "9ee31ce7.88d9c" + "6f6ad84e.d9222" ] ] }, { - "id": "ab1f24d.d9492d8", + "id": "9f81fc0f.814988", "type": "rpi-gpio out", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "", "pin": "40", - "set": "", + "set": true, "level": "0", "freq": "", "out": "out", @@ -4070,15 +3692,15 @@ "wires": [] }, { - "id": "3977f843.8914b8", + "id": "60cc050b.8dae74", "type": "ui_template", - "z": "c4414305.176578", - "group": "6b2a8cdd.9f43cc", + "z": "e8d4d920.35344", + "group": "59434d8d.70ed94", "name": "Stream Pi Camera", "order": 1, "width": 24, "height": 19, - "format": "
", + "format": "
\n \n
", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": false, @@ -4090,111 +3712,95 @@ ] }, { - "id": "8d581456.eb62f8", - "type": "function", - "z": "c4414305.176578", - "name": "init LED", - "func": "msg.payload=1;\nreturn msg;", - "outputs": 1, - "noerr": 0, - "x": 280, - "y": 180, - "wires": [ - [ - "ab1f24d.d9492d8" - ] - ] - }, - { - "id": "f5241d95.8c94a8", - "type": "subflow:6bc47c75.93e24c", - "z": "c4414305.176578", + "id": "1f2d3f3d.5eeff9", + "type": "subflow:81483277.2521e", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 930, "y": 140, "wires": [ [ - "d00f18b.a005468" + "6725d70.a4aed28" ] ] }, { - "id": "129419c2.cd531e", - "type": "subflow:3df4e02.36602a", - "z": "c4414305.176578", + "id": "24a381c3.761f56", + "type": "subflow:a0f9bde.423644", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 920, "y": 360, "wires": [ [ - "d00f18b.a005468" + "6725d70.a4aed28" ] ] }, { - "id": "eb940d39.f6b0d", - "type": "subflow:b1edcbe7.366f7", - "z": "c4414305.176578", + "id": "6a7bbade.f3174c", + "type": "subflow:977131e7.c2e76", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 920, "y": 400, "wires": [ [ - "d00f18b.a005468" + "6725d70.a4aed28" ] ] }, { - "id": "f7d7ea16.bcd9d8", - "type": "subflow:5163a57b.0008b4", - "z": "c4414305.176578", + "id": "64b58fc8.cdc4e", + "type": "subflow:758bc08f.c57318", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 940, "y": 180, "wires": [ [ - "d00f18b.a005468" + "6725d70.a4aed28" ], [ - "9ee31ce7.88d9c" + "6f6ad84e.d9222" ] ] }, { - "id": "7f1ce067.e4ae9", - "type": "subflow:672ac548.1a9bac", - "z": "c4414305.176578", + "id": "c94db91f.7cf2b8", + "type": "subflow:1a447be0.198674", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 920, "y": 220, "wires": [ [ - "d00f18b.a005468" + "6725d70.a4aed28" ], [ - "9ee31ce7.88d9c" + "6f6ad84e.d9222" ] ] }, { - "id": "7dd20df2.204fa4", - "type": "subflow:7aea7e49.4a3c88", - "z": "c4414305.176578", + "id": "f1f6f8ca.a80bd", + "type": "subflow:9c3e6ad4.7471a8", + "z": "e8d4d920.35344", "name": "RPi commands", "env": [], "x": 920, - "y": 680, + "y": 820, "wires": [] }, { - "id": "d207c3d7.bb3c7", - "type": "subflow:20bc0424.146724", - "z": "c4414305.176578", + "id": "b8638c84.bcd58", + "type": "subflow:bee3b478.ef4b88", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 320, @@ -4202,48 +3808,48 @@ "wires": [] }, { - "id": "5c0870d2.949358", - "type": "subflow:4d3786dd.dd79f", - "z": "c4414305.176578", + "id": "70625f12.179948", + "type": "subflow:714bd4fe.163ab4", + "z": "e8d4d920.35344", "name": "Process metadata", "env": [], "x": 930, "y": 260, "wires": [ [ - "d00f18b.a005468" + "6725d70.a4aed28" ] ] }, { - "id": "3687179c.9ab2e8", - "type": "subflow:4273b9bf.bb07a8", - "z": "c4414305.176578", + "id": "f56b39e5.245b4", + "type": "subflow:1bc3f9f9.1ee996", + "z": "e8d4d920.35344", "name": "Acquisition inputs", "env": [], "x": 930, "y": 540, "wires": [ [ - "d00f18b.a005468" + "6725d70.a4aed28" ] ], "icon": "node-red-dashboard/ui_switch.png" }, { - "id": "43ea9a81.af94a4", - "type": "subflow:435c6174.e0c8b", - "z": "c4414305.176578", + "id": "172d187b.ad216", + "type": "subflow:d95291dc.83b0b8", + "z": "e8d4d920.35344", "name": "", "env": [], - "x": 320, + "x": 300, "y": 280, "wires": [] }, { - "id": "da33ab49.07225", - "type": "subflow:3f0fd072.06e2c8", - "z": "c4414305.176578", + "id": "590670c8.5b17c", + "type": "subflow:130e0533.4f1813", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 930, @@ -4251,9 +3857,9 @@ "wires": [] }, { - "id": "d4475439.1b745", + "id": "b902251f.c756f", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Inject config.json in GUI inputs", "info": "", "x": 660, @@ -4261,9 +3867,9 @@ "wires": [] }, { - "id": "ad502db0.d19c3", + "id": "e824bf49.c0716", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Edit config.json on changes", "info": "", "x": 1300, @@ -4271,19 +3877,19 @@ "wires": [] }, { - "id": "40d4f54b.baf6d4", - "type": "subflow:aafb7d9f.f516f", - "z": "c4414305.176578", + "id": "5836c9be.895de8", + "type": "subflow:21b3da63.2cef2e", + "z": "e8d4d920.35344", "name": "", "env": [], "x": 920, - "y": 720, + "y": 860, "wires": [] }, { - "id": "3ab2666b.b74962", + "id": "b94ee83b.5fc548", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Create and run python code receiving MQTT queries", "info": "", "x": 410, @@ -4291,9 +3897,9 @@ "wires": [] }, { - "id": "5562e88a.9f16a", + "id": "61f01a83.88da5c", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "On Load", "info": "", "x": 120, @@ -4301,9 +3907,9 @@ "wires": [] }, { - "id": "efcdeb9b.9d257", + "id": "fa75a682.73df7", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Get metadata from config.json", "info": "", "x": 340, @@ -4311,9 +3917,9 @@ "wires": [] }, { - "id": "55f198c4.c121d8", + "id": "5d22882.f86d2f8", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Turn on the white LED", "info": "", "x": 320, @@ -4321,9 +3927,9 @@ "wires": [] }, { - "id": "29c52f09.54d1f8", + "id": "d418fabe.d0b75", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Visible on the GUI", "info": "", "x": 930, @@ -4331,19 +3937,19 @@ "wires": [] }, { - "id": "3308bd4b.d469b2", + "id": "ac2a06c3.0eec2", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "RPi", "info": "", "x": 890, - "y": 640, + "y": 780, "wires": [] }, { - "id": "8b58c123.c1ea68", + "id": "630ed3ce.35374c", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Metadata", "info": "", "x": 900, @@ -4351,9 +3957,9 @@ "wires": [] }, { - "id": "ee4608b3.725f", + "id": "1fc17ae3.11b69d", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Actuation", "info": "", "x": 900, @@ -4361,13 +3967,594 @@ "wires": [] }, { - "id": "adcdfe58.613838", + "id": "dada61e7.83ee38", "type": "comment", - "z": "c4414305.176578", + "z": "e8d4d920.35344", "name": "Acquisition", "info": "", "x": 900, "y": 500, "wires": [] + }, + { + "id": "f44de853.0c855", + "type": "ui_switch", + "z": "e8d4d920.35344", + "name": "", + "label": "LED", + "tooltip": "", + "group": "59434d8d.70ed94", + "order": 7, + "width": "0", + "height": "0", + "passthru": true, + "decouple": "false", + "topic": "", + "style": "", + "onvalue": "true", + "onvalueType": "bool", + "onicon": "", + "oncolor": "", + "offvalue": "false", + "offvalueType": "bool", + "officon": "", + "offcolor": "", + "x": 270, + "y": 180, + "wires": [ + [ + "9f81fc0f.814988" + ] + ] + }, + { + "id": "22392d.ef1a66d4", + "type": "status", + "z": "1a447be0.198674", + "name": "GPS Status", + "scope": [ + "cb77803f.357f88" + ], + "x": 110, + "y": 400, + "wires": [ + [ + "a30fe92c.6b73e8" + ] + ] + }, + { + "id": "a30fe92c.6b73e8", + "type": "ui_text", + "z": "1a447be0.198674", + "group": "ab1d06d6.bf9898", + "order": 4, + "width": 6, + "height": 1, + "name": "GPS Status Display", + "label": "GPS Status:", + "format": "{{msg.status.text}}", + "layout": "row-left", + "x": 610, + "y": 400, + "wires": [] + }, + { + "id": "ec0d715e.19fdd", + "type": "ui_text", + "z": "1a447be0.198674", + "group": "ab1d06d6.bf9898", + "order": 5, + "width": 6, + "height": 1, + "name": "Latitude", + "label": "Latitude", + "format": "{{msg.payload.lat.deg}}°{{msg.payload.lat.min}}'{{msg.payload.lat.sec}}{{msg.payload.lat.dir}}", + "layout": "row-left", + "x": 640, + "y": 300, + "wires": [] + }, + { + "id": "f8c5f7ef.e93908", + "type": "ui_text", + "z": "1a447be0.198674", + "group": "ab1d06d6.bf9898", + "order": 6, + "width": 6, + "height": 1, + "name": "Longitude", + "label": "Longitude", + "format": "{{msg.payload.lon.deg}}°{{msg.payload.lon.min}}'{{msg.payload.lon.sec}}{{msg.payload.lon.dir}}", + "layout": "row-right", + "x": 640, + "y": 340, + "wires": [] + }, + { + "id": "3f718ceb.5e9f34", + "type": "function", + "z": "1a447be0.198674", + "name": "Convert DD to DMS", + "func": "function ConvertDDToDMS(D, lng){\n // from https://stackoverflow.com/a/5786281/2108279\n return {\n dir : D<0?lng?'W':'S':lng?'E':'N',\n deg : 0|(D<0?D=-D:D),\n min : 0|D%1*60,\n sec :(0|D*60%1*6000)/100\n };\n}\n\nmsg.payload = {\n \"lat\":ConvertDDToDMS(msg.payload.lat, false),\n \"lon\":ConvertDDToDMS(msg.payload.lon, true)\n};\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 350, + "y": 320, + "wires": [ + [ + "ec0d715e.19fdd", + "f8c5f7ef.e93908" + ] + ] + }, + { + "id": "74554b33.14ab6c", + "type": "file", + "z": "1a447be0.198674", + "name": "gpsd_output", + "filename": "/home/pi/gpsd.json", + "appendNewline": true, + "createDir": false, + "overwriteFile": "false", + "x": 630, + "y": 240, + "wires": [ + [] + ] + }, + { + "id": "b69e435f.b93558", + "type": "function", + "z": "a0f9bde.423644", + "name": "round distance", + "func": "// Here we change the focus_distance to 25 µm increment\nif (msg.payload%25 <= 12.5){\n msg.payload = msg.payload - msg.payload%25\n}\nelse {\n msg.payload = msg.payload + msg.payload%25\n}\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 820, + "y": 40, + "wires": [ + [ + "f49b3397.2599f8" + ] + ] + }, + { + "id": "afa9f4b.2e77988", + "type": "delay", + "z": "d95291dc.83b0b8", + "name": "", + "pauseType": "delay", + "timeout": "2", + "timeoutUnits": "seconds", + "rate": "1", + "nbRateUnits": "1", + "rateUnits": "second", + "randomFirst": "1", + "randomLast": "5", + "randomUnits": "seconds", + "drop": false, + "x": 460, + "y": 140, + "wires": [ + [ + "9998aa86.74bb" + ] + ] + }, + { + "id": "1df370a8.6c70ff", + "type": "ui_button", + "z": "d95291dc.83b0b8", + "name": "Restart Python", + "group": "adcd0ef7.81a7f8", + "order": 1, + "width": 0, + "height": 0, + "passthru": false, + "label": "Restart Python", + "tooltip": "", + "color": "", + "bgcolor": "#AD1625", + "icon": "", + "payload": "", + "payloadType": "str", + "topic": "", + "x": 100, + "y": 140, + "wires": [ + [ + "afa9f4b.2e77988", + "672d89a8.4e6968" + ] + ] + }, + { + "id": "2aa5b118.d75f2e", + "type": "json", + "z": "bee3b478.ef4b88", + "name": "", + "property": "payload", + "action": "obj", + "pretty": true, + "x": 210, + "y": 300, + "wires": [ + [ + "ecedbda4.fed15" + ] + ] + }, + { + "id": "1dd0c3dd.60236c", + "type": "template", + "z": "bee3b478.ef4b88", + "name": "Create sentence", + "field": "payload", + "fieldType": "msg", + "format": "handlebars", + "syntax": "mustache", + "template": "The {{topic}} is {{payload.status}}", + "output": "str", + "x": 1120, + "y": 280, + "wires": [ + [ + "1403e1a4.528926" + ] + ] + }, + { + "id": "117aad13.53e11b", + "type": "change", + "z": "bee3b478.ef4b88", + "name": "Remove high-level topic", + "rules": [ + { + "t": "change", + "p": "topic", + "pt": "msg", + "from": "status/", + "fromt": "str", + "to": "", + "tot": "str" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 890, + "y": 280, + "wires": [ + [ + "1dd0c3dd.60236c" + ] + ] + }, + { + "id": "8ec68b82.17e3d8", + "type": "ui_button", + "z": "4b508eab.dccae8", + "name": "Start segmentation", + "group": "566dcd6a.03db74", + "order": 2, + "width": "16", + "height": "1", + "passthru": false, + "label": "Start segmentation", + "tooltip": "", + "color": "", + "bgcolor": "", + "icon": "", + "payload": "{\"action\":\"segment\"}", + "payloadType": "json", + "topic": "segmenter/segment", + "x": 270, + "y": 200, + "wires": [ + [ + "16f3cef4.0acac9" + ] + ] + }, + { + "id": "27be7971.b3fbce", + "type": "ui_button", + "z": "4b508eab.dccae8", + "name": "Stop segmentation", + "group": "566dcd6a.03db74", + "order": 1, + "width": "8", + "height": "1", + "passthru": true, + "label": "Stop segmentation", + "tooltip": "", + "color": "", + "bgcolor": "#AD1625", + "icon": "", + "payload": "{\"action\":\"stop\"}", + "payloadType": "json", + "topic": "segmenter/segment", + "x": 270, + "y": 320, + "wires": [ + [ + "16f3cef4.0acac9" + ] + ] + }, + { + "id": "16f3cef4.0acac9", + "type": "mqtt out", + "z": "4b508eab.dccae8", + "name": "", + "topic": "", + "qos": "", + "retain": "", + "broker": "8dc3722c.06efa8", + "x": 610, + "y": 260, + "wires": [] + }, + { + "id": "f8eade62.0eb118", + "type": "subflow:4b508eab.dccae8", + "z": "e8d4d920.35344", + "name": "", + "env": [], + "x": 930, + "y": 720, + "wires": [] + }, + { + "id": "30fc0aaa.8fdd96", + "type": "comment", + "z": "e8d4d920.35344", + "name": "Segmentation", + "info": "", + "x": 910, + "y": 680, + "wires": [] + }, + { + "id": "cd84b9db.02778", + "type": "mqtt out", + "z": "e8d4d920.35344", + "name": "", + "topic": "", + "qos": "", + "retain": "", + "broker": "8dc3722c.06efa8", + "x": 1310, + "y": 920, + "wires": [] + }, + { + "id": "3b040a6c.6b3dbe", + "type": "function", + "z": "e8d4d920.35344", + "name": "Encapsulate config", + "func": "msg.payload = {\n \"action\":\"update_config\", \n \"config\":{\n \"sample_project\":global.get(\"sample_project\"),\n \"sample_id\":global.get(\"sample_id\"),\n \"sample_ship\":global.get(\"sample_ship\"),\n \"sample_operator\":global.get(\"sample_operator\"),\n \"sample_sampling_gear\":global.get(\"sample_sampling_gear\"),\n \n \"acq_id\":global.get(\"acq_id\"),\n \"acq_instrument\":global.get(\"acq_instrument\"),\n //\"acq_instrument_id\":global.get(\"acq_instrument_id\"),\n \"acq_celltype\":global.get(\"acq_celltype\"),\n \"acq_minimum_mesh\":global.get(\"acq_minimum_mesh\"),\n \"acq_maximum_mesh\":global.get(\"acq_maximum_mesh\"),\n \"acq_min_esd\":global.get(\"acq_min_esd\"),\n \"acq_max_esd\":global.get(\"acq_max_esd\"),\n \"acq_volume\":global.get(\"acq_volume\"),\n \"acq_magnification\":global.get(\"magnification\"),\n \"acq_fnumber_objective\":global.get(\"acq_fnumber_objective\"),\n \n \"acq_camera_name\":\"Pi Camera V2.1 - 8MP\",\n \n \"object_date\":global.get(\"object_date\"),\n \"object_time\":global.get(\"object_time\"),\n \"object_lat\":global.get(\"object_lat\"),\n \"object_lon\":global.get(\"object_lon\"),\n \"object_depth_min\":global.get(\"object_depth_min\"),\n \"object_depth_max\":global.get(\"object_depth_max\"),\n \n \"custom_nb_frame\":global.get(\"custom_nb_frame\"),\n \"custom_nb_step\":global.get(\"custom_nb_step\"),\n \"custom_segmentation\":global.get(\"custom_segmentation\"),\n \"custom_sleep_before\":global.get(\"custom_sleep_before\"),\n \"focus_nb_step\":global.get(\"focus_nb_step\"),\n \"pump_flowrate\":global.get(\"pump_flowrate\"),\n \"pump_manual_volume\":global.get(\"pump_manual_volume\"),\n \n \"process_pixel\":global.get(\"process_pixel\"),\n \"process_id\":global.get(\"process_id\")\n }\n};\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 1130, + "y": 920, + "wires": [ + [ + "cd84b9db.02778" + ] + ] + }, + { + "id": "eb27a17a.6a8e", + "type": "ui_button", + "z": "e8d4d920.35344", + "name": "", + "group": "71c63dd4.311c44", + "order": 7, + "width": 0, + "height": 0, + "passthru": false, + "label": "Update config", + "tooltip": "", + "color": "", + "bgcolor": "", + "icon": "", + "payload": "", + "payloadType": "str", + "topic": "", + "x": 920, + "y": 920, + "wires": [ + [ + "3b040a6c.6b3dbe" + ] + ] + }, + { + "id": "16aa0238.209276", + "type": "ui_slider", + "z": "674362b7.9b6574", + "name": "Iso slider", + "label": "ISO", + "tooltip": "Possible values are 100, 200, 320, 400, 500, 640, 800", + "group": "e8b67968.dea6", + "order": 1, + "width": 0, + "height": 0, + "passthru": true, + "outs": "end", + "topic": "imager/image", + "min": "100", + "max": "800", + "step": "20", + "x": 300, + "y": 100, + "wires": [ + [ + "bb090334.1e21a8" + ] + ] + }, + { + "id": "bb090334.1e21a8", + "type": "function", + "z": "674362b7.9b6574", + "name": "round iso", + "func": "// Iso should be one of 60, 100, 200, 320, 400, 500, 640, 800\n\nif (msg.payload <= 80){\n msg.payload = 60;\n return msg;\n}\n\nif (msg.payload <= 150){\n msg.payload = 100;\n return msg;\n}\n\nif (msg.payload <= 260){\n msg.payload = 200;\n return msg;\n}\n\nif (msg.payload <= 360){\n msg.payload = 320;\n return msg;\n}\n\nif (msg.payload <= 450){\n msg.payload = 400;\n return msg;\n}\n\nif (msg.payload <= 565){\n msg.payload = 500;\n return msg;\n}\n\nif (msg.payload <= 700){\n msg.payload = 640;\n return msg;\n}\n\n\nif (700 < msg.payload){\n msg.payload = 800;\n return msg;\n}\n", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 470, + "y": 100, + "wires": [ + [ + "16aa0238.209276", + "8ea9dc9a.c7d87" + ] + ] + }, + { + "id": "8ea9dc9a.c7d87", + "type": "function", + "z": "674362b7.9b6574", + "name": "Encapsulate settings", + "func": "msg.payload = {\n \"action\":\"settings\", \n \"settings\":{\"iso\":msg.payload}\n}\nmsg.topic = \"imager/image\"\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 680, + "y": 100, + "wires": [ + [ + "845e06e1.0d812" + ] + ] + }, + { + "id": "5765a825.a595c8", + "type": "ui_slider", + "z": "674362b7.9b6574", + "name": "Shutter speed slider", + "label": "Shutter Speed", + "tooltip": "In microseconds, up to 5000µs, 500µs by default", + "group": "e8b67968.dea6", + "order": 2, + "width": 0, + "height": 0, + "passthru": true, + "outs": "end", + "topic": "imager/image", + "min": "10", + "max": "5000", + "step": "10", + "x": 340, + "y": 220, + "wires": [ + [ + "c38509b.fb08af8" + ] + ] + }, + { + "id": "c38509b.fb08af8", + "type": "function", + "z": "674362b7.9b6574", + "name": "Encapsulate settings", + "func": "msg.payload = {\n \"action\":\"settings\", \n \"settings\":{\"shutter_speed\":msg.payload}\n}\nmsg.topic = \"imager/image\"\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 680, + "y": 220, + "wires": [ + [ + "845e06e1.0d812" + ] + ] + }, + { + "id": "845e06e1.0d812", + "type": "mqtt out", + "z": "674362b7.9b6574", + "name": "", + "topic": "", + "qos": "", + "retain": "", + "broker": "8dc3722c.06efa8", + "x": 1020, + "y": 160, + "wires": [] + }, + { + "id": "2350e507.d4e302", + "type": "inject", + "z": "674362b7.9b6574", + "name": "", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "1", + "topic": "", + "payload": "500", + "payloadType": "num", + "x": 120, + "y": 220, + "wires": [ + [ + "5765a825.a595c8" + ] + ] + }, + { + "id": "2edd922b.a471a6", + "type": "inject", + "z": "674362b7.9b6574", + "name": "", + "props": [ + { + "p": "payload" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "1", + "topic": "", + "payload": "100", + "payloadType": "num", + "x": 110, + "y": 100, + "wires": [ + [ + "16aa0238.209276" + ] + ] + }, + { + "id": "649552a6.bf68fc", + "type": "subflow:674362b7.9b6574", + "z": "e8d4d920.35344", + "name": "", + "env": [], + "x": 920, + "y": 640, + "wires": [] } -] +] \ No newline at end of file diff --git a/hardware.json b/hardware.json new file mode 100644 index 0000000..98b6fe0 --- /dev/null +++ b/hardware.json @@ -0,0 +1,7 @@ +{ + "stepper_reverse" : 1, + "focus_steps_per_mm" : 40, + "pump_steps_per_ml" : 614, + "focus_max_speed" : 0.5, + "pump_max_speed" : 30 +} diff --git a/package.json b/package.json index 6fdcfd9..f8248ca 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "description": "Planktonscope main software project", "version": "0.1", "dependencies": { - "node-red-contrib-gpsd": "1.0.2", - "node-red-contrib-interval": "0.0.1", - "node-red-contrib-python3-function": "0.0.4", - "node-red-contrib-web-worldmap": "2.4.1", - "node-red-dashboard": "2.23.0", - "node-red-node-pi-gpio": "1.1.1" + "node-red-contrib-gpsd": "^1.0.2", + "node-red-contrib-interval": "^0.0.1", + "node-red-contrib-python3-function": "^0.0.4", + "node-red-contrib-web-worldmap": "^2.5.4", + "node-red-dashboard": "^2.23.4", + "node-red-node-pi-gpio": "^1.2.0", + "node-red-contrib-camerapi": "0.0.38" }, "node-red": { "settings": { diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..291748b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +RPi.GPIO==0.7.0 +Adafruit-Blinka==5.2.3 +adafruit-circuitpython-motorkit==1.5.2 +Adafruit-SSD1306==1.6.2 +paho-mqtt==1.5.0 +picamera==1.13 +opencv-contrib-python==4.1.0.25 +morphocut==0.1.1+42.g01a051e +scipy==1.1.0 \ No newline at end of file diff --git a/scripts/fan.py b/scripts/fan.py deleted file mode 100644 index c4c65d9..0000000 --- a/scripts/fan.py +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/python -import smbus -import sys - -state = str(sys.argv[1]) - -bus = smbus.SMBus(1) - -DEVICE_ADDRESS = 0x0d - -def fan(state): - if state == "false": - bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x00) - bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x00) - if state == "true": - bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x01) - bus.write_byte_data(DEVICE_ADDRESS, 0x08, 0x01) - -fan(state) \ No newline at end of file diff --git a/scripts/focus.py b/scripts/focus.py deleted file mode 100644 index ab6bdcd..0000000 --- a/scripts/focus.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python - -from adafruit_motor import stepper -from adafruit_motorkit import MotorKit -from time import sleep -import sys - -nb_step = int(sys.argv[1]) -orientation = str(sys.argv[2]) - -kit = MotorKit() -stage = kit.stepper2 -stage.release() - -#0.25mm/step -#31um/microsteps - -if orientation == 'up': - for i in range(nb_step): - stage.onestep(direction=stepper.FORWARD, style=stepper.MICROSTEP) - sleep(0.001) - -if orientation == 'down': - for i in range(nb_step): - stage.onestep(direction=stepper.BACKWARD, style=stepper.MICROSTEP) - sleep(0.001) - -stage.release() diff --git a/scripts/image.py b/scripts/image.py deleted file mode 100644 index 7678991..0000000 --- a/scripts/image.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python - -#Imaging a volume with a flowrate -#in folder named "/home/pi/PlanktonScope/acquisitions/sample_project/sample_id/acq_id" -#python3.7 $HOME/PlanktonScope/scripts/image.py tara_pacific sample_project sample_id acq_id volume flowrate - -import time -from time import sleep -from picamera import PiCamera -from datetime import datetime, timedelta -import os -import sys - -path=str(sys.argv[1]) - -#[i] : ex:24ml -volume=int(sys.argv[2]) - -#[f] : ex:3.2ml/min -flowrate = float(sys.argv[3]) - -warm_up_duration=3 - -duration = (volume/flowrate)*60 - warm_up_duration - -max_fps = 0.7 - -nb_frame = int(duration/max_fps) - -if not os.path.exists(path): - os.makedirs(path) - -camera = PiCamera() - -camera.resolution = (3280, 2464) -camera.iso = 60 - - -def image(nb_frame, path): - - sleep(3) - - for frame in range(nb_frame): - - time = datetime.now().timestamp() - - filename=path+"/"+str(time)+".jpg" - - camera.capture(filename) - - print(time) - sleep(0.1) - - -image(nb_frame, path) diff --git a/scripts/light.py b/scripts/light.py deleted file mode 100644 index 2669800..0000000 --- a/scripts/light.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python -#Turn on using this command line : -#python3.7 path/to/file/light.py on - -#Turn off using this command line : -#python3.7 path/to/file/light.py off - -import RPi.GPIO as GPIO -import sys - - -GPIO.setmode(GPIO.BCM) -GPIO.setwarnings(False) -GPIO.setup(21,GPIO.OUT) - -state = str(sys.argv[1]) - - -def light(state): - - if state == "true": - GPIO.output(21,GPIO.HIGH) - if state == "false": - GPIO.output(21,GPIO.LOW) - -light(state) diff --git a/scripts/main.py b/scripts/main.py new file mode 100644 index 0000000..fefe4e2 --- /dev/null +++ b/scripts/main.py @@ -0,0 +1,150 @@ +# Logger library compatible with multiprocessing +from loguru import logger +import sys + +# multiprocessing module +import multiprocessing + +# Time module so we can sleep +import time + +# signal module is used to manage SINGINT/SIGTERM +import signal + +# os module is used create paths +import os + +# enqueue=True is necessary so we can log accross modules +# rotation happens everyday at 01:00 if not restarted +logger.add( + # sys.stdout, + "PlanktoScope_{time}.log", + rotation="01:00", + retention="1 month", + compression=".tar.gz", + enqueue=True, + level="INFO", +) + +# The available level for the logger are as follows: +# Level name Severity Logger method +# TRACE 5 logger.trace() +# DEBUG 10 logger.debug() +# INFO 20 logger.info() +# SUCCESS 25 logger.success() +# WARNING 30 logger.warning() +# ERROR 40 logger.error() +# CRITICAL 50 logger.critical() + +logger.info("Starting the PlanktoScope python script!") + +# Library for exchaning messages with Node-RED +import planktoscope.mqtt + +# Import the planktonscope stepper module +import planktoscope.stepper + +# Import the planktonscope imager module +import planktoscope.imager + +# Import the planktonscope segmenter module +import planktoscope.segmenter + +# Import the planktonscope LED module +import planktoscope.light + + +# global variable that keeps the wheels spinning +run = True + + +def handler_stop_signals(signum, frame): + """This handler simply stop the forever running loop in __main__""" + global run + run = False + + +if __name__ == "__main__": + logger.info("Welcome!") + logger.info( + "Initialising signals handling and sanitizing the directories (step 1/4)" + ) + signal.signal(signal.SIGINT, handler_stop_signals) + signal.signal(signal.SIGTERM, handler_stop_signals) + + # check if gpu_mem configuration is at least 256Meg, otherwise the camera will not run properly + with open("/boot/config.txt", "r") as config_file: + for i, line in enumerate(config_file): + if line.startswith("gpu_mem"): + if int(line.split("=")[1].strip()) < 256: + logger.error( + "The GPU memory size is less than 256, this will prevent the camera from running properly" + ) + logger.error( + "Please edit the file /boot/config.txt to change the gpu_mem value to at least 256" + ) + logger.error( + "or use raspi-config to change the memory split, in menu 7 Advanced Options, A3 Memory Split" + ) + sys.exit(1) + + # Let's make sure the used base path exists + img_path = "/home/pi/PlanktonScope/img" + # check if this path exists + if not os.path.exists(img_path): + # create the path! + os.makedirs(img_path) + + export_path = "/home/pi/PlanktonScope/export" + # check if this path exists + if not os.path.exists(export_path): + # create the path! + os.makedirs(export_path) + + with open("/sys/firmware/devicetree/base/serial-number", "r") as config_file: + logger.info(f"This PlanktoScope unique ID is {config_file.readline()}") + + # Prepare the event for a gracefull shutdown + shutdown_event = multiprocessing.Event() + shutdown_event.clear() + + # Starts the stepper process for actuators + logger.info("Starting the stepper control process (step 2/4)") + stepper_thread = planktoscope.stepper.StepperProcess(shutdown_event) + stepper_thread.start() + + # Starts the imager control process + logger.info("Starting the imager control process (step 3/4)") + imager_thread = planktoscope.imager.ImagerProcess(shutdown_event) + imager_thread.start() + + # Starts the segmenter process + logger.info("Starting the segmenter control process (step 4/4)") + segmenter_thread = planktoscope.segmenter.SegmenterProcess(shutdown_event) + segmenter_thread.start() + + logger.info("Looks like everything is set up and running, have fun!") + + while run: + # TODO look into ways of restarting the dead threads + logger.trace("Running around in circles while waiting for someone to die!") + if not stepper_thread.is_alive(): + logger.error("The stepper process died unexpectedly! Oh no!") + break + if not imager_thread.is_alive(): + logger.error("The imager process died unexpectedly! Oh no!") + break + if not segmenter_thread.is_alive(): + logger.error("The segmenter process died unexpectedly! Oh no!") + break + time.sleep(1) + + logger.info("Shutting down the shop") + shutdown_event.set() + stepper_thread.join() + imager_thread.join() + segmenter_thread.join() + stepper_thread.close() + imager_thread.close() + segmenter_thread.close() + logger.info("Bye") \ No newline at end of file diff --git a/scripts/mqtt_pump_focus_image_v2.py b/scripts/mqtt_pump_focus_image_v2.py deleted file mode 100644 index 4b51328..0000000 --- a/scripts/mqtt_pump_focus_image_v2.py +++ /dev/null @@ -1,389 +0,0 @@ -import paho.mqtt.client as mqtt -from picamera import PiCamera -from datetime import datetime, timedelta -from adafruit_motor import stepper -from adafruit_motorkit import MotorKit -from time import sleep -import json - -import os -import subprocess - -from skimage.util import img_as_ubyte - -from morphocut import Call -from morphocut.contrib.ecotaxa import EcotaxaWriter -from morphocut.contrib.zooprocess import CalculateZooProcessFeatures -from morphocut.core import Pipeline -from morphocut.file import Find -from morphocut.image import ( - ExtractROI, - FindRegions, - ImageReader, - ImageWriter, - RescaleIntensity, - RGB2Gray, -) - -from morphocut.stat import RunningMedian -from morphocut.str import Format -from morphocut.stream import TQDM, Enumerate, FilterVariables - -from skimage.feature import canny -from skimage.color import rgb2gray, label2rgb -from skimage.morphology import disk -from skimage.morphology import erosion, dilation, closing -from skimage.measure import label, regionprops -import cv2, shutil - -import smbus -#fan -bus = smbus.SMBus(1) -################################################################################ -kit = MotorKit() -pump_stepper = kit.stepper1 -pump_stepper.release() -focus_stepper = kit.stepper2 -focus_stepper.release() - -################################################################################ - -camera = PiCamera() -camera.resolution = (3280, 2464) -camera.iso = 60 -camera.shutter_speed = 500 -camera.exposure_mode = 'fixedfps' - -################################################################################ -message = '' -topic = '' -count='' - -################################################################################ - -def on_connect(client, userdata, flags, rc): - print("Connected! - " + str(rc)) - client.subscribe("actuator/#") - rgb(0,255,0) -def on_subscribe(client, obj, mid, granted_qos): - print("Subscribed! - "+str(mid)+" "+str(granted_qos)) - -def on_message(client, userdata, msg): - print(msg.topic+" "+str(msg.qos)+" "+str(msg.payload)) - global message - global topic - global count - message=str(msg.payload.decode()) - topic=msg.topic.split("/")[1] - count=0 - -def on_log(client, obj, level, string): - print(string) - -def rgb(R,G,B): - bus.write_byte_data(0x0d, 0x00, 0) - bus.write_byte_data(0x0d, 0x01, R) - bus.write_byte_data(0x0d, 0x02, G) - bus.write_byte_data(0x0d, 0x03, B) - - bus.write_byte_data(0x0d, 0x00, 1) - bus.write_byte_data(0x0d, 0x01, R) - bus.write_byte_data(0x0d, 0x02, G) - bus.write_byte_data(0x0d, 0x03, B) - - bus.write_byte_data(0x0d, 0x00, 2) - bus.write_byte_data(0x0d, 0x01, R) - bus.write_byte_data(0x0d, 0x02, G) - bus.write_byte_data(0x0d, 0x03, B) - cmd="i2cdetect -y 1" - subprocess.Popen(cmd.split(),stdout=subprocess.PIPE) - -################################################################################ -client = mqtt.Client() -client.connect("127.0.0.1",1883,60) -client.on_connect = on_connect -client.on_subscribe = on_subscribe -client.on_message = on_message -client.on_log = on_log -client.loop_start() - - -local_metadata = { - "process_datetime": datetime.now(), - "acq_camera_resolution" : camera.resolution, - "acq_camera_iso" : camera.iso, - "acq_camera_shutter_speed" : camera.shutter_speed -} - -config_txt = open('/home/pi/PlanktonScope/config.txt','r') -node_red_metadata = json.loads(config_txt.read()) - -global_metadata = {**local_metadata, **node_red_metadata} - -archive_fn = os.path.join("/home/pi/PlanktonScope/","export", "ecotaxa_export.zip") -# Define processing pipeline -with Pipeline() as p: - # Recursively find .jpg files in import_path. - # Sort to get consective frames. - abs_path = Find("/home/pi/PlanktonScope/tmp", [".jpg"], sort=True, verbose=True) - - # Extract name from abs_path - name = Call(lambda p: os.path.splitext(os.path.basename(p))[0], abs_path) - - Call(rgb, 0,255,0) - # Read image - img = ImageReader(abs_path) - - # Show progress bar for frames - #TQDM(Format("Frame {name}", name=name)) - - # Apply running median to approximate the background image - flat_field = RunningMedian(img, 5) - - # Correct image - img = img / flat_field - - # Rescale intensities and convert to uint8 to speed up calculations - img = RescaleIntensity(img, in_range=(0, 1.1), dtype="uint8") - -# frame_fn = Format(os.path.join("/home/pi/PlanktonScope/tmp","CLEAN", "{name}.jpg"), name=name) - -# ImageWriter(frame_fn, img) - - # Convert image to uint8 gray - img_gray = RGB2Gray(img) - - # ? - img_gray = Call(img_as_ubyte, img_gray) - - #Canny edge detection - img_canny = Call(cv2.Canny, img_gray, 50,100) - - #Dilate - kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15)) - img_dilate = Call(cv2.dilate, img_canny, kernel, iterations=2) - - #Close - kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (5, 5)) - img_close = Call(cv2.morphologyEx, img_dilate, cv2.MORPH_CLOSE, kernel, iterations=1) - - #Erode - kernel = Call(cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15)) - mask = Call(cv2.erode, img_close, kernel, iterations=2) - - # Find objects - regionprops = FindRegions( - mask, img_gray, min_area=1000, padding=10, warn_empty=name - ) - - Call(rgb, 255,0,255) - # For an object, extract a vignette/ROI from the image - roi_orig = ExtractROI(img, regionprops, bg_color=255) - - # Generate an object identifier - i = Enumerate() - #Call(print,i) - - object_id = Format("{name}_{i:d}", name=name, i=i) - #Call(print,object_id) - - object_fn = Format(os.path.join("/home/pi/PlanktonScope/","OBJECTS", "{name}.jpg"), name=object_id) - - ImageWriter(object_fn, roi_orig) - - # Calculate features. The calculated features are added to the global_metadata. - # Returns a Variable representing a dict for every object in the stream. - meta = CalculateZooProcessFeatures( - regionprops, prefix="object_", meta=global_metadata - ) - -# json_meta = Call(json.dumps,meta, sort_keys=True, default=str) - -# Call(client.publish, "receiver/segmentation/metric", json_meta) - - # Add object_id to the metadata dictionary - meta["object_id"] = object_id - - # Generate object filenames - orig_fn = Format("{object_id}.jpg", object_id=object_id) - - # Write objects to an EcoTaxa archive: - # roi image in original color, roi image in grayscale, metadata associated with each object - EcotaxaWriter(archive_fn, (orig_fn, roi_orig), meta) - - # Progress bar for objects - TQDM(Format("Object {object_id}", object_id=object_id)) - -# Call(client.publish, "receiver/segmentation/object_id", object_id) - - -camera.start_preview(fullscreen=False, window = (160, 0, 640, 480)) -################################################################################ -while True: - -################################################################################ - if (topic=="pump"): - rgb(0,0,255) - direction=message.split(" ")[0] - delay=float(message.split(" ")[1]) - nb_step=int(message.split(" ")[2]) - - client.publish("receiver/pump", "Start"); - - - while True: - - if direction == "BACKWARD": - direction=stepper.BACKWARD - if direction == "FORWARD": - direction=stepper.FORWARD - count+=1 -# print(count,nb_step) - pump_stepper.onestep(direction=direction, style=stepper.DOUBLE) - sleep(delay) - - if topic!="pump": - pump_stepper.release() - print("The pump has been interrompted.") - client.publish("receiver/pump", "Interrompted"); - rgb(0,255,0) - break - - if count>nb_step: - pump_stepper.release() - print("The pumping is done.") - topic="wait" - client.publish("receiver/pump", "Done"); - rgb(0,255,0) - break - -################################################################################ - - elif (topic=="focus"): - - rgb(255,255,0) - direction=message.split(" ")[0] - nb_step=int(message.split(" ")[1]) - client.publish("receiver/focus", "Start"); - - - while True: - - if direction == "FORWARD": - direction=stepper.FORWARD - if direction == "BACKWARD": - direction=stepper.BACKWARD - count+=1 -# print(count,nb_step) - focus_stepper.onestep(direction=direction, style=stepper.MICROSTEP) - - if topic!="focus": - focus_stepper.release() - print("The stage has been interrompted.") - client.publish("receiver/focus", "Interrompted"); - rgb(0,255,0) - break - - if count>nb_step: - focus_stepper.release() - print("The focusing is done.") - topic="wait" - client.publish("receiver/focus", "Done"); - rgb(0,255,0) - break - -################################################################################ - - elif (topic=="image"): - - - sleep_before=int(message.split(" ")[0]) - - nb_step=int(message.split(" ")[1]) - - path=str(message.split(" ")[2]) - - nb_frame=int(message.split(" ")[3]) - - sleep_during=int(message.split(" ")[4]) - - #sleep a duration before to start - sleep(sleep_before) - - client.publish("receiver/image", "Start"); - - #flushing before to begin - - rgb(0,0,255) - for i in range(nb_step): - pump_stepper.onestep(direction=stepper.FORWARD, style=stepper.DOUBLE) - sleep(0.01) - rgb(0,255,0) - - while True: - - count+=1 -# print(count,nb_frame) - - filename = os.path.join("/home/pi/PlanktonScope/tmp",datetime.now().strftime("%M_%S_%f")+".jpg") - - rgb(0,255,255) - camera.capture(filename) - rgb(0,255,0) - - client.publish("receiver/image", datetime.now().strftime("%M_%S_%f")+".jpg has been imaged."); - - rgb(0,0,255) - for i in range(10): - pump_stepper.onestep(direction=stepper.FORWARD, style=stepper.DOUBLE) - sleep(0.01) - sleep(0.5) - rgb(0,255,0) - - if(count>nb_frame): - -# camera.stop_preview() - - client.publish("receiver/image", "Completed"); - # Meta data that is added to every object - - - - client.publish("receiver/segmentation", "Start"); - # Define processing pipeline - - p.run() - - #remove directory - #shutil.rmtree(import_path) - - client.publish("receiver/segmentation", "Completed"); - - command = os.popen("rm -rf /home/pi/PlanktonScope/tmp/*.jpg") - - rgb(255,255,255) - sleep(sleep_during) - rgb(0,255,0) - - - rgb(0,0,255) - for i in range(nb_step): - pump_stepper.onestep(direction=stepper.FORWARD, style=stepper.DOUBLE) - sleep(0.01) - rgb(0,255,0) - - count=0 - - if topic!="image": - pump_stepper.release() - print("The imaging has been interrompted.") - client.publish("receiver/image", "Interrompted"); - rgb(0,255,0) - count=0 - break - - else: -# print("Waiting") - sleep(1) - - diff --git a/scripts/planktoscope/__init__.py b/scripts/planktoscope/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/planktoscope/imager.py b/scripts/planktoscope/imager.py new file mode 100644 index 0000000..d791b95 --- /dev/null +++ b/scripts/planktoscope/imager.py @@ -0,0 +1,518 @@ +################################################################################ +# Practical Libraries +################################################################################ + +# Logger library compatible with multiprocessing +from loguru import logger + +# Library to get date and time for folder name and filename +import datetime + +# Library to be able to sleep for a given duration +import time + +# Libraries manipulate json format, execute bash commands +import json, shutil, os + +# Library to control the PiCamera +import picamera + +# Library for starting processes +import multiprocessing + +# Basic planktoscope libraries +import planktoscope.mqtt +import planktoscope.light + +# import planktoscope.streamer +import planktoscope.imager_state_machine + + +################################################################################ +# Streaming PiCamera over server +################################################################################ +import io +import socketserver +import http.server +import threading + +################################################################################ +# Classes for the PiCamera Streaming +################################################################################ +class StreamingOutput(object): + def __init__(self): + self.frame = None + self.buffer = io.BytesIO() + self.condition = threading.Condition() + + def write(self, buf): + if buf.startswith(b"\xff\xd8"): + # New frame, copy the existing buffer's content and notify all + # clients it's available + self.buffer.truncate() + with self.condition: + self.frame = self.buffer.getvalue() + self.condition.notify_all() + self.buffer.seek(0) + return self.buffer.write(buf) + + +class StreamingHandler(http.server.BaseHTTPRequestHandler): + # Webpage content containing the PiCamera Streaming + PAGE = """\ + + + PlanktonScope v2 | PiCamera Streaming + + + + + + """ + + @logger.catch + def do_GET(self): + if self.path == "/": + self.send_response(301) + self.send_header("Location", "/index.html") + self.end_headers() + elif self.path == "/index.html": + content = self.PAGE.encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.send_header("Content-Length", len(content)) + self.end_headers() + self.wfile.write(content) + elif self.path == "/stream.mjpg": + self.send_response(200) + self.send_header("Age", 0) + self.send_header("Cache-Control", "no-cache, private") + self.send_header("Pragma", "no-cache") + self.send_header( + "Content-Type", "multipart/x-mixed-replace; boundary=FRAME" + ) + + self.end_headers() + try: + while True: + with output.condition: + output.condition.wait() + frame = output.frame + self.wfile.write(b"--FRAME\r\n") + self.send_header("Content-Type", "image/jpeg") + self.send_header("Content-Length", len(frame)) + self.end_headers() + self.wfile.write(frame) + self.wfile.write(b"\r\n") + except Exception as e: + logger.exception(f"Removed streaming client {self.client_address}") + else: + self.send_error(404) + self.end_headers() + + +class StreamingServer(socketserver.ThreadingMixIn, http.server.HTTPServer): + allow_reuse_address = True + daemon_threads = True + + +output = StreamingOutput() + +logger.info("planktoscope.imager is loaded") + + +################################################################################ +# Main Imager class +################################################################################ +class ImagerProcess(multiprocessing.Process): + """This class contains the main definitions for the imager of the PlanktoScope""" + + @logger.catch + def __init__(self, event, resolution=(3280, 2464), iso=60, shutter_speed=500): + """Initialize the Imager class + + Args: + event (multiprocessing.Event): shutdown event + resolution (tuple, optional): Camera native resolution. Defaults to (3280, 2464). + iso (int, optional): ISO sensitivity. Defaults to 60. + shutter_speed (int, optional): Shutter speed of the camera. Defaults to 500. + """ + super(ImagerProcess, self).__init__(name="imager") + + logger.info("planktoscope.imager is initialising") + + self.stop_event = event + self.__imager = planktoscope.imager_state_machine.Imager() + self.__img_goal = 0 + self.__img_done = 0 + self.__sleep_before = None + self.__pump_volume = None + self.__img_goal = None + self.imager_client = None + self.__camera = None + self.__resolution = resolution + self.__iso = iso + self.__shutter_speed = shutter_speed + self.__exposure_mode = "fixedfps" + self.__base_path = "/home/pi/PlanktonScope/img" + self.__export_path = "" + self.__global_metadata = None + + # TODO implement a way to receive directly the metadata from Node-Red via MQTT + # FIXME We should save the metadata to a file in the folder too + # TODO create a directory structure per day/per imaging session + + logger.info("planktoscope.imager is initialised and ready to go!") + + @logger.catch + def start_camera(self): + """Start the camera streaming process""" + self.__camera.start_recording(output, format="mjpeg", resize=(640, 480)) + + def pump_callback(self, client, userdata, msg): + # Print the topic and the message + logger.info(f"{self.name}: {msg.topic} {str(msg.qos)} {str(msg.payload)}") + if msg.topic != "status/pump": + logger.error( + f"The received message has the wrong topic {msg.topic}, payload was {str(msg.payload)}" + ) + return + payload = json.loads(msg.payload.decode()) + logger.debug(f"parsed payload is {payload}") + if self.__imager.state.name is "waiting": + if payload["status"] == "Done": + self.__imager.change(planktoscope.imager_state_machine.Capture) + self.imager_client.client.message_callback_remove("status/pump") + self.imager_client.client.unsubscribe("status/pump") + else: + logger.info(f"the pump is not done yet {payload}") + else: + logger.error( + "There is an error, status is not waiting for the pump and yet we received a pump message" + ) + + @logger.catch + def treat_message(self): + action = "" + if self.imager_client.new_message_received(): + logger.info("We received a new message") + last_message = self.imager_client.msg["payload"] + logger.debug(last_message) + action = self.imager_client.msg["payload"]["action"] + logger.debug(action) + self.imager_client.read_message() + + # If the command is "image" + if action == "image": + # {"action":"image","sleep":5,"volume":1,"nb_frame":200} + if ( + "sleep" not in last_message + or "volume" not in last_message + or "nb_frame" not in last_message + ): + logger.error( + f"The received message has the wrong argument {last_message}" + ) + self.imager_client.client.publish("status/imager", '{"status":"Error"}') + return + + # Change the state of the machine + self.__imager.change(planktoscope.imager_state_machine.Imaging) + + # Get duration to wait before an image from the different received arguments + self.__sleep_before = float(last_message["sleep"]) + # Get volume in between two images from the different received arguments + self.__pump_volume = float(last_message["volume"]) + # Get the number of frames to image from the different received arguments + self.__img_goal = int(last_message["nb_frame"]) + + self.imager_client.client.publish("status/imager", '{"status":"Started"}') + + elif action == "stop": + # Remove callback for "status/pump" and unsubscribe + self.imager_client.client.message_callback_remove("status/pump") + self.imager_client.client.unsubscribe("status/pump") + + # Stops the pump + self.imager_client.client.publish("actuator/pump", '{"action": "stop"}') + + logger.info("The imaging has been interrupted.") + + # Publish the status "Interrupted" to via MQTT to Node-RED + self.imager_client.client.publish( + "status/imager", '{"status":"Interrupted"}' + ) + + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 0) + + # Change state to Stop + self.__imager.change(planktoscope.imager_state_machine.Stop) + + elif action == "update_config": + if self.__imager.state.name is "stop": + if "config" not in last_message: + logger.error( + f"The received message has the wrong argument {last_message}" + ) + self.imager_client.client.publish( + "status/imager", '{"status":"Configuration message error"}' + ) + return + logger.info("Updating the configuration now with the received data") + # Updating the configuration with the passed parameter in payload["config"] + nodered_metadata = last_message["config"] + # Definition of the few important metadata + local_metadata = { + "process_datetime": datetime.datetime.now().isoformat(), + "acq_camera_resolution": self.__resolution, + "acq_camera_iso": self.__iso, + "acq_camera_shutter_speed": self.__shutter_speed, + } + # Concat the local metadata and the metadata from Node-RED + self.__global_metadata = {**local_metadata, **nodered_metadata} + + # Publish the status "Config updated" to via MQTT to Node-RED + self.imager_client.client.publish( + "status/imager", '{"status":"Config updated"}' + ) + logger.info("Configuration has been updated") + else: + logger.error("We can't update the configuration while we are imaging.") + # Publish the status "Interrupted" to via MQTT to Node-RED + self.imager_client.client.publish("status/imager", '{"status":"Busy"}') + pass + + elif action == "settings": + if self.__imager.state.name is "stop": + if "settings" not in last_message: + logger.error( + f"The received message has the wrong argument {last_message}" + ) + self.imager_client.client.publish( + "status/imager", '{"status":"Camera settings error"}' + ) + return + logger.info("Updating the camera settings now with the received data") + # Updating the configuration with the passed parameter in payload["config"] + settings = last_message["settings"] + if "resolution" in settings: + self.__resolution = settings.get("resolution", self.__resolution) + logger.debug( + f"Updating the camera resolution to {self.__resolution}" + ) + self.__camera.resolution = self.__resolution + + if "iso" in settings: + self.__iso = settings.get("iso", self.__iso) + logger.debug(f"Updating the camera iso to {self.__iso}") + self.__camera.iso = self.__iso + + if "shutter_speed" in settings: + self.__shutter_speed = settings.get( + "shutter_speed", self.__shutter_speed + ) + logger.debug( + f"Updating the camera shutter speed to {self.__shutter_speed}" + ) + self.__camera.shutter_speed = self.__shutter_speed + + # Publish the status "Config updated" to via MQTT to Node-RED + self.imager_client.client.publish( + "status/imager", '{"status":"Camera settings updated"}' + ) + logger.info("Camera settings have been updated") + else: + logger.error( + "We can't update the camera settings while we are imaging." + ) + # Publish the status "Interrupted" to via MQTT to Node-RED + self.imager_client.client.publish("status/imager", '{"status":"Busy"}') + pass + + elif action != "": + logger.warning( + f"We did not understand the received request {action} - {last_message}" + ) + + @logger.catch + def state_machine(self): + if self.__imager.state.name is "imaging": + # subscribe to status/pump + self.imager_client.client.subscribe("status/pump") + self.imager_client.client.message_callback_add( + "status/pump", self.pump_callback + ) + + logger.info("Setting up the directory structure for storing the pictures") + self.__export_path = os.path.join( + self.__base_path, + # We only keep the date '2020-09-25T15:25:21.079769' + self.__global_metadata["process_datetime"].split("T")[0], + str(self.__global_metadata["sample_id"]), + ) + if not os.path.exists(self.__export_path): + # create the path! + os.makedirs(self.__export_path) + + # Export the metadata to a json file + logger.info("Exporting the metadata to a metadata.json") + config_path = os.path.join(self.__export_path, "metadata.json") + with open(config_path, "w") as metadata_file: + json.dump(self.__global_metadata, metadata_file) + logger.debug( + f"Metadata dumped in {metadata_file} are {self.__global_metadata}" + ) + + # Sleep a duration before to start acquisition + time.sleep(self.__sleep_before) + + # Set the LEDs as Blue + planktoscope.light.setRGB(0, 0, 255) + self.imager_client.client.publish( + "actuator/pump", + json.dumps( + { + "action": "move", + "direction": "BACKWARD", + "volume": self.__pump_volume, + "flowrate": 2, + } + ), + ) + # FIXME We should probably update the global metadata here with the current datetime/position/etc... + + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 0) + + # Change state towards Waiting for pump + self.__imager.change(planktoscope.imager_state_machine.Waiting) + return + + elif self.__imager.state.name is "capture": + # Set the LEDs as Cyan + planktoscope.light.setRGB(0, 255, 255) + + filename = f"{datetime.datetime.now().strftime('%H_%M_%S_%f')}.jpg" + + # Define the filename of the image + filename_path = os.path.join(self.__export_path, filename) + + logger.info(f"Capturing an image to {filename_path}") + + # Capture an image with the proper filename + self.__camera.capture(filename_path) + + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 0) + + # Publish the name of the image to via MQTT to Node-RED + self.imager_client.client.publish( + "status/imager", + f'{{"status":"{filename} has been imaged."}}', + ) + + # Increment the counter + self.__img_done += 1 + + # If counter reach the number of frame, break + if self.__img_done >= self.__img_goal: + # Reset the counter to 0 + self.__img_done = 0 + + # Publish the status "Done" to via MQTT to Node-RED + self.imager_client.client.publish("status/imager", '{"status":"Done"}') + + # Change state towards done + self.__imager.change(planktoscope.imager_state_machine.Stop) + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 255) + return + else: + # We have not reached the final stage, let's keep imaging + # Set the LEDs as Blue + planktoscope.light.setRGB(0, 0, 255) + + # subscribe to status/pump + self.imager_client.client.subscribe("status/pump") + self.imager_client.client.message_callback_add( + "status/pump", self.pump_callback + ) + + # Pump during a given volume + self.imager_client.client.publish( + "actuator/pump", + json.dumps( + { + "action": "move", + "direction": "BACKWARD", + "volume": self.__pump_volume, + "flowrate": 2, + } + ), + ) + + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 0) + + # Change state towards Waiting for pump + self.__imager.change(planktoscope.imager_state_machine.Waiting) + return + + elif self.__imager.state.name is "waiting": + return + + elif self.__imager.state.name is "stop": + return + + ################################################################################ + # While loop for capturing commands from Node-RED + ################################################################################ + @logger.catch + def run(self): + """This is the function that needs to be started to create a thread""" + logger.info( + f"The imager control thread has been started in process {os.getpid()}" + ) + # MQTT Service connection + self.imager_client = planktoscope.mqtt.MQTT_Client( + topic="imager/#", name="imager_client" + ) + + # PiCamera settings + self.__camera = picamera.PiCamera(resolution=self.__resolution) + self.__camera.iso = self.__iso + self.__camera.shutter_speed = self.__shutter_speed + self.__camera.exposure_mode = self.__exposure_mode + + address = ("", 8000) + server = StreamingServer(address, StreamingHandler) + # Starts the streaming server process + logger.info("Starting the streaming server thread") + self.start_camera() + self.streaming_thread = threading.Thread( + target=server.serve_forever, daemon=True + ) + self.streaming_thread.start() + + # Publish the status "Ready" to via MQTT to Node-RED + self.imager_client.client.publish("status/imager", '{"status":"Ready"}') + + logger.info("Let's rock and roll!") + + # This is the loop + while not self.stop_event.is_set(): + self.treat_message() + self.state_machine() + time.sleep(0.001) + + logger.info("Shutting down the imager process") + self.imager_client.client.publish("status/imager", '{"status":"Dead"}') + logger.debug("Stopping the camera") + self.__camera.stop_recording() + self.__camera.close() + logger.debug("Stopping the streaming thread") + server.shutdown() + self.imager_client.shutdown() + # self.streaming_thread.kill() + logger.info("Imager process shut down! See you!") diff --git a/scripts/planktoscope/imager_state_machine.py b/scripts/planktoscope/imager_state_machine.py new file mode 100644 index 0000000..e9d0514 --- /dev/null +++ b/scripts/planktoscope/imager_state_machine.py @@ -0,0 +1,71 @@ +# Logger library compatible with multiprocessing +from loguru import logger + +# TODO rewrite this in PlantUML +# This works with https://www.diagram.codes/d/state-machine +# "wait for pump" as pump +# "start imager" as imager +# "capture image" as capture +# +# START->imager["init"] +# imager->pump["start pumping"] +# pump->stop["stop"] +# stop->imager["start"] +# pump->capture["pumping is done"] +# capture->pump["start pump"] +# capture->stop["stop or done"] + + +# State machine class +class ImagerState(object): + name = "state" + allowed = [] + + def switch(self, state): + """ Switch to new state """ + if state.name in self.allowed: + logger.info(f"Current:{self} => switched to new state {state.name}") + self.__class__ = state + else: + logger.error(f"Current:{self} => switching to {state.name} not possible.") + + def __str__(self): + return self.name + + +class Stop(ImagerState): + name = "stop" + allowed = ["imaging"] + + +class Imaging(ImagerState): + """ State of getting ready to start """ + + name = "imaging" + allowed = ["waiting"] + + +class Waiting(ImagerState): + """ State of waiting for the pump to finish """ + + name = "waiting" + allowed = ["stop", "capture"] + + +class Capture(ImagerState): + """ State of capturing image """ + + name = "capture" + allowed = ["stop", "waiting"] + + +class Imager(object): + """ A class representing the imager """ + + def __init__(self): + # State of the imager - default is stop. + self.state = Stop() + + def change(self, state): + """ Change state """ + self.state.switch(state) diff --git a/scripts/planktoscope/light.py b/scripts/planktoscope/light.py new file mode 100644 index 0000000..c455f96 --- /dev/null +++ b/scripts/planktoscope/light.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# Turn on using this command line : +# python3.7 path/to/file/light.py on + +# Turn off using this command line : +# python3.7 path/to/file/light.py off + +# Library to send command over I2C for the light module on the fan +import smbus +import RPi.GPIO +import subprocess + +# define the bus used to actuate the light module on the fan +bus = smbus.SMBus(1) + +DEVICE_ADDRESS = 0x0D +rgb_effect_reg = 0x04 +rgb_speed_reg = 0x05 +rgb_color_reg = 0x06 +rgb_off_reg = 0x07 + +################################################################################ +# LEDs functions +################################################################################ +def setRGB(R, G, B): + """Update all LED at the same time""" + bus.write_byte_data(DEVICE_ADDRESS, 0x00, 0xFF) + bus.write_byte_data(DEVICE_ADDRESS, 0x01, R & 0xFF) + bus.write_byte_data(DEVICE_ADDRESS, 0x02, G & 0xFF) + bus.write_byte_data(DEVICE_ADDRESS, 0x03, B & 0xFF) + + # Update the I2C Bus in order to really update the LEDs new values + cmd = "i2cdetect -y 1" + subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) + + +def setRGBOff(): + """Turn off the RGB LED""" + bus.write_byte_data(DEVICE_ADDRESS, 0x07, 0x00) + + # Update the I2C Bus in order to really update the LEDs new values + cmd = "i2cdetect -y 1" + subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) + + +def setRGBEffect(effect): + """Choose an effect, 0-4 + + 0: Water light + 1: Breathing light + 2: Marquee + 3: Rainbow lights + 4: Colorful lights + """ + + if effect >= 0 and effect <= 4: + bus.write_byte_data(DEVICE_ADDRESS, rgb_effect_reg, effect & 0xFF) + + +def setRGBSpeed(speed): + """Set the effect speed, 1-3, 3 being the fastest speed""" + if speed >= 1 and speed <= 3: + bus.write_byte_data(DEVICE_ADDRESS, rgb_speed_reg, speed & 0xFF) + + +def setRGBColor(color): + """Set the color of the water light and breathing light effect, 0-6 + + 0: Red + 1: Green (default) + 2: Blue + 3: Yellow + 4: Purple + 5: Cyan + 6: White + """ + + if color >= 0 and color <= 6: + bus.write_byte_data(DEVICE_ADDRESS, rgb_color_reg, color & 0xFF) + + +def light(state): + """Turn the LED on or off""" + + if state == "on": + RPi.GPIO.output(21, RPi.GPIO.HIGH) + if state == "off": + RPi.GPIO.output(21, RPi.GPIO.LOW) + + +# This is called if this script is launched directly +if __name__ == "__main__": + import RPi.GPIO as GPIO + import sys + + GPIO.setmode(GPIO.BCM) + GPIO.setwarnings(False) + GPIO.setup(21, GPIO.OUT) + + state = str(sys.argv[1]) + + light(state) diff --git a/scripts/planktoscope/mqtt.py b/scripts/planktoscope/mqtt.py new file mode 100644 index 0000000..5e7ff8f --- /dev/null +++ b/scripts/planktoscope/mqtt.py @@ -0,0 +1,167 @@ +# Library for exchaning messages with Node-RED +# We are using MQTT V3.1.1 +# The documentation for Paho can be found here: +# https://www.eclipse.org/paho/clients/python/docs/ + +# MQTT Topics follows this architecture: +# - actuator : This topic adresses the stepper control thread +# No publication under this topic should happen from Python +# - actuator/pump : Control of the pump +# The message is a json object +# {"action":"move", "direction":"FORWARD", "volume":10, "flowrate":1} +# to move 10mL forward at 1mL/min +# action can be "move" or "stop" +# Receive only +# - actuator/focus : Control of the focus stage +# The message is a json object, speed is optional +# {"action":"move", "direction":"UP", "distance":0.26, "speed":1} +# to move up 10mm +# action can be "move" or "stop" +# Receive only +# - imager/image : This topic adresses the imaging thread +# Is a json object with +# {"action":"image","sleep":5,"volume":1,"nb_frame":200} +# sleep in seconds, volume in mL +# Receive only +# - segmenter/segment : This topic adresses the segmenter process +# Is a json object with +# {"action":"segment"} +# Receive only +# - status : This topics sends feedback to Node-Red +# No publication or receive at this level +# - status/pump : State of the pump +# Is a json object with +# {"status":"Start", "time_left":25} +# Status is one of Started, Ready, Done, Interrupted +# Publish only +# - status/focus : State of the focus stage +# Is a json object with +# {"status":"Start", "time_left":25} +# Status is one of Started, Ready, Done, Interrupted +# Publish only +# - status/imager : State of the imager +# Is a json object with +# {"status":"Start", "time_left":25} +# Status is one of Started, Ready, Completed or 12_11_15_0.1.jpg has been imaged. +# Publish only +# - status/segmenter : Status of the segmentation +# - status/segmenter/name +# - status/segmenter/object_id +# - status/segmenter/metric + +# TODO Evaluate the opportunity of saving the last x received messages in a queue for treatment +# We can use collections.deque https://docs.python.org/3/library/collections.html#collections.deque +import paho.mqtt.client as mqtt +import json +import planktoscope.light + +# Logger library compatible with multiprocessing +from loguru import logger + +logger.info("planktoscope.mqtt is loaded") + + +class MQTT_Client: + """A client for MQTT + + Do not forget to include the wildcards in the topic + when creating this object + """ + + def __init__(self, topic, server="127.0.0.1", port=1883, name="client"): + # Declare the global variables command and args + self.command = "" + self.args = "" + self.__new_message = False + self.msg = None + + # MQTT Client functions definition + self.client = mqtt.Client() + # self.client.enable_logger(logger) + self.topic = topic + self.server = server + self.port = port + self.name = name + self.connect() + + @logger.catch + def connect(self): + logger.info(f"trying to connect to {self.server}:{self.port}") + self.client.connect(self.server, self.port, 60) + self.client.on_connect = self.on_connect + self.client.on_subscribe = self.on_subscribe + self.client.on_message = self.on_message + self.client.on_disconnect = self.on_disconnect + self.client.loop_start() + + ################################################################################ + # MQTT core functions + ################################################################################ + + @logger.catch + # Run this function in order to connect to the client (Node-RED) + def on_connect(self, client, userdata, flags, rc): + reason = [ + "0: Connection successful", + "1: Connection refused - incorrect protocol version", + "2: Connection refused - invalid client identifier", + "3: Connection refused - server unavailable", + "4: Connection refused - bad username or password", + "5: Connection refused - not authorised", + ] + # Print when connected + logger.success( + f"{self.name} connected to {self.server}:{self.port}! - {reason[rc]}" + ) + # When connected, run subscribe() + self.client.subscribe(self.topic) + # Turn green the light module + planktoscope.light.setRGB(0, 255, 0) + + @logger.catch + # Run this function in order to subscribe to all the topics begining by actuator + def on_subscribe(self, client, obj, mid, granted_qos): + # Print when subscribed + logger.success( + f"{self.name} subscribed to {self.topic}! - mid:{str(mid)} qos:{str(granted_qos)}" + ) + + # Run this command when Node-RED is sending a message on the subscribed topic + @logger.catch + def on_message(self, client, userdata, msg): + # Print the topic and the message + logger.info(f"{self.name}: {msg.topic} {str(msg.qos)} {str(msg.payload)}") + # Parse the topic to find the command. ex : actuator/pump -> pump + # This only removes the top-level topic! + self.command = msg.topic.split("/", 1)[1] + logger.debug(f"command is {self.command}") + # Decode the message to find the arguments + self.args = json.loads(msg.payload.decode()) + logger.debug(f"args are {self.args}") + self.msg = {"topic": msg.topic, "payload": self.args} + logger.debug(f"msg is {self.msg} or {msg}") + self.__new_message = True + + @logger.catch + def on_disconnect(self, client, userdata, rc): + if rc != 0: + logger.error( + f"Connection to the MQTT server is unexpectedly lost by {self.name}" + ) + else: + logger.warning(f"Connection to the MQTT server is closed by {self.name}") + # TODO for now, we just log the disconnection, we need to evaluate what to do + # in case of communication loss with the server + + def new_message_received(self): + return self.__new_message + + def read_message(self): + logger.debug(f"clearing the __new_message flag") + self.__new_message = False + + @logger.catch + def shutdown(self, topic="", message=""): + logger.info(f"Shutting down mqtt client {self.name}") + self.client.loop_stop() + logger.debug(f"Mqtt client {self.name} shut down") \ No newline at end of file diff --git a/scripts/planktoscope/segmenter.py b/scripts/planktoscope/segmenter.py new file mode 100644 index 0000000..8a3eeed --- /dev/null +++ b/scripts/planktoscope/segmenter.py @@ -0,0 +1,338 @@ +################################################################################ +# Practical Libraries +################################################################################ + +# Logger library compatible with multiprocessing +from loguru import logger + +# Library to get date and time for folder name and filename +import datetime + +# Library to be able to sleep for a given duration +import time + +# Libraries manipulate json format, execute bash commands +import json, shutil, os + +# Library for starting processes +import multiprocessing + +# Basic planktoscope libraries +import planktoscope.mqtt +import planktoscope.light + + +################################################################################ +# Morphocut Libraries +################################################################################ +import morphocut +import morphocut.file +import morphocut.image +import morphocut.stat +import morphocut.stream +import morphocut.str +import morphocut.contrib.ecotaxa +import morphocut.contrib.zooprocess + +################################################################################ +# Other image processing Libraries +################################################################################ +import skimage.util +import cv2 + +logger.info("planktoscope.segmenter is loaded") + + +################################################################################ +# Main Segmenter class +################################################################################ +class SegmenterProcess(multiprocessing.Process): + """This class contains the main definitions for the segmenter of the PlanktoScope""" + + @logger.catch + def __init__(self, event): + """Initialize the Segmenter class + + Args: + event (multiprocessing.Event): shutdown event + """ + super(SegmenterProcess, self).__init__(name="segmenter") + + logger.info("planktoscope.segmenter is initialising") + + self.stop_event = event + self.__pipe = None + self.segmenter_client = None + self.__img_path = "/home/pi/PlanktonScope/img" + self.__export_path = "/home/pi/PlanktonScope/export" + self.__ecotaxa_path = os.path.join(self.__export_path, "ecotaxa") + self.__global_metadata = None + self.__working_path = "" + self.__archive_fn = "" + + if not os.path.exists(self.__ecotaxa_path): + # create the path! + os.makedirs(self.__ecotaxa_path) + # Morphocut's pipeline will be created at runtime otherwise shit ensues + + logger.info("planktoscope.segmenter is initialised and ready to go!") + + def __create_morphocut_pipeline(self): + """Creates the Morphocut Pipeline""" + logger.debug("Let's start creating the Morphocut Pipeline") + + with morphocut.Pipeline() as self.__pipe: + # TODO wrap morphocut.Call(logger.debug()) in something that allows it not to be added to the pipeline + # if the logger.level is not debug. Might not be as easy as it sounds. + # Recursively find .jpg files in import_path. + # Sort to get consective frames. + abs_path = morphocut.file.Find( + self.__working_path, [".jpg"], sort=True, verbose=True + ) + + # Extract name from abs_path + name = morphocut.Call( + lambda p: os.path.splitext(os.path.basename(p))[0], abs_path + ) + + # Set the LEDs as Green + morphocut.Call(planktoscope.light.setRGB, 0, 255, 0) + + # Read image + img = morphocut.image.ImageReader(abs_path) + + # Show progress bar for frames + morphocut.stream.TQDM(morphocut.str.Format("Frame {name}", name=name)) + + # Apply running median to approximate the background image + flat_field = morphocut.stat.RunningMedian(img, 5) + + # Correct image + img = img / flat_field + + # Rescale intensities and convert to uint8 to speed up calculations + img = morphocut.image.RescaleIntensity( + img, in_range=(0, 1.1), dtype="uint8" + ) + + # Filter variable to reduce memory load + morphocut.stream.FilterVariables(name, img) + + # Save cleaned images + # frame_fn = morphocut.str.Format(os.path.join("/home/pi/PlanktonScope/tmp","CLEAN", "{name}.jpg"), name=name) + # morphocut.image.ImageWriter(frame_fn, img) + + # Convert image to uint8 gray + img_gray = morphocut.image.RGB2Gray(img) + + # ? + img_gray = morphocut.Call(skimage.util.img_as_ubyte, img_gray) + + # Canny edge detection using OpenCV + img_canny = morphocut.Call(cv2.Canny, img_gray, 50, 100) + + # Dilate using OpenCV + kernel = morphocut.Call( + cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15) + ) + img_dilate = morphocut.Call(cv2.dilate, img_canny, kernel, iterations=2) + + # Close using OpenCV + kernel = morphocut.Call( + cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (5, 5) + ) + img_close = morphocut.Call( + cv2.morphologyEx, img_dilate, cv2.MORPH_CLOSE, kernel, iterations=1 + ) + + # Erode using OpenCV + kernel = morphocut.Call( + cv2.getStructuringElement, cv2.MORPH_ELLIPSE, (15, 15) + ) + mask = morphocut.Call(cv2.erode, img_close, kernel, iterations=2) + + # Find objects + regionprops = morphocut.image.FindRegions( + mask, img_gray, min_area=1000, padding=10, warn_empty=name + ) + + # Set the LEDs as Purple + morphocut.Call(planktoscope.light.setRGB, 255, 0, 255) + + # For an object, extract a vignette/ROI from the image + roi_orig = morphocut.image.ExtractROI(img, regionprops, bg_color=255) + + # Generate an object identifier + i = morphocut.stream.Enumerate() + + # morphocut.Call(print,i) + + # Define the ID of each object + object_id = morphocut.str.Format("{name}_{i:d}", name=name, i=i) + + # morphocut.Call(print,object_id) + + # Define the name of each object + object_fn = morphocut.str.Format( + os.path.join("/home/pi/PlanktonScope/", "OBJECTS", "{name}.jpg"), + name=object_id, + ) + + # Save the image of the object with its name + morphocut.image.ImageWriter(object_fn, roi_orig) + + # Calculate features. The calculated features are added to the global_metadata. + # Returns a Variable representing a dict for every object in the stream. + meta = morphocut.contrib.zooprocess.CalculateZooProcessFeatures( + regionprops, prefix="object_", meta=self.__global_metadata + ) + + # Get all the metadata + json_meta = morphocut.Call(json.dumps, meta, sort_keys=True, default=str) + + # Publish the json containing all the metadata to via MQTT to Node-RED + morphocut.Call( + self.segmenter_client.client.publish, + "status/segmentater/metric", + json_meta, + ) + + # Add object_id to the metadata dictionary + meta["object_id"] = object_id + + # Generate object filenames + orig_fn = morphocut.str.Format("{object_id}.jpg", object_id=object_id) + + # Write objects to an EcoTaxa archive: + # roi image in original color, roi image in grayscale, metadata associated with each object + morphocut.contrib.ecotaxa.EcotaxaWriter( + self.__archive_fn, (orig_fn, roi_orig), meta + ) + + # Progress bar for objects + morphocut.stream.TQDM( + morphocut.str.Format("Object {object_id}", object_id=object_id) + ) + + # Publish the object_id to via MQTT to Node-RED + morphocut.Call( + self.segmenter_client.client.publish, + "status/segmentater/object_id", + f'{{"object_id":"{object_id}"}}', + ) + + # Set the LEDs as Green + morphocut.Call(planktoscope.light.setRGB, 0, 255, 0) + logger.info("Morphocut's Pipeline has been created") + + @logger.catch + def treat_message(self): + action = "" + if self.segmenter_client.new_message_received(): + logger.info("We received a new message") + last_message = self.segmenter_client.msg["payload"] + logger.debug(last_message) + action = self.segmenter_client.msg["payload"]["action"] + self.segmenter_client.read_message() + + # If the command is "segment" + if action == "segment": + # {"action":"segment"} + # Publish the status "Started" to via MQTT to Node-RED + self.segmenter_client.client.publish( + "status/segmenter", '{"status":"Started"}' + ) + + img_paths = [x[0] for x in os.walk(self.__img_path)] + logger.info(f"The pipeline will be run in {len(img_paths)} directories") + for path in img_paths: + logger.info(f"Loading the metadata file for {path}") + with open(os.path.join(path, "metadata.json"), "r") as config_file: + self.__global_metadata = json.load(config_file) + logger.debug(f"Configuration loaded is {self.__global_metadata}") + + # Define the name of the .zip file that will contain the images and the .tsv table for EcoTaxa + self.__archive_fn = os.path.join( + self.__ecotaxa_path, + # filename includes project name, timestamp and sample id + f"export_{self.__global_metadata['sample_project']}_{self.__global_metadata['process_datetime']}_{self.__global_metadata['sample_id']}.zip", + ) + + logger.info(f"Starting the pipeline in {path}") + # Start the MorphoCut Pipeline on the found path + self.__working_path = path + + try: + self.__pipe.run() + except Exception as e: + logger.exception(f"There was an error in the pipeline {e}") + logger.info(f"Pipeline has been run for {path}") + + # remove directory + # shutil.rmtree(import_path) + + # Publish the status "Done" to via MQTT to Node-RED + self.segmenter_client.client.publish( + "status/segmenter", '{"status":"Done"}' + ) + + # Set the LEDs as White + planktoscope.light.setRGB(255, 255, 255) + + # cmd = os.popen("rm -rf /home/pi/PlanktonScope/tmp/*.jpg") + + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 0) + + elif action == "stop": + logger.info("The segmentation has been interrupted.") + + # Publish the status "Interrupted" to via MQTT to Node-RED + self.segmenter_client.client.publish( + "status/segmenter", '{"status":"Interrupted"}' + ) + + elif action == "update_config": + logger.error("We can't update the configuration while we are segmenting.") + # Publish the status "Interrupted" to via MQTT to Node-RED + self.segmenter_client.client.publish( + "status/segmenter", '{"status":"Busy"}' + ) + pass + + elif action != "": + logger.warning( + f"We did not understand the received request {action} - {last_message}" + ) + + ################################################################################ + # While loop for capturing commands from Node-RED + ################################################################################ + @logger.catch + def run(self): + """This is the function that needs to be started to create a thread""" + logger.info( + f"The segmenter control thread has been started in process {os.getpid()}" + ) + # MQTT Service connection + self.segmenter_client = planktoscope.mqtt.MQTT_Client( + topic="segmenter/#", name="segmenter_client" + ) + + # Instantiate the morphocut pipeline + self.__create_morphocut_pipeline() + + # Publish the status "Ready" to via MQTT to Node-RED + self.segmenter_client.client.publish("status/segmenter", '{"status":"Ready"}') + + logger.info("Ready to roll!") + + # This is the loop + while not self.stop_event.is_set(): + self.treat_message() + time.sleep(0.5) + + logger.info("Shutting down the segmenter process") + self.segmenter_client.client.publish("status/segmenter", '{"status":"Dead"}') + self.segmenter_client.shutdown() + logger.info("Segmenter process shut down! See you!") diff --git a/scripts/planktoscope/stepper.py b/scripts/planktoscope/stepper.py new file mode 100644 index 0000000..e62bd2e --- /dev/null +++ b/scripts/planktoscope/stepper.py @@ -0,0 +1,440 @@ +# Libraries to control the steppers for focusing and pumping +import adafruit_motor +import adafruit_motorkit +import time +import json +import os +import planktoscope.mqtt +import planktoscope.light +import multiprocessing + +# Logger library compatible with multiprocessing +from loguru import logger + +logger.info("planktoscope.stepper is loaded") + + +class stepper: + def __init__(self, stepper, style, size=0): + """Initialize the stepper class + + Args: + stepper (adafruit_motorkit.Motorkit().stepper): reference to the object that controls the stepper + style (adafruit_motor.stepper): style of the movement SINGLE, DOUBLE, MICROSTEP + size (int): maximum number of steps of this stepper (aka stage size). Can be 0 if not applicable + """ + self.__stepper = stepper + self.__style = style + self.__size = size + self.__position = 0 + self.__goal = 0 + self.__direction = "" + self.__next_step_date = time.monotonic() + self.__delay = 0 + # Make sure the stepper is released and do not use any power + self.__stepper.release() + + def step_waiting(self): + """Is there a step waiting to be actuated + + Returns: + Bool: if time has come to push the step + """ + return time.monotonic() > self.__next_step_date + + def at_goal(self): + """Is the motor at its goal + + Returns: + Bool: True if position and goal are identical + """ + return self.__position == self.__goal + + def next_step_date(self): + """set the next step date""" + self.__next_step_date = self.__next_step_date + self.__delay + + def initial_step_date(self): + """set the initial step date""" + self.__next_step_date = time.monotonic() + self.__delay + + def move(self): + """move the stepper + + Returns: + Bool: True if the step done was the last one of the movement + """ + if self.step_waiting() and not self.at_goal(): + self.__stepper.onestep( + direction=self.__direction, + style=self.__style, + ) + if self.__direction == adafruit_motor.stepper.FORWARD: + self.__position += 1 + elif self.__direction == adafruit_motor.stepper.BACKWARD: + self.__position -= 1 + if not self.at_goal(): + self.next_step_date() + else: + logger.success("The stepper has reached its goal") + self.__stepper.release() + return True + return False + + def go(self, direction, distance, delay): + """move in the given direction for the given distance + + Args: + direction (adafruit_motor.stepper): gives the movement direction + distance + delay + """ + self.__delay = delay + self.__direction = direction + if self.__direction == adafruit_motor.stepper.FORWARD: + self.__goal = self.__position + distance + elif self.__direction == adafruit_motor.stepper.BACKWARD: + self.__goal = self.__position - distance + else: + logger.error(f"The given direction is wrong {direction}") + self.initial_step_date() + + def shutdown(self): + """Shutdown everything ASAP""" + self.__goal = self.__position + self.__stepper.release() + + def release(self): + self.__stepper.release() + + +class StepperProcess(multiprocessing.Process): + + focus_steps_per_mm = 40 + # 507 steps per ml for Planktonscope standard + # 5200 for custom NEMA14 pump with 0.8mm ID Tube + pump_steps_per_ml = 507 + # focus max speed is in mm/sec and is limited by the maximum number of pulses per second the Planktonscope can send + focus_max_speed = 0.5 + # pump max speed is in ml/min + pump_max_speed = 30 + + def __init__(self, event): + super(StepperProcess, self).__init__() + + self.stop_event = event + + if os.path.exists("/home/pi/PlanktonScope/hardware.json"): + # load hardware.json + with open("/home/pi/PlanktonScope/hardware.json", "r") as config_file: + configuration = json.load(config_file) + logger.debug(f"Hardware configuration loaded is {configuration}") + else: + logger.info( + "The hardware configuration file doesn't exists, using defaults" + ) + configuration = dict() + + reverse = False + + # parse the config data. If the key is absent, we are using the default value + reverse = configuration.get("stepper_reverse", reverse) + self.focus_steps_per_mm = configuration.get( + "focus_steps_per_mm", self.focus_steps_per_mm + ) + self.pump_steps_per_ml = configuration.get( + "pump_steps_per_ml", self.pump_steps_per_ml + ) + self.focus_max_speed = configuration.get( + "focus_max_speed", self.focus_max_speed + ) + self.pump_max_speed = configuration.get("pump_max_speed", self.pump_max_speed) + + # define the names for the 2 exsting steppers + kit = adafruit_motorkit.MotorKit() + if reverse: + self.pump_stepper = stepper(kit.stepper2, adafruit_motor.stepper.DOUBLE) + self.focus_stepper = stepper( + kit.stepper1, adafruit_motor.stepper.MICROSTEP, 45 + ) + else: + self.pump_stepper = stepper(kit.stepper1, adafruit_motor.stepper.DOUBLE) + self.focus_stepper = stepper( + kit.stepper2, adafruit_motor.stepper.MICROSTEP, 45 + ) + + logger.debug(f"Stepper initialisation is over") + + def treat_command(self): + command = "" + if self.actuator_client.new_message_received(): + logger.info("We received a new message") + last_message = self.actuator_client.msg["payload"] + logger.debug(last_message) + command = self.actuator_client.msg["topic"].split("/", 1)[1] + logger.debug(command) + self.actuator_client.read_message() + + # If the command is "pump" + if command == "pump": + logger.debug("We have received a pumping command") + if last_message["action"] == "stop": + logger.debug("We have received a stop pump command") + self.pump_stepper.shutdown() + + # Print status + logger.info("The pump has been interrupted") + + # Publish the status "Interrupted" to via MQTT to Node-RED + self.actuator_client.client.publish( + "status/pump", '{"status":"Interrupted"}' + ) + + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 0) + + elif last_message["action"] == "move": + logger.debug("We have received a move pump command") + # Set the LEDs as Blue + planktoscope.light.setRGB(0, 0, 255) + + if ( + "direction" not in last_message + or "volume" not in last_message + or "flowrate" not in last_message + ): + logger.error( + f"The received message has the wrong argument {last_message}" + ) + self.actuator_client.client.publish( + "status/pump", '{"status":"Error"}' + ) + return + # Get direction from the different received arguments + direction = last_message["direction"] + # Get delay (in between steps) from the different received arguments + volume = float(last_message["volume"]) + # Get number of steps from the different received arguments + flowrate = float(last_message["flowrate"]) + + # Print status + logger.info("The pump is started.") + self.pump(direction, volume, flowrate) + else: + logger.warning( + f"The received message was not understood {last_message}" + ) + + # If the command is "focus" + elif command == "focus": + logger.debug("We have received a focusing request") + # If a new received command is "focus" but args contains "stop" we stop! + if last_message["action"] == "stop": + logger.debug("We have received a stop focus command") + self.focus_stepper.shutdown() + + # Print status + logger.info("The focus has been interrupted") + + # Publish the status "Interrupted" to via MQTT to Node-RED + self.actuator_client.client.publish( + "status/focus", '{"status":"Interrupted"}' + ) + + # Set the LEDs as Green + planktoscope.light.setRGB(0, 255, 0) + + elif last_message["action"] == "move": + logger.debug("We have received a move focus command") + # Set the LEDs as Yellow + planktoscope.light.setRGB(255, 255, 0) + + if ( + "direction" not in last_message + or "distance" not in last_message + ): + logger.error( + f"The received message has the wrong argument {last_message}" + ) + self.actuator_client.client.publish( + "status/focus", '{"status":"Error"}' + ) + # Get direction from the different received arguments + direction = last_message["direction"] + # Get number of steps from the different received arguments + distance = float(last_message["distance"]) + + # Print status + logger.info("The focus movement is started.") + self.focus(direction, distance) + else: + logger.warning( + f"The received message was not understood {last_message}" + ) + elif command != "": + logger.warning( + f"We did not understand the received request {command} - {last_message}" + ) + return + + def focus(self, direction, distance, speed=focus_max_speed): + """moves the focus stepper + + direction is either UP or DOWN + distance is received in mm + speed is in mm/sec""" + + logger.info( + f"The focus stage will move {direction} for {distance}mm at {speed}mm/sec" + ) + + # Validation of inputs + if direction != "UP" and direction != "DOWN": + logger.error("The direction command is not recognised") + logger.error("It should be either UP or DOWN") + return + + if distance > 45: + logger.error("You are trying to move more than the stage physical size") + return + + # We are going to use microsteps, so we need to multiply by 16 the steps number + nb_steps = self.focus_steps_per_mm * distance * 16 + logger.debug(f"The number of steps that will be applied is {nb_steps}") + steps_per_second = speed * self.focus_steps_per_mm * 16 + logger.debug(f"There will be a speed of {steps_per_second} steps per second") + + if steps_per_second > 400: + steps_per_second = 400 + logger.warning("The requested speed is faster than the maximum safe speed") + logger.warning( + f"The speed of the motor is going to be limited to {steps_per_second/16/self.focus_steps_per_mm}mm/sec" + ) + + # On linux, the minimal acceptable delay managed by the system is 0.1ms + # see https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep + # However we have a fixed delay of at least 2.5ms per step due to the library + # Our maximum speed is thus about 400 pulses per second or 0.5mm/sec of stage speed + delay = max((1 / steps_per_second) - 0.0025, 0) + logger.debug(f"The delay between two steps is {delay}s") + + # Publish the status "Started" to via MQTT to Node-RED + self.actuator_client.client.publish( + "status/focus", + f'{{"status":"Started", "duration":{nb_steps / steps_per_second}}}', + ) + + # Depending on direction, select the right direction for the focus + if direction == "UP": + self.focus_stepper.go(adafruit_motor.stepper.FORWARD, nb_steps, delay) + + if direction == "DOWN": + self.focus_stepper.go(adafruit_motor.stepper.BACKWARD, nb_steps, delay) + + # The pump max speed will be at about 400 full steps per second + # This amounts to 0.9mL per seconds maximum, or 54mL/min + # NEMA14 pump with 3 rollers is 0.509 mL per round, actual calculation at + # Stepper is 200 steps/round, or 393steps/ml + # https://www.wolframalpha.com/input/?i=pi+*+%280.8mm%29%C2%B2+*+54mm+*+3 + def pump(self, direction, volume, speed=pump_max_speed): + """moves the pump stepper + + direction is either FORWARD or BACKWARD + volume is in mL + speed is in mL/min""" + + logger.info(f"The pump will move {direction} for {volume}mL at {speed}mL/min") + + # Validation of inputs + if direction != "FORWARD" and direction != "BACKWARD": + logger.error("The direction command is not recognised") + logger.error("It should be either FORWARD or BACKWARD") + return + + nb_steps = self.pump_steps_per_ml * volume + logger.debug(f"The number of steps that will be applied is {nb_steps}") + steps_per_second = speed * self.pump_steps_per_ml / 60 + logger.debug(f"There will be a speed of {steps_per_second} steps per second") + + if steps_per_second > 400: + steps_per_second = 400 + logger.warning("The requested speed is faster than the maximum safe speed") + logger.warning( + f"The speed of the motor is going to be limited to {steps_per_second*60/self.pump_steps_per_ml}mL/min" + ) + # On linux, the minimal acceptable delay managed by the system is 0.1ms + # see https://stackoverflow.com/questions/1133857/how-accurate-is-pythons-time-sleep + # However we have a fixed delay of at least 2.5ms per step due to the library + # Our maximum speed is thus about 400 pulses per second or 2 turn per second of the pump + # 10mL => 6140 pas + # 1.18gr => 1.18mL + # Actual pas/mL => 5200 + # Max speed is 400 steps/sec, or 4.6mL/min + # 15mL at 3mL/min + # nb_steps = 5200 * 15 = 78000 + # sps = 3mL/min * 5200s/mL = 15600s/min / 60 => 260sps + delay = max((1 / steps_per_second) - 0.0025, 0) + logger.debug(f"The delay between two steps is {delay}s") + + # Publish the status "Started" to via MQTT to Node-RED + self.actuator_client.client.publish( + "status/pump", + f'{{"status":"Started", "duration":{nb_steps / steps_per_second}}}', + ) + + # Depending on direction, select the right direction for the focus + if direction == "FORWARD": + self.pump_stepper.go(adafruit_motor.stepper.FORWARD, nb_steps, delay) + + if direction == "BACKWARD": + self.pump_stepper.go(adafruit_motor.stepper.BACKWARD, nb_steps, delay) + + @logger.catch + def run(self): + """This is the function that needs to be started to create a thread""" + logger.info( + f"The stepper control process has been started in process {os.getpid()}" + ) + + # Creates the MQTT Client + # We have to create it here, otherwise when the process running run is started + # it doesn't see changes and calls made by self.actuator_client because this one + # only exist in the master process + # see https://stackoverflow.com/questions/17172878/using-pythons-multiprocessing-process-class + self.actuator_client = planktoscope.mqtt.MQTT_Client( + topic="actuator/#", name="actuator_client" + ) + # Publish the status "Ready" to via MQTT to Node-RED + self.actuator_client.client.publish("status/pump", '{"status":"Ready"}') + # Publish the status "Ready" to via MQTT to Node-RED + self.actuator_client.client.publish("status/focus", '{"status":"Ready"}') + while not self.stop_event.is_set(): + # check if a new message has been received + self.treat_command() + if self.pump_stepper.move(): + self.actuator_client.client.publish( + "status/pump", + '{"status":"Done"}', + ) + if self.focus_stepper.move(): + self.actuator_client.client.publish( + "status/focus", + '{"status":"Done"}', + ) + time.sleep(0) + logger.info("Shutting down the stepper process") + self.actuator_client.client.publish("status/pump", '{"status":"Dead"}') + self.actuator_client.client.publish("status/focus", '{"status":"Dead"}') + self.pump_stepper.shutdown() + self.focus_stepper.shutdown() + self.actuator_client.shutdown() + logger.info("Stepper process shut down! See you!") + + +# This is called if this script is launched directly +if __name__ == "__main__": + # Starts the stepper thread for actuators + # This needs to be in a threading or multiprocessing wrapper + stepper_thread = StepperProcess() + stepper_thread.start() + stepper_thread.join() \ No newline at end of file diff --git a/scripts/pump.py b/scripts/pump.py deleted file mode 100644 index 0e17830..0000000 --- a/scripts/pump.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python - -#Strating pumping foward a vol of 24ml with a flowrate of 3.2ml/min, use this command line : -#python3.7 path/to/file/pump.py 24 3.2 foward - -from adafruit_motor import stepper -from adafruit_motorkit import MotorKit -from time import sleep - -import sys - -volume = int(sys.argv[1]) -flowrate = float(sys.argv[2]) -action = str(sys.argv[3]) - -kit = MotorKit() - -pump_stepper = kit.stepper1 - -pump_stepper.release() - -def pump(volume, flowrate, action): - - if action == "foward": - action=stepper.BACKWARD - if action == "backward": - action=stepper.FORWARD - - nb_step=volume*507 #if sleep(0.05) in between 2 steps - #35000steps for 69g - - #nb_step=vol*460 if sleep(0) in between 2 steps - duration=(volume*60)/flowrate - - delay=(duration/nb_step)-0.005 - - for i in range(nb_step): - pump_stepper.onestep(direction=action, style=stepper.DOUBLE) - sleep(delay) - - sleep(1) - pump_stepper.release() - -#volume, flowrate (from 0 to 20), direction (foward or backward) -pump(volume, flowrate, action) -