The full code for Module 8 is also replicated here.

Generalized IoT Device Implementation

First, we will model the Grove Pi's ports: 

ports.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"""
Defines constants for each Grove Pi port.
"""
 
# Analog Ports
A0 = 0
A1 = 1
A2 = 2
 
# Digital Ports
D2 = 2
D3 = 3
D4 = 4
D5 = 5
D6 = 6
D7 = 7
D8 = 8
 
# I2C Ports
I2C_0 = 0
I2C_1 = 1
I2C_2 = 2
 
# Mode
INPUT = "INPUT"
OUTPUT = "OUTPUT"

Next, we will create a Grove Device abstraction that models each Grove Pi sensor and actuator:

GroveDevices.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""
A GroveDevice abstraction is created for each Grove Pi sensor and actuator.  The
GroveDevice provides a uniform read and write interface to each component and
maintains a representation of each component's state. Additionally, each
GroveDevice configures the underlying hardware as necessary for proper device
operation.
"""
 
import grovepi
import grove_rgb_lcd
import ports
 
 
class GroveDevice(object):
    def __init__(self, port=None, value=0):
        self.port = port
        self.value = value
 
    def read(self):
        return self.value
 
    def write(self, value):
        self.value = value
 
 
class LED(GroveDevice):
    def __init__(self, port=ports.D5):
        GroveDevice.__init__(self, port)
        grovepi.pinMode(port, ports.OUTPUT)
 
    def write(self, value):
        self.value = int(value)
        grovepi.analogWrite(self.port, self.value)
 
 
class LCD(GroveDevice):
    def write(self, value):
        self.value = value
        try:
            r, g, b = value["rgb"]
            grove_rgb_lcd.setRGB(r, g, b)
        except KeyError:
            pass
        try:
            text = value["text"]
            grove_rgb_lcd.setText(text)
        except KeyError:
            pass
 
 
class DHTSensor(GroveDevice):
    DHT11 = 0
    DHT22 = 1
    DHT21 = 2
 
    def __init__(self, port=ports.D7, dht_type=DHT11):
        GroveDevice.__init__(self, port)
        self.dht_type = dht_type
 
    def read(self):
        temperature, humidity = grovepi.dht(self.port, self.dht_type)
        self.value = {
            "temperature": temperature,
            "humidity": humidity
        }
        return self.value
 
 
class AnalogSensor(GroveDevice):
    def __init__(self, port):
        GroveDevice.__init__(self, port)
 
    def read(self):
        self.value = grovepi.analogRead(self.port)
        return self.value
 
 
class Potentiometer(AnalogSensor):
    def __init__(self, port=ports.A2):
        AnalogSensor.__init__(self, port)
 
 
class LightSensor(AnalogSensor):
    def __init__(self, port=ports.A1):
        AnalogSensor.__init__(self, port)
 
 
class SoundSensor(AnalogSensor):
    def __init__(self, port=ports.A0):
        AnalogSensor.__init__(self, port)
 
 
class Button(GroveDevice):
    def __init__(self, port=ports.D3):
        GroveDevice.__init__(self, port)
        grovepi.pinMode(port, ports.INPUT)
 
    def read(self):
        self.value = grovepi.digitalRead(self.port)
        return self.value
 
 
class Buzzer(GroveDevice):
    def __init__(self, port=ports.D2):
        GroveDevice.__init__(self, port)
        grovepi.pinMode(port, ports.OUTPUT)
 
    def write(self, value):
        self.value = value
        grovepi.digitalWrite(self.port, value)
 
 
class Relay(GroveDevice):
    def __init__(self, port=ports.D6):
        GroveDevice.__init__(self, port)
        grovepi.pinMode(port, ports.OUTPUT)
 
    def write(self, value):
        self.value = value
        grovepi.digitalWrite(self.port, value)
 
 
class UltrasonicRanger(GroveDevice):
    def __init__(self, port=ports.D4):
        GroveDevice.__init__(self, port)
 
    def read(self):
        self.value = grovepi.ultrasonicRead(self.port)
        return self.value

As in previous modules, we will use a UUID to identify IoT devices:

uuidgen.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"""
Generates UUIDs in a consistent manner.
"""
 
import uuid
import netifaces
 
 
def generateUuid(namespace='', domain='snhu.edu'):
    """ Generate a device specific Type 5 UUID within a namespace and domain.
 
    :param namespace: The namespace where the UUID is being generated
    :param domain: The domain where the UUID is being generated
    :return: Type 5 UUID
    """
    mac = netifaces.ifaddresses('eth0')[netifaces.AF_LINK][0]['addr']  
    return str(uuid.uuid5(uuid.NAMESPACE_DNS, mac+'.'+namespace+'.'+domain))

Perhaps the most interesting part of this example is the generalized IoT device code that implements a generic, reusable main loop:

IoTDevice.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"""
Implements a generalized IoT Device architecture and main loop. This IoT device
receives actuator control messages over MQTT and publishes changing sensor and
actuator data over MQTT.  The default configuration utilizes all Grove Pi
interfaces and demonstrates all sensor and actuator types provided in the
starter kit.
"""
 
import GroveDevices
import ports
import paho.mqtt.client as mqtt
import time
import json
import uuidgen
import Queue
 
DEVICE_UUID = uuidgen.generateUuid()
SENSOR_DATA_TOPIC = "SNHU/IT697/sensor/data/"+DEVICE_UUID
ACTUATOR_TOPIC = "SNHU/IT697/actuator/control/"+DEVICE_UUID
 
# Actuators
BUZZER = GroveDevices.Buzzer(ports.D2)
BLUE_LED = GroveDevices.LED(ports.D5)
RELAY = GroveDevices.Relay(ports.D6)
RED_LED = GroveDevices.LED(ports.D8)
LCD = GroveDevices.LCD()  # I2C-1 port
 
ACTUATORS = {
    "blue_led": BLUE_LED,
    "red_led": RED_LED,
    "lcd": LCD,
    "buzzer": BUZZER,
    "relay": RELAY
}
 
# Sensors
SOUND_SENSOR = GroveDevices.SoundSensor(ports.A0)
LIGHT_SENSOR = GroveDevices.LightSensor(ports.A1)
POTENTIOMETER = GroveDevices.Potentiometer(ports.A2)
BUTTON = GroveDevices.Button(ports.D3)
ULTRASONIC_RANGER = GroveDevices.UltrasonicRanger(ports.D4)
DHT_SENSOR = GroveDevices.DHTSensor(ports.D7)
 
SENSORS = [
    ("potentiometer", POTENTIOMETER, 1),
    ("light_sensor", LIGHT_SENSOR, 5),
    ("sound_sensor", SOUND_SENSOR, 5),
    ("button", BUTTON, 1),
    ("ultrasonic_ranger", ULTRASONIC_RANGER, 5),
    ("dht_sensor", DHT_SENSOR, 100)
]
 
 
def on_connect(client, userdata, flags, rc):
    """Called each time the client connects to the message broker
    :param client: The client object making the connection
    :param userdata: Arbitrary context specified by the user program
    :param flags: Response flags sent by the message broker
    :param rc: the connection result
    :return: None
    """
    # subscribe to the ACTUATOR topic when connected
    client.subscribe(ACTUATOR_TOPIC)
 
# A message queue is required to coordinate reads and writes from/to
# the GrovePi
MSG_QUEUE = Queue.Queue()
 
 
def on_message(client, userdata, msg):
    """Called for each message received
    :param client: The client object making the connection
    :param userdata: Arbitrary context specified by the user program
    :param msg: The message from the MQTT broker
    :return: None
    """
    MSG_QUEUE.put(msg.payload)
 
 
def calculate_delta(sensor_name, value, last_values, changed_values):
    """Determine which values have changed from their last values
    :param sensor_name: The sensor name the value was read from
    :param value: The new value
    :param last_values: The last set of checked values
    :param changed_values: The set of values that have changed
    """
    try:
        if last_values[sensor_name] == value:
            return
    except KeyError:
        pass
    changed_values[sensor_name] = value
    last_values[sensor_name] = value
 
 
read_count = 0
last_values = {}
 
 
def read_sensors_and_actuators():
    """ Reads sensors and actuators and returns the values that have changed
    since last read
    :return: The changed values since last read
    """
    global read_count
    changed_values = {}
    for sensor_name, sensor, priority in SENSORS:
        if not (read_count % priority):
            calculate_delta(sensor_name, sensor.read(), last_values,
                            changed_values)
    read_count += 1
    for actuator_name, actuator in ACTUATORS.iteritems():
        calculate_delta(actuator_name, actuator.read(), last_values,
                        changed_values)
    return changed_values
 
 
def publish_sensor_data(values):
    """ Publishes data over MQTT to the sensor data topic
    :param values: The sensor values to send
    :return:  None
    """
    values["timestamp"= int(time.time()*1000)
    out_str = json.dumps(values)
    mqtt_client.publish(SENSOR_DATA_TOPIC, out_str)
    print("==>> " + out_str)
 
 
def process_received_messages():
    """ Processes all MQTT messages placed in the message queue.
    :return: None
    """
    while True:
        try:
            payload = MSG_QUEUE.get(False)
            print("<<== " + payload)
            payload = json.loads(payload)
            for actuator, msg in payload.iteritems():
                try:
                    ACTUATORS[actuator].write(msg)
                except KeyError:
                    pass
        except Queue.Empty:
            break
 
 
MESSAGE_BROKER_URI = "localhost"
mqtt_client = mqtt.Client()
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
mqtt_client.connect(MESSAGE_BROKER_URI)
mqtt_client.loop_start()
 
time.sleep(1)  # give the hardware time to initialize
 
while True:
    try:
        changedValues = read_sensors_and_actuators()
 
        if changedValues:
            publish_sensor_data(changedValues)
 
        process_received_messages()
 
    except (IOError, TypeError) as e:
        print("Error", e)

Place all four of the above files in a directory of your choice (e.g. IoTDeviceArchitecture) and run IoTDevice.py:

python IoTDevice.py 

Implementing an Application in Node-RED

Once you have a generic IoT device, application logic can be implemented in Node-RED. In this example we will re-implement the Temperature and Humidity Sensor of Module One and LED control via the potentiometer similar to Module Four.

Copy the following json node definitions to your clipboard and import into a new Node-RED flow (the import menu is to the right of the Deploy button):

IoT Device Architecture Nodes
   
      "id":"fcd4d5d4.f1f3c8",
      "type":"mqtt-broker",
      "z":"1294dea6.abda71",
      "broker":"localhost",
      "port":"1883",
      "clientid":"",
      "usetls":false,
      "verifyservercert":true,
      "compatmode":true,
      "keepalive":"60",
      "cleansession":true,
      "willTopic":"",
      "willQos":"0",
      "willRetain":null,
      "willPayload":"",
      "birthTopic":"",
      "birthQos":"0",
      "birthRetain":null,
      "birthPayload":""
   },
   
      "id":"4a38b38a.722c0c",
      "type":"mqtt out",
      "z":"1294dea6.abda71",
      "name":"Raspberry Pi Actuators",
      "topic":"",
      "qos":"0",
      "retain":"",
      "broker":"fcd4d5d4.f1f3c8",
      "x":708,
      "y":190,
      "wires":[ 
 
      ]
   },
   
      "id":"87c27b64.367eb8",
      "type":"mqtt in",
      "z":"1294dea6.abda71",
      "name":"Raspberry Pi Sensor Data",
      "topic":"SNHU/IT697/sensor/data/#",
      "broker":"fcd4d5d4.f1f3c8",
      "x":120,
      "y":29,
      "wires":[ 
         
            "8429a230.c79dd"
         ]
      ]
   },
   
      "id":"c4ab8779.c202b8",
      "type":"function",
      "z":"1294dea6.abda71",
      "name":"Add Actuator Control Topic",
      "func":"msg.topic = \"SNHU/IT697/actuator/control/\" + msg.uuid; \nreturn msg;",
      "outputs":1,
      "noerr":0,
      "x":677,
      "y":110,
      "wires":[ 
         
            "4a38b38a.722c0c",
            "f29bc1dc.0a7d9"
         ]
      ]
   },
   
      "id":"61ec95f0.d06a9c",
      "type":"function",
      "z":"1294dea6.abda71",
      "name":"Extract UUID",
      "func":"msg.topicParts = msg.topic.split('/');\nmsg.uuid = msg.topicParts[msg.topicParts.length-1];\nreturn msg;",
      "outputs":1,
      "noerr":0,
      "x":121,
      "y":110,
      "wires":[ 
         
            "e8ecef47.b5248",
            "7503f9dd.86b598",
            "91cf4d59.c489"
         ]
      ]
   },
   
      "id":"e8ecef47.b5248",
      "type":"function",
      "z":"1294dea6.abda71",
      "name":"Potentiometer to Blue LED",
      "func":"if (msg.payload.potentiometer === undefined) {\n    return;\n}\n\nmsg.payload = {\n    blue_led: Math.floor(msg.payload.potentiometer/4)\n};\nreturn msg;",
      "outputs":1,
      "noerr":0,
      "x":386,
      "y":110,
      "wires":[ 
         
            "c4ab8779.c202b8"
         ]
      ]
   },
   
      "id":"8429a230.c79dd",
      "type":"json",
      "z":"1294dea6.abda71",
      "name":"",
      "x":307,
      "y":29,
      "wires":[ 
         
            "61ec95f0.d06a9c"
         ]
      ]
   },
   
      "id":"f29bc1dc.0a7d9",
      "type":"debug",
      "z":"1294dea6.abda71",
      "name":"",
      "active":false,
      "console":"false",
      "complete":"true",
      "x":748,
      "y":31,
      "wires":[ 
 
      ]
   },
   
      "id":"7503f9dd.86b598",
      "type":"debug",
      "z":"1294dea6.abda71",
      "name":"",
      "active":false,
      "console":"false",
      "complete":"false",
      "x":119,
      "y":174,
      "wires":[ 
 
      ]
   },
   
      "id":"91cf4d59.c489",
      "type":"function",
      "z":"1294dea6.abda71",
      "name":"Temperature and Humidity to LCD",
      "func":"if (msg.payload.dht_sensor === undefined) {\n    return;\n}\n\nvar temperature = msg.payload.dht_sensor.temperature;\nvar humidity = msg.payload.dht_sensor.humidity;\nvar lcd = {\n    rgb: [0, 255, 0],\n    text: \"Temp: \"+temperature+\"C\\n\"+\"Humidity: \"+humidity+\"%\"\n}\nmsg.payload = {\n    lcd: lcd\n};\nreturn msg;",
      "outputs":1,
      "noerr":0,
      "x":390,
      "y":153,
      "wires":[ 
         
            "c4ab8779.c202b8"
         ]
      ]
   }
]

Deploy the flow and observe the temperature and humidity sensor and adjust the potentiometer to see the LED dim and brighten.

As you can see, the sensors and actuators are now completely reprogrammable at run-time via Node-RED.