diff --git a/.travis.yml b/.travis.yml index 6ece79f..fcb0eed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,12 @@ python: - 3.6 # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors -install: pip install -U tox-travis +install: + - pip install -U tox-travis + - pip install . #execute setup.py with requirements + - pip install -r requirements_dev.txt + + # Command to run tests, e.g. python setup.py test script: tox diff --git a/docs/conf.py b/docs/conf.py index 46cbb02..c2ce916 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ import sys sys.path.insert(0, os.path.abspath('..')) -import smartagro +#import smartagro import sphinx_rtd_theme # -- General configuration --------------------------------------------- @@ -33,6 +33,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx_rtd_theme'] +autodoc_mock_imports = ["RPi", "paho", "mcp3008", "socket"] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -56,9 +57,9 @@ # the built documents. # # The short X.Y version. -version = smartagro.__version__ +#version = smartagro.__version__ # The full version, including alpha/beta/rc tags. -release = smartagro.__version__ +#release = smartagro.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 0000000..f221cfe --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,16 @@ +Package Modules +====================================== + +.. automodule:: smartagro.smart + :members: + +.. toctree:: + :maxdepth: 3 + :caption: Contents: + +.. automodule:: smartagro.utils + :members: + +.. toctree:: + :maxdepth: 3 + :caption: Contents: diff --git a/requirements_dev.txt b/requirements_dev.txt index 283f5d5..e549141 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ -pip==19.2.3 +pip>=19.2.3 bump2version==0.5.11 wheel==0.33.6 watchdog==0.9.0 @@ -9,4 +9,4 @@ Sphinx==1.8.5 twine==1.14.0 Click==7.0 pytest==4.6.5 -pytest-runner==5.1 \ No newline at end of file +pytest-runner==5.1 diff --git a/setup.py b/setup.py index e7d750c..b5a3985 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ with open('HISTORY.rst') as history_file: history = history_file.read() -requirements = ['Click>=7.0', 'paho-mqtt==1.5.1','mcp3008','sockets', ] #packages what the person using the +requirements = ['Click>=7.0', 'paho-mqtt==1.5.1','spidev', 'sockets', 'RPi.GPIO', ] #packages what the person using the # program needs. eg ADC libraries, etc that i will use. #requirements has debug n dev tools etc what u will include in venv @@ -28,7 +28,6 @@ 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Natural Language :: English', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', diff --git a/smartagro/mcp3008.py b/smartagro/mcp3008.py new file mode 100644 index 0000000..dce3dbd --- /dev/null +++ b/smartagro/mcp3008.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +RPi_mcp3008 is a library to listen to the MCP3008 A/D converter chip, +as described in the datasheet. +https://www.adafruit.com/datasheets/MCP3008.pdf + +Copyright (C) 2015 Luiz Eduardo Amaral + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +''' + +import spidev + +# Modes Single +CH0 = 8 # single-ended CH0 +CH1 = 9 # single-ended CH1 +CH2 = 10 # single-ended CH2 +CH3 = 11 # single-ended CH3 +CH4 = 12 # single-ended CH4 +CH5 = 13 # single-ended CH5 +CH6 = 14 # single-ended CH6 +CH7 = 15 # single-ended CH7 +# Modes Diff +DF0 = 0 # differential CH0 = IN+ CH1 = IN- +DF1 = 1 # differential CH0 = IN- CH1 = IN+ +DF2 = 2 # differential CH2 = IN+ CH3 = IN- +DF3 = 3 # differential CH2 = IN- CH3 = IN+ +DF4 = 4 # differential CH4 = IN+ CH5 = IN- +DF5 = 5 # differential CH4 = IN- CH5 = IN+ +DF6 = 6 # differential CH6 = IN+ CH7 = IN- +DF7 = 7 # differential CH6 = IN- CH7 = IN+ + +RESOLUTION = 1 << 10 # 10 bits resolution + +class MCP3008(spidev.SpiDev): + ''' + Object that listens the MCP3008 in the SPI port of the RPi. + Connects the object to the specified SPI device. + The initialization arguments are MCP3008(bus=0, device=0) where: + MCP3008(X, Y) will open /dev/spidev-X.Y, same as spidev.SpiDev.open(X, Y). + ''' + def __init__(self, bus=0, device=0): + self.bus = bus + self.device = device + self.open(self.bus, self.device) + self.modes = False + + def __del__(self): + self.close() + + def __enter__(self): + return self + + def __exit__(self, type, value, tb): + self.close() + + def __repr__(self): + return 'MCP3008 object at bus {0}, device {1}'.format(self.bus, self.device) + + def __call__(self, norm=False): + return self.read(self.modes, norm) + + @classmethod + def fixed(cls, modes, bus=0, device=0): + ''' + Initializes the class with fixed modes, which turns the instance callable. + The modes argument is a list with the modes of operation to be read (e.g. + [mcp3008.CH0,mcp3008.Df0]). + When calling the instance the object will execute a reading of and return the + values (e.g. print instance()). + When calling the instance, you can pass the optional argument norm to + normalize + the data (e.g. print instance(5.2)). + ''' + instance = cls(bus, device) + instance.modes = modes + return instance + + def _read_single(self, mode): + ''' + Returns the value of a single mode reading + ''' + if not 0 <= mode <= 15: + raise IndexError('Outside the channels scope, please use: 0, 1 ..., 7') + request = [0x1, mode << 4, 0x0] # [start bit, configuration, listen space] + _, byte1, byte2 = self.xfer2(request) + value = (byte1%4 << 8) + byte2 + return value + + def read(self, modes, norm=False): + ''' + Returns the raw value (0 ... 1024) of the reading. + The modes argument is a list with the modes of operation to be read (e.g. + [mcp3008.CH0,mcp3008.Df0]). + norm is a normalization factor, usually Vref. + ''' + reading = [] + for mode in modes: + reading.append(self._read_single(mode)) + if norm: + return [float(norm)*value/RESOLUTION for value in reading] + else: + return reading + + def read_all(self, norm=False): + ''' + Returns a list with the readings of all the modes + Data Order: + [DF0, DF1, DF2, DF3, DF4, DF5, DF6, DF7, + CH0, CH1, CH2, CH3, CH4, CH5, CH6, CH7] + norm is a normalization factor, usually Vref. + ''' + return self.read(range(16), norm) diff --git a/smartagro/smart.py b/smartagro/smart.py index 1783e68..9c0990f 100644 --- a/smartagro/smart.py +++ b/smartagro/smart.py @@ -1,5 +1,5 @@ """Main module.""" -from smartagro.utils import * +#from smartagro.utils import * def foo(): @@ -14,7 +14,7 @@ def __init__(self, broker_ip=None, broker_port=None, qos=0, devices=4): self.qos = qos self.devices = devices #if none, scan network for brokers and connect to identified broker. - config_broker(broker_ip, qos, broker_port, stream_schema=None) + #config_broker() def devices_init(self): pass diff --git a/smartagro/utils.py b/smartagro/utils.py index 6a1a9fa..f80ce01 100644 --- a/smartagro/utils.py +++ b/smartagro/utils.py @@ -2,38 +2,116 @@ import paho.mqtt.client as mqtt import os import socket +import smartagro.mcp3008 as mcp3008 + +try: + import RPi.GPIO as GPIO +except RuntimeError: + print("Error importing RPi.GPIO! Use sudo / run sudo usermod -aG gpio to get permission") + def bar(): + """ + Useless test function + :return: + """ from smartagro import __author__ print('[mod2] bar(){}'.format(__author__)) # f'haha{var}' not supported in python 3.5 -class Bar: - pass +topic = "SmartAgro/Sensors/" -def config_broker(broker="mqtt.eclipse.org", QS=0, PORT="1883", stream_schema=None): # mqtt broker configuration - client = mqtt.Client("RPi0-ZA") #create new client - client.connect(broker,PORT) #connect to broker - client.publish("dev/test","OFF ua") #TOPIC & test payload +class Bar: + pass -def discover_devices(comm_interface): # scans address space and ports to discover connected I2C or SPI devices - #os i2cdetect? - #1 I2C port and two SPI ports +def config_broker(broker="mqtt.eclipse.org", QS=0, port="1883", stream_schema="json"): + """ + Function to configure a new broker to be published to. + :param broker: The url or ip address of the broker + :param QS: quality of service determining how many times message is sent. 0,1,2 + :param port: broker port in use. default 1883, ssl 8883 + :param stream_schema: the data stream schema used. Default is json + :return: No return + """ + client = mqtt.Client("RPi0-ZA") # create new client + client.connect(broker, port) # connect to broker + client.publish("dev/test", "OFF ua") # TOPIC & test payload + # TODO test functionality + + +def discover_devices(comm_interface): + """ + scans address space and ports to discover connected I2C or SPI devices + uses os i2cdetect for the 1 I2C port and also scans two SPI ports + :param comm_interface: communication interface to be scanned. + :return: + """ print("Scan for connected I2C devices' addresses:") print(os.popen("i2cdetect -y 1").read()) - #TODO Add SPI scanning support + # TODO Add SPI scanning support - can only be done by sending a valid + # signal to spi mosi and geting a valid response on miso + print("Checking if SPI Module is loaded:") + print(os.popen("lsmod | grep spi").read()) + def create_sensor_type(read, write, config): # Add support for new sensor type pass + def sensor_attach_i2c(SensorType1, addr, sample_rate, broker): # Add sensor, assign broker and topic pass -def sensor__attach_serial(SensorType2, port, baud, broker): # Add sensor, assign broker and topic - pass +def sensor_attach_serial(SensorType2, spi_device, broker, baud=976000): # Add sensor, assign broker and topic + """ + Adds and configures an SPI device & adds its topic? + :param SensorType2: + :param spi_device: Either 0 or 1 as there are only 2 spo ports + :param baud: the bit rate, measured in bit/s clock rate used for device + :param broker: the MQTT broker in use + :return: No return + """ + spi = spidev.SpiDev() + spi.open(0, spi_device) + spi.max_speed_hz = 976000 + + CLK = 23 + MISO = 21 + MOSI = 19 + CS = 24 if spi_device == 0 else 26 # CE0 or CE1 + + GPIO.setup(CLK, GPIO.OUT) + GPIO.setup(MISO, GPIO.IN) + GPIO.setup(MOSI, GPIO.OUT) + GPIO.setup(CS, GPIO.OUT) + + +def sensor_read_analogue(channel): + """ + Reads an analogue signal from the connected SPI ADC device + :return: ADC output Normalized with Vref. + """ + # link with SPI device initialization. Docs: https://pypi.org/project/mcp3008/ + adc = mcp3008.MCP3008(bus=0,device=0) + ADC_values = adc.read_all() + print(ADC_values) + adc.close() + return ADC_values[channel + 8] + + +def control_fan(gpio_pin, state): + """ + Function to switch fan actuator ON or OFF + :param gpio_pin: The pin the fan relay (motor in demo) is connected to. + :param state: Boolean indicating whether fan is on or off. + :return: NO return + """ + if state: + GPIO.output(gpio_pin, GPIO.HIGH) + else: + GPIO.output(gpio_pin, GPIO.HIGH) def sensor_detach(stream, broker): # remove sensor from publishing topics @@ -47,8 +125,15 @@ def list_active_sensor_streams(broker): # show topics being published to broker def sensor_update(addr, new_sample_rate): # Dynamic adjustment of sensor details pass -def find_broker(): # Scan for a MQTT broker within network - # Add support to scan online hosts' ports to find broker. - still buggy + +def find_broker(): + """ + Scan for a MQTT broker within network by checking online hosts then scanning for + open MQTT ports + # TODO Add support to scan online hosts' ports to find broker. - still buggy + :return: No return + """ + online_dev = scan_network() for ip in online_dev: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -60,6 +145,10 @@ def find_broker(): # Scan for a MQTT broker within network def get_ip(): + """ + Ger the IP address other than the loopback IP that the device has been allocated by DHCP + :return: IP address + """ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(('10.255.255.255', 1)) @@ -70,7 +159,12 @@ def get_ip(): s.close() # close socket return IP + def scan_network(): + """ + Scan subnet /24 of IP address to check for brokers on the local network. + :return: list of online devices responding to ICMP echo request using ping. + """ my_ip = get_ip() net = my_ip.split(".")[:-1] online_dev = list() @@ -79,14 +173,24 @@ def scan_network(): dev = ".".join(net) + "." + str(host) response = os.popen(f"ping -w 1 {dev}") # -c 1 try: - if (response.readlines()[5]): + if response.readlines()[5]: print(f"{dev} is online") online_dev.append(dev) - except: + except IndexError: continue return online_dev + +def gpio_init(): + """ + Function to initialize the GPIO pins and numbering system used. + :return: No return + """ + GPIO.setmode(GPIO.BOARD) # Physical Pin Numbers + # remember to use GPIO.cleanup() and exit(0) for a graceful exit. + # functions to communicate with Seeed devices. ADC/Direct/? -# All of the above for actuator //Connecting actuator to raspberry pi, configuring to subscribe to mqtt topics that send commands to activate / deactivate. +# All of the above for actuator //Connecting actuator to raspberry pi, +# configuring to subscribe to mqtt topics that send commands to activate / deactivate. diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index a5fa120..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit test package for smartagro.""" diff --git a/tests/test_smart.py b/tests/test_smart.py index 2627715..8e76de2 100644 --- a/tests/test_smart.py +++ b/tests/test_smart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """Tests for `smart` module in Smartagro package.""" - +# useful lib https://pypi.org/project/mock/1.0.1/ import pytest from click.testing import CliRunner