Skip to content

Commit

Permalink
Merge pull request #8 from myDevicesIoT/feature/mqtt-support
Browse files Browse the repository at this point in the history
Feature/mqtt support
  • Loading branch information
jburhenn authored May 3, 2018
2 parents 96eb53c + 1e77550 commit 4a09a77
Show file tree
Hide file tree
Showing 48 changed files with 2,401 additions and 2,041 deletions.
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Hardware Info
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.system.hardware.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
--------
Expand Down
Binary file removed myDevices-test.tar.gz
Binary file not shown.
5 changes: 4 additions & 1 deletion myDevices/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@

"""
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'
14 changes: 9 additions & 5 deletions myDevices/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,20 @@ def setMemoryLimit(rsrc, megs=200):
pidfile = '/var/run/myDevices/cayenne.pid'
def signal_handler(signal, frame):
"""Handle program interrupt so the agent can exit cleanly"""
if client:
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 Down Expand Up @@ -89,8 +93,7 @@ def writePidToFile(pidfile):
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)
Expand Down Expand Up @@ -122,11 +125,12 @@ def main(argv):
writePidToFile(pidfile)
logToFile(logfile)
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')
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
246 changes: 246 additions & 0 deletions myDevices/cloud/cayennemqtt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import time
from json import loads, decoder
from ssl import PROTOCOL_TLSv1_2
import paho.mqtt.client as mqtt
from myDevices.utils.logger import debug, error, exception, info, logJson, warn

# Topics
DATA_TOPIC = 'data/json'
COMMAND_TOPIC = 'cmd'
COMMAND_JSON_TOPIC = 'cmd.json'
COMMAND_RESPONSE_TOPIC = 'response'

# Data Channels
SYS_HARDWARE_MAKE = 'sys:hw:make'
SYS_HARDWARE_MODEL = 'sys:hw:model'
SYS_OS_NAME = 'sys:os:name'
SYS_OS_VERSION = 'sys:os:version'
SYS_NET = 'sys:net'
SYS_STORAGE = 'sys:storage'
SYS_RAM = 'sys:ram'
SYS_CPU = 'sys:cpu'
SYS_I2C = 'sys:i2c'
SYS_SPI = 'sys:spi'
SYS_UART = 'sys:uart'
SYS_ONEWIRE = 'sys:1wire'
SYS_DEVICETREE = 'sys:devicetree'
SYS_GPIO = 'sys:gpio'
SYS_POWER_RESET = 'sys:pwr:reset'
SYS_POWER_HALT = 'sys:pwr:halt'
AGENT_VERSION = 'agent:version'
AGENT_DEVICES = 'agent:devices'
AGENT_MANAGE = 'agent:manage'
DEV_SENSOR = 'dev'

# Channel Suffixes
IP = 'ip'
SPEEDTEST = 'speedtest'
SSID = 'ssid'
USAGE = 'usage'
CAPACITY = 'capacity'
LOAD = 'load'
TEMPERATURE = 'temp'
VALUE = 'value'
FUNCTION = 'function'


class DataChannel:
@staticmethod
def add(data_list, prefix, channel=None, suffix=None, value=None, type=None, unit=None, name=None):
"""Create data channel dict and append it to a list"""
data_channel = prefix
if channel is not None:
data_channel += ':' + str(channel)
if suffix is not None:
data_channel += ';' + str(suffix)
data = {}
data['channel'] = data_channel
data['value'] = value
if type is not None:
data['type'] = type
if unit is not None:
data['unit'] = unit
if name is not None:
data['name'] = name
data_list.append(data)


class CayenneMQTTClient:
"""Cayenne MQTT Client class.
This is the main client class for connecting to Cayenne and sending and recFUeiving data.
Standard usage:
* Set on_message callback, if you are receiving data.
* Connect to Cayenne using the begin() function.
* Call loop() at intervals (or loop_forever() once) to perform message processing.
* Send data to Cayenne using write functions: virtualWrite(), celsiusWrite(), etc.
* Receive and process data from Cayenne in the on_message callback.
The on_message callback can be used by creating a function and assigning it to CayenneMQTTClient.on_message member.
The callback function should have the following signature: on_message(topic, message)
If it exists this callback is used as the default message handler.
"""
client = None
root_topic = ""
connected = False
on_message = None

def begin(self, username, password, clientid, hostname='mqtt.mydevices.com', port=8883):
"""Initializes the client and connects to Cayenne.
username is the Cayenne username.
password is the Cayenne password.
clientid is the Cayennne client ID for the device.
hostname is the MQTT broker hostname.
port is the MQTT broker port.
"""
self.root_topic = 'v1/{}/things/{}'.format(username, clientid)
self.client = mqtt.Client(client_id=clientid, clean_session=True, userdata=self)
self.client.on_connect = self.connect_callback
self.client.on_disconnect = self.disconnect_callback
self.client.on_message = self.message_callback
self.client.username_pw_set(username, password)
if port != 1883:
self.client.tls_set(ca_certs='/etc/ssl/certs/ca-certificates.crt', tls_version=PROTOCOL_TLSv1_2)
self.client.connect(hostname, port, 60)
info('Connecting to {}:{}'.format(hostname, port))

def connect_callback(self, client, userdata, flags, rc):
"""The callback for when the client connects to the server.
client is the client instance for this callback.
userdata is the private user data as set in Client() or userdata_set().
flags are the response flags sent by the broker.
rc is the connection result.
"""
if rc != 0:
# MQTT broker error codes
broker_errors = {
1 : 'unacceptable protocol version',
2 : 'identifier rejected',
3 : 'server unavailable',
4 : 'bad user name or password',
5 : 'not authorized',
}
raise Exception("Connection failed, " + broker_errors.get(rc, "result code " + str(rc)))
else:
info("Connected with result code "+str(rc))
self.connected = True
# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe(self.get_topic_string(COMMAND_TOPIC, True))
client.subscribe(self.get_topic_string(COMMAND_JSON_TOPIC, False))

def disconnect_callback(self, client, userdata, rc):
"""The callback for when the client disconnects from the server.
client is the client instance for this callback.
userdata is the private user data as set in Client() or userdata_set().
rc is the connection result.
"""
info("Disconnected with result code "+str(rc))
self.connected = False
reconnected = False
while not reconnected:
try:
self.client.reconnect()
reconnected = True
except:
print("Reconnect failed, retrying")
time.sleep(5)

def message_callback(self, client, userdata, msg):
"""The callback for when a message is received from the server.
client is the client instance for this callback.
userdata is the private user data as set in Client() or userdata_set().
msg is the received message.
"""
try:
message = {}
if msg.topic[-len(COMMAND_JSON_TOPIC):] == COMMAND_JSON_TOPIC:
payload = loads(msg.payload.decode())
message['payload'] = payload['value']
message['cmdId'] = payload['cmdId']
channel = payload['channel'].split('/')[-1].split(';')
else:
payload = msg.payload.decode().split(',')
if len(payload) > 1:
message['cmdId'] = payload[0]
message['payload'] = payload[1]
else:
message['payload'] = payload[0]
channel = msg.topic.split('/')[-1].split(';')
message['channel'] = channel[0]
if len(channel) > 1:
message['suffix'] = channel[1]
debug('message_callback: {}'.format(message))
if self.on_message:
self.on_message(message)
except:
exception('Error processing message: {} {}'.format(msg.topic, str(msg.payload)))

def get_topic_string(self, topic, append_wildcard=False):
"""Return a topic string.
topic: the topic substring
append_wildcard: if True append the single level topics wildcard (+)"""
if append_wildcard:
return '{}/{}/+'.format(self.root_topic, topic)
else:
return '{}/{}'.format(self.root_topic, topic)

def disconnect(self):
"""Disconnect from Cayenne.
"""
self.client.disconnect()

def loop(self, timeout=1.0):
"""Process Cayenne messages.
This should be called regularly to ensure Cayenne messages are sent and received.
timeout: The time in seconds to wait for incoming/outgoing network
traffic before timing out and returning.
"""
self.client.loop(timeout)

def loop_start(self):
"""This is part of the threaded client interface. Call this once to
start a new thread to process network traffic. This provides an
alternative to repeatedly calling loop() yourself.
"""
self.client.loop_start()

def loop_stop(self):
"""This is part of the threaded client interface. Call this once to
stop the network thread previously created with loop_start(). This call
will block until the network thread finishes.
"""
self.client.loop_stop()

def publish_packet(self, topic, packet, qos=0, retain=False):
"""Publish a packet.
topic: topic substring.
packet: JSON packet to publish.
qos: quality of service level to use.
retain: if True, the message will be set as the "last known good"/retained message for the topic.
"""
debug('Publish to {}'.format(self.get_topic_string(topic)))
self.client.publish(self.get_topic_string(topic), packet, qos, retain)

def publish_response(self, msg_id, error_message=None):
"""Send a command response to Cayenne.
This should be sent when a command message has been received.
msg_id is the ID of the message received.
error_message is the error message to send. This should be set to None if there is no error.
"""
topic = self.get_topic_string(COMMAND_RESPONSE_TOPIC)
if error_message:
payload = "error,%s=%s" % (msg_id, error_message)
else:
payload = "ok,%s" % (msg_id)
self.client.publish(topic, payload)
Loading

0 comments on commit 4a09a77

Please sign in to comment.