Skip to content

Commit

Permalink
Merge pull request #9 from myDevicesIoT/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
jburhenn authored May 3, 2018
2 parents 8a7b310 + 4a09a77 commit 0ab787f
Show file tree
Hide file tree
Showing 57 changed files with 2,944 additions and 2,589 deletions.
18 changes: 9 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
=============
Cayenne Agent
=============
The Cayenne agent is a full featured client for the `Cayenne IoT project builder <https://mydevices.com>`_. It sends system information as well as sensor and actuator date and responds to actuator messages initiated from the Cayenne dashboard and mobile apps. The Cayenne agent currently supports Rasbian on the Raspberry Pi but it can be extended to support additional Linux flavors and other platforms.
The Cayenne agent is a full featured client for the `Cayenne IoT project builder <https://mydevices.com>`_. It sends system information as well as sensor and actuator data and responds to actuator messages initiated from the Cayenne dashboard and mobile apps. The Cayenne agent currently supports Rasbian on the Raspberry Pi but it can be extended to support additional Linux flavors and other platforms.

************
Requirements
************
* `Python 3 <https://www.python.org/downloads/>`_.
* `Python 3.3 or newer <https://www.python.org/downloads/>`_.
* pip3 - Python 3 package manager. This should already be available in Python 3.4+ and above. If it isn't in can be installed using the system package manager. Via `apt-get` this would be:
::

Expand Down Expand Up @@ -127,9 +127,9 @@ To verify that the sensor/actuator works correctly you can test it with the foll
* Create a new sensor using ``myDevices.sensors.SensorsClient.AddSensor`` using the appropriate device name and any args required by your device.
* Get the sensor values using ``myDevices.sensors.SensorsClient.SensorsInfo`` and make sure the sensor data is correct.
* If the new device is an actuator set the actuator value using ``myDevices.sensors.SensorsClient.SensorCommand``.
* Delete the sensor using ``myDevices.sensors.SensorsClient.DeleteSensor``.
* Delete the sensor using ``myDevices.sensors.SensorsClient.RemoveSensor``.

An example demonstrating these functions is available in ``myDevices.test.client_test.py``.
An example demonstrating these functions is available in ``myDevices.test.sensors_test.py``.

*Note:* For security reasons the Cayenne agent is designed to be able to run from an account without root privileges. If any of your sensor/actuator code requires root access consider running just that portion of your code via a separate process that can be launched using sudo. For example, the ``myDevices.devices.digital.ds2408`` module uses this method to write data.

Expand Down Expand Up @@ -164,19 +164,19 @@ System info
Information about the device, including CPU, RAM, etc., is currently retrieved via a few different modules. To support a different board you may need to update the agent code for the following items, if applicable:

General System Info
General system info, including CPU, RAM, memory, etc. is retrieved via ``myDevices.os.systeminfo.py`` and ``myDevices.os.cpu.py``. These are mostly implemented using cross platform libraries so they may already provide support for your board. If not, they should be modified or overridden to provide the appropriate system info. If your board does not support all the data values currently implemented you can just provide default values where necessary, though this may affect the data display in the Cayenne dashboard.
General system info, including CPU, RAM, memory, etc. is retrieved via ``myDevices.system.systeminfo.py`` and ``myDevices.system.cpu.py``. These are mostly implemented using cross platform libraries so they may already provide support for your board. If not, they should be modified or overridden to provide the appropriate system info. If your board does not support all the data values currently implemented you can just provide default values where necessary, though this may affect the data display in the Cayenne dashboard.

Hardware Info
Hardware info, including make, model, etc. is retrieved via ``myDevices.os.hardware.py``. This should be modified or overridden to provide the appropriate hardware info for your board.
Hardware info, including make, model, etc. is retrieved via ``myDevices.system.hardware.py``. This should be modified or overridden to provide the appropriate hardware info for your board.

Pin Mapping
The mapping of the on-board pins is provided in ``myDevices.utils.version.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab.
The mapping of the on-board pins is provided in ``myDevices.devices.digital.gpio.py`` with the ``MAPPING`` list. This list provides the available GPIO pin numbers as well as the voltage ("V33", "V50"), ground ("GND") and do-not-connect ("DNC") pins. This should be updated with the mapping for your board. However, the Cayenne dashboard is currently built to display the Raspberry Pi GPIO layout so if your board's pin layout is significantly different it may not display correctly in the GPIO tab.

Settings
--------
Currently the Raspberry Pi agent has settings for enabling/disabling the device tree, SPI, I²C, serial and camera. These are set via the ``myDevices.os.raspiconfig`` module which runs a separate Bash script at ``/etc/myDevices/scripts/config.sh``. If any of these settings are available on your board and you would like to support them you can override or replace ``myDevices.os.raspiconfig.py``. Otherwise the settings functionality can be ignored.
Currently the Raspberry Pi agent has settings for enabling/disabling the device tree, SPI, I²C, serial and camera. These are set via the ``myDevices.system.raspiconfig`` module which runs a separate Bash script at ``/etc/myDevices/scripts/config.sh``. If any of these settings are available on your board and you would like to support them you can override or replace ``myDevices.system.raspiconfig.py``. Otherwise the settings functionality can be ignored.

*Note:* For security reasons the Cayenne agent is designed to be able to run from an account without root privileges. If any of your I/O, system info or settings code requires root access consider running it via a separate process that can be launched using sudo. For example, the ``myDevices.os.raspiconfig`` module uses this method to update config settings.
*Note:* For security reasons the Cayenne agent is designed to be able to run from an account without root privileges. If any of your I/O, system info or settings code requires root access consider running it via a separate process that can be launched using sudo. For example, the ``myDevices.system.raspiconfig`` module uses this method to update config settings.

************
Contributing
Expand Down
11 changes: 4 additions & 7 deletions myDevices/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from time import sleep

try:
import ipgetter
except:
pass

"""
This package contains the Cayenne agent, which is a full featured client for the Cayenne IoT project builder: https://cayenne.mydevices.com. It sends system information as well as sensor and actuator data and responds to actuator messages initiated from the Cayenne dashboard and mobile apps.
"""
__version__ = '2.0.0'
59 changes: 33 additions & 26 deletions myDevices/__main__.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
from myDevices.utils.config import Config
"""
This module is the main entry point for the Cayenne agent. It processes any command line parameters and launches the client.
"""
from os import path, getpid, remove
from sys import __excepthook__, argv, maxsize
from threading import Thread
from myDevices.utils.config import Config
from myDevices.cloud.client import CloudServerClient
from myDevices.utils.logger import exception, setDebug, info, debug, error, logToFile, setInfo
from sys import excepthook, __excepthook__, argv, maxsize
from threading import Thread
from signal import signal, SIGUSR1, SIGINT
from resource import getrlimit, setrlimit, RLIMIT_AS
from myDevices.os.services import ProcessInfo
from myDevices.os.daemon import Daemon
from myDevices.system.services import ProcessInfo
from myDevices.utils.daemon import Daemon

def setMemoryLimit(rsrc, megs = 200):
def setMemoryLimit(rsrc, megs=200):
"""Set the memory usage limit for the agent process"""
size = megs * 1048576
soft, hard = getrlimit(rsrc)
setrlimit(rsrc, (size, hard)) #limit to one kilobyte
soft, hard = getrlimit(rsrc)
info ('Limit changed to :'+ str( soft))
setrlimit(rsrc, (size, hard))

try:
#Only set memory limit on 32-bit systems
if maxsize <= 2**32:
setMemoryLimit(RLIMIT_AS)
except Exception as e:
error('Cannot set limit to memory: ' + str(e))
except Exception as ex:
print('Cannot set limit to memory: ' + str(ex))

client = None
pidfile = '/var/run/myDevices/cayenne.pid'
def signal_handler(signal, frame):
if client:
"""Handle program interrupt so the agent can exit cleanly"""
if client and client.connected:
if signal == SIGINT:
info('Program interrupt received, client exiting')
client.Destroy()
remove(pidfile)
else:
client.Restart()
elif signal == SIGINT:
remove(pidfile)
raise SystemExit
signal(SIGUSR1, signal_handler)
signal(SIGINT, signal_handler)


def exceptionHook(exc_type, exc_value, exc_traceback):
"""Make sure any uncaught exceptions are logged"""
debug('Daemon::exceptionHook ')
Expand All @@ -46,7 +54,7 @@ def exceptionHook(exc_type, exc_value, exc_traceback):

def threadExceptionHook():
"""Make sure any child threads hook exceptions. This should be called before any threads are created."""
debug('Daemon::threadExceptionHook ')
debug('Daemon::threadExceptionHook')
init_original = Thread.__init__
def init(self, *args, **kwargs):
init_original(self, *args, **kwargs)
Expand Down Expand Up @@ -79,51 +87,50 @@ def displayHelp():
exit()

def writePidToFile(pidfile):
"""Write the process ID to a file to prevent multiple agents from running at the same time"""
if path.isfile(pidfile):
info(pidfile + " already exists, exiting")
with open(pidfile, 'r') as file:
pid = int(file.read())
if ProcessInfo.IsRunning(pid) and pid != getpid():
Daemon.Exit()
return
raise SystemExit
pid = str(getpid())
with open(pidfile, 'w') as file:
file.write(pid)

def main(argv):
"""Main entry point for starting the agent client"""
global pidfile
configfile = None
scriptfile = None
logfile = None
isDebug = False
i = 1
setInfo()
while i < len(argv):
if argv[i] in ["-c", "-C", "--config-file"]:
configfile = argv[i+1]
i+=1
i += 1
elif argv[i] in ["-l", "-L", "--log-file"]:
logfile = argv[i+1]
i+=1
i += 1
elif argv[i] in ["-h", "-H", "--help"]:
displayHelp()
elif argv[i] in ["-d", "--debug"]:
setDebug()
elif argv[i] in ["-P", "--pidfile"]:
pidfile = argv[i+1]
i+=1
i+=1
i += 1
i += 1
if configfile == None:
configfile = '/etc/myDevices/Network.ini'
writePidToFile(pidfile)
logToFile(logfile)
# SET HOST AND PORT
config = Config(configfile)
HOST = config.get('CONFIG','ServerAddress', 'cloud.mydevices.com')
PORT = config.getInt('CONFIG','ServerPort', 8181)
HOST = config.get('CONFIG', 'ServerAddress', 'mqtt.mydevices.com')
PORT = config.getInt('CONFIG', 'ServerPort', 8883)
CayenneApiHost = config.get('CONFIG', 'CayenneApi', 'https://api.mydevices.com')
# CREATE SOCKET
global client
global client
client = CloudServerClient(HOST, PORT, CayenneApiHost)
client.Start()

if __name__ == "__main__":
try:
Expand Down
48 changes: 38 additions & 10 deletions myDevices/cloud/apiclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
from concurrent.futures import ThreadPoolExecutor
import json
from myDevices.utils.logger import error, exception
from myDevices.system.hardware import Hardware
from myDevices.system.systeminfo import SystemInfo
from myDevices.cloud import cayennemqtt
from myDevices.devices.digital.gpio import NativeGPIO

class CayenneApiClient:
def __init__(self, host):
Expand Down Expand Up @@ -36,31 +40,55 @@ def sendRequest(self, method, uri, body=None):
return None
return response
exception("No data received")

def getMessageBody(self, inviteCode):
body = {'id': inviteCode}
hardware = Hardware()
if hardware.Serial and hardware.isRaspberryPi():
body['type'] = 'rpi'
body['hardware_id'] = hardware.Serial
else:
hardware_id = hardware.getMac()
if hardware_id:
body['type'] = 'mac'
body['hardware_id'] = hardware_id
try:
system_data = []
cayennemqtt.DataChannel.add(system_data, cayennemqtt.SYS_HARDWARE_MAKE, value=hardware.getManufacturer(), type='string', unit='utf8')
cayennemqtt.DataChannel.add(system_data, cayennemqtt.SYS_HARDWARE_MODEL, value=hardware.getModel(), type='string', unit='utf8')
system_info = SystemInfo()
capacity_data = system_info.getMemoryInfo((cayennemqtt.CAPACITY,))
capacity_data += system_info.getDiskInfo((cayennemqtt.CAPACITY,))
for item in capacity_data:
system_data.append(item)
body['properties'] = {}
body['properties']['pinmap'] = NativeGPIO().MAPPING
if system_data:
body['properties']['sysinfo'] = system_data
except:
exception('Error getting system info')
return json.dumps(body)

def authenticate(self, inviteCode):
body = json.dumps({'id': inviteCode})
body = self.getMessageBody(inviteCode)
url = '/things/key/authenticate'
return self.sendRequest('POST', url, body)

def activate(self, inviteCode):
body = json.dumps({'id': inviteCode})
body = self.getMessageBody(inviteCode)
url = '/things/key/activate'
return self.sendRequest('POST', url, body)

def getId(self, content):
def getCredentials(self, content):
if content is None:
return None
body = content.decode("utf-8")
if body is None or body is "":
return None
return json.loads(body)['id']
return json.loads(body)

def loginDevice(self, inviteCode):
response = self.authenticate(inviteCode)
response = self.activate(inviteCode)
if response and response.status_code == 200:
return self.getId(response.content)
if not response or response.status_code == 412:
response = self.activate(inviteCode)
if response and response.status_code == 200:
return self.getId(response.content)
return self.getCredentials(response.content)
return None
Loading

0 comments on commit 0ab787f

Please sign in to comment.