The full code for Module 8 is also replicated here.
First, we will model the Grove Pi's ports:
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:
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:
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:
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 |
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):
[ { "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.