├── .gitignore ├── README.md ├── assets └── IoTInDataCenterDockerCon.pdf ├── consul ├── agent_run.sh ├── manage.sh ├── run.sh └── start_consul.sh ├── curl_test └── Dockerfile ├── docker-compose.yml ├── envirophat_base ├── .gitignore ├── Dockerfile └── sources.md ├── envirophat_sensor ├── .gitignore ├── Dockerfile ├── app.py ├── blinkt.py ├── motion.py ├── msense.py ├── old │ └── driver.py ├── rebuild.sh ├── reporting.py └── sensing.py ├── join.sh ├── join ├── auto_join.sh ├── auto_join_old.sh └── auto_join_wlan.sh ├── multi_dashboard ├── Dockerfile ├── app.py ├── rebuild.sh ├── reporter.py └── threadedsubscriber.py ├── non_demo ├── dashboard │ ├── app.py │ ├── reporter.py │ ├── scrollapp.py │ └── threadedsubscriber.py ├── explorer_sensor │ ├── Dockerfile │ ├── app.py │ ├── explorersensing.py │ └── reporting.py ├── fake_sensor │ ├── app.py │ ├── reporting.py │ ├── sensing.py │ └── tmp36sensing.py ├── redis_display │ ├── .gitignore │ ├── Dockerfile │ ├── app.js │ └── package.json ├── tmp36_sensor │ ├── app.py │ ├── reporting.py │ ├── sensing.py │ └── tmp36sensing.py └── unicorn_dashboard │ ├── Dockerfile │ ├── app.py │ ├── reporter.py │ └── threadedsubscriber.py ├── spec.md ├── start_manager.sh └── webdashboard ├── Dockerfile ├── app.py ├── get_js.sh ├── js ├── angular.js ├── controllers.js └── smoothie.js ├── reporter.py ├── templates ├── layout.html ├── nodes.html └── sensors.html └── threadedsubscriber.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.rdb 2 | *.pyc 3 | *.swp 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # datacenter-sensor 2 | 3 | 4 | ### Introduction 5 | 6 | This repository supports [my Dockercon talk and demo](http://dockercon2016.sched.org/event/6CF1) 7 | 8 | > **Docker and IoT securing the server-room with realtime microservices by Alex Ellis** 9 | 10 | > In this hack, we secure the data-center through a scaleable network of real-time sensors and microservices running Docker. Each rack in the server-room is filled with thousands of terabytes of priceless customer data, IoT lets us keep one step ahead and keep that data safe. The cluster deploys a set of smart sensors running the Docker Swarm agent to the rack panels. 11 | 12 | > Each sensor constantly samples the ambient temperature of the rack and sends a real-time alarm to the control room when levels rise above normal levels. An anti-tamper motion sensor picks up tiny vibrations and alerts the admins when someone is performing unscheduled maintenance on the equipment. A custom made RGB LED display made by Pimoroni for Dockercon shows the status of up to 8 racks in real-time. 13 | 14 | ![Swag](https://c3.staticflickr.com/8/7126/27279846650_da0c806fc1_c.jpg) 15 | 16 | ### Producing the Hack 17 | 18 | **Thanks for the support** 19 | 20 | A large percentage of the hardware for this hack is being supplied by [Pimoroni.com](http://pimoroni.com) - so a huge thanks to them for helping out. They also designed a brand new add-on board for the Raspberry Pi with 8 RGB LEDs just for this demo and gave me early access to their environmental sensing board - envirophat. 21 | 22 | Come to the live demo at Dockercon for all the rest of the details. We'll have a time for Q&A come prepared! 23 | 24 | ### See also: original Dockercon hack entry 25 | 26 | [Visualizing a production-ready load-balancer with LEDs and Docker Swarm](http://blog.alexellis.io/iot-docker-cluster/) 27 | 28 | ### Contributions 29 | 30 | I'm not accepting pull-requests to the demo code at present, but feel free to fork it or re-use elements in your own projects in-line with the MIT license. 31 | 32 | ### Booting up the demo 33 | 34 | #### Step 1 35 | 36 | Log into the manager and start the Docker Swarm manager image and consul as a key-value store with this shell script: 37 | 38 | ``` 39 | $ datacenter-sensor/start_manage.sh 40 | ``` 41 | 42 | #### Step 2 43 | 44 | Log into each additional Pi Zero and launc the Swarm Agent: 45 | 46 | ``` 47 | $ datacenter-sensor/join/auto_join.sh 48 | ``` 49 | 50 | > The Consul IP address has to be hard-coded into the systemd docker.service file and this batch file. 51 | 52 | #### Step 3 53 | 54 | Enter: Docker-compose 55 | 56 | Point the DOCKER_HOST environmental variable to the swarm manager and type in: 57 | 58 | ``` 59 | $ export DOCKER_HOST=tcp://manager1.local:2376 60 | 61 | $ docker-compose up -d 62 | ``` 63 | 64 | #### Step 4 65 | 66 | Then once running you can scale up the sensor service to the number of sensor Pis in your network. 67 | 68 | ``` 69 | $ docker-compose scale sensor=4 70 | ``` 71 | 72 | #### Step 5 73 | 74 | Profit. 75 | 76 | If you want to open the web-dashboard, then use `docker-compose ps` to find where it has been placed in the swarm and which port has been dynamically assigned to it. 77 | 78 | When you're done you can use `docker-compose stop` to pause the demo or `docker-compose down` to clean everything up. 79 | 80 | ### Sneak previews: 81 | 82 | Descending date of release. 83 | 84 | * [Starting up the demo - LED animations](https://twitter.com/alexellisuk/status/742411122591051777) 85 | * [Scaling-up monitoring two sensors](https://twitter.com/alexellisuk/status/741224768087674880) 86 | * [Detecting motion](https://twitter.com/alexellisuk/status/740824510849503232) 87 | * [Detecting temperature and showing alerts](https://twitter.com/alexellisuk/status/739736197442981888) 88 | * [Unicorn pHAT making rainbows](https://twitter.com/alexellisuk/status/739557889854066688) 89 | -------------------------------------------------------------------------------- /assets/IoTInDataCenterDockerCon.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexellis/datacenter-sensor/d51f8fe3debb7e11d0786c26f13c6a2fed7f659d/assets/IoTInDataCenterDockerCon.pdf -------------------------------------------------------------------------------- /consul/agent_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | hosts=(192.168.0.101 4 | 192.168.0.102 5 | 192.168.0.103 6 | 192.168.0.104 7 | ) 8 | 9 | exec_cmd() { 10 | echo "[$1] $2" 11 | echo "" 12 | ssh $1 $2 13 | echo "" 14 | } 15 | 16 | for host in "${hosts[@]}" 17 | do 18 | exec_cmd $host "$@" 19 | done 20 | -------------------------------------------------------------------------------- /consul/manage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is designed to be run on a 'regular' x64 machine 4 | # rather than a Raspberry PI itself. 5 | 6 | export primary_node=110 7 | export manage_port=2376 8 | export swarm_ip=192.168.0.$primary_node 9 | export consul_addr=192.168.0.$primary_node:8500 10 | 11 | export container=manage_armv6 12 | export image_name=alexellis2/swarm-arm:v6 13 | 14 | echo "Removing old $container container" 15 | docker rm -f $container 16 | 17 | echo "Starting new $container container" 18 | 19 | echo "Using version: $swarm_version" 20 | export image=swarm 21 | 22 | docker run -d -p 2376:2375 --name $container $image_name manage \ 23 | consul://$consul_addr/swarm 24 | -------------------------------------------------------------------------------- /consul/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | hosts=(192.168.0.100 4 | 192.168.0.101 5 | 192.168.0.102 6 | 192.168.0.103 7 | 192.168.0.104 8 | ) 9 | 10 | exec_cmd() { 11 | echo "[$1] $2" 12 | echo "" 13 | ssh $1 $2 14 | echo "" 15 | } 16 | 17 | for host in "${hosts[@]}" 18 | do 19 | exec_cmd $host "$@" 20 | done 21 | -------------------------------------------------------------------------------- /consul/start_consul.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Attempting to remove existing container" 4 | 5 | docker rm -f consul 6 | echo "Starting new alexellis2/consul-arm:v6 container" 7 | 8 | docker run -d \ 9 | --name consul \ 10 | -p 8400:8400 -p 8500:8500 -p 8600:53/udp \ 11 | -p 8301:8301 -p 8302:8302 \ 12 | alexellis2/consul-arm:v6 \ 13 | -server -bootstrap 14 | 15 | docker port consul 16 | -------------------------------------------------------------------------------- /curl_test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM resin/rpi-raspbian 2 | 3 | RUN apt-get -qy update && \ 4 | apt-get -qy install curl 5 | 6 | CMD ["curl"] 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.0" 2 | services: 3 | 4 | redis: 5 | image: alexellis2/redis-arm:v6 6 | ports: 7 | - 6379/TCP 8 | networks: 9 | - frontend 10 | environment: 11 | - "constraint:demo.web==1" 12 | 13 | web: 14 | build: 15 | context: "./webdashboard/" 16 | networks: 17 | - frontend 18 | privileged: true 19 | environment: 20 | - "constraint:demo.web==1" 21 | - QUIET=True 22 | ports: 23 | - "5001:5000" 24 | depends_on: 25 | - redis 26 | 27 | unicorn: 28 | build: 29 | context: "./multi_dashboard/" 30 | networks: 31 | - frontend 32 | privileged: true 33 | environment: 34 | - "constraint:demo.dashboard==1" 35 | - QUIET=True 36 | - TEMP_THRESHOLD=0.8 37 | depends_on: 38 | - redis 39 | 40 | sensor: 41 | build: 42 | context: "./envirophat_sensor/" 43 | networks: 44 | - frontend 45 | privileged: true 46 | environment: 47 | - "affinity:container!=datacentersensor_sensor_*" 48 | - "constraint:sensor==1" 49 | - TEMP_THRESHOLD=0.8 50 | - QUIET=True 51 | depends_on: 52 | - redis 53 | 54 | networks: 55 | frontend: 56 | driver: overlay 57 | -------------------------------------------------------------------------------- /envirophat_base/.gitignore: -------------------------------------------------------------------------------- 1 | Adafruit_Python_BME280/* 2 | adxl345-python/* 3 | 4 | -------------------------------------------------------------------------------- /envirophat_base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alexellis2/python-gpio-arm:v6-dev 2 | RUN apt update && apt -qy install \ 3 | git \ 4 | python-smbus 5 | 6 | RUN sudo pip install redis 7 | 8 | RUN git clone https://github.com/adafruit/Adafruit_Python_GPIO.git 9 | RUN cd Adafruit_Python_GPIO && \ 10 | sudo python setup.py install 11 | 12 | RUN git clone https://github.com/adafruit/Adafruit_Python_BME280 13 | RUN mv Adafruit_Python_BME280/* ./ 14 | 15 | RUN git clone https://github.com/adafruit/Adafruit_DotStar_Pi 16 | RUN cd Adafruit_DotStar_Pi && \ 17 | sudo python setup.py install 18 | -------------------------------------------------------------------------------- /envirophat_base/sources.md: -------------------------------------------------------------------------------- 1 | https://github.com/adafruit/Adafruit_Python_BME280 2 | 3 | -------------------------------------------------------------------------------- /envirophat_sensor/.gitignore: -------------------------------------------------------------------------------- 1 | Adafruit_Python_BME280/* 2 | adxl345-python/* 3 | 4 | -------------------------------------------------------------------------------- /envirophat_sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alexellis2/envirophat_base 2 | 3 | ADD ./*.py ./ 4 | 5 | CMD ["sudo", "-E", "python", "app.py"] 6 | -------------------------------------------------------------------------------- /envirophat_sensor/app.py: -------------------------------------------------------------------------------- 1 | from reporting import Reporter 2 | from sensing import Sensors 3 | from blinkt import Blinkt 4 | 5 | import time 6 | import os 7 | import signal 8 | import sys 9 | 10 | host = os.getenv("REDIS_HOST") 11 | if(host== None): 12 | host = "redis" 13 | 14 | baseline_threshold = os.getenv("TEMP_THRESHOLD") 15 | if(baseline_threshold != None): 16 | baseline_threshold = float(baseline_threshold) 17 | else: 18 | baseline_threshold = 0.5 19 | 20 | quiet = os.getenv("QUIET") 21 | if(quiet!=None): 22 | quiet = True 23 | else: 24 | quiet = False 25 | 26 | sample_rate = 0.25 27 | 28 | sensors = Sensors() 29 | reporter = Reporter(host, 6379, quiet) 30 | reporter.announce() 31 | 32 | host = reporter.get_name() 33 | blinkt = Blinkt(host) 34 | 35 | def sigterm_handler(_signo, _stack_frame): 36 | off() 37 | # reporting.delete_key(host+".live") # too late to handle this, redis may be down. 38 | sys.exit(0) 39 | 40 | signal.signal(signal.SIGTERM, sigterm_handler) 41 | 42 | def safeFloat(motion): 43 | if motion != None: 44 | return float(motion) 45 | return 0 46 | 47 | def is_hot(temp, baseline): 48 | diff = 0 49 | if(temp != None and baseline != None): 50 | baseline_float = round(float(baseline), 2) 51 | temp_float = round(float(temp), 2) 52 | diff = abs(temp_float - baseline_float) 53 | if(quiet == False): 54 | print("["+str(diff) + "] "+ str(temp_float) + " - " + str(baseline_float)) 55 | return diff > baseline_threshold 56 | 57 | def get_status_color(blinkt, output): 58 | rgb = None 59 | if safeFloat(output["motion"]) > 0: 60 | rgb = blinkt.to_rgb(0, 0, 255) 61 | elif is_hot(output["temp"], output["temp.baseline"]): 62 | rgb = blinkt.to_rgb(255, 0, 0) 63 | else: 64 | rgb = blinkt.to_rgb(0, 255, 0) 65 | return rgb 66 | 67 | def off(): 68 | off = blinkt.to_rgb(0, 0, 0) 69 | for x in range(0, 8): 70 | blinkt.show(off, x) 71 | 72 | def welcome(): 73 | on = blinkt.to_rgb(0, 0, 255) 74 | for x in range(0, 8): 75 | blinkt.show(on, x) 76 | time.sleep(0.2) 77 | off = blinkt.to_rgb(0, 0, 0) 78 | for x in range(0, 8): 79 | blinkt.show(off, x) 80 | time.sleep(0.1) 81 | 82 | def read_write_loop(): 83 | output = sensors.read() 84 | if(quiet == False): 85 | print(output) 86 | 87 | reporter.set(output) 88 | reporter.publish() 89 | 90 | output["temp.baseline"] = reporter.get_key(host + ".temp.baseline") 91 | if(output["temp.baseline"] == None): 92 | output["temp.baseline"] = output["temp"] 93 | 94 | color = get_status_color(blinkt, output) 95 | blinkt.show_all(color) 96 | 97 | if(__name__ == "__main__"): 98 | off() 99 | welcome() 100 | try: 101 | while(True): 102 | read_write_loop() 103 | time.sleep(sample_rate) 104 | except: 105 | off() 106 | -------------------------------------------------------------------------------- /envirophat_sensor/blinkt.py: -------------------------------------------------------------------------------- 1 | from dotstar import Adafruit_DotStar 2 | 3 | numpixels = 8 4 | datapin = 23 5 | clockpin = 24 6 | 7 | class Blinkt: 8 | def __init__(self, host): 9 | self.host = host 10 | self.strip = Adafruit_DotStar(numpixels, datapin, clockpin) 11 | self.strip.begin() 12 | self.strip.setBrightness(32) 13 | 14 | green = self.to_rgb(0,255,0) 15 | self.show_all(green) 16 | def to_rgb(self,r,g,b): 17 | return (g << 16) + (r << 8) + b 18 | def show(self, colour, pixel): 19 | self.strip.setPixelColor(pixel, colour) 20 | self.strip.show() 21 | def show_all(self, colour): 22 | for x in range(0,8): 23 | self.strip.setPixelColor(x, colour) 24 | self.strip.show() 25 | -------------------------------------------------------------------------------- /envirophat_sensor/motion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin.env python 2 | 3 | import smbus 4 | import time 5 | import math 6 | import RPi.GPIO as GPIO 7 | import struct 8 | 9 | ### LSM303 Address ### 10 | LSM303D_ADDR = 0x1D # Assuming SA0 grounded 11 | 12 | ### LSM303 Register definitions ### 13 | TEMP_OUT_L = 0x05 14 | TEMP_OUT_H = 0x06 15 | STATUS_REG_M = 0x07 16 | OUT_X_L_M = 0x08 17 | OUT_X_H_M = 0x09 18 | OUT_Y_L_M = 0x0A 19 | OUT_Y_H_M = 0x0B 20 | OUT_Z_L_M = 0x0C 21 | OUT_Z_H_M = 0x0D 22 | WHO_AM_I = 0x0F 23 | INT_CTRL_M = 0x12 24 | INT_SRC_M = 0x13 25 | INT_THS_L_M = 0x14 26 | INT_THS_H_M = 0x15 27 | OFFSET_X_L_M = 0x16 28 | OFFSET_X_H_M = 0x17 29 | OFFSET_Y_L_M = 0x18 30 | OFFSET_Y_H_M = 0x19 31 | OFFSET_Z_L_M = 0x1A 32 | OFFSET_Z_H_M = 0x1B 33 | REFERENCE_X = 0x1C 34 | REFERENCE_Y = 0x1D 35 | REFERENCE_Z = 0x1E 36 | CTRL_REG0 = 0x1F 37 | CTRL_REG1 = 0x20 38 | CTRL_REG2 = 0x21 39 | CTRL_REG3 = 0x22 40 | CTRL_REG4 = 0x23 41 | CTRL_REG5 = 0x24 42 | CTRL_REG6 = 0x25 43 | CTRL_REG7 = 0x26 44 | STATUS_REG_A = 0x27 45 | OUT_X_L_A = 0x28 46 | OUT_X_H_A = 0x29 47 | OUT_Y_L_A = 0x2A 48 | OUT_Y_H_A = 0x2B 49 | OUT_Z_L_A = 0x2C 50 | OUT_Z_H_A = 0x2D 51 | FIFO_CTRL = 0x2E 52 | FIFO_SRC = 0x2F 53 | IG_CFG1 = 0x30 54 | IG_SRC1 = 0x31 55 | IG_THS1 = 0x32 56 | IG_DUR1 = 0x33 57 | IG_CFG2 = 0x34 58 | IG_SRC2 = 0x35 59 | IG_THS2 = 0x36 60 | IG_DUR2 = 0x37 61 | CLICK_CFG = 0x38 62 | CLICK_SRC = 0x39 63 | CLICK_THS = 0x3A 64 | TIME_LIMIT = 0x3B 65 | TIME_LATENCY = 0x3C 66 | TIME_WINDOW = 0x3D 67 | ACT_THS = 0x3E 68 | ACT_DUR = 0x3F 69 | 70 | ### Mag scales ### 71 | MAG_SCALE_2 = 0x00 # full-scale is +/- 2 Gauss 72 | MAG_SCALE_4 = 0x20 # +/- 4 Guass 73 | MAG_SCALE_8 = 0x40 # +/- 8 Guass 74 | MAG_SCALE_12 = 0x60 # +/- 12 Guass 75 | 76 | ACCEL_SCALE = 2 # +/- 2g 77 | 78 | X = 0 79 | Y = 1 80 | Z = 2 81 | 82 | def twos_comp(val, bits): 83 | # Calculate the 2s complement of int:val # 84 | if(val&(1<<(bits-1)) != 0): 85 | val = val - (1< 2*math.pi: 143 | self.heading -= 2*math.pi 144 | 145 | self.headingDegrees = round(math.degrees(self.heading),2) 146 | 147 | def getTiltHeading(self): 148 | truncate = [0,0,0] 149 | for i in range(X, Z+1): 150 | truncate[i] = math.copysign(min(math.fabs(self.accel[i]), 1.0), self.accel[i]) 151 | try: 152 | pitch = math.asin(-1*truncate[X]) 153 | roll = math.asin(truncate[Y]/math.cos(pitch)) if abs(math.cos(pitch)) >= abs(truncate[Y]) else 0 154 | # set roll to zero if pitch approaches -1 or 1 155 | 156 | self.tiltcomp[X] = self.mag[X] * math.cos(pitch) + self.mag[Z] * math.sin(pitch) 157 | self.tiltcomp[Y] = self.mag[X] * math.sin(roll) * math.sin(pitch) + \ 158 | self.mag[Y] * math.cos(roll) - self.mag[Z] * math.sin(roll) * math.cos(pitch) 159 | self.tiltcomp[Z] = self.mag[X] * math.cos(roll) * math.sin(pitch) + \ 160 | self.mag[Y] * math.sin(roll) + \ 161 | self.mag[Z] * math.cos(roll) * math.cos(pitch) 162 | self.tiltHeading = math.atan2(self.tiltcomp[Y], self.tiltcomp[X]) 163 | 164 | if self.tiltHeading < 0: 165 | self.tiltHeading += 2*math.pi 166 | if self.tiltHeading > 2*math.pi: 167 | self.heading -= 2*math.pi 168 | 169 | self.tiltHeadingDegrees = round(math.degrees(self.tiltHeading),2) 170 | 171 | except Exception: 172 | print "AccelX {}, AccelY {}".format(self.accel[X], self.accel[Y]) 173 | print "TruncX {}, TruncY {}".format(truncate[X], truncate[Y]) 174 | print "Pitch {}, cos(pitch) {}, Bool(cos(pitch)) {}".format(pitch, math.cos(pitch), bool(math.cos(pitch))) 175 | 176 | 177 | def isMagReady(self): 178 | temp = self.bus.read_byte_data(LSM303D_ADDR, STATUS_REG_M) & 0x03 179 | 180 | return temp 181 | 182 | def update(self): 183 | 184 | self.getAccel() 185 | self.getMag() 186 | self.getHeading() 187 | self.getTiltHeading() 188 | 189 | # if __name__ == '__main__': 190 | 191 | # from time import sleep 192 | 193 | # buf = [] 194 | # buf_len = 5 195 | # last_avg = 0 196 | # threshold = 0.02 197 | 198 | # lsm = accelcomp() 199 | 200 | # while True: 201 | # lsm.getAccel() 202 | # buf.append(lsm.accel[X] + lsm.accel[Y] + lsm.accel[Z]) 203 | # buf = buf[-buf_len:] 204 | # avg = reduce(lambda x, y: x + y, buf) / len(buf) 205 | # if abs(avg - last_avg) > threshold: 206 | # print("MOTION!", abs(avg-last_avg)) 207 | # last_avg = avg 208 | 209 | # time.sleep(0.1) 210 | 211 | -------------------------------------------------------------------------------- /envirophat_sensor/msense.py: -------------------------------------------------------------------------------- 1 | from motion import * 2 | 3 | class MotionSense: 4 | def __init__(self): 5 | self.buffer = [] 6 | self.lsm = accelcomp() 7 | self.last_avg = 0 8 | 9 | def read(self): 10 | buf_len = 5 11 | 12 | threshold = 0.02 13 | 14 | self.lsm.getAccel() 15 | self.buffer.append(self.lsm.accel[X] + self.lsm.accel[Y] + self.lsm.accel[Z]) 16 | self.buffer = self.buffer[-buf_len:] 17 | avg = reduce(lambda x, y: x + y, self.buffer) / len(self.buffer) 18 | diff = abs(avg - self.last_avg) 19 | 20 | movement = 0 21 | if diff > threshold and diff < 1: 22 | movement = diff 23 | self.last_avg = avg 24 | return movement 25 | 26 | 27 | if __name__ == '__main__': 28 | 29 | from time import sleep 30 | sense = MotionSense() 31 | 32 | while True: 33 | val = sense.read() 34 | if(val > 0): 35 | print("Motion") 36 | time.sleep(0.1) 37 | -------------------------------------------------------------------------------- /envirophat_sensor/old/driver.py: -------------------------------------------------------------------------------- 1 | from motion import * 2 | 3 | if __name__ == '__main__': 4 | 5 | from time import sleep 6 | 7 | buf = [] 8 | buf_len = 5 9 | last_avg = 0 10 | threshold = 0.02 11 | 12 | lsm = accelcomp() 13 | 14 | while True: 15 | lsm.getAccel() 16 | buf.append(lsm.accel[X] + lsm.accel[Y] + lsm.accel[Z]) 17 | buf = buf[-buf_len:] 18 | avg = reduce(lambda x, y: x + y, buf) / len(buf) 19 | diff = abs(avg - last_avg) 20 | if diff > threshold and diff < 1: 21 | print("MOTION!", abs(avg-last_avg)) 22 | last_avg = avg 23 | 24 | time.sleep(0.1) 25 | 26 | -------------------------------------------------------------------------------- /envirophat_sensor/rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git pull 3 | docker build -t envirophat_sensor . 4 | docker run -e REDIS_HOST=192.168.0.38 --rm -ti --privileged envirophat_sensor 5 | 6 | 7 | -------------------------------------------------------------------------------- /envirophat_sensor/reporting.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import socket 3 | 4 | class Reporter: 5 | def __init__(self, host, port, quiet): 6 | self.host = host 7 | self.port = port 8 | 9 | self.name = socket.getfqdn() 10 | self.live_expiry = 30 11 | self.quiet = quiet 12 | 13 | def get_name(self): 14 | return self.name 15 | 16 | def get_key(self, key): 17 | return self.client.get(key) 18 | 19 | def set_live(self): 20 | self.overset_expiring_key("live", "1", self.live_expiry) 21 | 22 | def delete_key(self, key): 23 | self.client.delete(key) 24 | 25 | def announce(self): 26 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 27 | self.set_live() 28 | self.client.hset("members", self.name, "1") 29 | self.client.publish("members.add", self.name) 30 | 31 | def set_key(self, key, value): 32 | if(self.quiet == False): 33 | print(key, value) 34 | 35 | self.client.set(self.name+"."+key, round(value, 2)) 36 | 37 | def overset_expiring_key(self, key, value, timeout): 38 | 39 | self.client.set(self.name+"."+key, value) 40 | self.client.expire(self.name+"."+key, timeout) 41 | 42 | def set_expiring_key(self, key, value, timeout): 43 | if(self.quiet == False): 44 | print(key, value, timeout) 45 | 46 | exists = self.client.get(self.name+"."+key) 47 | if(exists==None): 48 | if(self.quiet == False): 49 | print(key+ ", value=" + str(value)) 50 | self.client.set(self.name+"."+key, value) 51 | self.client.expire(self.name+"."+key, timeout) 52 | 53 | def set(self, values): 54 | self.set_live() 55 | 56 | self.set_key("temp", values["temp"]) 57 | self.set_expiring_key("temp.baseline", round(values["temp"], 2), 300) 58 | 59 | self.overset_expiring_key("motion", values["motion"], 5) 60 | 61 | 62 | def publish(self): 63 | self.client.publish("sensors.data", self.name) 64 | -------------------------------------------------------------------------------- /envirophat_sensor/sensing.py: -------------------------------------------------------------------------------- 1 | import time 2 | from Adafruit_BME280 import * 3 | from msense import MotionSense 4 | 5 | class Sensors: 6 | def __init__(self): 7 | self.sensor = BME280(mode=BME280_OSAMPLE_8) 8 | self.motion_sense = MotionSense() 9 | 10 | self.last = self.read() 11 | 12 | def read(self): 13 | degrees = self.sensor.read_temperature() 14 | self.motion = self.motion_sense.read() 15 | 16 | return {"temp": degrees, "motion":self.motion} 17 | -------------------------------------------------------------------------------- /join.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd join 4 | ./auto_join.sh 5 | 6 | -------------------------------------------------------------------------------- /join/auto_join.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: auto_join_swarm.sh 4 | # Default: auto_join_swarm.sh 192.168.0.200:8500 5 | 6 | export consul_addr=192.168.0.110:8500 7 | 8 | if [ ! -z $1 ]; then 9 | export consul_addr=$1:8500 10 | fi 11 | 12 | echo "Using consul: $consul_addr" 13 | 14 | export eth0_addr=$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1) 15 | export node_ip=$eth0_addr:2375 16 | 17 | echo "Removing old container" 18 | 19 | docker rm -f join 20 | 21 | export image=hypriot/rpi-swarm:v1.2.4 22 | 23 | echo "Starting new container" 24 | 25 | docker run --name join -d $image join --advertise=$node_ip consul://$consul_addr/swarm 26 | -------------------------------------------------------------------------------- /join/auto_join_old.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: auto_join_swarm.sh 4 | # Default: auto_join_swarm.sh 192.168.0.200:8500 5 | 6 | export consul_addr=192.168.0.110:8500 7 | 8 | if [ ! -z $1 ]; then 9 | export consul_addr=$1:8500 10 | fi 11 | 12 | echo "Using consul: $consul_addr" 13 | 14 | export eth0_addr=$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1) 15 | export node_ip=$eth0_addr:2375 16 | 17 | echo "Removing old container" 18 | docker rm -f join 19 | 20 | if [[ -z $swarm_version ]]; then 21 | export swarm_version="v6" 22 | fi; 23 | 24 | echo "Using version: $swarm_version" 25 | export image=alexellis2/swarm-arm:$swarm_version 26 | 27 | echo "Starting new container" 28 | 29 | docker run --name join -d $image join --advertise=$node_ip consul://$consul_addr/swarm 30 | -------------------------------------------------------------------------------- /join/auto_join_wlan.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage: auto_join_swarm.sh 4 | # Default: auto_join_swarm.sh 192.168.0.200:8500 5 | 6 | export consul_addr=192.168.0.110:8500 7 | 8 | if [ ! -z $1 ]; then 9 | export consul_addr=$1:8500 10 | fi 11 | 12 | echo "Using consul: $consul_addr" 13 | 14 | export eth0_addr=$(ip addr show wlan0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1) 15 | export node_ip=$eth0_addr:2375 16 | 17 | echo "Removing old container" 18 | docker rm -f join 19 | 20 | if [[ -z $swarm_version ]]; then 21 | export swarm_version="v6" 22 | fi; 23 | 24 | echo "Using version: $swarm_version" 25 | export image=alexellis2/swarm-arm:$swarm_version 26 | 27 | echo "Starting new container" 28 | 29 | docker run --name join -d $image join --advertise=$node_ip consul://$consul_addr/swarm 30 | -------------------------------------------------------------------------------- /multi_dashboard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alexellis2/unicorn-hat:dev 2 | RUN sudo pip install redis 3 | 4 | ADD *.py ./ 5 | 6 | CMD ["sudo","-E", "python2", "app.py"] 7 | -------------------------------------------------------------------------------- /multi_dashboard/app.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | import signal 4 | import sys 5 | 6 | from reporter import Reporter 7 | import unicornhat as UH 8 | 9 | def clear_uh(): 10 | UH.clear() 11 | UH.show() 12 | 13 | def sigterm_handler(_signo, _stack_frame): 14 | clear_uh() 15 | sys.exit(0) 16 | 17 | signal.signal(signal.SIGTERM, sigterm_handler) 18 | 19 | # UH.set_layout(UH.PHAT) 20 | #max_pixels = 4 21 | max_pixels = 8 22 | 23 | host = os.getenv("REDIS_HOST") 24 | if(host == None): 25 | host = "redis" 26 | 27 | baseline_threshold = os.getenv("TEMP_THRESHOLD") 28 | if(baseline_threshold != None): 29 | baseline_threshold = float(baseline_threshold) 30 | else: 31 | baseline_threshold = 0.5 32 | 33 | quiet = os.getenv("QUIET") 34 | if(quiet!=None): 35 | quiet = True 36 | else: 37 | quiet = False 38 | 39 | clear_uh() 40 | 41 | r = Reporter(host, "6379") 42 | last_members = [] 43 | 44 | def on(column, r,g,b): 45 | x = column 46 | for y in range(0, max_pixels): 47 | UH.set_pixel(x, y, r, g, b) 48 | UH.show() 49 | 50 | def safeFloat(motion): 51 | if motion != None: 52 | return float(motion) 53 | 54 | def is_hot(temp, baseline): 55 | diff = 0 56 | if(temp != None and baseline != None): 57 | baseline_float = round(float(baseline), 2) 58 | temp_float = round(float(temp), 2) 59 | 60 | diff = abs(temp_float - baseline_float) 61 | if(quiet == False): 62 | print("["+str(diff) + "] "+ str(temp_float) + " - " + str(baseline_float)) 63 | return diff > baseline_threshold 64 | 65 | def paint(): 66 | global last_members 67 | index = 0 68 | members = r.find_members() 69 | if(len(last_members) != len(members)): 70 | UH.clear() 71 | UH.show() 72 | 73 | last_members = members 74 | for member in members: 75 | temp = r.get_key(member + ".temp") 76 | baseline = r.get_key(member + ".temp.baseline") 77 | motion = r.get_key(member + ".motion") 78 | 79 | if safeFloat(motion) > 0: 80 | if(quiet == False): 81 | print("moved") 82 | on(index, 0, 0, 255) 83 | elif is_hot(temp, baseline): 84 | on(index, 255, 0, 0) 85 | if(quiet == False): 86 | print("HOT") 87 | else: 88 | on(index, 0, 255, 0) 89 | index = index +1 90 | 91 | def on_sensor_data(channel, data): 92 | if(quiet == False): 93 | print(channel, data) 94 | paint() 95 | 96 | r.set_on_sensor_data(on_sensor_data) 97 | r.subscribe() 98 | 99 | while True: 100 | if(quiet == False): 101 | print (r.find_members()) 102 | if(len(r.find_members()) == 0): 103 | clear_uh() 104 | 105 | -------------------------------------------------------------------------------- /multi_dashboard/rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git pull 3 | docker build -t multi . 4 | docker run -e REDIS_HOST=192.168.0.38 --rm -ti --privileged multi 5 | -------------------------------------------------------------------------------- /multi_dashboard/reporter.py: -------------------------------------------------------------------------------- 1 | from threadedsubscriber import ThreadedSubscriber 2 | import redis 3 | import socket 4 | 5 | class Reporter: 6 | def __init__(self, host, port): 7 | self.host = host 8 | self.port = port 9 | 10 | self.name = socket.getfqdn() 11 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 12 | self.channels = ["sensors.data", "members.add"] 13 | 14 | def find_members(self): 15 | members = self.client.hgetall("members") 16 | live = [] 17 | 18 | for member in members: 19 | if(self.client.get(member+".live")): 20 | live.append(member) 21 | return live 22 | 23 | def on_message(self, channel, message): 24 | print("Channel: "+channel + " - " + message ) 25 | # print(channel, message) 26 | 27 | def set_on_sensor_data(self, cb): 28 | self.on_sensor_data_cb = cb 29 | 30 | def subscribe(self): 31 | self.subscriber = ThreadedSubscriber(self.client, self.channels, self.on_sensor_data_cb) 32 | self.subscriber.run() 33 | 34 | def set_key(self, key, value): 35 | self.client.set(key, value) 36 | 37 | def get_key(self, key): 38 | return self.client.get(key) 39 | -------------------------------------------------------------------------------- /multi_dashboard/threadedsubscriber.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | class ThreadedSubscriber(threading.Thread): 4 | def __init__(self, client, channels, callback): 5 | self.channels = channels 6 | self.client=client 7 | self.callback = callback 8 | 9 | def run(self): 10 | self.pubsub = self.client.pubsub() 11 | # self.pubsub.subscribe(self.channels) 12 | self.pubsub.subscribe("sensors.data", "members.add") 13 | 14 | while(True): 15 | for m in self.pubsub.listen(): 16 | # print m #'Recieved: {0}'.format(m['data']) 17 | if(m["type"]=="message"): 18 | if(self.callback != None): 19 | self.callback(m["channel"], m["data"]) 20 | -------------------------------------------------------------------------------- /non_demo/dashboard/app.py: -------------------------------------------------------------------------------- 1 | import time 2 | from reporter import Reporter 3 | import os 4 | 5 | host = os.getenv("REDIS_HOST") 6 | if(host== None): 7 | host = "redis" 8 | 9 | r = Reporter(host, "6379") 10 | 11 | def on_sensor_data(channel, data): 12 | print(channel, data) 13 | print(str(r.get_key(data+".temp"))) 14 | print(str(r.get_key(data+".motion"))) 15 | 16 | r.set_on_sensor_data(on_sensor_data) 17 | r.subscribe() 18 | 19 | while True: 20 | print (r.find_members()) 21 | time.sleep(5) 22 | -------------------------------------------------------------------------------- /non_demo/dashboard/reporter.py: -------------------------------------------------------------------------------- 1 | from threadedsubscriber import ThreadedSubscriber 2 | import redis 3 | import socket 4 | 5 | class Reporter: 6 | def __init__(self, host, port): 7 | self.host = host 8 | self.port = port 9 | 10 | self.name = socket.getfqdn() 11 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 12 | self.channels = ["sensors.data", "members.add"] 13 | 14 | def find_members(self): 15 | return self.client.hgetall("members") 16 | 17 | def on_message(self, channel, message): 18 | print("Channel: "+channel + " - " + message ) 19 | # print(channel, message) 20 | 21 | def set_on_sensor_data(self, cb): 22 | self.on_sensor_data_cb = cb 23 | 24 | def subscribe(self): 25 | self.subscriber = ThreadedSubscriber(self.client, self.channels, self.on_sensor_data_cb) 26 | self.subscriber.run() 27 | def get_key(self, key): 28 | return self.client.get(key) 29 | -------------------------------------------------------------------------------- /non_demo/dashboard/scrollapp.py: -------------------------------------------------------------------------------- 1 | import time 2 | from reporter import Reporter 3 | import os 4 | import scrollphat 5 | 6 | host = os.getenv("REDIS_HOST") 7 | if(host== None): 8 | host = "localhost" 9 | 10 | scrollphat.set_brightness(10) 11 | 12 | r = Reporter(host, "6379") 13 | 14 | def on_sensor_data(channel, data): 15 | print(channel, data) 16 | val = float(r.get_key(data+".temp")) 17 | print(str(val)) 18 | 19 | scrollphat.write_string( str(round(val))[:2] ) 20 | scrollphat.update() 21 | 22 | r.set_on_sensor_data(on_sensor_data) 23 | r.subscribe() 24 | 25 | while True: 26 | print (r.find_members()) 27 | time.sleep(5) 28 | -------------------------------------------------------------------------------- /non_demo/dashboard/threadedsubscriber.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | class ThreadedSubscriber(threading.Thread): 4 | def __init__(self, client, channels, callback): 5 | self.channels = channels 6 | self.client=client 7 | self.callback = callback 8 | 9 | def run(self): 10 | self.pubsub = self.client.pubsub() 11 | # self.pubsub.subscribe(self.channels) 12 | self.pubsub.subscribe("sensors.data", "members.add") 13 | 14 | while(True): 15 | for m in self.pubsub.listen(): 16 | # print m #'Recieved: {0}'.format(m['data']) 17 | if(m["type"]=="message"): 18 | if(self.callback != None): 19 | self.callback(m["channel"], m["data"]) 20 | -------------------------------------------------------------------------------- /non_demo/explorer_sensor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alexellis2/python-gpio-arm:v6-dev 2 | RUN pip2 install redis 3 | RUN pip2 install explorerhat 4 | RUN sudo apt-get -qy install python-smbus 5 | 6 | ADD *.py ./ 7 | 8 | CMD ["sudo", "-E", "python2", "app.py"] 9 | -------------------------------------------------------------------------------- /non_demo/explorer_sensor/app.py: -------------------------------------------------------------------------------- 1 | from reporting import Reporter 2 | from explorersensing import Sensors 3 | import time 4 | import os 5 | 6 | host = os.getenv("REDIS_HOST") 7 | if(host== None): 8 | host = "redis" 9 | 10 | sensors = Sensors() 11 | reporter = Reporter(host, 6379) 12 | reporter.announce() 13 | 14 | while(True): 15 | output = sensors.read() 16 | print(output) 17 | reporter.set(output) 18 | reporter.publish() 19 | time.sleep(3) 20 | -------------------------------------------------------------------------------- /non_demo/explorer_sensor/explorersensing.py: -------------------------------------------------------------------------------- 1 | import time 2 | import explorerhat 3 | 4 | class Sensors: 5 | def __init__(self): 6 | self.temp = self.read() 7 | 8 | def read(self): 9 | v1 = explorerhat.analog.four.read() 10 | celcius = 25 + (v1-0.75) * 100 11 | self.temp = celcius 12 | return {"temp": self.temp, "motion":1} 13 | -------------------------------------------------------------------------------- /non_demo/explorer_sensor/reporting.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import socket 3 | 4 | class Reporter: 5 | def __init__(self, host, port): 6 | self.host = host 7 | self.port = port 8 | 9 | self.name = socket.getfqdn() 10 | 11 | def announce(self): 12 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 13 | self.client.hset("members", self.name, "1") 14 | self.client.publish("members.add", self.name) 15 | 16 | def set_key(self, key, value): 17 | print(key,value) 18 | self.client.set(self.name+"."+key, value) 19 | 20 | def set_expiring_key(self, key, value, timeout): 21 | print(key,value,timeout) 22 | 23 | exists = self.client.setnx(self.name+"."+key, value) 24 | if(exists == 1): 25 | self.client.expire(self.name+"."+key, timeout) 26 | 27 | def set(self, values): 28 | self.set_key("temp", values["temp"]) 29 | self.set_expiring_key("temp.baseline", values["temp"], 60) 30 | self.set_key("motion", values["temp"]) 31 | 32 | def publish(self): 33 | self.client.publish("sensors.data", self.name) 34 | -------------------------------------------------------------------------------- /non_demo/fake_sensor/app.py: -------------------------------------------------------------------------------- 1 | from reporting import Reporter 2 | from sensing import Sensors 3 | import time 4 | import os 5 | 6 | host = os.getenv("REDIS_HOST") 7 | if(host== None): 8 | host = "redis" 9 | 10 | sensors = Sensors() 11 | reporter = Reporter(host, 6379) 12 | 13 | reporter.announce() 14 | 15 | while(True): 16 | output = sensors.read() 17 | print(output) 18 | reporter.set(output) 19 | reporter.publish() 20 | time.sleep(3) 21 | -------------------------------------------------------------------------------- /non_demo/fake_sensor/reporting.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import socket 3 | 4 | class Reporter: 5 | def __init__(self, host, port): 6 | self.host = host 7 | self.port = port 8 | 9 | self.name = socket.getfqdn() 10 | 11 | def announce(self): 12 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 13 | self.client.hset("members", self.name, "1") 14 | self.client.publish("members.add", self.name) 15 | 16 | def set_key(self, key, value): 17 | print(key,value) 18 | self.client.set(self.name+"."+key, value) 19 | 20 | def set_expiring_key(self, key, value, timeout): 21 | print(key,value,timeout) 22 | 23 | exists = self.client.setnx(self.name+"."+key, value) 24 | if(exists == 1): 25 | self.client.expire(self.name+"."+key, timeout) 26 | 27 | def set(self, values): 28 | 29 | self.set_key("temp", values["temp"]) 30 | self.set_expiring_key("temp.baseline", values["temp"], 10) 31 | self.set_key("motion", values["temp"]) 32 | 33 | def publish(self): 34 | self.client.publish("sensors.data", self.name) 35 | -------------------------------------------------------------------------------- /non_demo/fake_sensor/sensing.py: -------------------------------------------------------------------------------- 1 | class Sensors: 2 | def __init__(self): 3 | self.temp = 10 4 | self.motion = 1 5 | 6 | def read(self): 7 | self.motion = self.motion +0.1 8 | self.temp = self.temp + 0.2 9 | return {"temp": self.temp, "motion": self.motion} 10 | -------------------------------------------------------------------------------- /non_demo/fake_sensor/tmp36sensing.py: -------------------------------------------------------------------------------- 1 | import time 2 | import explorerhat 3 | 4 | class Sensors: 5 | def __init__(self): 6 | self.temp = self.read() 7 | 8 | def read(self): 9 | v1 = explorerhat.analog.four.read() 10 | celcius = 25 + (v1-0.75) * 100 11 | self.temp = celcius 12 | return {"temp": self.temp, "motion":1} 13 | -------------------------------------------------------------------------------- /non_demo/redis_display/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /non_demo/redis_display/Dockerfile: -------------------------------------------------------------------------------- 1 | from alexellis2/node4.x-arm:v6 2 | 3 | add package.json ./ 4 | run npm i 5 | 6 | add app.js ./ 7 | EXPOSE 9000 8 | cmd ["node", "app.js"] 9 | -------------------------------------------------------------------------------- /non_demo/redis_display/app.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | var redis = require('redis'); 4 | 5 | var express = require('express'); 6 | var app = express(); 7 | 8 | var client = redis.createClient({"host": process.env.REDIS_HOST || "redis"}); 9 | client.on('connect', () => { 10 | console.log("Connected"); 11 | }); 12 | client.on('error', (err) => { 13 | if(err) 14 | console.error(err); 15 | }); 16 | app.get('/', (req,res) => { 17 | client.incr("node_temp", (err) => { 18 | client.get("node_temp", (err, val) => { 19 | if(err) { console.log(err); return res.end(); } 20 | 21 | res.write(val); 22 | res.end(); 23 | }); 24 | }); 25 | }); 26 | 27 | app.listen(9000, () => { 28 | console.log("Listening") 29 | }); 30 | -------------------------------------------------------------------------------- /non_demo/redis_display/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis_display", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.13.4", 14 | "redis": "^2.6.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /non_demo/tmp36_sensor/app.py: -------------------------------------------------------------------------------- 1 | from reporting import Reporter 2 | from tmp36sensing import Sensors 3 | import time 4 | import os 5 | 6 | host = os.getenv("REDIS_HOST") 7 | if(host== None): 8 | host = "redis" 9 | 10 | sensors = Sensors() 11 | reporter = Reporter(host, 6379) 12 | reporter.announce() 13 | 14 | while(True): 15 | output = sensors.read() 16 | print(output) 17 | reporter.set(output) 18 | reporter.publish() 19 | time.sleep(3) 20 | -------------------------------------------------------------------------------- /non_demo/tmp36_sensor/reporting.py: -------------------------------------------------------------------------------- 1 | import redis 2 | import socket 3 | 4 | class Reporter: 5 | def __init__(self, host, port): 6 | self.host = host 7 | self.port = port 8 | 9 | self.name = socket.getfqdn() 10 | 11 | def announce(self): 12 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 13 | self.client.hset("members", self.name, "1") 14 | self.client.publish("members.add", self.name) 15 | 16 | def set_key(self, key, values): 17 | self.client.set(self.name+"."+key, values[key]) 18 | 19 | def set(self, values): 20 | self.set_key("temp", values) 21 | self.set_key("motion", values) 22 | 23 | def publish(self): 24 | self.client.publish("sensors.data", self.name) 25 | -------------------------------------------------------------------------------- /non_demo/tmp36_sensor/sensing.py: -------------------------------------------------------------------------------- 1 | class Sensors: 2 | def __init__(self): 3 | self.temp = 10 4 | self.motion = 1 5 | 6 | def read(self): 7 | self.motion = self.motion +0.1 8 | self.temp = self.temp + 0.2 9 | return {"temp": self.temp, "motion": self.motion} 10 | -------------------------------------------------------------------------------- /non_demo/tmp36_sensor/tmp36sensing.py: -------------------------------------------------------------------------------- 1 | import time 2 | import explorerhat 3 | 4 | class Sensors: 5 | def __init__(self): 6 | self.temp = self.read() 7 | 8 | def read(self): 9 | v1 = explorerhat.analog.four.read() 10 | celcius = 25 + (v1-0.75) * 100 11 | self.temp = celcius 12 | return {"temp": self.temp, "motion":1} 13 | -------------------------------------------------------------------------------- /non_demo/unicorn_dashboard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alexellis2/unicorn-hat:dev 2 | RUN sudo pip install redis 3 | 4 | ADD *.py ./ 5 | 6 | CMD ["sudo","-E", "python2", "app.py"] 7 | -------------------------------------------------------------------------------- /non_demo/unicorn_dashboard/app.py: -------------------------------------------------------------------------------- 1 | import time 2 | from reporter import Reporter 3 | import os 4 | import unicornhat as UH 5 | 6 | host = os.environ["REDIS_HOST"] 7 | 8 | if(host== None): 9 | host = "localhost" 10 | 11 | UH.clear() 12 | UH.show() 13 | 14 | r = Reporter(host, "6379") 15 | 16 | def on(r,g,b): 17 | for x in range(0,4): 18 | for y in range(0,11): 19 | UH.set_pixel(x,y,r,g,b) 20 | 21 | def on_sensor_data(channel, data): 22 | print(channel, data) 23 | motion = r.get_key(data+".motion") 24 | if(motion != None and float(motion) > 0): 25 | on(0,0,255) 26 | UH.show() 27 | return 28 | 29 | value = r.get_key(data+".temp") 30 | baseline = r.get_key(data+".temp.baseline") 31 | print(baseline,value) 32 | if(baseline != None and value != None): 33 | baseline = round(float(baseline), 2) 34 | value = round(float(value), 2) 35 | 36 | diff = abs(value - baseline) 37 | print(str(value) + " ~ " + str(baseline) + " = " + str(diff)) 38 | 39 | if(diff < 1.5): 40 | on(0,255,0) 41 | else: 42 | on(255,0,0) 43 | 44 | UH.show() 45 | 46 | 47 | r.set_on_sensor_data(on_sensor_data) 48 | r.subscribe() 49 | 50 | while True: 51 | print (r.find_members()) 52 | time.sleep(5) 53 | -------------------------------------------------------------------------------- /non_demo/unicorn_dashboard/reporter.py: -------------------------------------------------------------------------------- 1 | from threadedsubscriber import ThreadedSubscriber 2 | import redis 3 | import socket 4 | 5 | class Reporter: 6 | def __init__(self, host, port): 7 | self.host = host 8 | self.port = port 9 | 10 | self.name = socket.getfqdn() 11 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 12 | self.channels = ["sensors.data", "members.add"] 13 | 14 | def find_members(self): 15 | members = self.client.hgetall("members") 16 | live = [] 17 | 18 | for member in members: 19 | if(self.client.get(member+".live")): 20 | live.push(member) 21 | return live 22 | 23 | def on_message(self, channel, message): 24 | print("Channel: "+channel + " - " + message ) 25 | # print(channel, message) 26 | 27 | def set_on_sensor_data(self, cb): 28 | self.on_sensor_data_cb = cb 29 | 30 | def subscribe(self): 31 | self.subscriber = ThreadedSubscriber(self.client, self.channels, self.on_sensor_data_cb) 32 | self.subscriber.run() 33 | 34 | def get_key(self, key): 35 | return self.client.get(key) 36 | -------------------------------------------------------------------------------- /non_demo/unicorn_dashboard/threadedsubscriber.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | class ThreadedSubscriber(threading.Thread): 4 | def __init__(self, client, channels, callback): 5 | self.channels = channels 6 | self.client=client 7 | self.callback = callback 8 | 9 | def run(self): 10 | self.pubsub = self.client.pubsub() 11 | # self.pubsub.subscribe(self.channels) 12 | self.pubsub.subscribe("sensors.data", "members.add") 13 | 14 | while(True): 15 | for m in self.pubsub.listen(): 16 | # print m #'Recieved: {0}'.format(m['data']) 17 | if(m["type"]=="message"): 18 | if(self.callback != None): 19 | self.callback(m["channel"], m["data"]) 20 | -------------------------------------------------------------------------------- /spec.md: -------------------------------------------------------------------------------- 1 | Monitor polls redis set for members. 2 | 3 | Member reads temp as member.temp kvp 4 | Member reads motion level as member.motion kvp 5 | 6 | Sensor on load sets itself into set. 7 | Sensor updates member.temp and member.motion and member.temp.baseline 8 | 9 | Sensor submits onto channel "update", sensor name. 10 | 11 | -------------------------------------------------------------------------------- /start_manager.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd consul 4 | ./start_consul.sh 5 | ./manage.sh 6 | 7 | 8 | -------------------------------------------------------------------------------- /webdashboard/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alexellis2/python-gpio-flask:v6 2 | WORKDIR /root/ 3 | 4 | RUN sudo pip install redis 5 | 6 | ADD ./js/ ./js/ 7 | ADD ./*.py ./ 8 | ADD ./templates/ ./templates/ 9 | EXPOSE 5000 10 | 11 | CMD ["python", "app.py"] 12 | -------------------------------------------------------------------------------- /webdashboard/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import redis 3 | import json 4 | 5 | from flask import Flask, request, render_template, send_from_directory 6 | from reporter import Reporter 7 | 8 | host = os.getenv("REDIS_HOST") 9 | if(host == None): 10 | host = "redis" 11 | 12 | app = Flask(__name__) 13 | r = Reporter(host, 6379) 14 | 15 | def build_cache(): 16 | cache = [] 17 | members = r.find_members() 18 | 19 | for member in members: 20 | item = {} 21 | item["name"] = member 22 | item["temp"] = float( r.get_key(member + ".temp") ) 23 | item["temp.baseline"] = float( r.get_key(member + ".temp.baseline") ) 24 | item["motion"] = float( r.get_key(member + ".motion") ) 25 | try: 26 | item["temp.diff"] = float( round(abs(float(item["temp"]) - float(item["temp.baseline"])), 2) ) 27 | cache.append(item) 28 | except: 29 | print("oops " + member + "has bad data") 30 | return cache 31 | 32 | @app.route('/json', methods=['GET']) 33 | def home_json(): 34 | cache = build_cache() 35 | return json.dumps({"sensors": cache}) 36 | 37 | @app.route('/nodes/', methods=['GET']) 38 | def home(): 39 | hosts = build_cache() 40 | return render_template("nodes.html", hosts=hosts) 41 | 42 | @app.route('/js/') 43 | def send_js(path): 44 | return send_from_directory('js', path) 45 | 46 | @app.route('/', methods=['GET']) 47 | def sensors(): 48 | return render_template("sensors.html") 49 | 50 | if __name__ == '__main__': 51 | print("0.0.0.0") 52 | app.run(debug=True, host='0.0.0.0') 53 | -------------------------------------------------------------------------------- /webdashboard/get_js.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -sSLO https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js 4 | curl -sSLO http://github.com/joewalnes/smoothie/raw/master/smoothie.js 5 | mv *.js js/ 6 | 7 | -------------------------------------------------------------------------------- /webdashboard/js/controllers.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('dashboard', []); 2 | 3 | app.controller('SensorsController', function($scope, $http) { 4 | var refresh = 500; 5 | var chartRefresh = 1000; 6 | var totalLines =0; 7 | var lines = []; 8 | var colours = [ 9 | {r:255, g:0, b:0}, 10 | {r:0, g:255, b:0}, 11 | {r:0, g:0, b:255}, 12 | {r:100, g:255, b:100} 13 | ]; 14 | 15 | var getStyle = function getStyle(index) { 16 | return { 17 | strokeStyle: 'rgba(' +colours[index]['r'] + ', '+ colours[index]['g'] + ', ' + colours[index]['b'] + ', 1)', 18 | fillStyle: 'rgba(' +colours[index]['r'] + ', '+ colours[index]['g'] + ', ' + colours[index]['b'] + ', 0.2)', 19 | lineWidth:3 20 | }; 21 | }; 22 | 23 | var smoothie = new SmoothieChart(); 24 | smoothie.streamTo(document.getElementById("sensorCanvas"), chartRefresh /*delay*/); 25 | 26 | var interval = setInterval(function() { 27 | 28 | $http.get("/json").then(function(response) { 29 | $scope.sensors = response.data["sensors"]; 30 | var currentLines = $scope.sensors.length; 31 | if(!totalLines) { 32 | totalLines = currentLines; 33 | for(var j = 0; j < totalLines; j++) { 34 | var line = new TimeSeries(); 35 | smoothie.addTimeSeries(line, getStyle(j)); 36 | lines.push(line); 37 | } 38 | } 39 | if(lines.length) { 40 | for(var i = 0; i < $scope.sensors.length; i++) { 41 | var temp = $scope.sensors[i]["temp"]; 42 | lines[i].append(new Date().getTime(), temp); 43 | } 44 | } 45 | }).catch(function() { 46 | $scope.error = "Unable to reach stats API"; 47 | }); 48 | }, refresh); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /webdashboard/js/smoothie.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2013, Joe Walnes 4 | // 2013-2014, Drew Noakes 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | /** 25 | * Smoothie Charts - http://smoothiecharts.org/ 26 | * (c) 2010-2013, Joe Walnes 27 | * 2013-2014, Drew Noakes 28 | * 29 | * v1.0: Main charting library, by Joe Walnes 30 | * v1.1: Auto scaling of axis, by Neil Dunn 31 | * v1.2: fps (frames per second) option, by Mathias Petterson 32 | * v1.3: Fix for divide by zero, by Paul Nikitochkin 33 | * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds 34 | * v1.5: Set default frames per second to 50... smoother. 35 | * .start(), .stop() methods for conserving CPU, by Dmitry Vyal 36 | * options.interpolation = 'bezier' or 'line', by Dmitry Vyal 37 | * options.maxValue to fix scale, by Dmitry Vyal 38 | * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla 39 | * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin 40 | * Smooth rescaling, by Kostas Michalopoulos 41 | * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni 42 | * v1.9: Display timestamps along the bottom, by Nick and Stev-io 43 | * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D) 44 | * Refactored by Krishna Narni, to support timestamp formatting function 45 | * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh 46 | * v1.11: options.grid.sharpLines option added, by @drewnoakes 47 | * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes 48 | * v1.12: Support for horizontalLines added, by @drewnoakes 49 | * Support for yRangeFunction callback added, by @drewnoakes 50 | * v1.13: Fixed typo (#32), by @alnikitich 51 | * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano 52 | * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes 53 | * v1.15: Support for npm package (#18), by @dominictarr 54 | * Fixed broken removeTimeSeries function (#24) by @davidgaleano 55 | * Minor performance and tidying, by @drewnoakes 56 | * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes 57 | * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12) 58 | * Documentation and some local variable renaming for clarity, by @drewnoakes 59 | * v1.17: Allow control over font size (#10), by @drewnoakes 60 | * Timestamp text won't overlap, by @drewnoakes 61 | * v1.18: Allow control of max/min label precision, by @drewnoakes 62 | * Added 'borderVisible' chart option, by @drewnoakes 63 | * Allow drawing series with fill but no stroke (line), by @drewnoakes 64 | * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai 65 | * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes 66 | * v1.21: Add 'step' interpolation mode, by @drewnoakes 67 | * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic 68 | * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes 69 | * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf 70 | * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92 71 | * Draw time labels on top of series, by @comolosabia 72 | * Add TimeSeries.clear function, by @drewnoakes 73 | * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic 74 | * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush 75 | * v1.28: Add 'minValueScale' option, by @megawac 76 | */ 77 | 78 | ;(function(exports) { 79 | 80 | var Util = { 81 | extend: function() { 82 | arguments[0] = arguments[0] || {}; 83 | for (var i = 1; i < arguments.length; i++) 84 | { 85 | for (var key in arguments[i]) 86 | { 87 | if (arguments[i].hasOwnProperty(key)) 88 | { 89 | if (typeof(arguments[i][key]) === 'object') { 90 | if (arguments[i][key] instanceof Array) { 91 | arguments[0][key] = arguments[i][key]; 92 | } else { 93 | arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]); 94 | } 95 | } else { 96 | arguments[0][key] = arguments[i][key]; 97 | } 98 | } 99 | } 100 | } 101 | return arguments[0]; 102 | } 103 | }; 104 | 105 | /** 106 | * Initialises a new TimeSeries with optional data options. 107 | * 108 | * Options are of the form (defaults shown): 109 | * 110 | *
111 |    * {
112 |    *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
113 |    *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
114 |    * }
115 |    * 
116 | * 117 | * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. 118 | * 119 | * @constructor 120 | */ 121 | function TimeSeries(options) { 122 | this.options = Util.extend({}, TimeSeries.defaultOptions, options); 123 | this.clear(); 124 | } 125 | 126 | TimeSeries.defaultOptions = { 127 | resetBoundsInterval: 3000, 128 | resetBounds: true 129 | }; 130 | 131 | /** 132 | * Clears all data and state from this TimeSeries object. 133 | */ 134 | TimeSeries.prototype.clear = function() { 135 | this.data = []; 136 | this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. 137 | this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. 138 | }; 139 | 140 | /** 141 | * Recalculate the min/max values for this TimeSeries object. 142 | * 143 | * This causes the graph to scale itself in the y-axis. 144 | */ 145 | TimeSeries.prototype.resetBounds = function() { 146 | if (this.data.length) { 147 | // Walk through all data points, finding the min/max value 148 | this.maxValue = this.data[0][1]; 149 | this.minValue = this.data[0][1]; 150 | for (var i = 1; i < this.data.length; i++) { 151 | var value = this.data[i][1]; 152 | if (value > this.maxValue) { 153 | this.maxValue = value; 154 | } 155 | if (value < this.minValue) { 156 | this.minValue = value; 157 | } 158 | } 159 | } else { 160 | // No data exists, so set min/max to NaN 161 | this.maxValue = Number.NaN; 162 | this.minValue = Number.NaN; 163 | } 164 | }; 165 | 166 | /** 167 | * Adds a new data point to the TimeSeries, preserving chronological order. 168 | * 169 | * @param timestamp the position, in time, of this data point 170 | * @param value the value of this data point 171 | * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls 172 | * whether it is replaced, or the values summed (defaults to false.) 173 | */ 174 | TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { 175 | // Rewind until we hit an older timestamp 176 | var i = this.data.length - 1; 177 | while (i >= 0 && this.data[i][0] > timestamp) { 178 | i--; 179 | } 180 | 181 | if (i === -1) { 182 | // This new item is the oldest data 183 | this.data.splice(0, 0, [timestamp, value]); 184 | } else if (this.data.length > 0 && this.data[i][0] === timestamp) { 185 | // Update existing values in the array 186 | if (sumRepeatedTimeStampValues) { 187 | // Sum this value into the existing 'bucket' 188 | this.data[i][1] += value; 189 | value = this.data[i][1]; 190 | } else { 191 | // Replace the previous value 192 | this.data[i][1] = value; 193 | } 194 | } else if (i < this.data.length - 1) { 195 | // Splice into the correct position to keep timestamps in order 196 | this.data.splice(i + 1, 0, [timestamp, value]); 197 | } else { 198 | // Add to the end of the array 199 | this.data.push([timestamp, value]); 200 | } 201 | 202 | this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); 203 | this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); 204 | }; 205 | 206 | TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { 207 | // We must always keep one expired data point as we need this to draw the 208 | // line that comes into the chart from the left, but any points prior to that can be removed. 209 | var removeCount = 0; 210 | while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { 211 | removeCount++; 212 | } 213 | if (removeCount !== 0) { 214 | this.data.splice(0, removeCount); 215 | } 216 | }; 217 | 218 | /** 219 | * Initialises a new SmoothieChart. 220 | * 221 | * Options are optional, and should be of the form below. Just specify the values you 222 | * need and the rest will be given sensible defaults as shown: 223 | * 224 | *
225 |    * {
226 |    *   minValue: undefined,                      // specify to clamp the lower y-axis to a given value
227 |    *   maxValue: undefined,                      // specify to clamp the upper y-axis to a given value
228 |    *   maxValueScale: 1,                         // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
229 |    *   minValueScale: 1,                         // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
230 |    *   yRangeFunction: undefined,                // function({min: , max: }) { return {min: , max: }; }
231 |    *   scaleSmoothing: 0.125,                    // controls the rate at which y-value zoom animation occurs
232 |    *   millisPerPixel: 20,                       // sets the speed at which the chart pans by
233 |    *   enableDpiScaling: true,                   // support rendering at different DPI depending on the device
234 |    *   yMinFormatter: function(min, precision) { // callback function that formats the min y value label
235 |    *     return parseFloat(min).toFixed(precision);
236 |    *   },
237 |    *   yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
238 |    *     return parseFloat(max).toFixed(precision);
239 |    *   },
240 |    *   maxDataSetLength: 2,
241 |    *   interpolation: 'bezier'                   // one of 'bezier', 'linear', or 'step'
242 |    *   timestampFormatter: null,                 // optional function to format time stamps for bottom of chart
243 |    *                                             // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
244 |    *   scrollBackwards: false,                   // reverse the scroll direction of the chart
245 |    *   horizontalLines: [],                      // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
246 |    *   grid:
247 |    *   {
248 |    *     fillStyle: '#000000',                   // the background colour of the chart
249 |    *     lineWidth: 1,                           // the pixel width of grid lines
250 |    *     strokeStyle: '#777777',                 // colour of grid lines
251 |    *     millisPerLine: 1000,                    // distance between vertical grid lines
252 |    *     sharpLines: false,                      // controls whether grid lines are 1px sharp, or softened
253 |    *     verticalSections: 2,                    // number of vertical sections marked out by horizontal grid lines
254 |    *     borderVisible: true                     // whether the grid lines trace the border of the chart or not
255 |    *   },
256 |    *   labels
257 |    *   {
258 |    *     disabled: false,                        // enables/disables labels showing the min/max values
259 |    *     fillStyle: '#ffffff',                   // colour for text of labels,
260 |    *     fontSize: 15,
261 |    *     fontFamily: 'sans-serif',
262 |    *     precision: 2
263 |    *   }
264 |    * }
265 |    * 
266 | * 267 | * @constructor 268 | */ 269 | function SmoothieChart(options) { 270 | this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); 271 | this.seriesSet = []; 272 | this.currentValueRange = 1; 273 | this.currentVisMinValue = 0; 274 | this.lastRenderTimeMillis = 0; 275 | } 276 | 277 | SmoothieChart.defaultChartOptions = { 278 | millisPerPixel: 20, 279 | enableDpiScaling: true, 280 | yMinFormatter: function(min, precision) { 281 | return parseFloat(min).toFixed(precision); 282 | }, 283 | yMaxFormatter: function(max, precision) { 284 | return parseFloat(max).toFixed(precision); 285 | }, 286 | maxValueScale: 1, 287 | minValueScale: 1, 288 | interpolation: 'bezier', 289 | scaleSmoothing: 0.125, 290 | maxDataSetLength: 2, 291 | scrollBackwards: false, 292 | grid: { 293 | fillStyle: '#000000', 294 | strokeStyle: '#777777', 295 | lineWidth: 1, 296 | sharpLines: false, 297 | millisPerLine: 1000, 298 | verticalSections: 2, 299 | borderVisible: true 300 | }, 301 | labels: { 302 | fillStyle: '#ffffff', 303 | disabled: false, 304 | fontSize: 10, 305 | fontFamily: 'monospace', 306 | precision: 2 307 | }, 308 | horizontalLines: [] 309 | }; 310 | 311 | // Based on http://inspirit.github.com/jsfeat/js/compatibility.js 312 | SmoothieChart.AnimateCompatibility = (function() { 313 | var requestAnimationFrame = function(callback, element) { 314 | var requestAnimationFrame = 315 | window.requestAnimationFrame || 316 | window.webkitRequestAnimationFrame || 317 | window.mozRequestAnimationFrame || 318 | window.oRequestAnimationFrame || 319 | window.msRequestAnimationFrame || 320 | function(callback) { 321 | return window.setTimeout(function() { 322 | callback(new Date().getTime()); 323 | }, 16); 324 | }; 325 | return requestAnimationFrame.call(window, callback, element); 326 | }, 327 | cancelAnimationFrame = function(id) { 328 | var cancelAnimationFrame = 329 | window.cancelAnimationFrame || 330 | function(id) { 331 | clearTimeout(id); 332 | }; 333 | return cancelAnimationFrame.call(window, id); 334 | }; 335 | 336 | return { 337 | requestAnimationFrame: requestAnimationFrame, 338 | cancelAnimationFrame: cancelAnimationFrame 339 | }; 340 | })(); 341 | 342 | SmoothieChart.defaultSeriesPresentationOptions = { 343 | lineWidth: 1, 344 | strokeStyle: '#ffffff' 345 | }; 346 | 347 | /** 348 | * Adds a TimeSeries to this chart, with optional presentation options. 349 | * 350 | * Presentation options should be of the form (defaults shown): 351 | * 352 | *
353 |    * {
354 |    *   lineWidth: 1,
355 |    *   strokeStyle: '#ffffff',
356 |    *   fillStyle: undefined
357 |    * }
358 |    * 
359 | */ 360 | SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { 361 | this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); 362 | if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { 363 | timeSeries.resetBoundsTimerId = setInterval( 364 | function() { 365 | timeSeries.resetBounds(); 366 | }, 367 | timeSeries.options.resetBoundsInterval 368 | ); 369 | } 370 | }; 371 | 372 | /** 373 | * Removes the specified TimeSeries from the chart. 374 | */ 375 | SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { 376 | // Find the correct timeseries to remove, and remove it 377 | var numSeries = this.seriesSet.length; 378 | for (var i = 0; i < numSeries; i++) { 379 | if (this.seriesSet[i].timeSeries === timeSeries) { 380 | this.seriesSet.splice(i, 1); 381 | break; 382 | } 383 | } 384 | // If a timer was operating for that timeseries, remove it 385 | if (timeSeries.resetBoundsTimerId) { 386 | // Stop resetting the bounds, if we were 387 | clearInterval(timeSeries.resetBoundsTimerId); 388 | } 389 | }; 390 | 391 | /** 392 | * Gets render options for the specified TimeSeries. 393 | * 394 | * As you may use a single TimeSeries in multiple charts with different formatting in each usage, 395 | * these settings are stored in the chart. 396 | */ 397 | SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) { 398 | // Find the correct timeseries to remove, and remove it 399 | var numSeries = this.seriesSet.length; 400 | for (var i = 0; i < numSeries; i++) { 401 | if (this.seriesSet[i].timeSeries === timeSeries) { 402 | return this.seriesSet[i].options; 403 | } 404 | } 405 | }; 406 | 407 | /** 408 | * Brings the specified TimeSeries to the top of the chart. It will be rendered last. 409 | */ 410 | SmoothieChart.prototype.bringToFront = function(timeSeries) { 411 | // Find the correct timeseries to remove, and remove it 412 | var numSeries = this.seriesSet.length; 413 | for (var i = 0; i < numSeries; i++) { 414 | if (this.seriesSet[i].timeSeries === timeSeries) { 415 | var set = this.seriesSet.splice(i, 1); 416 | this.seriesSet.push(set[0]); 417 | break; 418 | } 419 | } 420 | }; 421 | 422 | /** 423 | * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. 424 | * 425 | * @param canvas the target canvas element 426 | * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series 427 | * from appearing on screen, with new values flashing into view, at the expense of some latency. 428 | */ 429 | SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { 430 | this.canvas = canvas; 431 | this.delay = delayMillis; 432 | this.start(); 433 | }; 434 | 435 | /** 436 | * Make sure the canvas has the optimal resolution for the device's pixel ratio. 437 | */ 438 | SmoothieChart.prototype.resize = function() { 439 | // TODO this function doesn't handle the value of enableDpiScaling changing during execution 440 | if (!this.options.enableDpiScaling || !window || window.devicePixelRatio === 1) 441 | return; 442 | 443 | var dpr = window.devicePixelRatio; 444 | var width = parseInt(this.canvas.getAttribute('width')); 445 | var height = parseInt(this.canvas.getAttribute('height')); 446 | 447 | if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) { 448 | this.originalWidth = width; 449 | this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); 450 | this.canvas.style.width = width + 'px'; 451 | this.canvas.getContext('2d').scale(dpr, dpr); 452 | } 453 | 454 | if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) { 455 | this.originalHeight = height; 456 | this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); 457 | this.canvas.style.height = height + 'px'; 458 | this.canvas.getContext('2d').scale(dpr, dpr); 459 | } 460 | }; 461 | 462 | /** 463 | * Starts the animation of this chart. 464 | */ 465 | SmoothieChart.prototype.start = function() { 466 | if (this.frame) { 467 | // We're already running, so just return 468 | return; 469 | } 470 | 471 | // Renders a frame, and queues the next frame for later rendering 472 | var animate = function() { 473 | this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { 474 | this.render(); 475 | animate(); 476 | }.bind(this)); 477 | }.bind(this); 478 | 479 | animate(); 480 | }; 481 | 482 | /** 483 | * Stops the animation of this chart. 484 | */ 485 | SmoothieChart.prototype.stop = function() { 486 | if (this.frame) { 487 | SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); 488 | delete this.frame; 489 | } 490 | }; 491 | 492 | SmoothieChart.prototype.updateValueRange = function() { 493 | // Calculate the current scale of the chart, from all time series. 494 | var chartOptions = this.options, 495 | chartMaxValue = Number.NaN, 496 | chartMinValue = Number.NaN; 497 | 498 | for (var d = 0; d < this.seriesSet.length; d++) { 499 | // TODO(ndunn): We could calculate / track these values as they stream in. 500 | var timeSeries = this.seriesSet[d].timeSeries; 501 | if (!isNaN(timeSeries.maxValue)) { 502 | chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; 503 | } 504 | 505 | if (!isNaN(timeSeries.minValue)) { 506 | chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; 507 | } 508 | } 509 | 510 | // Scale the chartMaxValue to add padding at the top if required 511 | if (chartOptions.maxValue != null) { 512 | chartMaxValue = chartOptions.maxValue; 513 | } else { 514 | chartMaxValue *= chartOptions.maxValueScale; 515 | } 516 | 517 | // Set the minimum if we've specified one 518 | if (chartOptions.minValue != null) { 519 | chartMinValue = chartOptions.minValue; 520 | } else { 521 | chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue); 522 | } 523 | 524 | // If a custom range function is set, call it 525 | if (this.options.yRangeFunction) { 526 | var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); 527 | chartMinValue = range.min; 528 | chartMaxValue = range.max; 529 | } 530 | 531 | if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { 532 | var targetValueRange = chartMaxValue - chartMinValue; 533 | var valueRangeDiff = (targetValueRange - this.currentValueRange); 534 | var minValueDiff = (chartMinValue - this.currentVisMinValue); 535 | this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; 536 | this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; 537 | this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; 538 | } 539 | 540 | this.valueRange = { min: chartMinValue, max: chartMaxValue }; 541 | }; 542 | 543 | SmoothieChart.prototype.render = function(canvas, time) { 544 | var nowMillis = new Date().getTime(); 545 | 546 | if (!this.isAnimatingScale) { 547 | // We're not animating. We can use the last render time and the scroll speed to work out whether 548 | // we actually need to paint anything yet. If not, we can return immediately. 549 | 550 | // Render at least every 1/6th of a second. The canvas may be resized, which there is 551 | // no reliable way to detect. 552 | var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel); 553 | 554 | if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) { 555 | return; 556 | } 557 | } 558 | 559 | this.resize(); 560 | 561 | this.lastRenderTimeMillis = nowMillis; 562 | 563 | canvas = canvas || this.canvas; 564 | time = time || nowMillis - (this.delay || 0); 565 | 566 | // Round time down to pixel granularity, so motion appears smoother. 567 | time -= time % this.options.millisPerPixel; 568 | 569 | var context = canvas.getContext('2d'), 570 | chartOptions = this.options, 571 | dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, 572 | // Calculate the threshold time for the oldest data points. 573 | oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), 574 | valueToYPixel = function(value) { 575 | var offset = value - this.currentVisMinValue; 576 | return this.currentValueRange === 0 577 | ? dimensions.height 578 | : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height)); 579 | }.bind(this), 580 | timeToXPixel = function(t) { 581 | if(chartOptions.scrollBackwards) { 582 | return Math.round((time - t) / chartOptions.millisPerPixel); 583 | } 584 | return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel)); 585 | }; 586 | 587 | this.updateValueRange(); 588 | 589 | context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; 590 | 591 | // Save the state of the canvas context, any transformations applied in this method 592 | // will get removed from the stack at the end of this method when .restore() is called. 593 | context.save(); 594 | 595 | // Move the origin. 596 | context.translate(dimensions.left, dimensions.top); 597 | 598 | // Create a clipped rectangle - anything we draw will be constrained to this rectangle. 599 | // This prevents the occasional pixels from curves near the edges overrunning and creating 600 | // screen cheese (that phrase should need no explanation). 601 | context.beginPath(); 602 | context.rect(0, 0, dimensions.width, dimensions.height); 603 | context.clip(); 604 | 605 | // Clear the working area. 606 | context.save(); 607 | context.fillStyle = chartOptions.grid.fillStyle; 608 | context.clearRect(0, 0, dimensions.width, dimensions.height); 609 | context.fillRect(0, 0, dimensions.width, dimensions.height); 610 | context.restore(); 611 | 612 | // Grid lines... 613 | context.save(); 614 | context.lineWidth = chartOptions.grid.lineWidth; 615 | context.strokeStyle = chartOptions.grid.strokeStyle; 616 | // Vertical (time) dividers. 617 | if (chartOptions.grid.millisPerLine > 0) { 618 | context.beginPath(); 619 | for (var t = time - (time % chartOptions.grid.millisPerLine); 620 | t >= oldestValidTime; 621 | t -= chartOptions.grid.millisPerLine) { 622 | var gx = timeToXPixel(t); 623 | if (chartOptions.grid.sharpLines) { 624 | gx -= 0.5; 625 | } 626 | context.moveTo(gx, 0); 627 | context.lineTo(gx, dimensions.height); 628 | } 629 | context.stroke(); 630 | context.closePath(); 631 | } 632 | 633 | // Horizontal (value) dividers. 634 | for (var v = 1; v < chartOptions.grid.verticalSections; v++) { 635 | var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections); 636 | if (chartOptions.grid.sharpLines) { 637 | gy -= 0.5; 638 | } 639 | context.beginPath(); 640 | context.moveTo(0, gy); 641 | context.lineTo(dimensions.width, gy); 642 | context.stroke(); 643 | context.closePath(); 644 | } 645 | // Bounding rectangle. 646 | if (chartOptions.grid.borderVisible) { 647 | context.beginPath(); 648 | context.strokeRect(0, 0, dimensions.width, dimensions.height); 649 | context.closePath(); 650 | } 651 | context.restore(); 652 | 653 | // Draw any horizontal lines... 654 | if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { 655 | for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { 656 | var line = chartOptions.horizontalLines[hl], 657 | hly = Math.round(valueToYPixel(line.value)) - 0.5; 658 | context.strokeStyle = line.color || '#ffffff'; 659 | context.lineWidth = line.lineWidth || 1; 660 | context.beginPath(); 661 | context.moveTo(0, hly); 662 | context.lineTo(dimensions.width, hly); 663 | context.stroke(); 664 | context.closePath(); 665 | } 666 | } 667 | 668 | // For each data set... 669 | for (var d = 0; d < this.seriesSet.length; d++) { 670 | context.save(); 671 | var timeSeries = this.seriesSet[d].timeSeries, 672 | dataSet = timeSeries.data, 673 | seriesOptions = this.seriesSet[d].options; 674 | 675 | // Delete old data that's moved off the left of the chart. 676 | timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); 677 | 678 | // Set style for this dataSet. 679 | context.lineWidth = seriesOptions.lineWidth; 680 | context.strokeStyle = seriesOptions.strokeStyle; 681 | // Draw the line... 682 | context.beginPath(); 683 | // Retain lastX, lastY for calculating the control points of bezier curves. 684 | var firstX = 0, lastX = 0, lastY = 0; 685 | for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { 686 | var x = timeToXPixel(dataSet[i][0]), 687 | y = valueToYPixel(dataSet[i][1]); 688 | 689 | if (i === 0) { 690 | firstX = x; 691 | context.moveTo(x, y); 692 | } else { 693 | switch (chartOptions.interpolation) { 694 | case "linear": 695 | case "line": { 696 | context.lineTo(x,y); 697 | break; 698 | } 699 | case "bezier": 700 | default: { 701 | // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves 702 | // 703 | // Assuming A was the last point in the line plotted and B is the new point, 704 | // we draw a curve with control points P and Q as below. 705 | // 706 | // A---P 707 | // | 708 | // | 709 | // | 710 | // Q---B 711 | // 712 | // Importantly, A and P are at the same y coordinate, as are B and Q. This is 713 | // so adjacent curves appear to flow as one. 714 | // 715 | context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop 716 | Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) 717 | Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) 718 | x, y); // endPoint (B) 719 | break; 720 | } 721 | case "step": { 722 | context.lineTo(x,lastY); 723 | context.lineTo(x,y); 724 | break; 725 | } 726 | } 727 | } 728 | 729 | lastX = x; lastY = y; 730 | } 731 | 732 | if (dataSet.length > 1) { 733 | if (seriesOptions.fillStyle) { 734 | // Close up the fill region. 735 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); 736 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); 737 | context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); 738 | context.fillStyle = seriesOptions.fillStyle; 739 | context.fill(); 740 | } 741 | 742 | if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { 743 | context.stroke(); 744 | } 745 | context.closePath(); 746 | } 747 | context.restore(); 748 | } 749 | 750 | // Draw the axis values on the chart. 751 | if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { 752 | var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision), 753 | minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision), 754 | labelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2; 755 | context.fillStyle = chartOptions.labels.fillStyle; 756 | context.fillText(maxValueString, labelPos, chartOptions.labels.fontSize); 757 | context.fillText(minValueString, labelPos, dimensions.height - 2); 758 | } 759 | 760 | // Display timestamps along x-axis at the bottom of the chart. 761 | if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) { 762 | var textUntilX = chartOptions.scrollBackwards 763 | ? context.measureText(minValueString).width 764 | : dimensions.width - context.measureText(minValueString).width + 4; 765 | for (var t = time - (time % chartOptions.grid.millisPerLine); 766 | t >= oldestValidTime; 767 | t -= chartOptions.grid.millisPerLine) { 768 | var gx = timeToXPixel(t); 769 | // Only draw the timestamp if it won't overlap with the previously drawn one. 770 | if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) { 771 | // Formats the timestamp based on user specified formatting function 772 | // SmoothieChart.timeFormatter function above is one such formatting option 773 | var tx = new Date(t), 774 | ts = chartOptions.timestampFormatter(tx), 775 | tsWidth = context.measureText(ts).width; 776 | 777 | textUntilX = chartOptions.scrollBackwards 778 | ? gx + tsWidth + 2 779 | : gx - tsWidth - 2; 780 | 781 | context.fillStyle = chartOptions.labels.fillStyle; 782 | if(chartOptions.scrollBackwards) { 783 | context.fillText(ts, gx, dimensions.height - 2); 784 | } else { 785 | context.fillText(ts, gx - tsWidth, dimensions.height - 2); 786 | } 787 | } 788 | } 789 | } 790 | 791 | context.restore(); // See .save() above. 792 | }; 793 | 794 | // Sample timestamp formatting function 795 | SmoothieChart.timeFormatter = function(date) { 796 | function pad2(number) { return (number < 10 ? '0' : '') + number } 797 | return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); 798 | }; 799 | 800 | exports.TimeSeries = TimeSeries; 801 | exports.SmoothieChart = SmoothieChart; 802 | 803 | })(typeof exports === 'undefined' ? this : exports); 804 | 805 | -------------------------------------------------------------------------------- /webdashboard/reporter.py: -------------------------------------------------------------------------------- 1 | from threadedsubscriber import ThreadedSubscriber 2 | import redis 3 | import socket 4 | 5 | class Reporter: 6 | def __init__(self, host, port): 7 | self.host = host 8 | self.port = port 9 | 10 | self.name = socket.getfqdn() 11 | self.client = redis.StrictRedis(host=self.host, port=self.port, db=0) 12 | self.channels = ["sensors.data", "members.add"] 13 | 14 | def find_members(self): 15 | members = self.client.hgetall("members") 16 | live = [] 17 | 18 | for member in members: 19 | if(self.client.get(member+".live")): 20 | live.append(member) 21 | return live 22 | 23 | def on_message(self, channel, message): 24 | print("Channel: "+channel + " - " + message ) 25 | # print(channel, message) 26 | 27 | def set_on_sensor_data(self, cb): 28 | self.on_sensor_data_cb = cb 29 | 30 | def subscribe(self): 31 | self.subscriber = ThreadedSubscriber(self.client, self.channels, self.on_sensor_data_cb) 32 | self.subscriber.run() 33 | 34 | def set_key(self, key, value): 35 | self.client.set(key, value) 36 | 37 | def get_key(self, key): 38 | return self.client.get(key) 39 | -------------------------------------------------------------------------------- /webdashboard/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 31 | Sensor dashboard 32 | 33 | 34 |
35 | {% block body %}{% endblock %} 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /webdashboard/templates/nodes.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% for host in hosts %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 |
NameTemp DiffTempBaseline TempMotion
{{host["name"]}}{{host["temp.diff"]}}{{host["temp"]}}{{host["temp.baseline"]}}{{host["motion"]}}
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /webdashboard/templates/sensors.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block body %} 3 |
4 | {% raw %} 5 | 6 |
7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
NameTemp DiffTempBaseline TempMotion
{{ sensor["name"] }}{{ sensor["temp.diff"].toFixed(2) }}{{ sensor["temp"].toFixed(2) }}{{ sensor["temp.baseline"].toFixed(2) }}{{ sensor["motion"].toFixed(3) }}
26 |
27 | 28 | {% endraw %} 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /webdashboard/threadedsubscriber.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | class ThreadedSubscriber(threading.Thread): 4 | def __init__(self, client, channels, callback): 5 | self.channels = channels 6 | self.client=client 7 | self.callback = callback 8 | 9 | def run(self): 10 | self.pubsub = self.client.pubsub() 11 | # self.pubsub.subscribe(self.channels) 12 | self.pubsub.subscribe("sensors.data", "members.add") 13 | 14 | while(True): 15 | for m in self.pubsub.listen(): 16 | # print m #'Recieved: {0}'.format(m['data']) 17 | if(m["type"]=="message"): 18 | if(self.callback != None): 19 | self.callback(m["channel"], m["data"]) 20 | 21 | --------------------------------------------------------------------------------