├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── administration.sh ├── cron.py ├── cron.txt ├── manifest.yaml ├── mpl3115a2.py ├── sensor21-server.py ├── setup.sh └── sqldb.py /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Pull requests 2 | 3 | ## Code style 4 | 5 | > Programs must be written for people to read, and only incidentally for 6 | > machines to execute. 7 | > 8 | > -- Harold Abelson, Structure and Interpretation of Computer Programs 9 | 10 | At a minimum, changes to the code should be compliant with [PEP 11 | 8](https://www.python.org/dev/peps/pep-0008/). For general design principles, 12 | consult [The Zen of Python](https://www.python.org/dev/peps/pep-0020/). 13 | 14 | ## Commit message format 15 | 16 | Commit your changes using the following commit message format (with the same 17 | capitalization, spacing, and punctuation): 18 | 19 | ``` 20 | logging: Move function to module level 21 | ``` 22 | 23 | The first part of the message is the scope and the second part is a description 24 | of the change, written in the imperative tense. 25 | 26 | ## Commit history 27 | 28 | Commits should be organized into logical units. Keep your git history clean by 29 | [rewriting](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) it as 30 | necessary using `git rebase -i` (interactive rebase) and `git push -f` (force 31 | push). Commits addressing review comments and test failures should be squashed 32 | if necessary. 33 | 34 | # Opening issues 35 | 36 | ## Bug reports 37 | 38 | Bug reports should include clear instructions to reproduce the bug. Include a 39 | stack trace if applicable. 40 | 41 | ## Security issues 42 | 43 | Critical security bugs should be sent via email to security@21.co and should not 44 | be publicly disclosed. Eligible security disclosures are eligible, at our sole 45 | discretion, for a monetary bounty of up to $1000 based on severity. 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017, 21 Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of 21 Inc. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to run and publish: _**sensor21**_ 2 | 3 | ## Prerequisites 4 | 5 | You will need: 6 | 7 | * A 21 Bitcoin Computer, or a DIY Bitcoin Computer 8 | * An Adafruit MPL3115A2 Breakout Board 9 | * A set of female to female jumper wires 10 | 11 | For a full walkthrough, see the sensor21 tutorial [here](https://21.co/learn/sensor21/). 12 | 13 | ## Hardware Setup 14 | 15 | Connect your 21BC / DIY 21BC to the MPL3115A2 breakout board. See the connection diagrams on the tutorial page for more information. 16 | 17 | * [DIY Connection Diagram](https://21.co/learn/sensor21/#step-1-connect-the-sensor-to-your-raspberry-pi) 18 | * [21 Bitcoin Comptuter Connection Diagram](http://21.co/learn/sensor21/#step-1-connect-the-sensor-to-your-21-bitcoin-computer) 19 | 20 | 21 | ## Software Setup 22 | 23 | ### Step 1: Install/update 21 24 | Get the latest version of the 21 software. If you don't have 21 installed yet, run `curl https://21.co | sh`. Then, join the 21 marketplace network. 25 | 26 | ``` bash 27 | # Install 21 if required 28 | curl https://21.co | sh 29 | 30 | # Run 21 update 31 | 21 update 32 | 33 | # Join the `21market` network 34 | 21 join 35 | ``` 36 | 37 | ### Step 2: Clone the repository 38 | Clone the sensor21 repository, and run the setup script. You will be asked for user input multiple times. 39 | 40 | ``` bash 41 | cd ~/ 42 | 43 | # Install git to clone the Sensor21 code 44 | sudo apt-get install git 45 | 46 | # git clone the code 47 | git clone https://github.com/21dotco/sensor21.git 48 | 49 | # run the setup script 50 | cd sensor21 51 | source setup.sh 52 | ``` 53 | 54 | ### Step 3: Start your server and publish your endpoint 55 | 56 | Start the server with the following: 57 | 58 | ``` bash 59 | python3 sensor21-server.py -d 60 | ``` 61 | 62 | After the server is running, pubish your endpoint with the 21 publish command. Replace the name and email fields with your information. 63 | 64 | ``` bash 65 | 21 publish submit manifest.yaml -p 'name="Joe Smith" email="joe@example.com" price="5" host="AUTO" port="6002"' 66 | ``` 67 | 68 | ### Step 4: Verify you are part of the aggregator pool 69 | 70 | After a few minutes, you should be able to use `21 publish list` 71 | to see the endpoint you just put up: 72 | 73 | ``` bash 74 | 21 publish list 75 | ``` 76 | 77 | You can also use the verify endpoint on the sensor21 aggregator. Load your zerotier ip address, and then query the enpoint to verify you are part of the pool. 78 | 79 | ``` bash 80 | # grab your ZeroTier IP address and save it to shell variable ZT_IP 81 | ZT_IP=$(ifconfig | grep -A2 zt | grep inet | sed 's|[^0-9. ]||g' | sed 's|[^ \t]*||' | awk 'NR==1{print $1}') 82 | 83 | # verify your endpoint is part of the pool 84 | curl https://mkt.21.co/21dotco/sensor21_aggregator/sensor21/verify?zt_ip=$ZT_IP 85 | ``` 86 | 87 | ### Step 5: View transactions with 21 log 88 | 89 | After your server has been online for ~30 minutes, you can start to see transaction logs from the aggregator in your 21 log. You can see your incomes from sensor21 by running 21 log. 90 | 91 | ``` bash 92 | 21 log 93 | ``` 94 | 95 | ## Trobuleshooting 96 | 97 | See the detail on troubleshooting your application [here](http://21.co/learn/sensor21/#troubleshooting). 98 | 99 | If you have further support requests, please join our public slack channel #iot [here](http://slack.21.co/). 100 | 101 | -------------------------------------------------------------------------------- /administration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Sensor21: MPL3115A2 Breakout Board Administration Script 4 | # 5 | # This script provides a few useful functions for working with 6 | # system processes related to the MPL3115A2 sensor21 implementation. 7 | # Note: pigpiod is only used for software I2C (used with 21 Bitcoin Computer) 8 | 9 | # Usage: bash administration.sh 10 | # bash administration.sh start_server 11 | # bash administration.sh restart_pigpiod 12 | # function list 13 | # start_pigpiod, stop_pigpiod, restart_pigpiod 14 | # start_server, stop_server, restart_server 15 | # start_cron_job, stop_cron_job 16 | 17 | ## Process names 18 | PIGPIOD="pigpiod" 19 | SERVER="sensor21-server.py" 20 | 21 | ## Helper functions 22 | # Check to see if a process is running 23 | # Returns 0 if yes, 1 if no 24 | check_process () { 25 | pgrep -f $1 > /dev/null 26 | return $? 27 | } 28 | 29 | # Stop process 30 | stop_process () { 31 | proc_num=$(ps aux | grep -v grep | pgrep -f $1) 32 | sudo kill $proc_num > /dev/null 33 | } 34 | 35 | ## PIGPIOD management 36 | # start pigpiod 37 | start_pigpiod () { 38 | if ! check_process $PIGPIOD; then 39 | sudo pigpiod 40 | fi 41 | } 42 | 43 | # stop pigpiod 44 | stop_pigpiod () { 45 | if check_process $PIGPIOD; then 46 | stop_process $PIGPIOD 47 | fi 48 | } 49 | 50 | # restart pigpiod 51 | restart_pigpiod () { 52 | stop_pigpiod 53 | start_pigpiod 54 | } 55 | 56 | ## Server Management 57 | # start server 58 | start_server () { 59 | start_pigpiod 60 | if ! check_process $SERVER; then 61 | python3 sensor21-server.py & 62 | fi 63 | } 64 | 65 | # stop server 66 | stop_server () { 67 | if check_process $SERVER; then 68 | stop_process $SERVER 69 | fi 70 | } 71 | 72 | # restart server 73 | restart_server () { 74 | stop_server 75 | start_server 76 | } 77 | 78 | ## Crontab management 79 | # start cron job 80 | start_cron_job () { 81 | crontab -l 2>&1 | grep '* * * * * /usr/bin/python3 /home/twenty/sensor21/cron.py >> /home/twenty/sensor21/cron_log.txt 2>&1' > /dev/null 82 | if [ "$?" = "1" ]; then 83 | crontab cron.txt 84 | fi 85 | } 86 | 87 | # stop cron job 88 | stop_cron_job () { 89 | crontab -l 2>&1 | grep '* * * * * /usr/bin/python3 /home/twenty/sensor21/cron.py >> /home/twenty/sensor21/cron_log.txt 2>&1' > /dev/null 90 | if [ "$?" = "0" ]; then 91 | crontab -r 92 | fi 93 | } 94 | 95 | # Print available functions if user runs with no arguments. 96 | if [ "$1" = "" ]; then 97 | echo "usage: source administration.sh " 98 | echo "Note: pigpiod is only used for software I2C (used on 21 Bitcoin Computer)" 99 | echo "" 100 | echo "Supported functions:" 101 | echo "start_pigpiod, stop_pigpiod, restart_pigpiod" 102 | echo "start_server, stop_server, restart_server" 103 | echo "start_cron_job, stop_cron_job" 104 | else 105 | # Run function calls from script arguments 106 | $@ 107 | fi -------------------------------------------------------------------------------- /cron.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sqldb as db 5 | 6 | db.Operate().update_table() -------------------------------------------------------------------------------- /cron.txt: -------------------------------------------------------------------------------- 1 | * * * * * /usr/bin/python3 {PWD}/cron.py >> {PWD}/cron_log.txt 2>&1 2 | -------------------------------------------------------------------------------- /manifest.yaml: -------------------------------------------------------------------------------- 1 | basePath: / 2 | definitions: 3 | BarometricPressure: 4 | properties: 5 | timestamp: {type: string, format: "YYYY-MM-DD 24HH:MM:SS.USSS"} 6 | temperature: {type: number, format: float} 7 | pressure: {type: number, format: float} 8 | type: object 9 | host: 10.244.000.00:5002 10 | info: 11 | contact: {email: tyler@21.co, name: Tyler Pate} 12 | description: Get remote sensor data on demand from multiple endpoints. 13 | title: sensor21 14 | version: '1.0' 15 | x-21-category: iot 16 | x-21-github-project-url: https://github.com/21dotco/sensor21 17 | x-21-implements: [231d2fa28a440a6bef4de445e5142657668fb91a] 18 | x-21-keywords: [sensor, pressure, temperature] 19 | x-21-quick-buy: "$ 21 buy sensor21/barometricpressure" 20 | x-21-total-price: {max: 5, min: 5} 21 | x-21-implements-meta: { 22 | "x-sensor21-city": "{CITY}", 23 | "x-sensor21-state": "{STATE}", 24 | "x-sensor21-country": "{COUNTRY}" 25 | } 26 | paths: 27 | /: 28 | get: 29 | consumes: [application/x-www-form-urlencoded] 30 | produces: [application/json] 31 | responses: 32 | '200': 33 | description: A list of MPL3115A2 measurements and location information. 34 | schema: 35 | properties: 36 | barometricpressure: {$ref: '#/definitions/BarometricPressure'} 37 | summary: Returns barometric pressure in Pascals and temperature in degrees C. 38 | schemes: [http] 39 | swagger: '2.0' 40 | x-21-manifest-path: /manifest -------------------------------------------------------------------------------- /mpl3115a2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import time 5 | import subprocess 6 | 7 | try: 8 | from periphery import I2C 9 | except ImportError: 10 | pass 11 | 12 | try: 13 | import pigpio 14 | except ImportError: 15 | pass 16 | 17 | 18 | class RPiHwI2C(object): 19 | 20 | """ python-perhiphery based HW I2C driver 21 | """ 22 | 23 | def open_bus(self): 24 | """ 25 | Creates a hardware I2C handle. 26 | """ 27 | 28 | self.handle = I2C("/dev/i2c-1") 29 | 30 | def close_bus(self): 31 | """ 32 | Closes an open HW handle. 33 | """ 34 | self.handle.close() 35 | 36 | def read(self, address, pointer_reg, read_bytes): 37 | """ 38 | Performs a standard I2C read operation on a 39 | particular address. Requires the 7-bit I2C address, 40 | the pointer register to write to, and the number 41 | of bytes to read from the pointer register. 42 | """ 43 | read_bytes += 1 44 | 45 | placeholder_bytes = [] 46 | for i in range(0, read_bytes): 47 | placeholder_bytes.append(0x00) 48 | 49 | msgs = [I2C.Message([pointer_reg]), I2C.Message( 50 | placeholder_bytes, read=True)] 51 | self.handle.transfer(address, msgs) 52 | 53 | output = bytearray(msgs[1].data[0:6]) 54 | return output 55 | 56 | def write(self, address, pointer_reg, data_array): 57 | """ 58 | Performs a standard I2C write opreation on a particular 59 | address. Requires the 7-bit I2C address, the pointer 60 | register, and a data array of the data package to send. 61 | 62 | """ 63 | msgs = [I2C.Message([pointer_reg, data_array], read=False)] 64 | self.handle.transfer(address, msgs) 65 | 66 | 67 | class PigpioI2CBitBang(object): 68 | 69 | """ 70 | PIGPIO Bitbang I2C Helper Class 71 | Provides device specific I2C interface 72 | instructions to PIGPIO daemon. 73 | """ 74 | 75 | # GPIO configuration and frequency constants 76 | SDA = 6 77 | SCL = 13 78 | CF = 80000 79 | 80 | # PIGPIO bitbang constants 81 | BB_END = 0 82 | BB_ESCAPE = 1 83 | BB_START = 2 84 | BB_STOP = 3 85 | BB_ADDRESS = 4 86 | BB_FLAGS = 5 87 | BB_READ = 6 88 | BB_WRITE = 7 89 | 90 | def __init__(self): 91 | """ 92 | Initalize the pigpio library. 93 | """ 94 | self.pi = pigpio.pi() 95 | 96 | def stop(self): 97 | """ 98 | Stop pigpio connection 99 | """ 100 | self.pi.stop() 101 | 102 | def open_bus(self): 103 | """ 104 | Creates a bitbang handle with a given SDA/SCL GPIO 105 | pair, and a I2C freqency set by CF. 106 | """ 107 | self.handle = self.pi.bb_i2c_open(self.SDA, self.SCL, self.CF) 108 | 109 | def close_bus(self): 110 | """ 111 | Closes an open bitbang handle. 112 | """ 113 | self.pi.bb_i2c_close(self.SDA) 114 | 115 | def read(self, address, pointer_reg, read_bytes): 116 | """ 117 | Performs a standard I2C read operation on a 118 | particular address. Requires the 7-bit I2C address, 119 | the pointer register to write to, and the number 120 | of bytes to read from the pointer register. 121 | """ 122 | read_bytes += 1 123 | arg_list = [self.BB_ADDRESS, address, self.BB_START, 124 | self.BB_WRITE, self.BB_ESCAPE, pointer_reg, 125 | self.BB_START, self.BB_READ, read_bytes, 126 | self.BB_STOP, self.BB_END] 127 | count, data = self.pi.bb_i2c_zip(self.SDA, arg_list) 128 | return data 129 | 130 | def write(self, address, pointer_reg, data_array): 131 | """ 132 | Performs a standard I2C write opreation on a particular 133 | address. Requires the 7-bit I2C address, the pointer 134 | register, and a data array of the data package to send. 135 | """ 136 | if type(data_array) is int: 137 | assert data_array < 256 138 | data_array = [data_array] 139 | arg_list = [self.BB_ADDRESS, address, self.BB_START, 140 | self.BB_WRITE, len(data_array)+1, pointer_reg 141 | ] + data_array + [self.BB_STOP, self.BB_END] 142 | count, data = self.pi.bb_i2c_zip(self.SDA, arg_list) 143 | return data 144 | 145 | 146 | class MPL3115A2(object): 147 | 148 | """ 149 | Freescale MPL3115A2 Driver 150 | """ 151 | 152 | # datasheet constants 153 | BAR_I2C = 0x60 154 | WHO_AM_I = 0x0C 155 | CONTROL_REG_ADDR = 0x26 156 | BAR_OSR_128 = 0x28 157 | BAR_ENABLE = 0x29 158 | DATA_FLAG_ADDR = 0x13 159 | ENABLE_DATA_FLAG = 0x07 160 | STATUS_REG = 0x00 161 | P_MSB = 0x01 162 | STATUS_RDY = 3 163 | SOFTWARE_RESET = 0x04 164 | 165 | def __init__(self, i2c_handle): 166 | """ 167 | Takes in a pigpio I2C channel handle, 168 | opens the bus, and writes initial 169 | configuration to the MPL3115A2. 170 | """ 171 | self.i2c_handle = i2c_handle 172 | self.i2c_handle.write(self.BAR_I2C, 173 | self.DATA_FLAG_ADDR, self.ENABLE_DATA_FLAG) 174 | 175 | def software_reset(self): 176 | """ 177 | Performs a software reset of the MPL3115A2. 178 | Perform a set_ method before re-read. 179 | """ 180 | self.i2c_handle.write(self.BAR_I2C, 181 | self.CONTROL_REG_ADDR, self.SOFTWARE_RESET) 182 | self.set_barometric() 183 | 184 | def is_alive(self): 185 | """ 186 | Reads the WHO_AM_I register to 187 | verify I2C communication. 188 | """ 189 | raw = self.i2c_handle.read(self.BAR_I2C, self.WHO_AM_I, 0) 190 | id_bytes = list(raw) 191 | hex_string = "".join('%02x' % i for i in id_bytes) 192 | return True if hex_string == 'c4' else False 193 | 194 | def set_barometric(self): 195 | """ 196 | Sets the MPL3115A2 to return 197 | barometric pressure in Pascals. 198 | """ 199 | self.i2c_handle.write(self.BAR_I2C, 200 | self.CONTROL_REG_ADDR, self.BAR_OSR_128) 201 | self.i2c_handle.write(self.BAR_I2C, 202 | self.CONTROL_REG_ADDR, self.BAR_ENABLE) 203 | 204 | def ready(self): 205 | """ 206 | Checks the MPL3115A2 status register to determine 207 | if the part has completed a measurement and can 208 | be accessed via I2C. 209 | """ 210 | data = self.i2c_handle.read(self.BAR_I2C, self.STATUS_REG, 1) 211 | if len(data) > 0 and data[0] & (1 << self.STATUS_RDY): 212 | return True 213 | return False 214 | 215 | def get_data(self): 216 | """ 217 | Grabs MPL3115A2 data package. 218 | """ 219 | if not self.ready(): 220 | return None, None 221 | raw = self.i2c_handle.read(self.BAR_I2C, self.P_MSB, 5) 222 | return raw 223 | 224 | def package_output(self, raw, measurement): 225 | """ 226 | Convert raw bitarray output from I2C read into 227 | pressure, 20 bit unsigned, altitude, 20 bit signed 228 | with decimal, or temperature 16 bit unsigned. 229 | measurement = 1 | 2 230 | measurement type = pressure | temperature 231 | """ 232 | if len(raw) != 6: 233 | return None 234 | elif measurement == 1: # pressure case 235 | pressure = int.from_bytes(raw[0:3], 236 | byteorder='big', signed=False) / 64.0 237 | return pressure 238 | elif measurement == 2: # temperature case 239 | temperature = int.from_bytes(raw[3:5], 240 | byteorder='big', signed=False) / 256.0 241 | return temperature 242 | 243 | 244 | class PressureAPI(object): 245 | 246 | """ 247 | API for MPL3115A2 breakout board. 248 | 249 | Creates an I2C channel, then 250 | passes the I2C handle into MPL3115A2 251 | object for I2C operations. 252 | 253 | Usage: 254 | PressureAPI.get_pressure() 255 | PressureAPI.get_temperature() 256 | 257 | Finish with PressureAPI.close() 258 | """ 259 | 260 | def __init__(self, i2c_type): 261 | """ 262 | Creates a I2C channel. 263 | i2c_type = 0: Hardware I2C 264 | i2c_type = 1: Software/Bitbang I2C 265 | """ 266 | self.i2c_type = i2c_type 267 | 268 | if self.i2c_type == 0: 269 | self.i2c_channel = RPiHwI2C() 270 | self.i2c_channel.open_bus() 271 | self.mpl3115a2 = MPL3115A2(self.i2c_channel) 272 | 273 | if self.i2c_type == 1: 274 | self.i2c_channel = PigpioI2CBitBang() 275 | self.i2c_channel.open_bus() 276 | self.mpl3115a2 = MPL3115A2(self.i2c_channel) 277 | 278 | self.mpl3115a2.set_barometric() 279 | self.max_loop_iterations = 10 280 | 281 | def close(self): 282 | """ 283 | Tears down the I2C channel. 284 | """ 285 | self.i2c_channel.close_bus() 286 | 287 | def check_comms(self): 288 | """ 289 | Returns current I2C communication status. 290 | """ 291 | return self.mpl3115a2.is_alive() 292 | 293 | def restart_pigpiod(self): 294 | """ 295 | Restarts the pigpiod daemon if there is a connection 296 | timeout. 297 | Used only with software I2C. 298 | """ 299 | if self.i2c_type == 1: 300 | # Exit resources and kill the daemon 301 | self.i2c_channel.close_bus() 302 | self.i2c_channel.stop() 303 | pigpio_kill_string = "sudo pkill pigpiod" 304 | subprocess.Popen(pigpio_kill_string, shell=True) 305 | time.sleep(5) 306 | 307 | # Restart the daemon & reinitalize connections 308 | pigpio_start_string = "sudo pigpiod" 309 | start = subprocess.Popen(pigpio_start_string, shell=True) 310 | time.sleep(5) 311 | 312 | self.i2c_channel = PigpioI2CBitBang() 313 | self.i2c_channel.open_bus() 314 | self.mpl3115a2 = MPL3115A2(self.i2c_channel) 315 | 316 | def i2c_loop(self, measurement_type): 317 | """ 318 | Loops through an I2C measurement and attempts to grab data, will 319 | return data if data package received. 320 | """ 321 | loop_check = 0 322 | while loop_check < self.max_loop_iterations: 323 | raw_data = self.mpl3115a2.get_data() 324 | output = self.mpl3115a2.package_output(raw_data, measurement_type) 325 | if output is not None: 326 | return output 327 | else: 328 | loop_check += 1 329 | time.sleep(1) 330 | continue 331 | 332 | def get_pressure(self): 333 | """ 334 | Returns current barometric pressure in Pascals. 335 | """ 336 | pressure = self.i2c_loop(1) 337 | if not pressure: 338 | self.mpl3115a2.software_reset() 339 | pressure = self.i2c_loop(1) 340 | if not pressure: 341 | if self.i2c_type == 1: 342 | self.restart_pigpiod() 343 | pressure = self.i2c_loop(1) 344 | if not pressure: 345 | raise ValueError("I2C Timeout Error.") 346 | raise ValueError("I2C Timeout Error.") 347 | return pressure 348 | 349 | def get_temperature(self): 350 | """ 351 | Returns current measured temperature in degrees C. 352 | """ 353 | temperature = self.i2c_loop(2) 354 | if not temperature: 355 | self.mpl3115a2.software_reset() 356 | temperature = self.i2c_loop(2) 357 | if not temperature: 358 | if self.i2c_type == 1: 359 | self.restart_pigpiod() 360 | temperature = self.i2c_loop(2) 361 | if not temperature: 362 | raise ValueError("I2C Timeout Error.") 363 | raise ValueError("I2C Timeout Error.") 364 | return temperature 365 | -------------------------------------------------------------------------------- /sensor21-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | import json 5 | import logging 6 | import yaml 7 | import os 8 | import subprocess 9 | 10 | from flask import Flask 11 | 12 | from two1.wallet.two1_wallet import Wallet 13 | from two1.bitserv.flask import Payment 14 | 15 | import sqldb as db 16 | 17 | app = Flask(__name__) 18 | 19 | # setup wallet 20 | wallet = Wallet() 21 | payment = Payment(app, wallet) 22 | 23 | # hide logging 24 | log = logging.getLogger('werkzeug') 25 | log.setLevel(logging.ERROR) 26 | 27 | # sqldb setup 28 | latest_measurement = db.Operate() 29 | 30 | 31 | @app.route('/manifest') 32 | def docs(): 33 | """ 34 | Provides the manifest.json file for the 21 endpoint crawler. 35 | """ 36 | with open('./manifest.yaml', 'r') as f: 37 | manifest = yaml.load(f) 38 | return json.dumps(manifest) 39 | 40 | 41 | @app.route('/server_status') 42 | def server_status(): 43 | """ 44 | Check the latest DB entry. If time is greater than 5 minutes 45 | since last update, server status is down. Returns number of 46 | seconds since last sensor update. Else server status returns 47 | up. Returns number of seconds since last sensor update. 48 | """ 49 | latest_measurement.read() 50 | timestamp = latest_measurement.barometer_package[0]['timestamp'] 51 | time_str = datetime.datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S.%f') 52 | time_diff = datetime.datetime.now() - time_str 53 | response = 'Server is %s. Last measurement was made %f seconds ago.' 54 | if time_diff.seconds > 300: 55 | return response % ('down', time_diff.seconds) 56 | return response % ('up', time_diff.seconds) 57 | 58 | 59 | @app.route('/') 60 | @payment.required(5) 61 | def measurement(): 62 | return latest_measurement.read() 63 | 64 | if __name__ == "__main__": 65 | import click 66 | 67 | @click.command() 68 | @click.option("-d", "--daemon", default=False, is_flag=True, 69 | help="Run in daemon mode.") 70 | def run(daemon): 71 | if daemon: 72 | pid_file = './sensor21.pid' 73 | if os.path.isfile(pid_file): 74 | pid = int(open(pid_file).read()) 75 | os.remove(pid_file) 76 | try: 77 | p = psutil.Process(pid) 78 | p.terminate() 79 | except: 80 | pass 81 | try: 82 | p = subprocess.Popen(['python3', 'sensor21-server.py']) 83 | open(pid_file, 'w').write(str(p.pid)) 84 | except subprocess.CalledProcessError: 85 | raise ValueError("error starting sensor21-server.py daemon") 86 | else: 87 | print("Server running...") 88 | app.run(host='::', port=5002) 89 | 90 | run() 91 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # sensor21 install script 4 | # 5 | # This script will help install dependencies for sensor21. It also 6 | # sets up the SQLite database to abstract the low level hardware 7 | # communications from server requests to eliminate bus collisions. 8 | 9 | ### List of packages to install 10 | declare -a APT_PACKAGES=("unzip" "python3-pip" "sqlite3") 11 | declare -a PIP3_PACKAGES=("flask" "click" "python-periphery") 12 | PACKAGE_PIGPIO="/usr/local/bin/pigpiod" 13 | 14 | # Helper functions: bash pretty printing, pip3 and apt-get package 15 | # installers / uninstallers 16 | 17 | # Pretty print helper functions 18 | # Print program step + text coloring 19 | print_step() { 20 | printf "\n\e[1;35m $1\e[0m\n" 21 | } 22 | 23 | # Print program error + text coloring 24 | print_error() { 25 | printf "\n\e[1;31mError: $1\e[0m\n" 26 | } 27 | 28 | # Print program warning + text coloring 29 | print_warning() { 30 | printf "\e[1;33m$1\e[0m\n" 31 | } 32 | 33 | # Print program good response + text coloring 34 | print_good() { 35 | printf "\e[1;32m$1\e[0m\n" 36 | } 37 | 38 | # Program installation functions 39 | # Check to see if a program exists on the local machine 40 | program_exists() { 41 | if ! type "$1" > /dev/null 2>&1; then 42 | return 1 43 | else 44 | return 0 45 | fi 46 | } 47 | 48 | # Check to see if a pip3 package exists 49 | pip_package_exists() { 50 | python3 -c "import $1" 2> /dev/null && return 0 || return 1 51 | } 52 | 53 | # Install apt packages 54 | apt_installer() { 55 | if ! program_exists $1; then 56 | print_warning "Installing $1." 57 | sudo apt-get --force-yes --yes install $1 58 | else 59 | print_good "$1 installed." 60 | fi 61 | } 62 | 63 | # Install pip3 modules 64 | pip3_installer() { 65 | # fix for python-periphery pip3 install vs package name 66 | if [ "$1" = "python-periphery" ]; then 67 | var="periphery" 68 | if ! pip_package_exists $var; then 69 | print_warning "Installing $1." 70 | sudo pip3 install $1 71 | else 72 | print_good "$1 installed." 73 | fi 74 | else 75 | if ! pip_package_exists $1; then 76 | print_warning "Installing $1." 77 | sudo pip3 install $1 78 | else 79 | print_good "$1 installed." 80 | fi 81 | fi 82 | } 83 | 84 | ### Main program execution 85 | print_good "Welcome to sensor21 installer!" 86 | 87 | UNAME=$(uname) 88 | case "${UNAME:-nil}" in 89 | Linux|LINUX) ##pass 90 | ;; 91 | Darwin) ## Mac OSX 92 | print_error "Sorry, $UNAME is not supported. Sensor21 is designed for Raspberry Pi based systems." 93 | exit 1 94 | ;; 95 | *) 96 | print_error "Sorry, $UNAME is not supported. Sensor21 is designed for Raspberry Pi based systems." 97 | exit 1 98 | ;; 99 | esac 100 | 101 | BC_FLAG=0 102 | 103 | # BC-specific 104 | if [ "$(cat /proc/device-tree/hat/product 2>/dev/null)" = "21 Bitcoin Computer" ]; then 105 | BC_FLAG=1 106 | print_good "21 Bitcoin Computer detected. Defaulting to software I2C." 107 | sed -i 's|'"{I2C_TYPE}"'|'1'|g' sqldb.py 108 | else 109 | print_good "Raspberry Pi detected. Defaulting to hardware I2C." 110 | sed -i 's|'"{I2C_TYPE}"'|'0'|g' sqldb.py 111 | print_good "Deploying HW I2C repeated start fix for RPi." 112 | echo -n 1 | sudo tee -a /sys/module/i2c_bcm2708/parameters/combined 113 | fi 114 | echo "" 115 | 116 | ## Overwrite path with present working directory. 117 | print_warning "Gathering present working directory for sensor21." 118 | print_warning "If you move sensor21 to another folder, you must manually edit sqldb.py and cron.txt and update paths to your present working directory." 119 | 120 | FULL_PATH="$(pwd)" 121 | DB_PATH="$FULL_PATH/measurements.db" 122 | sed -i 's|'"{PWD}"'|'"$DB_PATH"'|g' sqldb.py 123 | sed -i 's|'"{PWD}"'|'"$FULL_PATH"'|g' cron.txt 124 | 125 | ## Update apt-get package list 126 | print_step "updating package list" 127 | sudo apt-get update 128 | echo "" 129 | 130 | ## Install prerequisites 131 | print_step "checking/installing prerequisites" 132 | 133 | # Loop through apt packages array and check if present, then install if not. 134 | for var in "${APT_PACKAGES[@]}" 135 | do 136 | # fix for pip3 install vs package name 137 | if [ "$var" = "python3-pip" ]; then 138 | var="pip3" 139 | fi 140 | apt_installer $var 141 | done 142 | 143 | # Loop through pip3 packages array and check if present, then install if not. 144 | for var in "${PIP3_PACKAGES[@]}" 145 | do 146 | pip3_installer $var 147 | done 148 | 149 | # Pull down PIGPIO library, make binary, and then install. 150 | # PIGPIO enables software I2C communications on RPi GPIO pins 151 | if [ "$BC_FLAG" = "1" ]; then 152 | if ! program_exists $PACKAGE_PIGPIO; then 153 | print_warning "Installing PIGPIO." 154 | wget -N abyz.co.uk/rpi/pigpio/pigpio.zip 2>&1 | grep "not retrieving" 2>&1 > /dev/null || unzip pigpio.zip 155 | cd PIGPIO 156 | make 157 | sudo make install 158 | sudo pigpiod 159 | cd ~/sensor21 160 | sudo rm pigpio.zip 161 | else 162 | print_good "PIGPIO installed." 163 | sudo pigpiod 164 | fi 165 | fi 166 | 167 | print_good "Prerequisites installed." 168 | 169 | ## Initialize SQLite database and take first sensor21 reading. 170 | print_step "Setting up SQLite Database..." 171 | python3 sqldb.py 172 | 173 | ## Verify SQLite database insertion and reading. 174 | print_step "Verifying SQLite Datbase write." 175 | sqlite3 measurements.db "SELECT * FROM Barometer" 176 | echo "" 177 | 178 | ## Prompt user to replace crontab file. cron.txt automatically grabs sensor data every minute. 179 | print_warning "Warning: If you have an existing crontab, the next command will overwrite it." 180 | print_warning "If you are not familiar with crontab, choose Y." 181 | 182 | read -n1 -p "Proceed? [y/n] " CRON_RESPONSE 183 | case $CRON_RESPONSE in 184 | y|Y) print_step " Editing crontab..."; crontab cron.txt;; 185 | *) echo " Crontab not updated. Please run crontab cron.txt to set up the cron job.";; 186 | esac 187 | 188 | ## Success!!! 189 | print_good "Install complete." 190 | print_good "Database created, schema written to the table, and sensor readings written to DB every 1 minute." 191 | print_warning "Remove cron job with crontab -r." 192 | 193 | echo "" 194 | 195 | # Edit manifest file to include location information 196 | print_warning "Editing manifest.yaml to include location-specific information." 197 | print_warning "Input your city and hit enter. Example: San Francisco" 198 | read -p "City: " USER_CITY 199 | sed -i 's|'"{CITY}"'|'"$USER_CITY"'|g' manifest.yaml 200 | echo "" 201 | print_warning "Input your state abbreviation and hit enter. Example: CA" 202 | read -p "State: " USER_STATE 203 | sed -i 's|'"{STATE}"'|'"$USER_STATE"'|g' manifest.yaml 204 | echo "" 205 | print_warning "Input your country abbreviation and hit enter. Example: USA" 206 | read -p "Country: " USER_COUNTRY 207 | sed -i 's|'"{COUNTRY}"'|'"$USER_COUNTRY"'|g' manifest.yaml 208 | echo "" 209 | 210 | ## Set up 21 account 211 | print_good "Now running 21 status to verify user & wallet creation." 212 | print_warning "If you have not signed up for an account yet, go to https://21.co/signup/" 213 | 214 | 21 status -------------------------------------------------------------------------------- /sqldb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import datetime 5 | import json 6 | import mpl3115a2 as mpl 7 | import sqlite3 8 | import sys 9 | import os 10 | 11 | DATABASE_FILE = "{PWD}" 12 | 13 | 14 | class BarometerSQL(): 15 | 16 | """ 17 | Interface `measurements.db` and `mpl3115a2.py`. 18 | 19 | Install SQLite3, then run: 20 | $ sqlite3 measurements.db 21 | 22 | Set up database tables. 23 | $ python3 24 | > import db 25 | > a = BarometerSQL() 26 | > a.create_table() 27 | > a.close_connection 28 | """ 29 | 30 | def __init__(self): 31 | """ 32 | Connects to barometer.db and sets up an sql cursor. 33 | """ 34 | self.conn = sqlite3.connect(DATABASE_FILE) 35 | self.cursor = self.conn.cursor() 36 | 37 | def create_table(self): 38 | """ 39 | Creates default table for logging barometer sensor output. 40 | """ 41 | query = 'CREATE TABLE Barometer(Id INTEGER PRIMARY KEY AUTOINCREMENT, Datetime TEXT, Temperature REAL, Pressure REAL)' 42 | self.cursor.execute(query) 43 | 44 | def get_values(self): 45 | """ 46 | Pulls current values from bitsense instance. 47 | """ 48 | self.barometer = mpl.PressureAPI({I2C_TYPE}) 49 | date = str(datetime.datetime.now()) 50 | temperature = self.barometer.get_temperature() 51 | pressure = self.barometer.get_pressure() 52 | self.barometer.close() 53 | return [date, temperature, pressure] 54 | 55 | def write_values(self, inputs): 56 | """ 57 | Insert inputs list into barometer.db. 58 | """ 59 | query = 'INSERT INTO Barometer(Datetime, Temperature, Pressure) VALUES(?,?,?)' 60 | self.cursor.execute(query, (inputs)) 61 | self.conn.commit() 62 | 63 | def read_latest(self, index_height=1): 64 | """ 65 | Gets the index_height most recent entries in barometer.db. 66 | """ 67 | query = 'SELECT * FROM Barometer ORDER BY Id DESC LIMIT (?);' 68 | res = self.cursor.execute(query, (index_height,)) 69 | return res.fetchall() 70 | 71 | def close_connection(self): 72 | """ 73 | Close the sqlite3 handle. 74 | """ 75 | self.conn.close() 76 | 77 | 78 | class Operate(): 79 | 80 | """ 81 | Operator class that calls an instance of AltitudeSQL 82 | to update the db and read from it. 83 | """ 84 | 85 | def update_table(self): 86 | """ 87 | Grab the latest values from the barometer and 88 | committo barometer.db. 89 | """ 90 | self.handle = BarometerSQL() 91 | self.reading = self.handle.get_values() 92 | self.handle.write_values(self.reading) 93 | self.handle.close_connection() 94 | return self.reading 95 | 96 | def read(self, index_height=1): 97 | """ 98 | Grabs readings from barometer.db. 99 | """ 100 | self.handle = BarometerSQL() 101 | self.output = self.handle.read_latest(index_height) 102 | self.handle.close_connection() 103 | self.barometer_package = [] 104 | for x in range(0, index_height): 105 | self.barometer_package.append({ 106 | 'timestamp': self.output[x][1], 107 | 'temperature': self.output[x][2], 108 | 'pressure': self.output[x][3] 109 | }) 110 | return json.dumps(self.barometer_package) 111 | 112 | 113 | def main(): 114 | db_is_new = os.path.exists(DATABASE_FILE) 115 | db_setup = BarometerSQL() 116 | if db_is_new is False: 117 | db_setup.create_table() 118 | db_setup.close_connection() 119 | values = Operate() 120 | values.update_table() 121 | print('%s %s\n%s' % ( 122 | 'Database created, schema written, and first reading taken.', 123 | 'You should see sensor data printed now.', 124 | values.read())) 125 | 126 | if __name__ == "__main__": 127 | main() 128 | --------------------------------------------------------------------------------