2018-07-29 18:48:25 +02:00
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
2018-07-29 19:39:13 +02:00
|
|
|
"""
|
2018-07-29 18:48:25 +02:00
|
|
|
##########################
|
|
|
|
# Weather and GPS logger #
|
|
|
|
##########################
|
2018-07-29 19:39:13 +02:00
|
|
|
Author : Pierrick Couturier
|
2018-07-29 18:48:25 +02:00
|
|
|
|
2018-07-29 19:39:13 +02:00
|
|
|
Use with:
|
|
|
|
* Adafruit Feather M0 Express (CircuitPython firmware 3.0.0)
|
|
|
|
* Adafruit Ultimate GPS FeatherWing
|
|
|
|
* Bosch BME280 sensor (air temperature, humidity, atmospheric pressure) on I2C
|
|
|
|
|
2018-08-03 12:09:55 +02:00
|
|
|
https://framagit.org/arofarn/Cameteo
|
|
|
|
|
2018-07-29 19:39:13 +02:00
|
|
|
TODO for v1 :
|
2018-08-02 00:15:25 +02:00
|
|
|
* write data on flash drive (work-in-progress)
|
2018-08-03 22:44:41 +02:00
|
|
|
* send data through UART (work-in-progress)
|
2018-07-29 19:39:13 +02:00
|
|
|
"""
|
2018-08-03 22:44:41 +02:00
|
|
|
__version__ = 0.2
|
2018-07-28 18:42:27 +02:00
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
##########
|
2018-07-29 16:25:37 +02:00
|
|
|
# config #
|
2018-07-29 13:02:16 +02:00
|
|
|
##########
|
|
|
|
print_data = True
|
2018-08-03 22:44:01 +02:00
|
|
|
send_json_data = True
|
2018-08-02 00:09:13 +02:00
|
|
|
backup_data = True
|
2018-07-29 13:02:16 +02:00
|
|
|
data_to_neopixel = True
|
2018-07-29 15:27:37 +02:00
|
|
|
gps_enable = True
|
2018-08-03 10:59:14 +02:00
|
|
|
update_interval = const(10) # in seconds
|
2018-07-29 13:02:16 +02:00
|
|
|
send_json_data = True
|
2018-07-29 16:19:44 +02:00
|
|
|
datetime_format = "{:04}/{:02}/{:02}_{:02}:{:02}:{:02}"
|
2018-08-03 10:59:14 +02:00
|
|
|
neopixel_max_value =const(70) #max value instead of brightness to spare some mem
|
2018-07-29 13:02:16 +02:00
|
|
|
|
|
|
|
#######################
|
|
|
|
|
2018-08-03 19:59:34 +02:00
|
|
|
import microcontroller
|
2018-07-29 16:19:44 +02:00
|
|
|
import gc, os
|
|
|
|
# import micropython
|
|
|
|
import time, rtc
|
2018-07-28 18:42:27 +02:00
|
|
|
from busio import I2C, UART
|
|
|
|
from analogio import AnalogIn
|
2018-08-03 15:35:03 +02:00
|
|
|
from digitalio import DigitalInOut, Direction
|
2018-07-28 18:42:27 +02:00
|
|
|
|
|
|
|
from adafruit_bme280 import Adafruit_BME280_I2C
|
|
|
|
from adafruit_gps import GPS
|
|
|
|
import neopixel
|
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
###########
|
|
|
|
# Classes #
|
|
|
|
###########
|
|
|
|
|
|
|
|
class Data:
|
2018-07-29 16:19:44 +02:00
|
|
|
"""Class for handling data"""
|
2018-07-29 13:02:16 +02:00
|
|
|
def __init__(self):
|
2018-08-03 19:59:34 +02:00
|
|
|
self.data = {'SYS': {'time': "2000/01/01_00:00:00",
|
|
|
|
'vbat': int(),
|
|
|
|
'cput': float()},
|
|
|
|
'BME': {'temp': float(),
|
|
|
|
'hum': int(),
|
|
|
|
'press': float()},
|
|
|
|
'GPS': {'time': "2000/01/01_00:00:00",
|
|
|
|
'lat': float(),
|
|
|
|
'lon': float(),
|
|
|
|
'alt': float(),
|
|
|
|
'qual': int()}
|
2018-08-03 10:59:14 +02:00
|
|
|
}
|
2018-07-29 13:02:16 +02:00
|
|
|
|
|
|
|
def update(self):
|
|
|
|
"""Read the data from various sensors and update the data dict variable"""
|
|
|
|
#Data from Feather board
|
2018-08-03 19:59:34 +02:00
|
|
|
self.data['SYS']['time'] = datetime_format.format(*clock.datetime[0:6])
|
|
|
|
self.data['SYS']['vbat'] = round(measure_vbat(), 3)
|
|
|
|
self.data['SYS']['cput'] = round(microcontroller.cpu.temperature, 2)
|
2018-07-29 13:02:16 +02:00
|
|
|
|
|
|
|
#Data from BME280
|
2018-08-03 19:59:34 +02:00
|
|
|
self.data['BME']['temp'] = round(bme280.temperature, 1)
|
|
|
|
self.data['BME']['hum'] = int(bme280.humidity)
|
|
|
|
self.data['BME']['press'] = round(bme280.pressure, 2)
|
2018-08-03 12:09:55 +02:00
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
if gps_enable:
|
|
|
|
if gps.has_fix:
|
2018-08-03 19:59:34 +02:00
|
|
|
self.data['GPS']['time'] = datetime_format.format(gps.timestamp_utc.tm_year,
|
|
|
|
gps.timestamp_utc.tm_mon,
|
|
|
|
gps.timestamp_utc.tm_mday,
|
|
|
|
gps.timestamp_utc.tm_hour,
|
|
|
|
gps.timestamp_utc.tm_min,
|
|
|
|
gps.timestamp_utc.tm_sec)
|
|
|
|
self.data['GPS']['lat'] = gps.latitude
|
|
|
|
self.data['GPS']['lon'] = gps.longitude
|
|
|
|
self.data['GPS']['alt'] = gps.altitude_m
|
|
|
|
self.data['GPS']['qual'] = gps.fix_quality
|
2018-07-29 13:02:16 +02:00
|
|
|
else:
|
2018-08-03 19:59:34 +02:00
|
|
|
self.data['GPS']['lat'] = None
|
|
|
|
self.data['GPS']['lon'] = None
|
|
|
|
self.data['GPS']['alt'] = None
|
2018-07-29 13:02:16 +02:00
|
|
|
else:
|
|
|
|
self.data['GPS'] = None
|
|
|
|
|
|
|
|
def show(self):
|
2018-08-03 19:59:34 +02:00
|
|
|
"""Serialize data for visualization on serial console"""
|
2018-07-29 13:02:16 +02:00
|
|
|
for source in self.data.keys():
|
|
|
|
print(source + ": ")
|
|
|
|
if not self.data[source] == None:
|
|
|
|
for d in self.data[source].items():
|
2018-08-03 19:59:34 +02:00
|
|
|
print("\t{0}: {1}".format(d[0], d[1]))
|
2018-07-29 13:02:16 +02:00
|
|
|
|
|
|
|
def json(self):
|
|
|
|
"""Serialize data to json-formatted string"""
|
2018-08-03 20:38:46 +02:00
|
|
|
output = '{'
|
|
|
|
first_source = True
|
2018-07-29 13:02:16 +02:00
|
|
|
for source in self.data.keys():
|
2018-08-03 20:38:46 +02:00
|
|
|
if first_source:
|
|
|
|
first_source = False
|
|
|
|
comma_src = ""
|
|
|
|
else:
|
|
|
|
comma_src = ","
|
|
|
|
output = '{}{}"{}":'.format(output, comma_src, source)
|
2018-07-29 13:02:16 +02:00
|
|
|
if not self.data[source] == None:
|
2018-08-03 20:38:46 +02:00
|
|
|
output = output + '{'
|
|
|
|
first_data = True
|
2018-07-29 13:02:16 +02:00
|
|
|
for d in self.data[source].items():
|
2018-08-03 20:38:46 +02:00
|
|
|
if first_data:
|
|
|
|
comma = ""
|
|
|
|
first_data = False
|
|
|
|
else:
|
|
|
|
comma = ","
|
|
|
|
output = '{}{}"{}":"{}"'.format(output, comma, d[0], d[1])
|
|
|
|
output = output + '}'
|
|
|
|
output = output + '}'
|
2018-07-29 13:02:16 +02:00
|
|
|
return output
|
|
|
|
|
2018-08-03 15:00:28 +02:00
|
|
|
def atmo2rgb(self):
|
2018-08-03 19:59:34 +02:00
|
|
|
"""Convert atmospheric data from BME280 sensor into NeoPixel color
|
2018-08-03 15:00:28 +02:00
|
|
|
* RED => temperature : max = 35degC, min =10degC (range 25°C)
|
|
|
|
* BLUE => humidity : max= 100%, mini=0%
|
|
|
|
* GREEN => pression : mini=960hPa, maxi = 1030hPa (range 70hPa)
|
|
|
|
"""
|
|
|
|
|
2018-08-03 19:59:34 +02:00
|
|
|
red = int((self.data['BME']['temp']-10)*neopixel_max_value/25)
|
2018-08-03 15:00:28 +02:00
|
|
|
if red > neopixel_max_value:
|
|
|
|
red = neopixel_max_value
|
|
|
|
if red < 0:
|
|
|
|
red = 0
|
|
|
|
|
2018-08-03 19:59:34 +02:00
|
|
|
blue = int(self.data['BME']['hum']*neopixel_max_value/100)
|
2018-08-03 15:00:28 +02:00
|
|
|
|
2018-08-03 19:59:34 +02:00
|
|
|
green = int((self.data['BME']['press']-960)*neopixel_max_value/70)
|
2018-08-03 15:00:28 +02:00
|
|
|
if green > neopixel_max_value:
|
|
|
|
green = neopixel_max_value
|
|
|
|
if green < 0:
|
|
|
|
green = 0
|
|
|
|
|
|
|
|
if print_data:
|
|
|
|
print("Col:{}".format((red, green, blue)))
|
|
|
|
|
|
|
|
return (red, green, blue)
|
|
|
|
|
2018-08-02 00:09:13 +02:00
|
|
|
def write_on_flash(self):
|
|
|
|
"""Save the current data as csv file on SPI flash"""
|
|
|
|
try:
|
|
|
|
with open("data/data.csv", "a") as csv_file:
|
2018-08-03 19:59:34 +02:00
|
|
|
csv_file.write("{};{};{};{}\n".format(self.data['SYS']['time'],
|
|
|
|
self.data['BME']['temp'],
|
|
|
|
self.data['BME']['hum'],
|
|
|
|
self.data['BME']['press']))
|
2018-08-02 00:09:13 +02:00
|
|
|
except OSError as e:
|
2018-08-03 19:59:34 +02:00
|
|
|
print("Err {}: readonly".format(e))
|
2018-08-02 00:15:25 +02:00
|
|
|
backup_data = False #to avoid trying again till next reset
|
2018-08-03 19:59:34 +02:00
|
|
|
led13.value = True
|
2018-08-02 00:09:13 +02:00
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
#############
|
|
|
|
# Functions #
|
|
|
|
#############
|
|
|
|
|
|
|
|
def check_data_dir():
|
|
|
|
"""Check if data directories exists"""
|
|
|
|
if 'data' not in os.listdir():
|
|
|
|
os.mkdir('data')
|
|
|
|
os.mkdir('data/hourly')
|
|
|
|
os.mkdir('data/daily')
|
|
|
|
elif 'hourly' not in os.listdir('data'):
|
|
|
|
os.mkdir('data/hourly')
|
|
|
|
elif 'daily' not in os.listdir('data'):
|
|
|
|
os.mkdir('data/daily')
|
|
|
|
|
2018-07-29 17:27:52 +02:00
|
|
|
def set_clock_from_GPS():
|
|
|
|
if gps_enable and gps.has_fix:
|
2018-08-03 15:00:28 +02:00
|
|
|
#Congreen GPS timestamp into struct_time
|
2018-07-29 17:27:52 +02:00
|
|
|
gps_datetime = time.struct_time((gps.timestamp_utc.tm_year,
|
|
|
|
gps.timestamp_utc.tm_mon,
|
|
|
|
gps.timestamp_utc.tm_mday,
|
|
|
|
gps.timestamp_utc.tm_hour,
|
|
|
|
gps.timestamp_utc.tm_min,
|
|
|
|
gps.timestamp_utc.tm_sec, 0, 0, 0))
|
|
|
|
#Max difference between GPS and internal RTC (in seconds):
|
|
|
|
if abs(time.mktime(gps_datetime) - time.mktime(clock.datetime)) >= 5.0:
|
|
|
|
print("Clock difference with GPS!")
|
|
|
|
print("Previous date/time : " + datetime_format.format(*clock.datetime[0:6]))
|
|
|
|
clock.datetime = gps_datetime #Trust GPS if there is a bias
|
|
|
|
print("New date/time : " + datetime_format.format(*clock.datetime[0:6]))
|
2018-07-29 18:48:25 +02:00
|
|
|
|
2018-08-02 00:11:57 +02:00
|
|
|
def measure_vbat(samples=10, timestep=0.01):
|
|
|
|
"""Measure Vbattery as the mean of n samples with timestep second between
|
|
|
|
each measurement"""
|
|
|
|
# Note about v_bat calculation :
|
|
|
|
# 0.000100708 = 2*3.3/65536 with
|
|
|
|
# 2 : voltage is divided by 2
|
|
|
|
# 3.3 : Vref = 3.3V
|
|
|
|
# 65536 : 16bit ADC
|
|
|
|
v = 0
|
|
|
|
for i in range(samples):
|
|
|
|
v = v + vbat.value
|
|
|
|
time.sleep(timestep)
|
|
|
|
return v/samples*0.000100708
|
|
|
|
|
2018-07-28 18:42:27 +02:00
|
|
|
#########
|
|
|
|
# Setup #
|
|
|
|
#########
|
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
gc.collect()
|
2018-07-29 16:19:44 +02:00
|
|
|
#micropython.mem_info()
|
|
|
|
|
|
|
|
#Enable RTC of the feather M0 board
|
|
|
|
clock = rtc.RTC()
|
2018-07-29 17:27:52 +02:00
|
|
|
#clock.datetime = time.struct_time((2018, 7, 29, 15, 31, 30, 0, 0, 0))
|
2018-07-29 13:02:16 +02:00
|
|
|
|
2018-07-28 18:42:27 +02:00
|
|
|
# BME280 sensors (I2C)
|
2018-08-03 19:59:34 +02:00
|
|
|
i2c = I2C(microcontroller.pin.PA23, microcontroller.pin.PA22)
|
2018-07-29 13:02:16 +02:00
|
|
|
# i2c addresses for BME280 breakout :
|
2018-07-29 16:25:37 +02:00
|
|
|
# 0x77 = adafruit breakout board
|
|
|
|
# 0x76 = tiny chinese board
|
2018-07-29 13:02:16 +02:00
|
|
|
bme280 = Adafruit_BME280_I2C(i2c, address=0x76)
|
2018-07-28 18:42:27 +02:00
|
|
|
|
|
|
|
# Battery voltage
|
2018-08-03 19:59:34 +02:00
|
|
|
vbat = AnalogIn(microcontroller.pin.PA07)
|
2018-07-28 18:42:27 +02:00
|
|
|
|
2018-08-03 19:59:34 +02:00
|
|
|
gps_en_pin = DigitalInOut(microcontroller.pin.PB02)
|
2018-08-03 15:35:03 +02:00
|
|
|
gps_en_pin.direction = Direction.OUTPUT
|
|
|
|
|
2018-08-03 19:59:34 +02:00
|
|
|
led13 = DigitalInOut(microcontroller.pin.PA17)
|
|
|
|
led13.direction = Direction.OUTPUT
|
|
|
|
led13.value = False
|
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
# GPS on FeatherWing board
|
2018-08-03 15:35:03 +02:00
|
|
|
gps_en_pin.value = not gps_enable
|
2018-07-29 13:02:16 +02:00
|
|
|
if gps_enable:
|
2018-08-03 19:59:34 +02:00
|
|
|
gps_uart = UART(microcontroller.pin.PA10, microcontroller.pin.PA11,
|
|
|
|
baudrate=9600, timeout=3000)
|
2018-07-29 13:02:16 +02:00
|
|
|
gps = GPS(gps_uart)
|
|
|
|
# Turn on the basic GGA and RMC info
|
|
|
|
gps.send_command('PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0')
|
|
|
|
gps.send_command('PMTK220,1000') # 1000 ms refresh rate
|
2018-07-28 18:42:27 +02:00
|
|
|
|
2018-08-03 22:44:01 +02:00
|
|
|
# second UART to communicate with raspberry pi GPIO
|
|
|
|
if send_json_data:
|
|
|
|
rpi_uart = UART(microcontroller.pin.PB10, microcontroller.pin.PB11,
|
|
|
|
baudrate=115200, timeout=2000)
|
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
# Integrated Neopixel
|
|
|
|
if data_to_neopixel:
|
2018-08-03 19:59:34 +02:00
|
|
|
pixel = neopixel.NeoPixel(microcontroller.pin.PA06, 1, brightness=1)
|
2018-07-29 13:02:16 +02:00
|
|
|
else:
|
|
|
|
#if neopixel is disable : turn off the LED
|
2018-08-03 19:59:34 +02:00
|
|
|
pixel = neopixel.NeoPixel(microcontroller.pin.PA06, 1, brightness=1)
|
2018-07-29 13:02:16 +02:00
|
|
|
pixel[0] = (0,0,0)
|
|
|
|
pixel = None
|
2018-07-28 18:42:27 +02:00
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
check_data_dir()
|
2018-07-28 18:42:27 +02:00
|
|
|
|
|
|
|
#############
|
|
|
|
# Main loop #
|
|
|
|
#############
|
2018-07-29 13:02:16 +02:00
|
|
|
|
|
|
|
data = Data()
|
|
|
|
last_update = time.monotonic()
|
2018-07-29 16:19:44 +02:00
|
|
|
|
2018-07-28 18:42:27 +02:00
|
|
|
while True:
|
2018-07-29 13:02:16 +02:00
|
|
|
|
|
|
|
if gps_enable:
|
|
|
|
gps.update()
|
|
|
|
|
|
|
|
current = time.monotonic()
|
|
|
|
if current - last_update >= update_interval:
|
|
|
|
last_update = current
|
2018-07-29 17:27:52 +02:00
|
|
|
set_clock_from_GPS()
|
2018-07-29 13:02:16 +02:00
|
|
|
data.update()
|
|
|
|
if print_data:
|
|
|
|
data.show()
|
2018-08-03 22:44:01 +02:00
|
|
|
if send_json_data:
|
|
|
|
rpi_uart.write(data.json())
|
2018-08-02 00:09:13 +02:00
|
|
|
if backup_data:
|
|
|
|
data.write_on_flash()
|
2018-07-29 13:02:16 +02:00
|
|
|
if data_to_neopixel:
|
2018-08-03 15:00:28 +02:00
|
|
|
pixel[0] = data.atmo2rgb()
|
2018-08-03 12:09:55 +02:00
|
|
|
|
2018-07-29 13:02:16 +02:00
|
|
|
gc.collect()
|
|
|
|
# micropython.mem_info(1)
|
|
|
|
# print('Memory free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
|