From 7fb50ee8a472b9d18c6808ab7d5d36b87e8c9ea5 Mon Sep 17 00:00:00 2001 From: ahiuchingau <20424172+ahiuchingau@users.noreply.github.com> Date: Tue, 7 Apr 2020 19:16:58 -0400 Subject: [PATCH] refactor(api): deploy libgpiod for gpio control and deprecate sysfs closes #5310 --- api/Pipfile | 2 + api/Pipfile.lock | 122 +++++--- api/src/opentrons/drivers/rpi_drivers/gpio.py | 286 ++++++++---------- .../drivers/smoothie_drivers/driver_3_0.py | 47 +-- api/src/opentrons/hardware_control/api.py | 2 +- .../opentrons/hardware_control/controller.py | 26 +- api/src/opentrons/main.py | 3 + api/tests/opentrons/conftest.py | 2 + 8 files changed, 258 insertions(+), 232 deletions(-) diff --git a/api/Pipfile b/api/Pipfile index eea4116a2b9..918ee4c697f 100755 --- a/api/Pipfile +++ b/api/Pipfile @@ -24,6 +24,7 @@ twine = "==2.0.0" typing-extensions = "*" wheel = "==0.30.0" "e1839a8" = {editable = true, path = "."} +gpiod = {version = "*", markers = "sys_platform == 'linux'"} [packages] aiohttp = "==3.4.4" @@ -35,3 +36,4 @@ pyserial = "==3.4" systemd-python = {version="==234", sys_platform="== 'linux'"} urwid = "==1.3.1" pydantic = "==1.4.0" +gpiod = {version = "*", markers = "sys_platform=='linux'"} diff --git a/api/Pipfile.lock b/api/Pipfile.lock index 8fa1219098a..91cfc21017a 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ce1f50ab611e98587b1d1656beca77590d4a9a8f166d22f72cc74b2831f6ddd0" + "sha256": "6d496a60682121b84dbd2195738cf1b85c6be1e896d1e3bdd7630bdbb162f08b" }, "pipfile-spec": 6, "requires": { @@ -86,6 +86,14 @@ ], "version": "==1.0.2" }, + "gpiod": { + "hashes": [ + "sha256:86c651283f2af74036fccb8f9cec65af0bbccf140a4605d1e1b81c66e510b33f" + ], + "index": "pypi", + "markers": "sys_platform == 'linux'", + "version": "==0.6.0" + }, "idna": { "hashes": [ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", @@ -164,6 +172,20 @@ "index": "pypi", "version": "==1.15.1" }, + "pkgconfig": { + "hashes": [ + "sha256:97bfe3d981bab675d5ea3ef259045d7919c93897db7d3b59d4e8593cba8d354f", + "sha256:cddf2d7ecadb272178a942eb852a9dee46bda2adcc36c3416b0fef47a4ed9f38" + ], + "version": "==1.5.1" + }, + "pybind11": { + "hashes": [ + "sha256:205d9f9615eac190811cb8c3c2b2f95f6844ddba0fa0a1d45d00793338741601", + "sha256:ea5a4e7a880112915463826f1acbec5892df36dfe102ecb249229ac514fb54ad" + ], + "version": "==2.5.0" + }, "pydantic": { "hashes": [ "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752", @@ -186,9 +208,9 @@ }, "pyrsistent": { "hashes": [ - "sha256:cdc7b5e3ed77bed61270a47d35434a30617b9becdf2478af76ad2c6ade307280" + "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3" ], - "version": "==0.15.7" + "version": "==0.16.0" }, "pyserial": { "hashes": [ @@ -325,17 +347,17 @@ }, "bleach": { "hashes": [ - "sha256:44f69771e2ac81ff30d929d485b7f9919f3ad6d019b6b20c74f3b8687c3f70df", - "sha256:aa8b870d0f46965bac2c073a93444636b0e1ca74e9777e34f03dd494b8a59d48" + "sha256:cc8da25076a1fe56c3ac63671e2194458e0c4d9c7becfd52ca251650d517903c", + "sha256:e78e426105ac07026ba098f04de8abe9b6e3e98b5befbf89b51a5ef0a4292b03" ], - "version": "==3.1.1" + "version": "==3.1.4" }, "certifi": { "hashes": [ - "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", - "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" + "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304", + "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519" ], - "version": "==2019.11.28" + "version": "==2020.4.5.1" }, "chardet": { "hashes": [ @@ -415,6 +437,14 @@ "editable": true, "path": "." }, + "gpiod": { + "hashes": [ + "sha256:86c651283f2af74036fccb8f9cec65af0bbccf140a4605d1e1b81c66e510b33f" + ], + "index": "pypi", + "markers": "sys_platform == 'linux'", + "version": "==0.6.0" + }, "idna": { "hashes": [ "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", @@ -431,11 +461,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302", - "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b" + "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", + "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" ], "markers": "python_version < '3.8'", - "version": "==1.5.0" + "version": "==1.6.0" }, "jinja2": { "hashes": [ @@ -610,6 +640,13 @@ ], "version": "==20.3" }, + "pkgconfig": { + "hashes": [ + "sha256:97bfe3d981bab675d5ea3ef259045d7919c93897db7d3b59d4e8593cba8d354f", + "sha256:cddf2d7ecadb272178a942eb852a9dee46bda2adcc36c3416b0fef47a4ed9f38" + ], + "version": "==1.5.1" + }, "pkginfo": { "hashes": [ "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", @@ -631,6 +668,13 @@ ], "version": "==1.8.1" }, + "pybind11": { + "hashes": [ + "sha256:205d9f9615eac190811cb8c3c2b2f95f6844ddba0fa0a1d45d00793338741601", + "sha256:ea5a4e7a880112915463826f1acbec5892df36dfe102ecb249229ac514fb54ad" + ], + "version": "==2.5.0" + }, "pycodestyle": { "hashes": [ "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", @@ -669,16 +713,16 @@ }, "pyparsing": { "hashes": [ - "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", - "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "version": "==2.4.6" + "version": "==2.4.7" }, "pyrsistent": { "hashes": [ - "sha256:cdc7b5e3ed77bed61270a47d35434a30617b9becdf2478af76ad2c6ade307280" + "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3" ], - "version": "==0.15.7" + "version": "==0.16.0" }, "pyserial": { "hashes": [ @@ -721,19 +765,19 @@ }, "pyyaml": { "hashes": [ - "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6", - "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf", - "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5", - "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e", - "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811", - "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e", - "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d", - "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20", - "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689", - "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994", - "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615" + "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", + "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", + "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", + "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", + "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", + "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", + "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", + "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", + "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", + "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", + "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], - "version": "==5.3" + "version": "==5.3.1" }, "readme-renderer": { "hashes": [ @@ -822,10 +866,10 @@ }, "tqdm": { "hashes": [ - "sha256:0d8b5afb66e23d80433102e9bd8b5c8b65d34c2a2255b2de58d97bd2ea8170fd", - "sha256:f35fb121bafa030bd94e74fcfd44f3c2830039a2ddef7fc87ef1c2d205237b24" + "sha256:00339634a22c10a7a22476ee946bbde2dbe48d042ded784e4d88e0236eca5d81", + "sha256:ea9e3fd6bd9a37e8783d75bfc4c1faf3c6813da6bd1c3e776488b41ec683af94" ], - "version": "==4.43.0" + "version": "==4.45.0" }, "twine": { "hashes": [ @@ -863,12 +907,12 @@ }, "typing-extensions": { "hashes": [ - "sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", - "sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", - "sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" + "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", + "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", + "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" ], "index": "pypi", - "version": "==3.7.4.1" + "version": "==3.7.4.2" }, "urllib3": { "hashes": [ @@ -886,10 +930,10 @@ }, "wcwidth": { "hashes": [ - "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603", - "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8" + "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", + "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" ], - "version": "==0.1.8" + "version": "==0.1.9" }, "webencodings": { "hashes": [ diff --git a/api/src/opentrons/drivers/rpi_drivers/gpio.py b/api/src/opentrons/drivers/rpi_drivers/gpio.py index 400ae486972..448b6bb4159 100755 --- a/api/src/opentrons/drivers/rpi_drivers/gpio.py +++ b/api/src/opentrons/drivers/rpi_drivers/gpio.py @@ -1,6 +1,16 @@ +import asyncio import os from sys import platform -from typing import Tuple +import logging +from typing import Dict, Tuple + +try: + import gpiod +except ImportError: + gpiod = None # type: ignore + +import systemd.daemon + """ Raspberry Pi GPIO control module @@ -22,13 +32,9 @@ functions that accept a pin number. The OUTPUT_PINS and INPUT_PINS dicts provide pin-mappings so calling code does not need to use raw integers. """ -import time - -IN = "in" -OUT = "out" -LOW = "0" -HIGH = "1" +LOW = 0 +HIGH = 1 # Note: in test pins are sorted by value, so listing them in that order here # makes it easier to read the tests. Pin numbers defined by bridge wiring @@ -49,171 +55,121 @@ 'WINDOW_INPUT': 20 } -_path_prefix = "/sys/class/gpio" - - -def _enable_pin(pin, direction): - """ - In order to enable a GPIO pin, the pin number must be written into - /sys/class/gpio/export, and then the direction ("in" or "out" must be - written to /sys/class/gpio/gpio/direction - - :param pin: An integer corresponding to the GPIO number of the pin in RPi - GPIO board numbering (not physical pin numbering) - - :param direction: "in" or "out" - """ - _write_value(pin, "{}/export".format(_path_prefix)) - _write_value(direction, "{0}/gpio{1}/direction".format(_path_prefix, pin)) - - -def _write_value(value, path): - """ - Writes specified value into path. Note that the value is wrapped in single - quotes in the command, to prevent injecting bash commands. - :param value: The value to write (usually a number or string) - :param path: A valid system path - """ - base_command = "echo '{0}' > {1}" - # There is no common method for redirecting stderr to a null sink, so the - # command string is platform-dependent - if platform == 'win32': - command = "{0} > NUL".format(base_command) - else: - command = "exec 2> /dev/null; {0}".format(base_command) - os.system(command.format(value, path)) - - -def _read_value(path): - """ - Reads value of specified path. - :param path: A valid system path - """ - read_value = 0 - if not os.path.exists(path): - # Path will generally only exist on a Raspberry Pi - pass - else: - with open(path) as f: - read_value = int(f.read()) - return read_value - - -def set_high(pin): - """ - Sets a pin high by number. This pin must have been previously initialized - and set up as with direction of OUT, otherwise this operation will not - behave as expected. - - High represents "on" for lights, and represents normal running state for - HALT and RESET pins. - - :param pin: An integer corresponding to the GPIO number of the pin in RPi - GPIO board numbering (not physical pin numbering) - """ - _write_value(HIGH, "{0}/gpio{1}/value".format(_path_prefix, pin)) - - -def set_low(pin): - """ - Sets a pin low by number. This pin must have been previously initialized - and set up as with direction of OUT, otherwise this operation will not - behave as expected. - - Low represents "off" for lights, and writing the RESET or HALT pins low - will terminate Smoothie operation until written high again. - - :param pin: An integer corresponding to the GPIO number of the pin in RPi - GPIO board numbering (not physical pin numbering) - """ - _write_value(LOW, "{0}/gpio{1}/value".format(_path_prefix, pin)) - - -def read(pin): - """ - Reads a pin's value. If the pin has been previously initialized with - a direction of IN, the value will be the input signal. If pin is configured - as OUT, the value will be the current output state. - - :param pin: An integer corresponding to the GPIO number of the pin in RPi - GPIO board numbering (not physical pin numbering) - """ - return _read_value("{0}/gpio{1}/value".format(_path_prefix, pin)) - - -def initialize(): - """ - All named pins in OUTPUT_PINS and INPUT_PINS are exported, and set the - HALT pin high (normal running state), since the default value after export - is low. - """ - for pin in sorted(OUTPUT_PINS.values()): - _enable_pin(pin, OUT) - - for pin in sorted(INPUT_PINS.values()): - _enable_pin(pin, IN) - - -def robot_startup_sequence(): - """ - Gets the robot ready for operation by initializing GPIO pins, resetting - the Smoothie and enabling the audio pin. This only needs to be done - after power cycling the machine. - """ - initialize() - - # audio-enable pin can stay HIGH always, unless there is noise coming - # from the amplifier, then we can set to LOW to disable the amplifier - set_high(OUTPUT_PINS['AUDIO_ENABLE']) - - # smoothieware programming pins, must be in a known state (HIGH) - set_high(OUTPUT_PINS['HALT']) - set_high(OUTPUT_PINS['ISP']) - set_low(OUTPUT_PINS['RESET']) - time.sleep(0.25) - set_high(OUTPUT_PINS['RESET']) - time.sleep(0.25) - - -def turn_on_blue_button_light(): - set_button_light(blue=True) - - -def set_button_light(red=False, green=False, blue=False): - color_pins = { - OUTPUT_PINS['RED_BUTTON']: red, - OUTPUT_PINS['GREEN_BUTTON']: green, - OUTPUT_PINS['BLUE_BUTTON']: blue - } - for pin, state in color_pins.items(): - if state: - set_high(pin) +MODULE_LOG = logging.getLogger(__name__) + + +class GPIOCharDev: + def __init__(self, chip_name: str): + self._chip = gpiod.Chip(chip_name) + self._lines = self._initialize() + + @property + def chip(self): + return self._chip + + @property + def lines(self) -> Dict: + return self._lines + + def _request_line(self, offset, name, request_type): + line = self.chip.get_line(offset) + try: + line.request(consumer=name, type=request_type, default_vals=[0]) + except OSError: + MODULE_LOG.error(f'Line {offset} is busy. Cannot establish {name}') + return line + + def _initialize(self): + lines = {} + # setup input lines + for name, offset in INPUT_PINS.items(): + lines[offset] = self._request_line(offset, name, gpiod.LINE_REQ_DIR_IN) + # setup output lines + for name, offset in OUTPUT_PINS.items(): + if name is not 'BLUE_BUTTON': + lines[offset] = self._request_line(offset, name, gpiod.LINE_REQ_DIR_OUT) + MODULE_LOG.info(f'{lines}') + return lines + + async def setup_blue_button(self): + systemd.daemon.notify("READY=1") + await asyncio.sleep(0.25) + target = 'BLUE_BUTTON' + line = self._request_line(OUTPUT_PINS[target], target, gpiod.LINE_REQ_DIR_OUT) + self.lines[OUTPUT_PINS[target]] = line + self.set_button_light(blue=True) + + async def setup(self): + # smoothieware programming pins, must be in a known state (HIGH) + self.set_halt_pin(True) + self.set_isp_pin(True) + self.set_reset_pin(False) + await asyncio.sleep(0.25) + self.set_reset_pin(True) + await asyncio.sleep(0.25) + + def set_high(self, offset): + self.lines[offset].set_value(HIGH) + + def set_low(self, offset): + self.lines[offset].set_value(LOW) + + def set_button_light(self, + red: bool = False, + green: bool = False, + blue: bool = False): + color_pins = { + OUTPUT_PINS['RED_BUTTON']: red, + OUTPUT_PINS['GREEN_BUTTON']: green, + OUTPUT_PINS['BLUE_BUTTON']: blue} + for pin, state in color_pins.items(): + if state: + self.set_high(pin) + else: + self.set_low(pin) + + def set_rail_lights(self, on: bool = True): + if on: + self.set_high(OUTPUT_PINS['FRAME_LEDS']) else: - set_low(pin) + self.set_low(OUTPUT_PINS['FRAME_LEDS']) + def set_reset_pin(self, on: bool = True): + if on: + self.set_high(OUTPUT_PINS['RESET']) + else: + self.set_low(OUTPUT_PINS['RESET']) -def get_button_light() -> Tuple[bool, bool, bool]: - return (read(OUTPUT_PINS['RED_BUTTON']) == 1, - read(OUTPUT_PINS['GREEN_BUTTON']) == 1, - read(OUTPUT_PINS['BLUE_BUTTON']) == 1) - + def set_isp_pin(self, on: bool = True): + if on: + self.set_high(OUTPUT_PINS['ISP']) + else: + self.set_low(OUTPUT_PINS['ISP']) -def set_rail_lights(on=True): - if on: - set_high(OUTPUT_PINS['FRAME_LEDS']) - else: - set_low(OUTPUT_PINS['FRAME_LEDS']) + def set_halt_pin(self, on: bool = True): + if on: + self.set_high(OUTPUT_PINS['HALT']) + else: + self.set_low(OUTPUT_PINS['HALT']) + def _read(self, offset): + return self.lines[offset].get_value() -def get_rail_lights() -> bool: - value = read(OUTPUT_PINS['FRAME_LEDS']) - return True if value == 1 else False + def get_button_light(self) -> Tuple[bool, bool, bool]: + return (bool(self._read(OUTPUT_PINS['RED_BUTTON'])), + bool(self._read(OUTPUT_PINS['GREEN_BUTTON'])), + bool(self._read(OUTPUT_PINS['BLUE_BUTTON']))) + def get_rail_lights(self) -> bool: + return bool(self._read(OUTPUT_PINS['FRAME_LEDS'])) -def read_button(): - # button is normal-HIGH, so invert - return not bool(read(INPUT_PINS['BUTTON_INPUT'])) + def read_button(self) -> bool: + # button is normal-HIGH, so invert + return not bool(self._read(INPUT_PINS['BUTTON_INPUT'])) + def read_window_switches(self) -> bool: + return bool(self._read(INPUT_PINS['WINDOW_INPUT'])) -def read_window_switches(): - return bool(read(INPUT_PINS['WINDOW_INPUT'])) + def release_line(self, offset): + self.lines[offset].release() + self.lines.pop(offset) diff --git a/api/src/opentrons/drivers/smoothie_drivers/driver_3_0.py b/api/src/opentrons/drivers/smoothie_drivers/driver_3_0.py index a79a51ce2b8..9f2dbefdc1f 100755 --- a/api/src/opentrons/drivers/smoothie_drivers/driver_3_0.py +++ b/api/src/opentrons/drivers/smoothie_drivers/driver_3_0.py @@ -308,7 +308,7 @@ def _parse_homing_status_values(raw_homing_status_values): class SmoothieDriver_3_0_0: - def __init__(self, config, handle_locks=True): + def __init__(self, config, gpio_chardev=None, handle_locks=True): self.run_flag = Event() self.run_flag.set() @@ -322,6 +322,8 @@ def __init__(self, config, handle_locks=True): self._connection = None self._config = config + self._gpio_chardev = gpio_chardev + # Current settings: # The amperage of each axis, has been organized into three states: # Current-Settings is the amperage each axis was last set to @@ -1826,38 +1828,39 @@ def probe_axis( raise RuntimeError("Cant probe axis {}".format(axis)) def turn_on_blue_button_light(self): - gpio.set_button_light(blue=True) + self._gpio_chardev.set_button_light(blue=True) def turn_on_red_button_light(self): - gpio.set_button_light(red=True) + self._gpio_chardev.set_button_light(red=True) def turn_off_button_light(self): - gpio.set_button_light(red=False, green=False, blue=False) + self._gpio_chardev.set_button_light( + red=False, green=False, blue=False) def turn_on_rail_lights(self): - gpio.set_rail_lights(True) + self._gpio_chardev.set_rail_lights(True) def turn_off_rail_lights(self): - gpio.set_rail_lights(False) + self._gpio_chardev.set_rail_lights(False) def get_rail_lights_on(self): - return gpio.get_rail_lights() + return self._gpio_chardev.get_rail_lights() def read_button(self): - return gpio.read_button() + return self._gpio_chardev.read_button() def read_window_switches(self): - return gpio.read_window_switches() + return self._gpio_chardev.read_window_switches() def set_lights(self, button: bool = None, rails: bool = None): if button is not None: - gpio.set_button_light(blue=button) + self._gpio_chardev.set_button_light(blue=button) if rails is not None: - gpio.set_rail_lights(rails) + self._gpio_chardev.set_rail_lights(rails) def get_lights(self) -> Dict[str, bool]: - return {'button': gpio.get_button_light()[2], - 'rails': gpio.get_rail_lights()} + return {'button': self._gpio_chardev.get_button_light()[2], + 'rails': self._gpio_chardev.get_rail_lights()} def kill(self): """ @@ -1892,10 +1895,10 @@ def _smoothie_reset(self): if self.simulating: pass else: - gpio.set_low(gpio.OUTPUT_PINS['RESET']) - gpio.set_high(gpio.OUTPUT_PINS['ISP']) + self._gpio_chardev.set_reset_pin(False) + self._gpio_chardev.set_isp_pin(True) sleep(0.25) - gpio.set_high(gpio.OUTPUT_PINS['RESET']) + self._gpio_chardev.set_reset_pin(True) sleep(0.25) self._wait_for_ack() self._reset_from_error() @@ -1906,12 +1909,12 @@ def _smoothie_programming_mode(self): if self.simulating: pass else: - gpio.set_low(gpio.OUTPUT_PINS['RESET']) - gpio.set_low(gpio.OUTPUT_PINS['ISP']) + self._gpio_chardev.set_reset_pin(False) + self._gpio_chardev.set_isp_pin(False) sleep(0.25) - gpio.set_high(gpio.OUTPUT_PINS['RESET']) + self._gpio_chardev.set_reset_pin(True) sleep(0.25) - gpio.set_high(gpio.OUTPUT_PINS['ISP']) + self._gpio_chardev.set_isp_pin(True) sleep(0.25) def hard_halt(self): @@ -1921,9 +1924,9 @@ def hard_halt(self): pass else: self._is_hard_halting.set() - gpio.set_low(gpio.OUTPUT_PINS['HALT']) + self._gpio_chardev.set_halt_pin(False) sleep(0.25) - gpio.set_high(gpio.OUTPUT_PINS['HALT']) + self._gpio_chardev.set_halt_pin(True) sleep(0.25) self.run_flag.set() diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index e2a399109cd..ed1f1f8dd8e 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -20,7 +20,6 @@ MustHomeError, NoTipAttachedError) from . import modules - mod_log = logging.getLogger(__name__) @@ -92,6 +91,7 @@ async def build_hardware_controller( """ checked_loop = use_or_initialize_loop(loop) backend = Controller(config) + await backend.setup_gpio_chardev() await backend.connect(port) api_instance = cls(backend, loop=checked_loop, config=config) diff --git a/api/src/opentrons/hardware_control/controller.py b/api/src/opentrons/hardware_control/controller.py index b253cc92e09..298a7178d34 100644 --- a/api/src/opentrons/hardware_control/controller.py +++ b/api/src/opentrons/hardware_control/controller.py @@ -41,9 +41,17 @@ def __init__(self, config): 'will fail') self.config = config or opentrons.config.robot_configs.load() + + try: + self._gpio_chardev = gpio.GPIOCharDev('gpiochip0') + except AttributeError: + self._gpio_chardev = None + MODULE_LOG.warning( + 'Failed to initialize character device, cannot control gpios') # We handle our own locks in the hardware controller thank you self._smoothie_driver = driver_3_0.SmoothieDriver_3_0_0( - config=self.config, handle_locks=False) + config=self.config, gpio_chardev=self._gpio_chardev, + handle_locks=False) self._cached_fw_version: Optional[str] = None try: self._module_watcher = aionotify.Watcher() @@ -56,6 +64,14 @@ def __init__(self, config): 'Failed to initiate aionotify, cannot watch modules,' 'likely because not running on linux') + @property + def gpio_chardev(self): + return self._gpio_chardev + + async def setup_gpio_chardev(self): + if self._gpio_chardev: + await self.gpio_chardev.setup() + def update_position(self) -> Dict[str, float]: self._smoothie_driver.update_position() return self._smoothie_driver.position @@ -215,15 +231,15 @@ def disengage_axes(self, axes: List[str]): def set_lights(self, button: Optional[bool], rails: Optional[bool]): if opentrons.config.IS_ROBOT: if button is not None: - gpio.set_button_light(blue=button) + self.gpio_chardev.set_button_light(blue=button) if rails is not None: - gpio.set_rail_lights(rails) + self.gpio_chardev.set_rail_lights(rails) def get_lights(self) -> Dict[str, bool]: if not opentrons.config.IS_ROBOT: return {} - return {'button': gpio.get_button_light()[2], - 'rails': gpio.get_rail_lights()} + return {'button': self.gpio_chardev.get_button_light()[2], + 'rails': self.gpio_chardev.get_rail_lights()} def pause(self): self._smoothie_driver.pause() diff --git a/api/src/opentrons/main.py b/api/src/opentrons/main.py index 3ae5d68fc2c..02e4276b971 100644 --- a/api/src/opentrons/main.py +++ b/api/src/opentrons/main.py @@ -131,6 +131,9 @@ async def initialize_robot() -> ThreadManager: log.info("Homing Z axes") await hardware.home_z() + if hardware._backend.gpio_chardev: + await hardware._backend.gpio_chardev.setup_blue_button() + return hardware diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index 7b9ecf8e99a..e6ac3129c0d 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -5,8 +5,10 @@ try: import aionotify + import gpiod except OSError: aionotify = None # type: ignore + gpiod = None # type: ignore import asyncio import os import io