From 55c6199b419c4459249085dd01b3d10fb6f92880 Mon Sep 17 00:00:00 2001 From: Utkarsh Goel Date: Mon, 15 Mar 2021 20:30:55 +0800 Subject: [PATCH] Add Dropbox, GPS, Wiper (#13) * Start major code refactor add constantS replace Camera class objects with PiCamera fix missing imports create a camera package remove relative imports from main.py fix capture.py import remove relative imports update requirements.txt add more requirements fix camera imports refactor main.py minor fixes minor fixes remove thread_active from app_server Fix bug with load_scheduler data function Fix viewConfig endpoint fix camera errors Start major code refactor add constantS replace Camera class objects with PiCamera fix missing imports create a camera package remove relative imports from main.py fix capture.py import remove relative imports add more requirements fix camera imports refactor main.py minor fixes remove thread_active from app_server Fix bug with load_scheduler data function Fix viewConfig endpoint fix camera errors Fix sensor class Add utils for camera to get camera name Fix issue: JSON parsing of camera config in viewConfig Add clearSchedule endpoint Fix json load for clearSchedule add shutter speed to capture_video fix json load for clearSchedule logging debug messages to capture images add debug logs to camera thread add more debug logs to capture_images fix shutter speed spelling add exception handling for sensor not connected add error handling to sensors wrap sensor in try catch wrap sensor in try catch change resolution to int tuple in Scheduler.py convert datetime to string for video filename add update route to the server Start major code refactor add constantS replace Camera class objects with PiCamera fix missing imports create a camera package fix capture.py import remove relative imports add more requirements refactor main.py minor fixes Start major code refactor add constantS replace Camera class objects with PiCamera fix missing imports create a camera package remove relative imports from main.py fix capture.py import remove relative imports add more requirements fix camera imports refactor main.py minor fixes remove thread_active from app_server Fix bug with load_scheduler data function Fix viewConfig endpoint Fix sensor class Add utils for camera to get camera name add shutter speed to capture_video logging debug messages to capture images add debug logs to camera thread add more debug logs to capture_images fix shutter speed spelling add error handling to sensors wrap sensor in try catch wrap sensor in try catch change resolution to int tuple in Scheduler.py convert datetime to string for video filename fix start recording in testPhotoMem add sleep to capture continuous Start major code refactor replace Camera class objects with PiCamera fix missing imports create a camera package fix capture.py import remove relative imports update requirements.txt add more requirements fix camera imports refactor main.py minor fixes minor fixes Start major code refactor add constantS replace Camera class objects with PiCamera fix missing imports create a camera package remove relative imports from main.py fix capture.py import remove relative imports add more requirements fix camera imports refactor main.py minor fixes remove thread_active from app_server Fix viewConfig endpoint Fix sensor class Add utils for camera to get camera name Fix issue: JSON parsing of camera config in viewConfig add shutter speed to capture_video fix json load for clearSchedule logging debug messages to capture images add debug logs to camera thread add more debug logs to capture_images fix shutter speed spelling add error handling to sensors wrap sensor in try catch wrap sensor in try catch change resolution to int tuple in Scheduler.py convert datetime to string for video filename add update route to the server fix start recording in testPhotoMem Start major code refactor replace Camera class objects with PiCamera fix missing imports create a camera package fix capture.py import remove relative imports update requirements.txt add more requirements refactor main.py minor fixes Fix viewConfig endpoint fix camera errors Start major code refactor add constantS replace Camera class objects with PiCamera fix missing imports create a camera package remove relative imports from main.py fix capture.py import remove relative imports add more requirements fix camera imports refactor main.py minor fixes remove thread_active from app_server Fix viewConfig endpoint Fix sensor class Add utils for camera to get camera name Fix issue: JSON parsing of camera config in viewConfig add shutter speed to capture_video fix json load for clearSchedule logging debug messages to capture images add debug logs to camera thread add more debug logs to capture_images fix shutter speed spelling add error handling to sensors wrap sensor in try catch wrap sensor in try catch change resolution to int tuple in Scheduler.py convert datetime to string for video filename add update route to the server add sleep to capture continuous fix shutter speed in capture_video * add witty pi commands to clear schedule * Add wiper code * Add wiper run on each slot start with wiper as on * Add GPS code * Fix wiper module imports * Add pwm mode on pin enable * migrate to subprocess * Fix gps update * Integrate uploader and GPS * Fix upload import * Dropbox from environment * Fix resolving fields for wiper and uplaod from schedule * Add dropbox handlers in socket : * Make auth static methods * Fix socket connection request, make non-static * Add logging for auth url * Add user details response email * Fix wiper_status variable * Fix sensor loading logic * Fix GPS object creation * Add logging for upload * Add auth check dropbox * Remove redundant data argument * Add optimisation for upload, add wiper code with correct angle * Fix sensor error handling * Add dropbox auth error handling * Integrate subsea light sensor * Remove object from Subsealight class * add error handling for get user * Add handler for dropbox credential file creation * Add update script for users Co-authored-by: Manya Agarwal --- .gitignore | 4 +- etc/dhcpcd.conf | 66 ++ etc/dnsmasq.conf | 4 + openoceancamera/Camera.py | 81 --- openoceancamera/Scheduler.py | 73 +- openoceancamera/TSL2561/.categories | 5 - openoceancamera/TSL2561/Arduino/TSL2561.ino | 74 --- openoceancamera/TSL2561/C/TSL2561.c | 60 -- openoceancamera/TSL2561/Java/TSL2561.java | 46 -- .../TSL2561/Onion Omega Python/TSL2561.py | 39 -- openoceancamera/TSL2561/Python/TSL2561.py | 43 -- openoceancamera/TSL2561/README.md | 84 --- openoceancamera/TSL2561/TSL2561_I2CS.png | Bin 123405 -> 0 bytes .../{ => appserver}/StreamingOutput.py | 0 openoceancamera/appserver/__init__.py | 296 +++++++++ openoceancamera/base_camera.py | 111 ---- openoceancamera/camera/__init__.py | 2 + openoceancamera/camera/camera_thread.py | 56 ++ openoceancamera/camera/capture.py | 112 ++++ openoceancamera/camera/upload.py | 31 + openoceancamera/camera/utils.py | 10 + openoceancamera/camera_pi.py | 23 - openoceancamera/constants.py | 7 + openoceancamera/logger.py | 5 + openoceancamera/main.py | 626 +----------------- openoceancamera/ms5837-python/LICENSE | 21 - openoceancamera/ms5837-python/README.md | 134 ---- openoceancamera/ms5837-python/example.py | 57 -- openoceancamera/requirements.txt | 3 + openoceancamera/restart.py | 10 + openoceancamera/sensors/__init__.py | 4 + openoceancamera/sensors/gps.py | 31 + .../{ms5837-python => sensors}/ms5837.py | 235 ++++--- openoceancamera/sensors/sensors.py | 211 ++++++ openoceancamera/sensors/subsealight.py | 27 + openoceancamera/sensors/tsl2561.py | 334 ++++++++++ .../build/lib/tsys01 => sensors}/tsys01.py | 91 +-- openoceancamera/tsys01-python/LICENSE | 21 - openoceancamera/tsys01-python/README.md | 50 -- .../bluerobotics_tsys01.egg-info/PKG-INFO | 61 -- .../bluerobotics_tsys01.egg-info/SOURCES.txt | 8 - .../dependency_links.txt | 1 - .../top_level.txt | 1 - .../build/lib/tsys01/__init__.py | 1 - .../dist/bluerobotics_tsys01-0.0.1-py3.7.egg | Bin 3040 -> 0 bytes openoceancamera/tsys01-python/example.py | 19 - openoceancamera/tsys01-python/setup.py | 22 - .../tsys01-python/tsys01/__init__.py | 1 - .../tsys01-python/tsys01/tsys01.py | 91 --- openoceancamera/uploader/__init__.py | 1 + openoceancamera/uploader/dropbox_uploader.py | 102 +++ openoceancamera/wiper/__init__.py | 1 + openoceancamera/wiper/servo.py | 25 + openoceancamera/wiper/wiper.py | 30 + scripts/update.sh | 3 +- 55 files changed, 1602 insertions(+), 1852 deletions(-) create mode 100644 etc/dhcpcd.conf create mode 100644 etc/dnsmasq.conf delete mode 100755 openoceancamera/Camera.py delete mode 100755 openoceancamera/TSL2561/.categories delete mode 100755 openoceancamera/TSL2561/Arduino/TSL2561.ino delete mode 100755 openoceancamera/TSL2561/C/TSL2561.c delete mode 100755 openoceancamera/TSL2561/Java/TSL2561.java delete mode 100755 openoceancamera/TSL2561/Onion Omega Python/TSL2561.py delete mode 100755 openoceancamera/TSL2561/Python/TSL2561.py delete mode 100755 openoceancamera/TSL2561/README.md delete mode 100755 openoceancamera/TSL2561/TSL2561_I2CS.png rename openoceancamera/{ => appserver}/StreamingOutput.py (100%) create mode 100644 openoceancamera/appserver/__init__.py delete mode 100755 openoceancamera/base_camera.py create mode 100644 openoceancamera/camera/__init__.py create mode 100644 openoceancamera/camera/camera_thread.py create mode 100644 openoceancamera/camera/capture.py create mode 100644 openoceancamera/camera/upload.py create mode 100644 openoceancamera/camera/utils.py delete mode 100755 openoceancamera/camera_pi.py create mode 100644 openoceancamera/constants.py create mode 100644 openoceancamera/logger.py delete mode 100755 openoceancamera/ms5837-python/LICENSE delete mode 100755 openoceancamera/ms5837-python/README.md delete mode 100755 openoceancamera/ms5837-python/example.py create mode 100644 openoceancamera/restart.py create mode 100644 openoceancamera/sensors/__init__.py create mode 100644 openoceancamera/sensors/gps.py rename openoceancamera/{ms5837-python => sensors}/ms5837.py (50%) mode change 100755 => 100644 create mode 100644 openoceancamera/sensors/sensors.py create mode 100644 openoceancamera/sensors/subsealight.py create mode 100644 openoceancamera/sensors/tsl2561.py rename openoceancamera/{tsys01-python/build/lib/tsys01 => sensors}/tsys01.py (58%) mode change 100755 => 100644 delete mode 100755 openoceancamera/tsys01-python/LICENSE delete mode 100755 openoceancamera/tsys01-python/README.md delete mode 100755 openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/PKG-INFO delete mode 100755 openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/SOURCES.txt delete mode 100755 openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/dependency_links.txt delete mode 100755 openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/top_level.txt delete mode 100755 openoceancamera/tsys01-python/build/lib/tsys01/__init__.py delete mode 100755 openoceancamera/tsys01-python/dist/bluerobotics_tsys01-0.0.1-py3.7.egg delete mode 100755 openoceancamera/tsys01-python/example.py delete mode 100755 openoceancamera/tsys01-python/setup.py delete mode 100755 openoceancamera/tsys01-python/tsys01/__init__.py delete mode 100755 openoceancamera/tsys01-python/tsys01/tsys01.py create mode 100644 openoceancamera/uploader/__init__.py create mode 100644 openoceancamera/uploader/dropbox_uploader.py create mode 100644 openoceancamera/wiper/__init__.py create mode 100644 openoceancamera/wiper/servo.py create mode 100644 openoceancamera/wiper/wiper.py diff --git a/.gitignore b/.gitignore index 1f05a21..c80648b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,6 @@ !ngrok !.github !.github/** -!scripts \ No newline at end of file +!scripts +!etc/ +!etc/** diff --git a/etc/dhcpcd.conf b/etc/dhcpcd.conf new file mode 100644 index 0000000..3486049 --- /dev/null +++ b/etc/dhcpcd.conf @@ -0,0 +1,66 @@ +# A sample configuration for dhcpcd. +# See dhcpcd.conf(5) for details. +# Allow users of this group to interact with dhcpcd via the control socket. +#controlgroup wheel + +# Inform the DHCP server of our hostname for DDNS. +hostname + +# Use the hardware address of the interface for the Client ID. +clientid +# or +# Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361. +# Some non-RFC compliant DHCP servers do not reply with this set. +# In this case, comment out duid and enable clientid above. +#duid + +# Persist interface configuration when dhcpcd exits. +persistent + +# Rapid commit support. +# Safe to enable by default because it requires the equivalent option set +# on the server to actually work. +option rapid_commit + +# A list of options to request from the DHCP server. +option domain_name_servers, domain_name, domain_search, host_name +option classless_static_routes +# Respect the network MTU. This is applied to DHCP routes. +option interface_mtu + +# Most distributions have NTP support. +#option ntp_servers + +# A ServerID is required by RFC2131. +require dhcp_server_identifier + +# Generate SLAAC address using the Hardware Address of the interface +#slaac hwaddr +# OR generate Stable Private IPv6 Addresses based from the DUID +slaac private + +# Example static IP configuration: +#interface eth0 +#static ip_address=192.168.0.10/24 +#static ip6_address=fd51:42f8:caae:d92e::ff/64 +#static routers=192.168.0.1 +#static domain_name_servers=192.168.0.1 8.8.8.8 fd51:42f8:caae:d92e::1 + +# It is possible to fall back to a static IP if DHCP fails: +# define static profile +#profile static_eth0 +#static ip_address=192.168.1.23/24 +#static routers=192.168.1.1 +#static domain_name_servers=192.168.1.1 + +# fallback to static profile on eth0 +#interface eth0 +#fallback static_eth0 +inteface wlan0 + static ip_address=192.168.5.1/24 + nohook wpa_supplicant + +interface eth0 + static ip_address=192.168.8.106/24 + static routers=192.168.8.1 + static domain_name_server=8.8.8.8 diff --git a/etc/dnsmasq.conf b/etc/dnsmasq.conf new file mode 100644 index 0000000..b6e5adb --- /dev/null +++ b/etc/dnsmasq.conf @@ -0,0 +1,4 @@ +interface=wlan0 +dhcp-range=192.168.5.2,192.168.5.20,255.255.255.0,24h +domain=wlan +address=/gw.wlan/192.168.5.1 diff --git a/openoceancamera/Camera.py b/openoceancamera/Camera.py deleted file mode 100755 index a2e6ccc..0000000 --- a/openoceancamera/Camera.py +++ /dev/null @@ -1,81 +0,0 @@ -try: - from picamera import PiCamera - from time import sleep,time - from datetime import datetime -except: - print( - "Program is not running on a Raspberry Pi. The camera module cannot be loaded" - ) - exit(0) - - -class Camera(object): - def __init__(self, resolution=(1920, 1080), shutter_speed=5000, iso=0, frequency=3): - self.camera = PiCamera() - self.set_camera_resolution(resolution) - self.set_shutter_speed(shutter_speed) - self.set_iso(iso) - self.set_capture_frequency(frequency) - - def do_capture(self, filename="test.jpg", continuous=False, slot=None): - if continuous: - #try: - # for f in self.camera.capture_continuous('/media/pi/OPENOCEANCA/img{timestamp:%Y-%m-%d-%H-%M-%S}.jpg'): - # sleep((self.frequency-1)) - # print (f) - # currenttime=datetime.now() - # if currenttime= datetime.now(): + future_slots.append(slot) + # Sorts the slots in case they may not be + future_slots = sorted(future_slots, key=lambda x: x["start"]) + if len(future_slots): + return future_slots[0] + else: + return None + def time_to_nearest_schedule(self): - self.update_current_time() possible_slots = [] # Get the future slots for slot in self.schedule_data: - if slot["start"] >= self.time_now: + if slot["start"] >= datetime.now(): possible_slots.append(slot) # Sorts the slots in case they may not be possible_slots = sorted(possible_slots, key=lambda x: x["start"]) - print(f"The time now is: {self.time_now}") + print(f"The time now is: {datetime.now()}") print(f"The future slots are: {possible_slots}") # Take the difference between the most recent slot's start time and time now - delta = possible_slots[0]["start"] - self.time_now + delta = possible_slots[0]["start"] - datetime.now() print(f"The time difference is: {delta}") # Returns an integer value for the time difference in seconds return int(delta.total_seconds()) - - def next_future_timeslot(self): - self.update_current_time() - - future_slots = [] - # Get the future slots - for slot in self.schedule_data: - if slot["start"] >= self.time_now: - future_slots.append(slot) - # Sorts the slots in case they may not be - future_slots = sorted(future_slots, key=lambda x: x["start"]) - if len(future_slots): - return future_slots[0] - else: - return None diff --git a/openoceancamera/TSL2561/.categories b/openoceancamera/TSL2561/.categories deleted file mode 100755 index e8afca7..0000000 --- a/openoceancamera/TSL2561/.categories +++ /dev/null @@ -1,5 +0,0 @@ -lang:java -lang:python -lang:arduino -lang:C -sku:TSL2561_I2CS diff --git a/openoceancamera/TSL2561/Arduino/TSL2561.ino b/openoceancamera/TSL2561/Arduino/TSL2561.ino deleted file mode 100755 index 3555067..0000000 --- a/openoceancamera/TSL2561/Arduino/TSL2561.ino +++ /dev/null @@ -1,74 +0,0 @@ -// Distributed with a free-will license. -// Use it any way you want, profit or free, provided it fits in the licenses of its associated works. -// TSL2561 -// This code is designed to work with the TSL2561_I2CS I2C Mini Module available from ControlEverything.com. -// https://www.controleverything.com/content/Light?sku=TSL2561_I2CS#tabs-0-product_tabset-2 - -#include - -// TSL2561 I2C address is 0x39(57) -#define Addr 0x39 - -void setup() -{ - // Initialise I2C communication as MASTER - Wire.begin(); - // Initialise serial communication, set baud rate = 9600 - Serial.begin(9600); - - // Starts I2C communication - Wire.beginTransmission(Addr); - // Select control register - Wire.write(0x00 | 0x80); - // Power ON mode - Wire.write(0x03); - // Stop I2C Transmission - Wire.endTransmission(); - - // Starts I2C communication - Wire.beginTransmission(Addr); - // Select timing register - Wire.write(0x01 | 0x80); - // Nominal integration time = 402ms - Wire.write(0x02); - // Stop I2C Transmission - Wire.endTransmission(); - delay(300); - -} - -void loop() -{ - unsigned int data[4]; - for(int i = 0; i < 4; i++) - { - // Starts I2C communication - Wire.beginTransmission(Addr); - // Select data register - Wire.write((140 + i)); - // Stop I2C Transmission - Wire.endTransmission(); - - // Request 1 byte of data - Wire.requestFrom(Addr, 1); - - // Read 1 bytes of data - if(Wire.available() == 1) - { - data[i] = Wire.read(); - } - delay(200); - } - - // Convert the data - double ch0 = ((data[1] & 0xFF) * 256) + (data[0] & 0xFF); - double ch1 = ((data[3] & 0xFF) * 256) + (data[2] & 0xFF); - - // Output data to serial monitor - Serial.print("Full Spectrum(IR + Visible) :"); - Serial.println(ch0); - Serial.print("Infrared Value :"); - Serial.println(ch1); - Serial.print("Visible Value :"); - Serial.println(ch0-ch1); -} diff --git a/openoceancamera/TSL2561/C/TSL2561.c b/openoceancamera/TSL2561/C/TSL2561.c deleted file mode 100755 index 4f50805..0000000 --- a/openoceancamera/TSL2561/C/TSL2561.c +++ /dev/null @@ -1,60 +0,0 @@ -// Distributed with a free-will license. -// Use it any way you want, profit or free, provided it fits in the licenses of its associated works. -// TSL2561 -// This code is designed to work with the TSL2561_I2CS I2C Mini Module available from ControlEverything.com. -// https://www.controleverything.com/content/Light?sku=TSL2561_I2CS#tabs-0-product_tabset-2 - -#include -#include -#include -#include -#include -#include - -void main() -{ - // Create I2C bus - int file; - char *bus = "/dev/i2c-1"; - if((file = open(bus, O_RDWR)) < 0) - { - printf("Failed to open the bus. \n"); - exit(1); - } - // Get I2C device, TSL2561 I2C address is 0x39(57) - ioctl(file, I2C_SLAVE, 0x39); - - // Select control register(0x00 | 0x80) - // Power ON mode(0x03) - char config[2] = {0}; - config[0] = 0x00 | 0x80; - config[1] = 0x03; - write(file, config, 2); - // Select timing register(0x01 | 0x80) - // Nominal integration time = 402ms(0x02) - config[0] = 0x01 | 0x80; - config[1] = 0x02; - write(file, config, 2); - sleep(1); - - // Read 4 bytes of data from register(0x0C | 0x80) - // ch0 lsb, ch0 msb, ch1 lsb, ch1 msb - char reg[1] = {0x0C | 0x80}; - write(file, reg, 1); - char data[4] = {0}; - if(read(file, data, 4) != 4) - { - printf("Erorr : Input/output Erorr \n"); - } - else - { - // Convert the data - float ch0 = (data[1] * 256 + data[0]); - float ch1 = (data[3] * 256 + data[2]); - - // Output data to screen - printf("Full Spectrum(IR + Visible) : %.2f lux \n", ch0); - printf("Infrared Value : %.2f lux \n", ch1); - printf("Visible Value : %.2f lux \n", (ch0 - ch1)); - } -} diff --git a/openoceancamera/TSL2561/Java/TSL2561.java b/openoceancamera/TSL2561/Java/TSL2561.java deleted file mode 100755 index 04235f3..0000000 --- a/openoceancamera/TSL2561/Java/TSL2561.java +++ /dev/null @@ -1,46 +0,0 @@ -// Distributed with a free-will license. -// Use it any way you want, profit or free, provided it fits in the licenses of its associated works. -// TSL2561 -// This code is designed to work with the TSL2561_I2CS I2C Mini Module available from ControlEverything.com. -// https://www.controleverything.com/content/Light?sku=TSL2561_I2CS#tabs-0-product_tabset-2 - -import com.pi4j.io.i2c.I2CBus; -import com.pi4j.io.i2c.I2CDevice; -import com.pi4j.io.i2c.I2CFactory; -import java.io.IOException; -import java.text.DecimalFormat; -import java.text.NumberFormat; - -public class TSL2561 -{ - public static void main(String args[]) throws Exception - { - // Create I2C bus - I2CBus Bus = I2CFactory.getInstance(I2CBus.BUS_1); - // Get I2C device, TSL2561 I2C address is 0x39(57) - I2CDevice device = Bus.getDevice(0x39); - - // Select control register - // Power ON mode - device.write(0x00 | 0x80, (byte)0x03); - // Select timing register - // Nominal integration time = 402ms - device.write(0x01 | 0x80, (byte)0x02); - Thread.sleep(500); - - // Read 4 bytes of data - // ch0 lsb, ch0 msb, ch1 lsb, ch1 msb - byte[] data=new byte[4]; - device.read(0x0C | 0x80, data, 0, 4); - - // Convert the data - double ch0 = ((data[1] & 0xFF)* 256 + (data[0] & 0xFF)); - double ch1 = ((data[3] & 0xFF)* 256 + (data[2] & 0xFF)); - - // Output data to screen - System.out.printf("Full Spectrum(IR + Visible) : %.2f lux %n", ch0); - System.out.printf("Infrared Value : %.2f lux %n", ch1); - System.out.printf("Visible Value : %.2f lux %n", (ch0 - ch1)); - } -} - \ No newline at end of file diff --git a/openoceancamera/TSL2561/Onion Omega Python/TSL2561.py b/openoceancamera/TSL2561/Onion Omega Python/TSL2561.py deleted file mode 100755 index 6f002d6..0000000 --- a/openoceancamera/TSL2561/Onion Omega Python/TSL2561.py +++ /dev/null @@ -1,39 +0,0 @@ -# Distributed with a free-will license. -# Use it any way you want, profit or free, provided it fits in the licenses of its associated works. -# TSL2561 -# This code is designed to work with the TSL2561_I2CS I2C Mini Module available from ControlEverything.com. -# https://www.controleverything.com/content/Light?sku=TSL2561_I2CS#tabs-0-product_tabset-2 - -from OmegaExpansion import onionI2C -import time - -# Get I2C bus -i2c = onionI2C.OnionI2C() - -# TSL2561 address, 0x39(57) -# Select control register, 0x00(00) with command register, 0x80(128) -# 0x03(03) Power ON mode -i2c.writeByte(0x39, 0x00 | 0x80, 0x03) -# TSL2561 address, 0x39(57) -# Select timing register, 0x01(01) with command register, 0x80(128) -# 0x02(02) Nominal integration time = 402ms -i2c.writeByte(0x39, 0x01 | 0x80, 0x02) - -time.sleep(0.5) - -# Read data back from 0x0C(12) with command register, 0x80(128), 2 bytes -# ch0 LSB, ch0 MSB -data = i2c.readBytes(0x39, 0x0C | 0x80, 2) - -# Read data back from 0x0E(14) with command register, 0x80(128), 2 bytes -# ch1 LSB, ch1 MSB -data1 = i2c.readBytes(0x39, 0x0E | 0x80, 2) - -# Convert the data -ch0 = data[1] * 256 + data[0] -ch1 = data1[1] * 256 + data1[0] - -# Output data to screen -print "Full Spectrum(IR + Visible) :%d lux" %ch0 -print "Infrared Value :%d lux" %ch1 -print "Visible Value :%d lux" %(ch0 - ch1) diff --git a/openoceancamera/TSL2561/Python/TSL2561.py b/openoceancamera/TSL2561/Python/TSL2561.py deleted file mode 100755 index 9f82d9d..0000000 --- a/openoceancamera/TSL2561/Python/TSL2561.py +++ /dev/null @@ -1,43 +0,0 @@ -# Distributed with a free-will license. -# Use it any way you want, profit or free, provided it fits in the licenses of its associated works. -# TSL2561 -# This code is designed to work with the TSL2561_I2CS I2C Mini Module available from ControlEverything.com. -# https://www.controleverything.com/content/Light?sku=TSL2561_I2CS#tabs-0-product_tabset-2 - -import smbus -import time - -# Get I2C bus -bus = smbus.SMBus(1) -try: - # TSL2561 address, 0x39(57) - # Select control register, 0x00(00) with command register, 0x80(128) - # 0x03(03) Power ON mode - bus.write_byte_data(0x39, 0x00 | 0x80, 0x03) - # TSL2561 address, 0x39(57) - # Select timing register, 0x01(01) with command register, 0x80(128) - # 0x02(02) Nominal integration time = 402ms - bus.write_byte_data(0x39, 0x01 | 0x80, 0x02) - - time.sleep(0.5) - - # Read data back from 0x0C(12) with command register, 0x80(128), 2 bytes - # ch0 LSB, ch0 MSB - data = bus.read_i2c_block_data(0x39, 0x0C | 0x80, 2) - - # Read data back from 0x0E(14) with command register, 0x80(128), 2 bytes - # ch1 LSB, ch1 MSB - data1 = bus.read_i2c_block_data(0x39, 0x0E | 0x80, 2) - - # Convert the data - ch0 = data[1] * 256 + data[0] - ch1 = data1[1] * 256 + data1[0] - - # Output data to screen - #print "Full Spectrum(IR + Visible) :%d lux" %ch0 - print ch1 - #print "Infrared Value :%d lux" %ch1 - #print "Visible Value :%d lux" %(ch0 - ch1) - -except: - print "TSL2561 not connected" diff --git a/openoceancamera/TSL2561/README.md b/openoceancamera/TSL2561/README.md deleted file mode 100755 index b91645c..0000000 --- a/openoceancamera/TSL2561/README.md +++ /dev/null @@ -1,84 +0,0 @@ -[![TSL2561](TSL2561_I2CS.png)](https://www.controleverything.com/content/Light?sku=TSL2561_I2CS) -# TSL2561 -TSL2561 Light-to-Digital Converter - -The TSL2561 is a light-to-digital converter that transforms light intensity to a digital signal output. - -This Device is available from ControlEverything.com [SKU: TSL2561_I2CS] - -https://www.controleverything.com/content/Light?sku=TSL2561_I2CS - -This Sample code can be used with Raspberry Pi, Arduino, Beaglebone Black and Onion Omega. - -## Java -Download and install pi4j library on Raspberry pi. Steps to install pi4j are provided at: - -http://pi4j.com/install.html - -Download (or git pull) the code in pi. - -Compile the java program. -```cpp -$> pi4j TSL2561.java -``` - -Run the java program. -```cpp -$> pi4j TSL2561 -``` - -## Python -Download and install smbus library on Raspberry pi. Steps to install smbus are provided at: - -https://pypi.python.org/pypi/smbus-cffi/0.5.1 - -Download (or git pull) the code in pi. Run the program. - -```cpp -$> python TSL2561.py -``` - -## Arduino -Download and install Arduino Software (IDE) on your machine. Steps to install Arduino are provided at: - -https://www.arduino.cc/en/Main/Software - -Download (or git pull) the code and double click the file to run the program. - -Compile and upload the code on Arduino IDE and see the output on Serial Monitor. - - -## C - -Download (or git pull) the code in Beaglebone Black. - -Compile the c program. -```cpp -$>gcc TSL2561.c -o TSL2561 -``` -Run the c program. -```cpp -$>./TSL2561 -``` - -## Onion Omega - -Get Started and setting up the Onion Omega according to steps provided at : - -https://wiki.onion.io/Get-Started - -To install the Python module, run the following commands: -```cpp -opkg update -``` -```cpp -opkg install python-light pyOnionI2C -``` - -Download (or git pull) the code in Onion Omega. Run the program. - -```cpp -$> python TSL2561.py -``` - -#####The code output is the lux value of ambient light. diff --git a/openoceancamera/TSL2561/TSL2561_I2CS.png b/openoceancamera/TSL2561/TSL2561_I2CS.png deleted file mode 100755 index bcfe5ad5023f6c69f8f970fae2fdbf4653d8b187..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123405 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

0P>r*Xhz226&t6nCV$6d-oB> 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 Z-vL!7xFbsV_&fjr002ovPDHLkV1j& 5: - remove = ident - if remove: - del self.events[remove] - - def clear(self): - """Invoked from each client's thread after a frame was processed.""" - self.events[get_ident()][0].clear() - - -class BaseCamera(object): - thread = None # background thread that reads frames from camera - frame = None # current frame is stored here by background thread - last_access = 0 # time of last client access to the camera - event = CameraEvent() - - def __init__(self, time_duration=0): - """Start the background camera thread if it isn't running yet.""" - if BaseCamera.thread is None: - BaseCamera.last_access = time.time() - - # start background frame thread - BaseCamera.thread = threading.Thread(target=self._thread, args=(time_duration,)) - BaseCamera.thread.start() - - # wait until frames are available - while self.get_frame() is None: - time.sleep(0) - - def get_frame(self): - """Return the current camera frame.""" - BaseCamera.last_access = time.time() - - # wait for a signal from the camera thread - BaseCamera.event.wait() - BaseCamera.event.clear() - - return BaseCamera.frame - - @staticmethod - def frames(): - """"Generator that returns frames from the camera.""" - raise RuntimeError('Must be implemented by subclasses.') - - @classmethod - def _thread(cls, time_duration): - """Camera background thread.""" - print('Starting camera thread.') - frames_iterator = cls.frames() - timeout_start = time.time() - for frame in frames_iterator: - BaseCamera.frame = frame - BaseCamera.event.set() # send signal to clients - time.sleep(0) - - # if there hasn't been any clients asking for frames in - # the last 10 seconds then stop the thread - if time_duration > 0: - if time.time() > timeout_start + time_duration: - frames_iterator.close() - print("Finished streaming for given time", time_duration) - break - elif time.time() - BaseCamera.last_access > 10: - frames_iterator.close() - print('Stopping camera thread due to inactivity.') - break - BaseCamera.thread = None diff --git a/openoceancamera/camera/__init__.py b/openoceancamera/camera/__init__.py new file mode 100644 index 0000000..ba065d8 --- /dev/null +++ b/openoceancamera/camera/__init__.py @@ -0,0 +1,2 @@ +# from .capture import start_capture +from .camera_thread import camera_thread diff --git a/openoceancamera/camera/camera_thread.py b/openoceancamera/camera/camera_thread.py new file mode 100644 index 0000000..c2f1595 --- /dev/null +++ b/openoceancamera/camera/camera_thread.py @@ -0,0 +1,56 @@ +from Scheduler import Scheduler +from subsealight import PWM +import json +from .capture import start_capture +from .upload import start_upload +from datetime import datetime, timedelta +import os +from logger import logger + +def camera_thread(): + # load the schedule from the schedule json + camera_schedule = Scheduler() + PWM.switch_off() + + try: + with open("/home/pi/openoceancamera/schedule.json") as f: + data = json.load(f) + camera_schedule.load_scheduler_data(data) + except: + return + + logger.debug("In Camera thread") + while True: + # check if a schedule slot needs to run + slot_index = camera_schedule.should_start() + # if it needs to run, call the correct function to start the slot (photo/video) + if (slot_index >= 0): + slot = camera_schedule.get_slot(slot_index) + if(slot["upload"]): + start_upload(slot) + else: + start_capture(slot) + # else check when the next schedule is + next_slot = camera_schedule.next_future_timeslot() + slot_index = camera_schedule.should_start() + if slot_index == -1 and next_slot is not None: + # if the camera needs to shutdown, do wittypi stuff to shutdown the camera and set restart time and stop this loop + mins_to_next_slot = int(camera_schedule.time_to_nearest_schedule() / 60) + if mins_to_next_slot > 10: + shutdown_time = datetime.now() + timedelta(minutes=2) + shutdown_time = shutdown_time.strftime("%d %H:%M") + reboot_time = next_slot["start"] - timedelta(minutes=2) + reboot_time = reboot_time.strftime("%d %H:%M:%S") + + reboot_cmd = ( + 'sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 5 "' + reboot_time + '"' + ) + + os.system(reboot_cmd) + logger.info(f"The reboot time has been set to {reboot_time}") + shutdown_cmd = ( + 'sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 4 "' + shutdown_time + '"' + ) + os.system(shutdown_cmd) + logger.info(f"The camera will shut down at {shutdown_time}") + break diff --git a/openoceancamera/camera/capture.py b/openoceancamera/camera/capture.py new file mode 100644 index 0000000..2610967 --- /dev/null +++ b/openoceancamera/camera/capture.py @@ -0,0 +1,112 @@ +from picamera import PiCamera +from datetime import datetime +from time import sleep +import json +from constants import EXTERNAL_DRIVE +# from .sensors import readSensorData, writeSensorData +from sensors import Sensor +from logger import logger +from subsealight import PWM +from restart import reboot_camera +from .utils import get_camera_name +from wiper import run_wiper + +def capture_video(slot): + resolution = slot["resolution"] + framerate = slot["framerate"] + iso = slot["iso"] + exposure_mode = slot["exposure_mode"] + exposure_compensation = slot["exposure_compensation"] + light = slot["light"] + shutter_speed = slot["shutter_speed"] + try: + sensor = Sensor() + except Exception as err: + logger.error(err) + camera_name = get_camera_name() + wiper_status = slot["wiper"] + if wiper_status: + run_wiper(3) + try: + with PiCamera(resolution=resolution, framerate=framerate) as camera: + camera.iso = iso + camera.exposure_mode = exposure_mode + camera.exposure_compensation = exposure_compensation + camera.shutter_speed = shutter_speed + slot_name = f"{slot['start'].strftime('%Y-%m-%d_%H-%M-%S')}_{slot['stop'].strftime('%Y-%m-%d_%H-%M-%S')}.h264" + filename = f"{EXTERNAL_DRIVE}/{camera_name}_{slot_name}" + PWM.switch_on(light) + camera.start_recording(filename, format="h264") + current_time = datetime.now() + sensor.write_sensor_data() + sensor_data = sensor.get_sensor_data() + while current_time < slot["stop"]: + camera.annotate_text = f"{current_time.strftime('%Y-%m-%d %H:%M:%S')} @ {slot['framerate']} fps" + sensor.write_sensor_data() + sensor_data = sensor.get_sensor_data() + sleep(1) + current_time = datetime.now() + camera.stop_recording() + PWM.switch_off() + except Exception as err: + PWM.switch_off() + logger.error(err) + reboot_camera() + +def capture_images(slot): + try: + logger.debug("Going to set camera config") + resolution = slot["resolution"] + iso = slot["iso"] + exposure_mode = slot["exposure_mode"] + exposure_compensation = slot["exposure_compensation"] + light = slot["light"] + frequency = slot["frequency"] + shutter_speed = slot["shutter_speed"] + try: + sensor = Sensor() + except Exception as err: + logger.error(err) + camera_name = get_camera_name() + wiper_status = slot.get("wiper", False) + if wiper_status: + run_wiper(3) + logger.debug(f"Assigning camera config to {camera_name}") + try: + with PiCamera(resolution=resolution) as camera: + camera.iso = iso + camera.exposure_mode = exposure_mode + camera.exposure_compensation = exposure_compensation + camera.shutter_speed = shutter_speed + PWM.switch_on(light) + sensor.write_sensor_data() + sensor_data = sensor.get_sensor_data() + sensor_data["camera_name"] = camera_name + camera.exif_tags["IFDO.ImageDescription"] = json.dumps(sensor_data) + logger.debug("Entering continuous capture") + for f in camera.capture_continuous(f'{EXTERNAL_DRIVE}/{camera_name}_'+'img{timestamp:%Y-%m-%d-%H-%M-%S}.jpg', use_video_port=True): + PWM.switch_off() + currenttime = datetime.now() + if currenttime < slot["stop"]: + sleep(frequency-1) + sensor.write_sensor_data() + sensor_data = sensor.get_sensor_data() + sensor_data["camera_name"] = camera_name + camera.exif_tags["IFDO.ImageDescription"] = json.dumps(sensor_data) + else: + PWM.switch_off() + break + except Exception as err: + PWM.switch_off() + logger.error(err) + reboot_camera() + except Exception as err: + PWM.switch_off() + logger.error(err) + +def start_capture(slot): + logger.debug("Going to capture") + if slot["video"]: + capture_video(slot) + else: + capture_images(slot) diff --git a/openoceancamera/camera/upload.py b/openoceancamera/camera/upload.py new file mode 100644 index 0000000..2b4ff1b --- /dev/null +++ b/openoceancamera/camera/upload.py @@ -0,0 +1,31 @@ +import os +import json +from datetime import datetime, timedelta +from constants import EXTERNAL_DRIVE +from uploader import DropboxUploader +import logging + +logging.basicConfig(filename="system_logs.txt", format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) +logger = logging.getLogger('main') +logger.setLevel(logging.DEBUG) + + +if not os.path.exists(os.path.join(EXTERNAL_DRIVE, "uploads.txt")): + with open(os.path.join(EXTERNAL_DRIVE, "uploads.txt"), 'w') as uploads: + pass + +def start_upload(slot): + logger.info("Starting upload slot") + upload_handler = DropboxUploader() + uploaded_files = [] + with open(os.path.join(EXTERNAL_DRIVE, "uploads.txt"), 'r') as uploads: + for filename in uploads: + uploaded_files.append(filename) + for root, dirs, files in os.walk(EXTERNAL_DRIVE): + for f in filter(lambda x: uploaded_files.index(x), sorted(files, reverse=True)): + if slot["stop"] > datetime.now(): + logger.info(f"Uploading {f}") + upload_handler.upload_file(os.path.join(root, f)) + logger.info(f"Uploaded {f}") + else: + break diff --git a/openoceancamera/camera/utils.py b/openoceancamera/camera/utils.py new file mode 100644 index 0000000..13a7757 --- /dev/null +++ b/openoceancamera/camera/utils.py @@ -0,0 +1,10 @@ +from logger import logger + +def get_camera_name(): + try: + with open("/home/pi/openoceancamera/camera_name.txt") as f: + name = f.read() + return name + except Exception as err: + logger.error(f"Error: {str(err)}") + return "OOCAM" \ No newline at end of file diff --git a/openoceancamera/camera_pi.py b/openoceancamera/camera_pi.py deleted file mode 100755 index 4cdcef8..0000000 --- a/openoceancamera/camera_pi.py +++ /dev/null @@ -1,23 +0,0 @@ -import io -import time -import picamera -from base_camera import BaseCamera - - -class Camera_Pi(BaseCamera): - def __init__(self, time_duration=0): - super().__init__(time_duration) - @staticmethod - def frames(): - with picamera.PiCamera() as camera: - - time.sleep(2) - - stream = io.BytesIO() - for _ in camera.capture_continuous(stream, 'jpeg', use_video_port=True): - - stream.seek(0) - yield stream.read() - - stream.seek(0) - stream.truncate() diff --git a/openoceancamera/constants.py b/openoceancamera/constants.py new file mode 100644 index 0000000..fed3745 --- /dev/null +++ b/openoceancamera/constants.py @@ -0,0 +1,7 @@ +import os + +SCHEDULE_FILE_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "schedule.json" +) +EXTERNAL_DRIVE = "/media/pi/OPENOCEANCA" +LOG_FILE = f"{EXTERNAL_DRIVE}/log.txt" diff --git a/openoceancamera/logger.py b/openoceancamera/logger.py new file mode 100644 index 0000000..ab0b16b --- /dev/null +++ b/openoceancamera/logger.py @@ -0,0 +1,5 @@ +import logging + +logging.basicConfig(filename="system_logs.txt", format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) +logger = logging.getLogger('main') +logger.setLevel(logging.DEBUG) diff --git a/openoceancamera/main.py b/openoceancamera/main.py index b91d46e..77eaedb 100755 --- a/openoceancamera/main.py +++ b/openoceancamera/main.py @@ -1,628 +1,28 @@ import os import io import sys -import subprocess -import smbus import threading import base64 -import logging import json -from uuid import uuid1 from time import sleep, time, gmtime, strftime -from os import path from datetime import datetime, timedelta -from flask import Flask, request, send_file, jsonify, Response -from flask_cors import CORS -from flask_socketio import SocketIO, send, emit from picamera import PiCamera - -from StreamingOutput import StreamingOutput +from logger import logger +from camera import camera_thread +from sensors import Sensor from Scheduler import Scheduler -from camera_pi import Camera_Pi -from subsealight import PWM + +from appserver import start_api_server import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) -GPIO.setup(17, GPIO.OUT) - - -camera_name = "OpenOceanCamera" -camera = None -logging.basicConfig(filename="system_logs.txt", format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) -logger = logging.getLogger('main') -logger.setLevel(logging.DEBUG) - -try: - with open("/home/pi/openoceancamera/camera_name.txt", "r") as camera_name_file: - camera_name = camera_name_file.read() -except Exception as err: - with open("/home/pi/openoceancamera/camera_name.txt", "w") as camera_name_file: - camera_name_file.write(camera_name) - -logger.info(f"Loaded camera name: {camera_name}") - -sys.path.append("/usr/lib/python3.5/") -sys.path.append("/usr/lib/python3/") -cmdoff = "sudo ifconfig wlan0 down" -cmdoff1 = "sudo service dnsmasq stop" - -try: - from Camera import Camera -except: - logger.error("Could not initialise camera") - -threads = [] - -app = Flask("OpenOceanCam") -app.config["CORS_HEADERS"] = "Content-Type" -CORS(app) -socketio = SocketIO(app, cors_allowed_origins="*", ping_timeout=5, ping_interval=5) - -external_drive = "/media/pi/OPENOCEANCA" -camera_config = [] -thread_active = False - -last_file_name = "" -stream_duration = 0 -lightSensor = -1.0 -temperatureSensor = -1.0 -pressureSensor = -1.0 -mstemperatureSensor = -1.0 -depthSensor = -1.0 - -def readSensorData(): - return { - "luminosity": lightSensor, - "temp": temperatureSensor, - "pressure": pressureSensor, - "mstemp": mstemperatureSensor, - "depth": depthSensor, - } - -def writeSensorData(sensor_data): - log_filename = f"{external_drive}/log.txt" - file_mode = None - if os.path.exists(log_filename): - file_mode = "a" - else: - file_mode = "w" - try: - with open(log_filename, file_mode) as f: - f.write( - json.dumps( - { - "timestamp": datetime.now().strftime( - "%m/%d/%Y, %H:%M:%S" - ), - "luminosity": sensor_data["luminosity"], - "temp": sensor_data["temp"], - "pressure": sensor_data["pressure"], - "mstemp": sensor_data["mstemp"], - "depth": sensor_data["depth"], - } - ) - ) - f.write("\n") - except: - logging.warn("Sensor data file did not exist. Making it now") - with open(log_filename, "w"): - pass - -def start_capture(video, slot): - global external_drive, last_file_name - global camera - filename = "" - print("start capture") - logger.info("Starting to capture") - try: - camera = Camera() - logger.info("Camera object initialised") - camera.set_iso(slot.get("iso", 0)) - camera.set_camera_resolution((int(slot["resolution"]["x"]), int(slot["resolution"]["y"]))) - camera.set_camera_exposure_mode(slot.get("exposure_mode", "auto")) - camera.set_camera_exposure_compensation(int(slot.get("exposure_compensation", 0))) - camera.set_shutter_speed(slot.get("shutter_speed", 0)) - logger.info(f"Finised setting up the camera for the slot {slot}") - try: - if not video: - camera.set_capture_frequency(slot["frequency"]) - filename = external_drive + "/" + datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".jpg" - last_file_name = filename - print("Capturing:") - #Don't do capture inside camera class - - PWM.switch_on(slot["light"]) - try: - logger.info("Going to capture continuous capture mode") - sensor_data = readSensorData() - writeSensorData(sensor_data) - sensor_data["camera_name"] = camera_name - camera.camera.exif_tags["IFD0.ImageDescription"] = json.dumps(sensor_data) - for f in camera.camera.capture_continuous(f"/media/pi/OPENOCEANCA/{camera_name}_" + 'img{timestamp:%Y-%m-%d-%H-%M-%S}.jpg', use_video_port=True): - if thread_active: - PWM.switch_off() - sleep(camera.frequency-1) - PWM.switch_on(slot["light"]) - currenttime=datetime.now() - if currenttime= 0: # if slot open - sensor_data = readSensorData() - writeSensorData(sensor_data) - light_mode = data[slot]["light"] - if not data[slot]["video"]: # slot for photo - start_capture(False, data[slot]) - else: - start_capture(True, data[slot]) - switch_flag = 0 - - else: - if switch_flag == 0: - logging.info("Stop: " + str(datetime.now())) - switch_flag = 1 - - next_slot = my_schedule.next_future_timeslot() - slot = my_schedule.should_start() - if next_slot is not None: - mins_to_next_slot = int(my_schedule.time_to_nearest_schedule() / 60) - print(f"We have {mins_to_next_slot} mins to next slot") - if (mins_to_next_slot > 10) and slot == -1: - logger.info("Camera is going to prepare to go to sleep") - sleeptime = datetime.now() + timedelta(minutes=5) - next_reboot = next_slot["start"] - timedelta(minutes=2) - sleeptime = sleeptime.strftime("%d %H:%M") - print(f"I will wake up at {next_reboot}") - logger.info(f"The reboot time has been set to {next_reboot}") - next_reboot = next_reboot.strftime("%d %H:%M:%S") - print(next_reboot) - startup_cmd = ( - 'sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 5 "' + next_reboot + '"' - ) - os.system(startup_cmd) - logger.info(startup_cmd) - print( - "raspberry pi is going to sleep now in 5 min, do not disturb" - ) - shutdown_cmd = ( - 'sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 4 "' + sleeptime + '"' - ) - os.system(shutdown_cmd) - logger.info(shutdown_cmd) - thread_active = False - break - except Exception as e: - if camera is not None: - camera.do_close() - print(e) - PWM.switch_off() - logger.error(e) - #threading.Thread(target=restart_code).start() - -def update_config(): - pass - -def restart_code(): - sleep(5) - os.system("sudo reboot") - -def reboot_camera(): - sleep(300) - os.system("sudo reboot") - -@app.route("/setCameraName", methods=["POST"]) -def set_camera_name(): - global camera_name - if request.method == 'POST': - camera_name = request.get_json()["name"] - with open("/home/pi/openoceancamera/camera_name.txt", "w") as camera_name_file: - camera_name_file.write(camera_name) - return camera_name - -@app.route("/syncTime", methods=["GET", "POST"]) -def sync_time(): - global thread_active - if request.method == "POST": - try: - data = request.get_json() - date_input = data["date"] - timezone = data["timezone"] - clear_cmd = ('sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 10 6') - os.system(clear_cmd) - os.system(f"sudo timedatectl set-timezone {timezone}") - os.system(f"sudo date -s '{date_input}'") - # Save the system time to RTC - - os.system("sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 1") - os.system("sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 2") - threading.Thread(target=restart_code).start() - return "OK", 200 - except Exception as err: - return str(err), 400 - -@app.route("/clearSchedule", methods=["GET"]) -def clearSchedule(): - try: - with open("/home/pi/openoceancamera/schedule.json", "w") as outfile: - json.dump(json.loads("[]"), outfile) - clear_cmd = ('sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 10 6') - os.system(clear_cmd) - threading.Thread(target=restart_code).start() - return "OK", 200 - except Exception as err: - return str(err), 400 - -@app.route("/setSchedule", methods=["POST", "GET"]) -def app_connect(): - global external_drive - global camera_config - global thread_active - if request.method == "POST": - thread_active = False - print(request.get_json()) - camera_config = request.get_json() - try: - with open("/home/pi/openoceancamera/schedule.json", "w") as outfile: - json.dump(camera_config, outfile) - date_input = camera_config[0]["date"] - timezone = camera_config[0]["timezone"] - try: - clear_cmd = ('sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 10 6') - os.system(clear_cmd) - os.system(f"sudo timedatectl set-timezone {timezone}") - print(timezone) - print(date_input) - # Sets the system time to the user's phone time - os.system(f"sudo date -s '{date_input}'") - # Save the system time to RTC - - os.system("sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 1") - os.system("sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 2") - pathv = path.exists(external_drive) - threading.Thread(target=restart_code).start() - if pathv: - thread_active = True - return "OK", 200 - else: - logger.error("Error: USB storage device not mounted") - return "Error: USB storage device not mounted", 400 - except Exception as err: - logger.error(err) - return str(err), 400 - except Exception as err: - logger.error(err) - return "Schedule could not be set", 400 - -@app.route("/viewConfig", methods=["GET"]) -def returnConfig(): - if request.method == "GET": - try: - response = { - "local_time": datetime.now().strftime("%d-%B-%Y %H:%M:%S"), - "local_timezone": str(datetime.utcnow().astimezone().tzinfo) , - "config": json.dumps(camera_config), - "camera_name": camera_name - } - return response, 200 - except Exception as err: - logger.error(err) - return str(err), 400 - -@app.route("/getLogs", methods=["GET"]) -def getLogs(): - if request.method == "GET": - try: - lines = int(request.args.get("lines")) + 1 - with open("/home/pi/system_logs.txt", 'r') as f: - data = f.read() - logs = data.split('\n') - result = "\n" - return result.join(logs[-lines:]) - except Exception as err: - logger.error(err) - return str(err), 400 - -@app.route("/clearLogs", methods=["GET"]) -def clearLogs(): - if request.method == "GET": - try: - open("/home/pi/system_logs.txt", 'w').close() - return "OK", 200 - except Exception as err: - return str(err), 400 - -@app.route("/testPhoto", methods=["POST", "GET"]) -def sendTestPic(): - camera = Camera() - if request.method == "POST": - try: - data = request.get_json(force=True) - print(data) - PWM.switch_on(data[0]["light"]) - camera.set_iso(data[0]["iso"]) - camera.set_shutter_speed(data[0]["shutter_speed"]) - camera.set_camera_exposure_mode(data[0].get("exposure_mode", 'auto')) - camera.set_camera_exposure_compensation(int(data[0].get("exposure_compensation", 0))) - if data[0].get("resolution", None): - camera.set_camera_resolution((int(data[0]["resolution"].get("x", 1920)), int(data[0]["resolution"].get("y", 1080)))) - - camera.do_capture("/home/pi/openoceancamera/test.jpg") - with open("/home/pi/openoceancamera/test.jpg", "rb") as image: - img_base64 = base64.b64encode(image.read()) - camera.do_close() - sensor_data = readSensorData() - response = { - "image": img_base64.decode("utf-8"), - "sensors": json.dumps(sensor_data), - } - PWM.switch_off() - return jsonify(response), 200 - except Exception as err: - logger.error(err) - camera.do_close() - PWM.switch_off() - return str(err), 400 - -@app.route("/testPhotoMem", methods=["POST", "GET"]) -def sendTestPicMem(): - if request.method == "POST": - data = request.get_json(force=True) - PWM.switch_on(data[0]["light"]) - flag = "SUCCESS" - pathv = path.exists(external_drive) - if pathv: - camera = None - try: - camera = Camera() - camera.set_iso(data[0]["iso"]) - camera.set_shutter_speed(data[0]["shutter_speed"]) - filename1 = external_drive + "/" + str(uuid1()) + ".jpg" - camera.do_capture(filename=filename1) - print("Written") - camera.do_close() - - camera = Camera() - camera.set_camera_resolution((1920, 1080)) - camera.set_camera_frame_rate(30) - filename2 = external_drive + "/" + str(uuid1()) + ".h264" - camera.do_record(filename=filename2) - print("Started recording") - sleep(3) - camera.do_close() - except Exception as err: - camera.do_close() - return "Please check if the USB storage device is connected properly", 400 - else: - return "USB storage device with name OOCAM required", 400 - PWM.switch_off() - sensor_data = readSensorData() - response = { - "sensors": json.dumps(sensor_data), - } - return jsonify(response), 200 - - -def gen(camera): - while True: - frame = camera.get_frame() - yield (b'--frame\r\n' - b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') - - -@app.route("/stream", methods=["GET", "POST"]) -def get_video(): - global stream_duration - if request.method == "GET": - return Response(gen(Camera_Pi(stream_duration)),mimetype='multipart/x-mixed-replace; boundary=frame' ) - if request.method == "POST": - time_duration = request.get_json()["time_duration"] - stream_duration = int(time_duration) - return "OK" - -def pull_updated_code(): - os.system("bash /home/pi/update.sh >> /home/pi/system_logs.txt 2>&1") - -@app.route("/update", methods=["GET","POST"]) -def update_code(): - if request.method == "POST": - try: - os.system("sudo sh /home/pi/openoceancamera/wittypi/wittycam.sh 10 6") - data = request.get_json() - ssid = data["ssid"] - psk = data["psk"] - os.system(f"sudo bash /home/pi/connect_to_wifi.sh {ssid} {psk}") - threading.Thread(target=pull_updated_code).start() - return "OK" , 200 - except Exception as err: - logger.error(f"Error: {err}") - return str(err) , 400 - -livestream_running = False -run_livestream = False - -@socketio.on("connect") -def on_connect(): - logger.debug("new device connected") - -@socketio.on("disconnect") -def on_disconnect(): - global run_livestream - logger.debug("device disconnected") - run_livestream = False - -@socketio.on("livestream") -def livestream(): - global livestream_running - global run_livestream - run_livestream = True - if livestream_running: - return - try: - with PiCamera(resolution=(640, 480)) as camera: - output = StreamingOutput() - logger.debug("Starting livestream") - camera.start_recording(output, format='mjpeg') - run_livestream = True - while run_livestream: - socketio.sleep(0) - if output.frame: - emit("livestream_data", output.frame) - except Exception as err: - emit("livestream_data", json.dumps({"error": err})) - logger.error(err) - livestream_running = False - logger.debug("Closed livestream") - -@socketio.on("close_livestream") -def close_livestream(): - global run_livestream - run_livestream = False - logger.debug("Closed livestream") - -def start_api_server(): - socketio.run(app, host="0.0.0.0", port=8000) - -def start_sensor_reading(): - global lightSensor, temperatureSensor, pressureSensor, mstemperatureSensor, depthSensor - while True: - try: - lightSensor = float( - str( - subprocess.check_output( - "python /home/pi/openoceancamera/TSL2561/Python/TSL2561.py", shell=True, text=True - ) - ) - ) - except Exception as e: - lightSensor = -1.0 - - try: - temperatureSensor = float( - str( - subprocess.check_output( - "python /home/pi/openoceancamera/tsys01-python/example.py", shell=True, text=True - ) - ) - ) - except Exception as e: - temperatureSensor = -1.0 - - try: - pressureSensorReadings = subprocess.check_output( - "python /home/pi/openoceancamera/ms5837-python/example.py", shell=True, text=True - ) - - pressureSensorReadings = pressureSensorReadings.split() - - pressureSensor, mstemperatureSensor, depthSensor = ( - float(pressureSensorReadings[0]), - float(pressureSensorReadings[1]), - float(pressureSensorReadings[2]), - ) - except Exception as e: - pressureSensor = -1.0 - mstemperatureSensor = -1.0 - depthSensor = -1.0 - if __name__ == "__main__": - # if len(sys.argv) < 2: - # print("Usage: python main.py ") - # exit(0) - try: - with open("/home/pi/openoceancamera/schedule.json") as f: - camera_config = json.load(f) - thread_active = True - logger.info("schedule opened, should start new thread") - except IOError as err: - logger.info(err) - finally: - api_thread = threading.Thread(target=start_api_server) - sensor_thread = threading.Thread(target=start_sensor_reading) - main_thread = threading.Thread(target=main) - api_thread.start() - main_thread.start() - sensor_thread.start() - logger.info("Started all threads") - sensor_thread.join() - main_thread.join() - api_thread.join() - logger.info("Program is shutting down") + api_thread = threading.Thread(target=start_api_server) + main_thread = threading.Thread(target=camera_thread) + api_thread.start() + main_thread.start() + logger.info("Started all threads") + main_thread.join() + api_thread.join() + logger.info("Program is shutting down") diff --git a/openoceancamera/ms5837-python/LICENSE b/openoceancamera/ms5837-python/LICENSE deleted file mode 100755 index ef810cd..0000000 --- a/openoceancamera/ms5837-python/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Blue Robotics - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/openoceancamera/ms5837-python/README.md b/openoceancamera/ms5837-python/README.md deleted file mode 100755 index 7b2ab4a..0000000 --- a/openoceancamera/ms5837-python/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# ms5837-python - -A python module to interface with MS5837-30BA and MS5837-02BA waterproof pressure and temperature sensors. Tested on Raspberry Pi 3 with Raspbian. - -# Installation - -The python SMBus library must be installed. - - sudo apt-get install python-smbus - -Download this repository by clicking on the download button in this webpage, or using git: - -```sh -git clone https://github.com/bluerobotics/ms5837-python -``` - -If you would like to try the example, move to the directory where you downloaded the repository, and run `python example.py`. To use the library, copy the `ms5837.py` file to your project/program directory and use this import statement in your program: `import ms5837`. - -### Raspberry Pi - -If you are using a Raspberry Pi, the i2c interface must be enabled. Run `sudo raspi-config`, and choose to enable the i2c interface in the `interfacing options`. - -# Usage - - import ms5837 - -ms5837 provides a generic MS5837 class for use with different models - - MS5837(model=ms5837.MODEL_30BA, bus=1) - -These model-specific classes inherit from MS5837 and don't have any unique members - - MS5837_30BA(bus=1) - MS5837_02BA(bus=1) - -An MS5837 object can be constructed by specifiying the model and the bus - - sensor = ms5837.MS5837() # Use defaults (MS5837-30BA device on I2C bus 1) - sensor = ms5837.MS5837(ms5837.MODEL_02BA, 0) # Specify MS5837-02BA device on I2C bus 0 - -Or by creating a model-specific object - - sensor = ms5837.MS5837_30BA() # Use default I2C bus (1) - sensor = ms5837.MS5837_30BA(0) # Specify I2C bus 0 - -### init() - -Initialize the sensor. This needs to be called before using any other methods. - - sensor.init() - -Returns true if the sensor was successfully initialized, false otherwise. - -### read(oversampling=OSR_8192) - -Read the sensor and update the pressure and temperature. The sensor will be read with the supplied oversampling setting. Greater oversampling increases resolution, but takes longer and increases current consumption. - - sensor.read(ms5837.OSR_256) - -Valid arguments are: - - ms5837.OSR_256 - ms5837.OSR_512 - ms5837.OSR_1024 - ms5837.OSR_2048 - ms5837.OSR_4096 - ms5837.OSR_8192 - -Returns True if read was successful, False otherwise. - -### setFluidDensity(density) - -Sets the density in (kg/m^3) of the fluid for depth measurements. The default fluid density is ms5837.DENISTY_FRESHWATER. - - sensor.setFluidDensity(1000) # Set fluid density to 1000 kg/m^3 - sensor.setFluidDensity(ms5837.DENSITY_SALTWATER) # Use predefined saltwater density - -Some convenient constants are: - - ms5837.DENSITY_FRESHWATER = 997 - ms5837.DENSITY_SALTWATER = 1029 - -### pressure(conversion=UNITS_mbar) - -Get the most recent pressure measurement. - - sensor.pressure() # Get pressure in default units (millibar) - sensor.pressure(ms5837.UNITS_atm) # Get pressure in atmospheres - sensor.pressure(ms5837.UNITS_kPa) # Get pressure in kilopascal - -Some convenient constants are: - - ms5837.UNITS_Pa = 100.0 - ms5837.UNITS_hPa = 1.0 - ms5837.UNITS_kPa = 0.1 - ms5837.UNITS_mbar = 1.0 - ms5837.UNITS_bar = 0.001 - ms5837.UNITS_atm = 0.000986923 - ms5837.UNITS_Torr = 0.750062 - ms5837.UNITS_psi = 0.014503773773022 - -Returns the most recent pressure in millibar * conversion. Call read() to update. - -### temperature(conversion=UNITS_Centigrade) - -Get the most recent temperature measurement. - - sensor.temperature() # Get temperature in default units (Centigrade) - sensor.temperature(ms5837.UNITS_Farenheit) # Get temperature in Farenheit - -Valid arguments are: - - ms5837.UNITS_Centigrade - ms5837.UNITS_Farenheit - ms5837.UNITS_Kelvin - -Returns the most recent temperature in the requested units, or temperature in degrees Centigrade if invalid units specified. Call read() to update. - -### depth() - -Get the most recent depth measurement in meters. - - sensor.depth() - -Returns the most recent depth in meters using the fluid density (kg/m^3) configured by setFluidDensity(). Call read() to update. - -### altitude() - -Get the most recent altitude measurement relative to Mean Sea Level pressure in meters. - - sensor.altitude() - -Returns the most recent altitude in meters relative to MSL pressure using the density of air at MSL. Call read() to update. - diff --git a/openoceancamera/ms5837-python/example.py b/openoceancamera/ms5837-python/example.py deleted file mode 100755 index 36b1960..0000000 --- a/openoceancamera/ms5837-python/example.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/python -import ms5837 -import time - -sensor = ms5837.MS5837_30BA() # Default I2C bus is 1 (Raspberry Pi 3) -# sensor = ms5837.MS5837_30BA(0) # Specify I2C bus -# sensor = ms5837.MS5837_02BA() -# sensor = ms5837.MS5837_02BA(0) -# sensor = ms5837.MS5837(model=ms5837.MS5837_MODEL_30BA, bus=0) # Specify model and bus -try: - # We must initialize the sensor before reading it - if not sensor.init(): - print "Sensor could not be initialized" - exit(1) -except: - exit(1) - -# We have to read values from sensor to update pressure and temperature -if not sensor.read(): - print "Sensor read failed!" - exit(1) - -# print("Pressure: %.2f atm %.2f Torr %.2f psi") % ( -# sensor.pressure(ms5837.UNITS_atm), -# sensor.pressure(ms5837.UNITS_Torr), -# sensor.pressure(ms5837.UNITS_psi)) - -# print("Temperature: %.2f C %.2f F %.2f K") % ( -# sensor.temperature(ms5837.UNITS_Centigrade), -# sensor.temperature(ms5837.UNITS_Farenheit), -# sensor.temperature(ms5837.UNITS_Kelvin)) - -# freshwaterDepth = sensor.depth() # default is freshwater -sensor.setFluidDensity(ms5837.DENSITY_SALTWATER) -# saltwaterDepth = sensor.depth() # No nead to read() again -# sensor.setFluidDensity(1000) # kg/m^3 -# print("Depth: %.3f m (freshwater) %.3f m (saltwater)") % (freshwaterDepth, saltwaterDepth) - -# fluidDensity doesn't matter for altitude() (always MSL air density) -# print("MSL Relative Altitude: %.2f m") % sensor.altitude() # relative to Mean Sea Level pressure in air - -# time.sleep(5) - -# Spew readings -# while True: -if sensor.read(): - # print("P: %0.1f mbar %0.3f psi") % ( - # print("P: %0.1f mbar %0.3f psi\tT: %0.2f C %0.2f F") % ( - print "{} {} {}".format( - sensor.pressure(), sensor.temperature(), sensor.depth() - ) # Default is mbar (no arguments) -# sensor.pressure(ms5837.UNITS_psi)) # Request psi -# sensor.temperature(), # Default is degrees C (no arguments) -# sensor.temperature(ms5837.UNITS_Farenheit)) # Request Farenheit -else: - print ("Sensor read failed!") - exit(1) diff --git a/openoceancamera/requirements.txt b/openoceancamera/requirements.txt index 9235b3b..c7bd942 100755 --- a/openoceancamera/requirements.txt +++ b/openoceancamera/requirements.txt @@ -1,6 +1,8 @@ asn1crypto==0.24.0 astroid==2.2.5 attrs==20.3.0 +Adafruit-GPIO==1.0.3 +Adafruit-PureIO==1.1.5 certifi==2018.8.24 chardet==3.0.4 Click==7.0 @@ -37,6 +39,7 @@ six==1.12.0 smbus==1.1.post2 ssh-import-id==5.7 toml==0.10.2 +tsl2561==3.4.0 typed-ast==1.4.0 typing-extensions==3.7.4.3 urllib3==1.24.1 diff --git a/openoceancamera/restart.py b/openoceancamera/restart.py new file mode 100644 index 0000000..f9baad4 --- /dev/null +++ b/openoceancamera/restart.py @@ -0,0 +1,10 @@ +import os +from time import sleep + +def restart_code(): + sleep(5) + os.system("sudo reboot") + +def reboot_camera(): + sleep(300) + os.system("sudo reboot") \ No newline at end of file diff --git a/openoceancamera/sensors/__init__.py b/openoceancamera/sensors/__init__.py new file mode 100644 index 0000000..9034b07 --- /dev/null +++ b/openoceancamera/sensors/__init__.py @@ -0,0 +1,4 @@ +from .sensors import PressureSensor +from .sensors import LuminositySensor +from .sensors import TemperatureSensor +from .sensors import Sensor diff --git a/openoceancamera/sensors/gps.py b/openoceancamera/sensors/gps.py new file mode 100644 index 0000000..e7ed404 --- /dev/null +++ b/openoceancamera/sensors/gps.py @@ -0,0 +1,31 @@ +import time +import threading +import board +import busio +import adafruit_gps +from adafruit_gps import GPS_GtopI2C + +class GPS(GPS_GtopI2C): + def __init__(self): + super().__init__(board.I2C()) + self.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") + self.send_command(b"PMTK220,1000") + threading.Timer(1, self.update).start() + threading.Timer(5, self.request_firmware).start() + + def request_firmware(self): + self.send_command(b"PMTK605") + threading.Timer(5, self.request_firmware).start() + + def update(self): + super().update() + threading.Timer(1, self.update).start() + + +if __name__ == "__main__": + gps = GPS() + while not gps.has_fix: + continue + print("GPS fixed") + while gps.has_fix: + print(f"{gps.latitude}, {gps.longitude}") \ No newline at end of file diff --git a/openoceancamera/ms5837-python/ms5837.py b/openoceancamera/sensors/ms5837.py old mode 100755 new mode 100644 similarity index 50% rename from openoceancamera/ms5837-python/ms5837.py rename to openoceancamera/sensors/ms5837.py index 7d63e7e..7263daa --- a/openoceancamera/ms5837-python/ms5837.py +++ b/openoceancamera/sensors/ms5837.py @@ -1,8 +1,4 @@ -try: - import smbus -except: - print('Try sudo apt-get install python-smbus') - +import smbus2 as smbus from time import sleep # Models @@ -10,8 +6,8 @@ MODEL_30BA = 1 # Oversampling options -OSR_256 = 0 -OSR_512 = 1 +OSR_256 = 0 +OSR_512 = 1 OSR_1024 = 2 OSR_2048 = 3 OSR_4096 = 4 @@ -22,212 +18,235 @@ DENSITY_SALTWATER = 1029 # Conversion factors (from native unit, mbar) -UNITS_Pa = 100.0 -UNITS_hPa = 1.0 -UNITS_kPa = 0.1 -UNITS_mbar = 1.0 -UNITS_bar = 0.001 -UNITS_atm = 0.000986923 -UNITS_Torr = 0.750062 -UNITS_psi = 0.014503773773022 +UNITS_Pa = 100.0 +UNITS_hPa = 1.0 +UNITS_kPa = 0.1 +UNITS_mbar = 1.0 +UNITS_bar = 0.001 +UNITS_atm = 0.000986923 +UNITS_Torr = 0.750062 +UNITS_psi = 0.014503773773022 # Valid units UNITS_Centigrade = 1 -UNITS_Farenheit = 2 -UNITS_Kelvin = 3 +UNITS_Farenheit = 2 +UNITS_Kelvin = 3 + - class MS5837(object): - + # Registers - _MS5837_ADDR = 0x76 - _MS5837_RESET = 0x1E - _MS5837_ADC_READ = 0x00 - _MS5837_PROM_READ = 0xA0 - _MS5837_CONVERT_D1_256 = 0x40 - _MS5837_CONVERT_D2_256 = 0x50 - + _MS5837_ADDR = 0x76 + _MS5837_RESET = 0x1E + _MS5837_ADC_READ = 0x00 + _MS5837_PROM_READ = 0xA0 + _MS5837_CONVERT_D1_256 = 0x40 + _MS5837_CONVERT_D2_256 = 0x50 + def __init__(self, model=MODEL_30BA, bus=1): self._model = model - + try: self._bus = smbus.SMBus(bus) except: - print("Bus %d is not available.") % bus + print(("Bus %d is not available.") % bus) print("Available busses are listed as /dev/i2c*") self._bus = None - + self._fluidDensity = DENSITY_FRESHWATER self._pressure = 0 self._temperature = 0 self._D1 = 0 self._D2 = 0 - + def init(self): if self._bus is None: "No bus!" return False - + self._bus.write_byte(self._MS5837_ADDR, self._MS5837_RESET) - + # Wait for reset to complete sleep(0.01) - + self._C = [] - + # Read calibration values and CRC for i in range(7): - c = self._bus.read_word_data(self._MS5837_ADDR, self._MS5837_PROM_READ + 2*i) - c = ((c & 0xFF) << 8) | (c >> 8) # SMBus is little-endian for word transfers, we need to swap MSB and LSB + c = self._bus.read_word_data( + self._MS5837_ADDR, self._MS5837_PROM_READ + 2 * i + ) + c = ((c & 0xFF) << 8) | ( + c >> 8 + ) # SMBus is little-endian for word transfers, we need to swap MSB and LSB self._C.append(c) - + crc = (self._C[0] & 0xF000) >> 12 if crc != self._crc4(self._C): print("PROM read error, CRC failed!") return False - + return True - + def read(self, oversampling=OSR_8192): if self._bus is None: print("No bus!") return False - + if oversampling < OSR_256 or oversampling > OSR_8192: print("Invalid oversampling option!") return False - + # Request D1 conversion (temperature) - self._bus.write_byte(self._MS5837_ADDR, self._MS5837_CONVERT_D1_256 + 2*oversampling) - + self._bus.write_byte( + self._MS5837_ADDR, self._MS5837_CONVERT_D1_256 + 2 * oversampling + ) + # Maximum conversion time increases linearly with oversampling # max time (seconds) ~= 2.2e-6(x) where x = OSR = (2^8, 2^9, ..., 2^13) # We use 2.5e-6 for some overhead - sleep(2.5e-6 * 2**(8+oversampling)) - + sleep(2.5e-6 * 2 ** (8 + oversampling)) + d = self._bus.read_i2c_block_data(self._MS5837_ADDR, self._MS5837_ADC_READ, 3) self._D1 = d[0] << 16 | d[1] << 8 | d[2] - + # Request D2 conversion (pressure) - self._bus.write_byte(self._MS5837_ADDR, self._MS5837_CONVERT_D2_256 + 2*oversampling) - + self._bus.write_byte( + self._MS5837_ADDR, self._MS5837_CONVERT_D2_256 + 2 * oversampling + ) + # As above - sleep(2.5e-6 * 2**(8+oversampling)) - + sleep(2.5e-6 * 2 ** (8 + oversampling)) + d = self._bus.read_i2c_block_data(self._MS5837_ADDR, self._MS5837_ADC_READ, 3) self._D2 = d[0] << 16 | d[1] << 8 | d[2] # Calculate compensated pressure and temperature # using raw ADC values and internal calibration self._calculate() - + return True - + def setFluidDensity(self, denisty): self._fluidDensity = denisty - + # Pressure in requested units # mbar * conversion def pressure(self, conversion=UNITS_mbar): return self._pressure * conversion - + # Temperature in requested units # default degrees C def temperature(self, conversion=UNITS_Centigrade): degC = self._temperature / 100.0 if conversion == UNITS_Farenheit: - return (9.0/5.0)*degC + 32 + return (9.0 / 5.0) * degC + 32 elif conversion == UNITS_Kelvin: return degC + 273 return degC - + # Depth relative to MSL pressure in given fluid density def depth(self): - return (self.pressure(UNITS_Pa)-101300)/(self._fluidDensity*9.80665) - + return (self.pressure(UNITS_Pa) - 101300) / (self._fluidDensity * 9.80665) + # Altitude relative to MSL pressure def altitude(self): - return (1-pow((self.pressure()/1013.25),.190284))*145366.45*.3048 - + return (1 - pow((self.pressure() / 1013.25), 0.190284)) * 145366.45 * 0.3048 + # Cribbed from datasheet def _calculate(self): OFFi = 0 SENSi = 0 Ti = 0 - dT = self._D2-self._C[5]*256 + dT = self._D2 - self._C[5] * 256 if self._model == MODEL_02BA: - SENS = self._C[1]*65536+(self._C[3]*dT)/128 - OFF = self._C[2]*131072+(self._C[4]*dT)/64 - self._pressure = (self._D1*SENS/(2097152)-OFF)/(32768) + SENS = self._C[1] * 65536 + (self._C[3] * dT) / 128 + OFF = self._C[2] * 131072 + (self._C[4] * dT) / 64 + self._pressure = (self._D1 * SENS / (2097152) - OFF) / (32768) else: - SENS = self._C[1]*32768+(self._C[3]*dT)/256 - OFF = self._C[2]*65536+(self._C[4]*dT)/128 - self._pressure = (self._D1*SENS/(2097152)-OFF)/(8192) - - self._temperature = 2000+dT*self._C[6]/8388608 + SENS = self._C[1] * 32768 + (self._C[3] * dT) / 256 + OFF = self._C[2] * 65536 + (self._C[4] * dT) / 128 + self._pressure = (self._D1 * SENS / (2097152) - OFF) / (8192) + + self._temperature = 2000 + dT * self._C[6] / 8388608 # Second order compensation if self._model == MODEL_02BA: - if (self._temperature/100) < 20: # Low temp - Ti = (11*dT*dT)/(34359738368) - OFFi = (31*(self._temperature-2000)*(self._temperature-2000))/8 - SENSi = (63*(self._temperature-2000)*(self._temperature-2000))/32 - + if (self._temperature / 100) < 20: # Low temp + Ti = (11 * dT * dT) / (34359738368) + OFFi = ( + 31 * (self._temperature - 2000) * (self._temperature - 2000) + ) / 8 + SENSi = ( + 63 * (self._temperature - 2000) * (self._temperature - 2000) + ) / 32 + else: - if (self._temperature/100) < 20: # Low temp - Ti = (3*dT*dT)/(8589934592) - OFFi = (3*(self._temperature-2000)*(self._temperature-2000))/2 - SENSi = (5*(self._temperature-2000)*(self._temperature-2000))/8 - if (self._temperature/100) < -15: # Very low temp - OFFi = OFFi+7*(self._temperature+1500l)*(self._temperature+1500) - SENSi = SENSi+4*(self._temperature+1500l)*(self._temperature+1500) - elif (self._temperature/100) >= 20: # High temp - Ti = 2*(dT*dT)/(137438953472) - OFFi = (1*(self._temperature-2000)*(self._temperature-2000))/16 + if (self._temperature / 100) < 20: # Low temp + Ti = (3 * dT * dT) / (8589934592) + OFFi = (3 * (self._temperature - 2000) * (self._temperature - 2000)) / 2 + SENSi = ( + 5 * (self._temperature - 2000) * (self._temperature - 2000) + ) / 8 + if (self._temperature / 100) < -15: # Very low temp + OFFi = OFFi + 7 * (self._temperature + 1500) * ( + self._temperature + 1500 + ) + SENSi = SENSi + 4 * (self._temperature + 1500) * ( + self._temperature + 1500 + ) + elif (self._temperature / 100) >= 20: # High temp + Ti = 2 * (dT * dT) / (137438953472) + OFFi = ( + 1 * (self._temperature - 2000) * (self._temperature - 2000) + ) / 16 SENSi = 0 - - OFF2 = OFF-OFFi - SENS2 = SENS-SENSi - + + OFF2 = OFF - OFFi + SENS2 = SENS - SENSi + if self._model == MODEL_02BA: - self._temperature = (self._temperature-Ti) - self._pressure = (((self._D1*SENS2)/2097152-OFF2)/32768)/100.0 + self._temperature = self._temperature - Ti + self._pressure = (((self._D1 * SENS2) / 2097152 - OFF2) / 32768) / 100.0 else: - self._temperature = (self._temperature-Ti) - self._pressure = (((self._D1*SENS2)/2097152-OFF2)/8192)/10.0 - + self._temperature = self._temperature - Ti + self._pressure = (((self._D1 * SENS2) / 2097152 - OFF2) / 8192) / 10.0 + # Cribbed from datasheet def _crc4(self, n_prom): n_rem = 0 - - n_prom[0] = ((n_prom[0]) & 0x0FFF) + + n_prom[0] = (n_prom[0]) & 0x0FFF n_prom.append(0) - + for i in range(16): - if i%2 == 1: - n_rem ^= ((n_prom[i>>1]) & 0x00FF) + if i % 2 == 1: + n_rem ^= (n_prom[i >> 1]) & 0x00FF else: - n_rem ^= (n_prom[i>>1] >> 8) - - for n_bit in range(8,0,-1): + n_rem ^= n_prom[i >> 1] >> 8 + + for n_bit in range(8, 0, -1): if n_rem & 0x8000: n_rem = (n_rem << 1) ^ 0x3000 else: - n_rem = (n_rem << 1) + n_rem = n_rem << 1 + + n_rem = (n_rem >> 12) & 0x000F - n_rem = ((n_rem >> 12) & 0x000F) - self.n_prom = n_prom self.n_rem = n_rem - + return n_rem ^ 0x00 - + + class MS5837_30BA(MS5837): def __init__(self, bus=1): MS5837.__init__(self, MODEL_30BA, bus) - + + class MS5837_02BA(MS5837): def __init__(self, bus=1): MS5837.__init__(self, MODEL_02BA, bus) - + + diff --git a/openoceancamera/sensors/sensors.py b/openoceancamera/sensors/sensors.py new file mode 100644 index 0000000..6eab175 --- /dev/null +++ b/openoceancamera/sensors/sensors.py @@ -0,0 +1,211 @@ +import os +import json +from logger import logger +from datetime import datetime +from constants import LOG_FILE + +# sensors_logger = logging.getLogger(__name__) + +from .ms5837 import MS5837_30BA, DENSITY_SALTWATER, UNITS_Centigrade, UNITS_mbar +from .tsys01 import TSYS01_30BA, UNITS_Centigrade +from .tsl2561 import TSL2561_30BA +from .gps import GPS + + +class Sensor: + def __init__(self): + super().__init__() + self.pressure_data = -1 + self.temperature_data = -1 + self.luminosity_data = -1 + self.log_filename = LOG_FILE + + try: + self.gps = GPS() + except Exception as err: + logger.error(f"Sensor Error: {err}") + try: + self.pressure_sensor = PressureSensor() + except Exception as err: + logger.error(f"Sensor Error: {err}") + try: + self.temperature_sensor = TemperatureSensor() + except Exception as err: + logger.error(f"Sensor Error: {err}") + try: + self.luminosity_sensor = LuminositySensor() + except Exception as err: + logger.error(f"Sensor Error: {err}") + + def write_sensor_data(self): + if os.path.exists(self.log_filename): + file_mode = "a" + else: + file_mode = "w" + try: + self.read_sensor_data() + with open(self.log_filename, file_mode) as f: + f.write( + json.dumps( + { + "timestamp": datetime.now().strftime("%m/%d/%Y, %H:%M:%S"), + "luminosity": self.luminosity_data, + "temp": self.temperature_data, + "pressure": self.pressure_data, + "gps": { + "lat": self.gps.latitude, + "long": self.gps.longitude + } + } + ) + ) + f.write("\n") + except: + with open(self.log_filename, "w"): + pass + + def read_sensor_data(self): + try: + self.pressure_data = self.pressure_sensor.pressure() + except PressureSensorCannotReadException as err: + self.pressure_data = -1 + logger.error(f"Error: {err}") + except Exception as err: + logger.error(f"Sensor error: {err}") + pass + + try: + self.temperature_data = self.temperature_sensor.temperature() + except TemperatureSensorCannotReadException as err: + self.temperature_data = -1 + logger.error(f"Error: {err}") + except Exception as err: + logger.error(f"Sensor error: {err}") + pass + + try: + self.luminosity_data = self.luminosity_sensor.luminosity() + except LuminositySensorCannotReadException as err: + self.luminosity_data = -1 + logger.error(f"Error: {err}") + except Exception as err: + logger.error(f"Sensor error: {err}") + pass + + def get_sensor_data(self): + return { + "pressure": self.pressure_data, + "temperature" : self.temperature_data, + "luminosity" : self.luminosity_data, + "gps": { + "lat": self.gps.latitude, + "long": self.gps.longitude + } if self.gps else None + } + +class PressureSensorNotConnectedException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class PressureSensorCannotReadException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class PressureSensor(MS5837_30BA): + def __init__(self, bus=1): + super().__init__(bus=bus) + if not super().init(): + logger.warning("MS5837_30BA may not be connected") + raise PressureSensorNotConnectedException( + "MS5837_30BA may not be connected" + ) + self.setFluidDensity(DENSITY_SALTWATER) + + def pressure(self, conversion=UNITS_mbar): + if self.read(): + data = super().pressure(conversion=conversion) + logger.info(f"Reading pressure data from the sensor: {data}") + return data + else: + raise PressureSensorCannotReadException("Could not read pressure values") + + def temperature(self, conversion=UNITS_Centigrade): + if self.read(): + data = super().temperature(conversion=conversion) + logger.info(f"Reading temperature data from the sensor: {data}") + return data + else: + raise PressureSensorCannotReadException("Could not read temperature values") + + def depth(self): + if self.read(): + data = super().depth() + logger.info(f"Reading depth data from the sensor: {data}") + return data + else: + raise PressureSensorCannotReadException("Could not read depth values") + + +class TemperatureSensorNotConnectedException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class TemperatureSensorCannotReadException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class TemperatureSensor(TSYS01_30BA): + def __init__(self, bus=1): + super().__init__(bus=bus) + if not super().init(): + logger.warning("TSYS01_30BA may not be connected") + raise TemperatureSensorNotConnectedException( + "TSYS01_30BA may not be connected" + ) + + def temperature(self, conversion=UNITS_Centigrade): + if self.read(): + data = super().temperature(conversion=conversion) + logger.info(f"Reading temperature data from the sensor: {data}") + return data + else: + raise TemperatureSensorCannotReadException( + "Could not read temperature values" + ) + +class LuminositySensorNotConnectedException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class LuminositySensorCannotReadException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class LuminositySensor(TSL2561_30BA): + def __init__(self, bus=1): + super().__init__(bus=bus) + if not super().init(): + logger.warning("TSL2561_30BA may not be connected") + raise LuminositySensorNotConnectedException( + "TSL2561_30BA may not be connected" + ) + + def luminosity(self): + if self.read(): + data = super().lux() + logger.info(f"Reading luminosity data from the sensor: {data}") + return data + else: + raise LuminositySensorCannotReadException( + "Could not read luminosity values" + ) + +if __name__ == "__main__": + pass + diff --git a/openoceancamera/sensors/subsealight.py b/openoceancamera/sensors/subsealight.py new file mode 100644 index 0000000..33a7fe2 --- /dev/null +++ b/openoceancamera/sensors/subsealight.py @@ -0,0 +1,27 @@ +import RPi.GPIO as GPIO +import datetime +import sys +from logger import logger + +class SubSeaLight: + + def __init__(self): + GPIO.setwarnings(False) + # originally 11 + GPIO.setmode(GPIO.BCM) + GPIO.setup(24, GPIO.OUT) + try: + self.pwm = GPIO.PWM(24,500) # PIN 12 = Board 32 + except Exception as err: + logger.error(f"PWM not found") + + def switch_off(self): + self.pwm.ChangeDutyCycle(0) + self.pwm.stop() + + + def switch_on(self,dc): + print(dc) + self.pwm.start(dc) + + diff --git a/openoceancamera/sensors/tsl2561.py b/openoceancamera/sensors/tsl2561.py new file mode 100644 index 0000000..debfcad --- /dev/null +++ b/openoceancamera/sensors/tsl2561.py @@ -0,0 +1,334 @@ +import time +import typing +import smbus2 as smbus +from Adafruit_GPIO import I2C +from tsl2561.constants import * # pylint: disable=unused-wildcard-import,wildcard-import + +# Models +MODEL_30BA = 1 + + +class TSL2561: + """Driver for the TSL2561 digital luminosity (light) sensors.""" + + def __init__( + self, + address: typing.Optional[int] = None, + reset=0x1E, + busnum: typing.Optional[int] = None, + integration_time: int = TSL2561_INTEGRATIONTIME_402MS, + gain: int = TSL2561_GAIN_1X, + autogain: bool = False, + debug: bool = False, + bus=1, + model=MODEL_30BA, + ) -> None: + + # Set default address and bus number if not given + if address is not None: + self.address = address + else: + self.address = TSL2561_ADDR_FLOAT + + self.reset = reset + + if busnum is None: + self.busnum = 1 + + self._model = model + + try: + self._bus = smbus.SMBus(bus) + except: + print(("Bus %d is not available.") % bus) + print("Available busses are listed as /dev/i2c*") + self._bus = None + + self.i2c = I2C.get_i2c_device(self.address, busnum=busnum) + + self.debug = debug + self.integration_time = integration_time + self.gain = gain + self.autogain = autogain + + if self.integration_time == TSL2561_INTEGRATIONTIME_402MS: + self.delay_time = TSL2561_DELAY_INTTIME_402MS + + elif self.integration_time == TSL2561_INTEGRATIONTIME_101MS: + self.delay_time = TSL2561_DELAY_INTTIME_101MS + + elif self.integration_time == TSL2561_INTEGRATIONTIME_13MS: + self.delay_time = TSL2561_DELAY_INTTIME_13MS + + self._begin() + + def init(self): + if self._bus is None: + print("No bus!") + return False + + self._bus.write_byte(self.address, self.reset) + + # Wait for reset to complete + sleep(0.01) + + return True + + def _begin(self) -> None: + """Initializes I2C and configures the sensor (call this function before + doing anything else) + """ + # Make sure we're actually connected + x = self.i2c.readU8(TSL2561_REGISTER_ID) + + if not x & 0x0A: + raise Exception("TSL2561 not found!") + ########## + + # Set default integration time and gain + self.set_integration_time(self.integration_time) + self.set_gain(self.gain) + + # Note: by default, the device is in power down mode on bootup + self.disable() + + def enable(self) -> None: + """Enable the device by setting the control bit to 0x03""" + self.i2c.write8( + TSL2561_COMMAND_BIT | TSL2561_REGISTER_CONTROL, TSL2561_CONTROL_POWERON + ) + + def disable(self) -> None: + """Disables the device (putting it in lower power sleep mode)""" + self.i2c.write8( + TSL2561_COMMAND_BIT | TSL2561_REGISTER_CONTROL, TSL2561_CONTROL_POWEROFF + ) + + @staticmethod + def delay(value: int) -> None: + """Delay times must be specified in milliseconds but as the python + sleep function only takes (float) seconds we need to convert the sleep + time first + """ + time.sleep(value / 1000.0) + + def _get_data(self) -> typing.Tuple[int, int]: + """Private function to read luminosity on both channels""" + + # Enable the device by setting the control bit to 0x03 + self.enable() + + # Wait x ms for ADC to complete + TSL2561.delay(self.delay_time) + + # Reads a two byte value from channel 0 (visible + infrared) + broadband = self.i2c.readU16( + TSL2561_COMMAND_BIT | TSL2561_WORD_BIT | TSL2561_REGISTER_CHAN0_LOW + ) + + # Reads a two byte value from channel 1 (infrared) + ir = self.i2c.readU16( + TSL2561_COMMAND_BIT | TSL2561_WORD_BIT | TSL2561_REGISTER_CHAN1_LOW + ) + + # Turn the device off to save power + self.disable() + + return (broadband, ir) + + def set_integration_time(self, integration_time: int) -> None: + """Sets the integration time for the TSL2561""" + + # Enable the device by setting the control bit to 0x03 + self.enable() + + self.integration_time = integration_time + + # Update the timing register + self.i2c.write8( + TSL2561_COMMAND_BIT | TSL2561_REGISTER_TIMING, + self.integration_time | self.gain, + ) + + # Turn the device off to save power + self.disable() + + def set_gain(self, gain: int) -> None: + """Adjusts the gain on the TSL2561 (adjusts the sensitivity to light) + """ + + # Enable the device by setting the control bit to 0x03 + self.enable() + + self.gain = gain + + # Update the timing register + self.i2c.write8( + TSL2561_COMMAND_BIT | TSL2561_REGISTER_TIMING, + self.integration_time | self.gain, + ) + + # Turn the device off to save power + self.disable() + + def set_auto_range(self, value: bool) -> None: + """Enables or disables the auto-gain settings when reading + data from the sensor + """ + self.autogain = value + + def _get_luminosity(self) -> typing.Tuple[int, int]: + """Gets the broadband (mixed lighting) and IR only values from + the TSL2561, adjusting gain if auto-gain is enabled + """ + valid = False + + # If Auto gain disabled get a single reading and continue + if not self.autogain: + return self._get_data() + + # Read data until we find a valid range + _agcCheck = False + broadband = 0 + ir = 0 + + while not valid: + if self.integration_time == TSL2561_INTEGRATIONTIME_13MS: + _hi = TSL2561_AGC_THI_13MS + _lo = TSL2561_AGC_TLO_13MS + elif self.integration_time == TSL2561_INTEGRATIONTIME_101MS: + _hi = TSL2561_AGC_THI_101MS + _lo = TSL2561_AGC_TLO_101MS + else: + _hi = TSL2561_AGC_THI_402MS + _lo = TSL2561_AGC_TLO_402MS + + _b, _ir = self._get_data() + + # Run an auto-gain check if we haven't already done so ... + if not _agcCheck: + if _b < _lo and self.gain == TSL2561_GAIN_1X: + # Increase the gain and try again + self.set_gain(TSL2561_GAIN_16X) + # Drop the previous conversion results + _b, _ir = self._get_data() + # Set a flag to indicate we've adjusted the gain + _agcCheck = True + elif _b > _hi and self.gain == TSL2561_GAIN_16X: + # Drop gain to 1x and try again + self.set_gain(TSL2561_GAIN_1X) + # Drop the previous conversion results + _b, _ir = self._get_data() + # Set a flag to indicate we've adjusted the gain + _agcCheck = True + else: + # Nothing to look at here, keep moving .... + # Reading is either valid, or we're already at the chips + # limits + broadband = _b + ir = _ir + valid = True + else: + # If we've already adjusted the gain once, just return the new + # results. + # This avoids endless loops where a value is at one extreme + # pre-gain, and the the other extreme post-gain + broadband = _b + ir = _ir + valid = True + + return (broadband, ir) + + def _calculate_lux(self, broadband: int, ir: int) -> int: + """Converts the raw sensor values to the standard SI lux equivalent. + Returns 0 if the sensor is saturated and the values are unreliable. + """ + # Make sure the sensor isn't saturated! + if self.integration_time == TSL2561_INTEGRATIONTIME_13MS: + clipThreshold = TSL2561_CLIPPING_13MS + elif self.integration_time == TSL2561_INTEGRATIONTIME_101MS: + clipThreshold = TSL2561_CLIPPING_101MS + else: + clipThreshold = TSL2561_CLIPPING_402MS + + # Return 0 lux if the sensor is saturated + if broadband > clipThreshold or ir > clipThreshold: + raise Exception("Sensor is saturated") + + # Get the correct scale depending on the integration time + if self.integration_time == TSL2561_INTEGRATIONTIME_13MS: + chScale = TSL2561_LUX_CHSCALE_TINT0 + elif self.integration_time == TSL2561_INTEGRATIONTIME_101MS: + chScale = TSL2561_LUX_CHSCALE_TINT1 + else: + chScale = 1 << TSL2561_LUX_CHSCALE + + # Scale for gain (1x or 16x) + if not self.gain: + chScale = chScale << 4 + + # Scale the channel values + channel0 = (broadband * chScale) >> TSL2561_LUX_CHSCALE + channel1 = (ir * chScale) >> TSL2561_LUX_CHSCALE + + # Find the ratio of the channel values (Channel1/Channel0) + ratio1 = 0 + if channel0 != 0: + ratio1 = (channel1 << (TSL2561_LUX_RATIOSCALE + 1)) // channel0 + + # round the ratio value + ratio = (int(ratio1) + 1) >> 1 + + b = 0 + m = 0 + + if ratio >= 0 and ratio <= TSL2561_LUX_K1T: + b = TSL2561_LUX_B1T + m = TSL2561_LUX_M1T + elif ratio <= TSL2561_LUX_K2T: + b = TSL2561_LUX_B2T + m = TSL2561_LUX_M2T + elif ratio <= TSL2561_LUX_K3T: + b = TSL2561_LUX_B3T + m = TSL2561_LUX_M3T + elif ratio <= TSL2561_LUX_K4T: + b = TSL2561_LUX_B4T + m = TSL2561_LUX_M4T + elif ratio <= TSL2561_LUX_K5T: + b = TSL2561_LUX_B5T + m = TSL2561_LUX_M5T + elif ratio <= TSL2561_LUX_K6T: + b = TSL2561_LUX_B6T + m = TSL2561_LUX_M6T + elif ratio <= TSL2561_LUX_K7T: + b = TSL2561_LUX_B7T + m = TSL2561_LUX_M7T + elif ratio > TSL2561_LUX_K8T: + b = TSL2561_LUX_B8T + m = TSL2561_LUX_M8T + + temp = (channel0 * b) - (channel1 * m) + + # Do not allow negative lux value + if temp < 0: + temp = 0 + + # Round lsb (2^(LUX_SCALE-1)) + temp += 1 << (TSL2561_LUX_LUXSCALE - 1) + + # Strip off fractional portion + lux = temp >> TSL2561_LUX_LUXSCALE + + # Signal I2C had no errors + return lux + + def lux(self) -> int: + """Read sensor data, convert it to LUX and return it""" + broadband, ir = self._get_luminosity() + return self._calculate_lux(broadband, ir) + + +class TSL2561_30BA(TSL2561): + def __init__(self, bus=1): + TSL2561.__init__(self, MODEL_30BA, bus) + diff --git a/openoceancamera/tsys01-python/build/lib/tsys01/tsys01.py b/openoceancamera/sensors/tsys01.py old mode 100755 new mode 100644 similarity index 58% rename from openoceancamera/tsys01-python/build/lib/tsys01/tsys01.py rename to openoceancamera/sensors/tsys01.py index 1565d02..98495f3 --- a/openoceancamera/tsys01-python/build/lib/tsys01/tsys01.py +++ b/openoceancamera/sensors/tsys01.py @@ -1,70 +1,73 @@ -try: - import smbus -except: - print 'Try sudo apt-get install python-smbus' - +import smbus2 as smbus + from time import sleep +# Models +MODEL_30BA = 1 + # Valid units UNITS_Centigrade = 1 -UNITS_Farenheit = 2 -UNITS_Kelvin = 3 +UNITS_Farenheit = 2 +UNITS_Kelvin = 3 + - class TSYS01(object): - + # Registers - _TSYS01_ADDR = 0x77 - _TSYS01_PROM_READ = 0xA0 - _TSYS01_RESET = 0x1E - _TSYS01_CONVERT = 0x48 - _TSYS01_READ = 0x00 - - def __init__(self, bus=1): + _TSYS01_ADDR = 0x77 + _TSYS01_PROM_READ = 0xA0 + _TSYS01_RESET = 0x1E + _TSYS01_CONVERT = 0x48 + _TSYS01_READ = 0x00 + + def __init__(self, model=MODEL_30BA, bus=1): # Degrees C self._temperature = 0 self._k = [] - + + self._model = model + try: self._bus = smbus.SMBus(bus) except: - print("Bus %d is not available.") % bus + print(("Bus %d is not available.") % bus) print("Available busses are listed as /dev/i2c*") self._bus = None - - + def init(self): if self._bus is None: - "No bus!" + print("No bus!") return False - + self._bus.write_byte(self._TSYS01_ADDR, self._TSYS01_RESET) - + # Wait for reset to complete sleep(0.1) - + self._k = [] # Read calibration values # Read one 16 bit byte word at a time - for prom in range(0xAA, 0xA2-2, -2): + for prom in range(0xAA, 0xA2 - 2, -2): k = self._bus.read_word_data(self._TSYS01_ADDR, prom) - k = ((k & 0xFF) << 8) | (k >> 8) # SMBus is little-endian for word transfers, we need to swap MSB and LSB + k = ((k & 0xFF) << 8) | ( + k >> 8 + ) # SMBus is little-endian for word transfers, we need to swap MSB and LSB self._k.append(k) - + return True - + def read(self): if self._bus is None: - print "No bus!" + print("No bus!") return False - + # Request conversion self._bus.write_byte(self._TSYS01_ADDR, self._TSYS01_CONVERT) - + # Max conversion time = 9.04 ms sleep(0.01) - + adc = self._bus.read_i2c_block_data(self._TSYS01_ADDR, self._TSYS01_READ, 3) adc = adc[0] << 16 | adc[1] << 8 | adc[2] self._calculate(adc) @@ -74,18 +77,24 @@ def read(self): # default degrees C def temperature(self, conversion=UNITS_Centigrade): if conversion == UNITS_Farenheit: - return (9/5) * self._temperature + 32 + return (9 / 5) * self._temperature + 32 elif conversion == UNITS_Kelvin: return self._temperature - 273 return self._temperature # Cribbed from datasheet def _calculate(self, adc): - adc16 = adc/256 - self._temperature = -2 * self._k[4] * 10**-21 * adc16**4 + \ - 4 * self._k[3] * 10**-16 * adc16**3 + \ - -2 * self._k[2] * 10**-11 * adc16**2 + \ - 1 * self._k[1] * 10**-6 * adc16 + \ - -1.5 * self._k[0] * 10**-2 - - \ No newline at end of file + adc16 = adc / 256 + self._temperature = ( + -2 * self._k[4] * 10 ** -21 * adc16 ** 4 + + 4 * self._k[3] * 10 ** -16 * adc16 ** 3 + + -2 * self._k[2] * 10 ** -11 * adc16 ** 2 + + 1 * self._k[1] * 10 ** -6 * adc16 + + -1.5 * self._k[0] * 10 ** -2 + ) + + +class TSYS01_30BA(TSYS01): + def __init__(self, bus=1): + TSYS01.__init__(self, MODEL_30BA, bus) + diff --git a/openoceancamera/tsys01-python/LICENSE b/openoceancamera/tsys01-python/LICENSE deleted file mode 100755 index ef810cd..0000000 --- a/openoceancamera/tsys01-python/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Blue Robotics - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/openoceancamera/tsys01-python/README.md b/openoceancamera/tsys01-python/README.md deleted file mode 100755 index 5a8e615..0000000 --- a/openoceancamera/tsys01-python/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# tsys01-python - -A Python module to interface with the TSYS01 temperature sensor. Tested on Raspberry Pi 3 with Raspbian. - -The python SMBus library must be installed. - - sudo apt-get install python-smbus - -# Usage - - import tsys01 - -### TSYS01(bus=1) - - sensor = tsys01.TSYS01() # Use default I2C bus 1 - sensor = tsys01.TSYS01(0) # Specify I2C bus 0 - -### init() - -Initialize the sensor. This needs to be called before using any other methods. - - sensor.init() - -Returns true if the sensor was successfully initialized, false otherwise. - -### read() - -Read the sensor and update the temperature. - - sensor.read() - -Returns True if read was successful, False otherwise. - -### temperature(conversion=UNITS_Centigrade) - -Get the most recent temperature measurement. - - sensor.temperature() # Get temperature in default units (Centigrade) - sensor.temperature(ms5837.UNITS_Farenheit) # Get temperature in Farenheit - -Valid arguments are: - - tsys01.UNITS_Centigrade - tsys01.UNITS_Farenheit - tsys01.UNITS_Kelvin - -Returns the most recent temperature in the requested units, or temperature in degrees Centigrade if invalid units specified. Call read() to update. - - - diff --git a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/PKG-INFO b/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/PKG-INFO deleted file mode 100755 index 98eb001..0000000 --- a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/PKG-INFO +++ /dev/null @@ -1,61 +0,0 @@ -Metadata-Version: 2.1 -Name: bluerobotics-tsys01 -Version: 0.0.1 -Summary: A python module for the TSYS01 digital temperature sensor -Home-page: https://www.bluerobotics.com -Author: Blue Robotics -Author-email: support@bluerobotics.com -License: UNKNOWN -Description: # tsys01-python - - A Python module to interface with the TSYS01 temperature sensor. Tested on Raspberry Pi 3 with Raspbian. - - The python SMBus library must be installed. - - sudo apt-get install python-smbus - - # Usage - - import tsys01 - - ### TSYS01(bus=1) - - sensor = tsys01.TSYS01() # Use default I2C bus 1 - sensor = tsys01.TSYS01(0) # Specify I2C bus 0 - - ### init() - - Initialize the sensor. This needs to be called before using any other methods. - - sensor.init() - - Returns true if the sensor was successfully initialized, false otherwise. - - ### read() - - Read the sensor and update the temperature. - - sensor.read() - - Returns True if read was successful, False otherwise. - - ### temperature(conversion=UNITS_Centigrade) - - Get the most recent temperature measurement. - - sensor.temperature() # Get temperature in default units (Centigrade) - sensor.temperature(ms5837.UNITS_Farenheit) # Get temperature in Farenheit - - Valid arguments are: - - tsys01.UNITS_Centigrade - tsys01.UNITS_Farenheit - tsys01.UNITS_Kelvin - - Returns the most recent temperature in the requested units, or temperature in degrees Centigrade if invalid units specified. Call read() to update. - -Platform: UNKNOWN -Classifier: Programming Language :: Python -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent -Description-Content-Type: text/markdown diff --git a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/SOURCES.txt b/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/SOURCES.txt deleted file mode 100755 index 738542e..0000000 --- a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/SOURCES.txt +++ /dev/null @@ -1,8 +0,0 @@ -README.md -setup.py -bluerobotics_tsys01.egg-info/PKG-INFO -bluerobotics_tsys01.egg-info/SOURCES.txt -bluerobotics_tsys01.egg-info/dependency_links.txt -bluerobotics_tsys01.egg-info/top_level.txt -tsys01/__init__.py -tsys01/tsys01.py \ No newline at end of file diff --git a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/dependency_links.txt b/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/dependency_links.txt deleted file mode 100755 index 8b13789..0000000 --- a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/top_level.txt b/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/top_level.txt deleted file mode 100755 index 8c4e31c..0000000 --- a/openoceancamera/tsys01-python/bluerobotics_tsys01.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -tsys01 diff --git a/openoceancamera/tsys01-python/build/lib/tsys01/__init__.py b/openoceancamera/tsys01-python/build/lib/tsys01/__init__.py deleted file mode 100755 index 33bd81f..0000000 --- a/openoceancamera/tsys01-python/build/lib/tsys01/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from tsys01 import TSYS01 diff --git a/openoceancamera/tsys01-python/dist/bluerobotics_tsys01-0.0.1-py3.7.egg b/openoceancamera/tsys01-python/dist/bluerobotics_tsys01-0.0.1-py3.7.egg deleted file mode 100755 index 2f420cc8d5a371fd5541cd02a9e40b5d4474db3f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3040 zcmai02{crFAHE}Nc4^)wMufuH*P;evd$Z3l2pJ3+8DlbI9a-iLudXp0QdnB`6b8s zk)>lIn*gAW4*$pnaz=2}qc!lk8gLdY6;S(lu3c zzQmPSc%v)s8S)+qLc#v=`OKrX&f5oObx!Di@W<_oU}jc_SAREFH_ z)B`F>I~^+@=NM&iiY6xaSraY;8jc?`*5gBlxqBCbCoF>>LoaH#&qkQ*u}F%cANk5v z38-5rZeAqH)<7o@t-ByKppTIC&z7|n$gY*f`MD^D`N_f4dIk_Qvr6!Tbm@Ux?lsK# zT3PkCs%C{2tqWCDWHLBZ|l}O@rILvSOoTdcg{`lp?h3M9h2hq=C zg@gJVPlRd!(5B`%Q(SXZ{=o&ejDjaiZISxRT8Qzdy#gsv)0nZ|3|!4b($Q8@V@PeO zrRleS7VE1MkNh3HdXuG6t_EwzUo#+5`9IZB-&7S(Ek8UXy1czDk%)6C{RV^Uf37g?g{Bf9!0EYn&6@#snIEuSA?$FzLp-eZ~ z)or6*c5SWmIa=@wXL&RrHICkyXIbr z`0S|kOvyg9r&)YQnT;i#8ra|5JEob%7;w%LtqYBN*1zE5fyt>2$)n@Fgp^uAce=$= zLcTWKP!O9-nJZtx@tD~1zQ1E^=d^oKczx0}nV;bu&}OR~0I>_oIjNsN(VyVwPrycD z@PULN(%Q)5T)?mBp5RU z1Bq%RpTqvYw7YO{%8``dn=j37vz=WM>>`t*NSaz27)&4`kc`2o6QgWk?cHdl@BN%x zJJeb=RFM1v=|UU2uW>;T;R^TJD~D|c0I_whACzpSt1iJIOiQtqu7A!;9O^DM!JB!h z8`|$o_!FJ$$@AfwO4@ZEQNN7laJt}uqNTaKLsD3&60}F0?5F8z9i8rd)lz5cz4YQ` z?xkEYPZ;UAU6r6An2NqDwtJuR``GMy>Ikp+4Yx3t()eVQmYi%X+5tJJB{iB0P4kxs zHklJ`%k(7x{^vB{ z{P8D1M9GLpP0pv>vKLlzGOp*UukF=K1(&{h#KkpHF4wN8H!Gjl6VaKeXUg1j++&xY zU8{7}(nVt8$~Sbu+3kXFg3Q|d6#RGmmDd=O+TsmSltlLqmhppoc7aPp3OXYX+7DEK zl=K<^BU5lAwdJV&i7_O<(XLxc%|L)$5b@)!AORe{JH7D2NfB;j_JISfxulb{xpAO`271pQ#YuVcc|8cr@NN zA3im(MYHK?-d0cL1gOxaF2tD%WpiX@=z(N2sY~JrgjKjw0k**+omC0#lMx;n}?i4;s!SniCu z%UaN0nwNpqJdRhrMHQE?!4HHR)CC+)eBdf-YQ5*3MqOV@PWLTD&nQ!%QW3eQu9~!J zGJZMQDcj%aB}Fs7ohKT-d&}DgQx}9Qa)QSBUPmSCc-1FddcmlD{O}a?y?Qj7l|J%G z_k}_Xe0P6GgV^1Hc>yH!U5l9+Dx2(4c_O&DE5|ZFI$$!6VjNz7G2x@x>S?%V$U79OmvV=gZ^&y^qKS6&m@Pb=1|M-w6Ux{uE(%{N!h1GfbHrDMkbYEzA~e63TFM#= z@7}D`l|cO}KeNG+>&CbV>1>&ovrA=N?idU)3hRT#`C~BaHi9Kak#QjeH5~)C4`4~m zi3wkTY7sKTyc+)j%d61H%@8s1R0R}J+fds`+d?5%wm~4M^HhlL1q`(hr~SfL@Vn)F zc;a`$_lNhXa!q-*IWmNb8W|ePMZFytC6|k`I+zmliyig7o1ZfZN3^S@m-{}DkNvH_ z{N+nWeMjb(NIJFMLt6jNqVmzYz zd^tJ(jTlb&?-FJmp2dd$ciwEoZ%mbS{L6Lx&y3+juMNL(ldhw?`8m4(-!1EC!^69V Q> 8) # SMBus is little-endian for word transfers, we need to swap MSB and LSB - self._k.append(k) - - return True - - def read(self): - if self._bus is None: - print "No bus!" - return False - - # Request conversion - self._bus.write_byte(self._TSYS01_ADDR, self._TSYS01_CONVERT) - - # Max conversion time = 9.04 ms - sleep(0.01) - - adc = self._bus.read_i2c_block_data(self._TSYS01_ADDR, self._TSYS01_READ, 3) - adc = adc[0] << 16 | adc[1] << 8 | adc[2] - self._calculate(adc) - return True - - # Temperature in requested units - # default degrees C - def temperature(self, conversion=UNITS_Centigrade): - if conversion == UNITS_Farenheit: - return (9/5) * self._temperature + 32 - elif conversion == UNITS_Kelvin: - return self._temperature - 273 - return self._temperature - - # Cribbed from datasheet - def _calculate(self, adc): - adc16 = adc/256 - self._temperature = -2 * self._k[4] * 10**-21 * adc16**4 + \ - 4 * self._k[3] * 10**-16 * adc16**3 + \ - -2 * self._k[2] * 10**-11 * adc16**2 + \ - 1 * self._k[1] * 10**-6 * adc16 + \ - -1.5 * self._k[0] * 10**-2 - - \ No newline at end of file diff --git a/openoceancamera/uploader/__init__.py b/openoceancamera/uploader/__init__.py new file mode 100644 index 0000000..d669209 --- /dev/null +++ b/openoceancamera/uploader/__init__.py @@ -0,0 +1 @@ +from .dropbox_uploader import DropboxUploader \ No newline at end of file diff --git a/openoceancamera/uploader/dropbox_uploader.py b/openoceancamera/uploader/dropbox_uploader.py new file mode 100644 index 0000000..61c50e8 --- /dev/null +++ b/openoceancamera/uploader/dropbox_uploader.py @@ -0,0 +1,102 @@ +import os +import json +import pickle +import dropbox +from dropbox import DropboxOAuth2FlowNoRedirect + +import logging +logging.basicConfig(filename="system_logs.txt", format="%(asctime)s - %(levelname)s - %(message)s", level=logging.INFO) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +CREDENTIAL_STORE = os.path.join(os.environ["HOME"], ".oocam", "credentials") +APP_KEY=os.environ.get("DROPBOX_KEY", "") +APP_SECRET=os.environ.get("DROPBOX_SECRET", "") + +def load_credentials_file(): + if os.path.exists(os.path.dirname(CREDENTIAL_STORE)) and os.path.exists(CREDENTIAL_STORE): + with open(CREDENTIAL_STORE, 'rb') as f: + data = pickle.load(f) + return data + elif os.path.exists(os.path.dirname(CREDENTIAL_STORE)): + with open(CREDENTIAL_STORE, 'wb') as f: + pass + return None + else: + os.makedirs(os.path.dirname(CREDENTIAL_STORE)) + return None + +class DropboxUploader: + def __init__(self): + self.auth_flow = DropboxOAuth2FlowNoRedirect(APP_KEY, APP_SECRET, token_access_type="legacy") + self.oauth_result = load_credentials_file() + if self.oauth_result is not None: + self.isLoggedIn = True + else: + self.isLoggedIn = False + + def start_auth_flow(self): + """ + Returns: Authorisation URL + """ + url = self.auth_flow.start() + with open(CREDENTIAL_STORE, 'wb') as credential_store: + pickle.dump(self.auth_flow, credential_store) + return url + + def complete_auth_flow(self, auth_code): + try: + self.auth_flow = load_credentials_file() + self.oauth_result = self.auth_flow.finish(auth_code) + with open(CREDENTIAL_STORE, 'wb') as credential_store: + pickle.dump(self.oauth_result, credential_store) + self.isLoggedIn = True + except Exception as e: + logger.error(e) + + def get_user_details(self): + with dropbox.Dropbox(oauth2_access_token=self.oauth_result.access_token) as dbx: + return dbx.users_get_current_account() + + def upload_small_file(self, file_path): + with open(file_path, 'rb') as f: + filename = os.path.join("/", os.path.basename(f.name)) + data = f.read() + dbx = dropbox.Dropbox(oauth2_access_token=self.oauth_result.access_token) + upload_result = dbx.files_upload(data, filename, mode=dropbox.files.WriteMode.overwrite, mute=True) + return upload_result + + def upload_file(self, file_path): + with open(file_path, 'rb') as f: + filename = os.path.join("/", os.path.basename(f.name)) + dbx = dropbox.Dropbox(oauth2_access_token=self.oauth_result.access_token) + try: + metadata = dbx.files_get_metadata(filename) + if metadata: + logger.info(f"File alredy exists on server") + return metadata + except dropbox.exceptions.ApiError: + chunk_size = 4 * 1024 * 1024 + file_size = os.path.getsize(file_path) + if file_size < chunk_size: + return self.upload_small_file(file_path) + upload_session = dbx.files_upload_session_start(f.read(chunk_size)) + cursor = dropbox.files.UploadSessionCursor(session_id=upload_session.session_id, offset=f.tell()) + commit = dropbox.files.CommitInfo(path=filename) + while f.tell() < file_size: + if file_size - f.tell() <= chunk_size: + return dbx.files_upload_session_finish(f.read(chunk_size), cursor, commit) + else: + dbx.files_upload_session_append_v2(f.read(chunk_size), cursor) + cursor.offset = f.tell() + +if __name__ == "__main__": + dbx = DropboxUploader() + if not dbx.isLoggedIn: + print(DropboxUploader.start_auth_flow()) + code = input("Auth code: ") + DropboxUploader.complete_auth_flow(code) + logger.info("Logged in to Dropbox") + print("Starting upload") + dbx = DropboxUploader() + dbx.upload_file("/Users/utkarsh/Downloads/sample.pdf") \ No newline at end of file diff --git a/openoceancamera/wiper/__init__.py b/openoceancamera/wiper/__init__.py new file mode 100644 index 0000000..40c4fac --- /dev/null +++ b/openoceancamera/wiper/__init__.py @@ -0,0 +1 @@ +from .wiper import run_wiper diff --git a/openoceancamera/wiper/servo.py b/openoceancamera/wiper/servo.py new file mode 100644 index 0000000..7f74c65 --- /dev/null +++ b/openoceancamera/wiper/servo.py @@ -0,0 +1,25 @@ +import time +import wiringpi + +# use 'GPIO naming' +wiringpi.wiringPiSetupGpio() + +# set #18 to be a PWM output +wiringpi.pinMode(18, wiringpi.GPIO.PWM_OUTPUT) + +# set the PWM mode to milliseconds stype +wiringpi.pwmSetMode(wiringpi.GPIO.PWM_MODE_MS) + +# divide down clock +wiringpi.pwmSetClock(192) +wiringpi.pwmSetRange(2000) + +delay_period = 0.01 + +while True: + for pulse in range(50, 250, 1): + wiringpi.pwmWrite(18, pulse) + time.sleep(delay_period) + for pulse in range(250, 50, -1): + wiringpi.pwmWrite(18, pulse) + time.sleep(delay_period) diff --git a/openoceancamera/wiper/wiper.py b/openoceancamera/wiper/wiper.py new file mode 100644 index 0000000..229829f --- /dev/null +++ b/openoceancamera/wiper/wiper.py @@ -0,0 +1,30 @@ +import subprocess +import time +import sys + +SERVO_LOWER_LIMIT=50 +SERVO_UPPER_LIMIT=250 + +class Wiper: + def __init__(self): + subprocess.run("gpio -g mode 18 pwm", shell=True) + subprocess.run("gpio pwm-ms",shell=True) + subprocess.run("gpio pwmc 192",shell=True) + subprocess.run("gpio pwmr 2000",shell=True) + + def set_angle(self, angle): + pwm_output = (SERVO_UPPER_LIMIT * angle + SERVO_LOWER_LIMIT * (270-angle))/270 + subprocess.run(f"gpio -g pwm 18 {pwm_output}", shell=True) + +def run_wiper(sweeps): + wiper_front = Wiper() + wiper_front.set_angle(26) + for i in range(sweeps): + wiper_front.set_angle(68) + time.sleep(1) + wiper_front.set_angle(26) + time.sleep(2) + +if __name__ == "__main__": + run_wiper(10) + diff --git a/scripts/update.sh b/scripts/update.sh index 02b3b94..b400afd 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -1,4 +1,5 @@ #!/bin/bash git reset --hard git pull -sudo mv /etc/wpa_supplicant/wpa_supplicant.conf.backup /etc/wpa_supplicant/wpa_supplicant.conf \ No newline at end of file +cp $HOME/etc/dhcpcd.conf /etc/dhcpcd.conf.backup +sudo mv /etc/wpa_supplicant/wpa_supplicant.conf.backup /etc/wpa_supplicant/wpa_supplicant.conf