diff --git a/README.md b/README.md index 4c05b3a..051322a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ -# light-meter +# Return to Riterdon: Light Meter -A Python based project which measures the light levels of an environment and forwards those measurements on to a server (for documentation purposes). This project is one of several which forms the software-side of the "Return to Ritherdon" project. \ No newline at end of file +Light Meter is a Python 3 based project for taking light readings (in +the welding booths in Ritherdon) and sending them to the 'Midpoint' +project for storage. Light Meter is one of three projects which +combine to make the 'Personal Flash in Real-Time' artwork -- which is +one of several artworks created as part of the 'Return to Ritherdon' +project. For more information on this repository and 'Return to +Ritherdon', please use the links below: + +- [Return to Ritherdon Overview](https://git.abbether.net/return-to-ritherdon/rtr-docs) +- [Light Meter Documentation](https://git.abbether.net/return-to-ritherdon/rtr-docs/src/branch/master/light-meter/rtr-light-meter.md) + diff --git a/attachments/hardware-layout.png b/attachments/hardware-layout.png new file mode 100644 index 0000000..d1180c2 Binary files /dev/null and b/attachments/hardware-layout.png differ diff --git a/attachments/light-meter-extension.png b/attachments/light-meter-extension.png new file mode 100644 index 0000000..ce9f846 Binary files /dev/null and b/attachments/light-meter-extension.png differ diff --git a/attachments/light-meter-logo.png b/attachments/light-meter-logo.png new file mode 100644 index 0000000..ae06299 Binary files /dev/null and b/attachments/light-meter-logo.png differ diff --git a/attachments/main-proj-architecture.png b/attachments/main-proj-architecture.png new file mode 100644 index 0000000..2bd3b31 Binary files /dev/null and b/attachments/main-proj-architecture.png differ diff --git a/attachments/midpoint-logo.png b/attachments/midpoint-logo.png new file mode 100644 index 0000000..e0a8862 Binary files /dev/null and b/attachments/midpoint-logo.png differ diff --git a/attachments/relay-logo.png b/attachments/relay-logo.png new file mode 100644 index 0000000..af32da7 Binary files /dev/null and b/attachments/relay-logo.png differ diff --git a/attachments/return-to-ritherdon-project-overview.png b/attachments/return-to-ritherdon-project-overview.png new file mode 100644 index 0000000..e22e2b7 Binary files /dev/null and b/attachments/return-to-ritherdon-project-overview.png differ diff --git a/attachments/rtrp-logo.png b/attachments/rtrp-logo.png new file mode 100644 index 0000000..149e914 Binary files /dev/null and b/attachments/rtrp-logo.png differ diff --git a/cli_meter.py b/cli_meter.py new file mode 100755 index 0000000..15018ca --- /dev/null +++ b/cli_meter.py @@ -0,0 +1,131 @@ +#!/usr/bin/python3 + +""" +C.L.I. Meter +====================================================================== +I wrote this script with the intention of using it as one part within +a single cron-job. The cron-job begins with "startup.sh" and that, +in-turn, will call this script when it is ready to do so. + +This scrip takes a reading of the current light-level in the room, +using the Raspberry Pi (and the attached light-meter attached) it is +running on. From there, it sends a J.S.O.N. object to the server +(specified at "api_url"). The process is then repeated until it is +killed manually or via "shutdown.sh" (as a cron-job most likely). + +You can use this script on Raspberry Pis without a G.U.I. because it +is all C.L.I. based. If you prefer something more graphical, you can +use "light_meter.py" -- which is part of this repository at time of +writing. + +To end this script manually, use the usual "Ctrl-c" keyboard +interrupt -- or something like Htop... doesn't matter. +""" + +import json +import RPi.GPIO as GPIO +import time +import math +import requests +from datetime import datetime +import platform +# import pdb # For testing. + +# Using BCM (Broadcom) names when referencing the GPIO pins. +GPIO.setmode(GPIO.BCM) +GPIO.setwarnings(True) + +a_pin = 18 # Charges the capacitor. +b_pin = 23 # Discharges the capacitor. + +device_id = platform.node() # For servers logs. +session = requests.Session() # Persist connection for REST calls. + +def get_api_url(): + # Yes, I know I could do this better. Stop moaning. + if (device_id == "factory1"): + return "http://ritherdon.abbether.net/api/readings/add/1" + elif (device_id == "factory2"): + return "http://ritherdon.abbether.net/api/readings/add/2" + +api_url = get_api_url() + +def discharge(): + GPIO.setup(a_pin, GPIO.IN) + GPIO.setup(b_pin, GPIO.OUT) + GPIO.output(b_pin, False) + time.sleep(0.01) # 0.01 -- initial/default value. + +def charge_time(): + GPIO.setup(b_pin, GPIO.IN) + GPIO.setup(a_pin, GPIO.OUT) + GPIO.output(a_pin, True) + t1 = time.time() + while not GPIO.input(b_pin): + pass + t2 = time.time() + return (t2 - t1) * 1000000 + +def analog_read(): + discharge() + return charge_time() + +def read_resistance(): + n = 20 + total = 0 + for i in range(1, n): + total = total + analog_read() + reading = total / float(n) + resistance = reading * 6.05 - 939 + return resistance + +def light_from_r(R): + return math.log(1000000.0/R) * 10.0 + +def get_timestamp(): + return datetime.now().strftime(("%Y-%m-%d %H:%M:%S")) + +def push_reading(lvalue): + time = get_timestamp() + headers = {"content-type": "application/json"} + payload = {"reading": int(lvalue), "time": time, + "token": "QWERTYuiopasdfghjklzxcvbnm_1234567890"} + print(payload) # For testing. + # res = requests.post(api_url, data=json.dumps(payload), headers=headers) + res = session.post(api_url, data=json.dumps(payload), headers=headers) + +def update_reading(): + light = light_from_r(read_resistance()) + reading_str = "{:.0f}".format(light) + # print(reading_str) # For testing. + push_reading(light) + + +def main(): + try: + while True: + # pdb.set_trace() # For testing. + update_reading() + except KeyboardInterrupt: + print("[INFO] KEYBOARD INTERRUPT: quitting program.") + except requests.exceptions.ConnectionError: + pause = 60 + time.sleep(60) + print(f"[WARNING] MAX. REQUESTS EXCEEDED: Pausing requests for {pause} seconds...") + pass + except requests.exceptions.Timeout: + t_stamp = datetime.datetime.now() + print(f"[WARNING] TIMEOUT EXCEPTION: Request timed-out at {t_stamp}.") + time.sleep(60) + pass + except Exception as e: + print(f"[ERROR] GENERAL EXCEPTION: {e}") + finally: + print("[INFO] Terminating relay.py...") + print("[INFO] Cleaning up GPIO before closing...") + GPIO.cleanup() + + +if __name__ == "__main__": + # time.sleep(60) # For testing/debugging. + main() diff --git a/light-meter.service b/light-meter.service new file mode 100644 index 0000000..281b5b7 --- /dev/null +++ b/light-meter.service @@ -0,0 +1,16 @@ +[Unit] +Description=Light-Meter service for Return to Ritherdon project by Nicola Ellis +After=network.service + +[Service] +Type=simple +ExecStart=bash /home/rtrp/repos/light-meter/startup.sh +WorkingDirectory=/home/rtrp/repos/light-meter +StandardOutput=inherit +StandardError=inherit +Restart=always +RestartSec=60 +user=rtrp + +[Install] +WantedBy=multi-user.target diff --git a/light_meter.py b/light_meter.py new file mode 100755 index 0000000..0b1a3db --- /dev/null +++ b/light_meter.py @@ -0,0 +1,129 @@ +# 07_light_meter.py +# From the code for the Electronics Starter Kit for the Raspberry Pi by MonkMakes.com + +#from Tkinter import * +from tkinter import * +import json +import RPi.GPIO as GPIO +import time, math +import requests # for pushing the readings to roaming-light-api +from datetime import datetime + +# Configure the Pi to use the BCM (Broadcom) pin names, rather than the pin positions +GPIO.setmode(GPIO.BCM) + +# This project uses a photoresistor, a component whose resistance varies with the light falling on it. +# To measure its resistance, the code records the time it takes for a capacitor to fill +# when supplied by a current passing through the resistor. The lower the resistance the faster +# it fills up. +# +# You can think of a capacitor as a tank of electricity, and as it fills with charge, the voltage +# across it increases. We cannot measure that voltage directly, because the Raspberry Pi +# does not have an analog to digital convertor (ADC or analog input). However, we can time how long it +# takes for the capacitor to fill with charge to the extent that it gets above the 1.65V or so +# that counts as being a high digital input. +# +# For more information on this technique take a look at: +# learn.adafruit.com/basic-resistor-sensor-reading-on-raspberry-pi +# The code here is based on that in the Raspberry Pi Cookbook (Recipes 12.1 to 12.3) + + +# Pin a charges the capacitor through a fixed 1k resistor and the thermistor in series +# pin b discharges the capacitor through a fixed 1k resistor +a_pin = 18 +b_pin = 23 + +# empty the capacitor ready to start filling it up +def discharge(): + GPIO.setup(a_pin, GPIO.IN) + GPIO.setup(b_pin, GPIO.OUT) + GPIO.output(b_pin, False) + time.sleep(0.01) # 0.01 -- initial value + +# return the time taken for the voltage on the capacitor to count as a digital input HIGH +# than means around 1.65V +def charge_time(): + GPIO.setup(b_pin, GPIO.IN) + GPIO.setup(a_pin, GPIO.OUT) + GPIO.output(a_pin, True) + t1 = time.time() + while not GPIO.input(b_pin): + pass + t2 = time.time() + return (t2 - t1) * 1000000 + +# Take an analog readin as the time taken to charge after first discharging the capacitor +def analog_read(): + discharge() + return charge_time() + +# Convert the time taken to charge the cpacitor into a value of resistance +# To reduce errors, do it 100 times and take the average. +def read_resistance(): + n = 20 + total = 0; + for i in range(1, n): + total = total + analog_read() + reading = total / float(n) + resistance = reading * 6.05 - 939 + return resistance + +def light_from_r(R): + # Log the reading to compress the range + return math.log(1000000.0/R) * 10.0 + +# Gets the time and date (at the time of the reading) +def get_timestamp(): + return datetime.now().strftime(("%Y-%m-%d %H:%M:%S")) + +# Pushes the reading to roaming-light-api +def push_reading(lvalue): + act = False + if lvalue > 20: + act = True + time = get_timestamp() + headers = {"content-type": "application/json"} +# payload = { "active": act, "light-reading": int(lvalue), "timestamp": time } + payload = { "reading": int(lvalue), "time": time} +# res = requests.post("http://192.168.1.228:5000/api/readings/add/1", data=json.dumps(payload), headers=headers) + res = requests.post("http://ritherdon.abbether.net/api/readings/add/1", data=json.dumps(payload), headers=headers) +# print(int(lvalue)) +# print(res.status_code) + print(payload) + +# group together all of the GUI code into a class called App +class App: + + # this function gets called when the app is created + def __init__(self, master): + self.master = master + frame = Frame(master, bg="black") + frame.pack(expand=1) + label = Label(frame, text='Light', fg="white", bg="black", font=("Helvetica", 32)) + label.grid(row=0) + self.reading_label = Label(frame, text='12.34', bg="black", fg="white", font=("Helvetica", 110)) + self.reading_label.grid(row=1) + self.update_reading() + + # Update the reading + def update_reading(self): + light = light_from_r(read_resistance()) + reading_str = "{:.0f}".format(light) + self.reading_label.configure(text=reading_str) + self.master.after(200, self.update_reading) + push_reading(light) + +# Set the GUI running, give the window a title, size and position +root = Tk() +# Uncomment when app. is ready for deployment +#root.attributes("-fullscreen", True) +root.attributes("-zoomed", True) +root.configure(background="black") +root.wm_title('Light Meter') +app = App(root) +root.geometry("400x300+0+0") +try: + root.mainloop() +finally: + print("Cleaning up") + GPIO.cleanup() diff --git a/make-log-files.sh b/make-log-files.sh new file mode 100644 index 0000000..a15c828 --- /dev/null +++ b/make-log-files.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# This script creates the log files and the fold they will reside in. +# This script must be executed before you set-up the start-up and +# shutdown cron-jobs on the system. + +mkdir ~/logs/ +touch ~/logs/startup-logs.txt +touch ~/logs/shutdown-logs.txt diff --git a/shutdown.sh b/shutdown.sh new file mode 100755 index 0000000..ff0ca61 --- /dev/null +++ b/shutdown.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Shutdown Script +# ==================================================================== +# I wrote this script with the intention of using it as part of a +# cron-job. Before you set up the cron-job, you must make sure you +# have either ran the "make-log-files.sh" script or created the +# appropriate log file and folder at the location specified at +# "logFile" below. +# As an aside, I usually set-up as alias for "shutdown" called, +# "powerdown". When I enter "powerdown" into the terminal, this script +# should ran and then the "shutdown" command is ran at the end. The +# "sleep" part is to give "curl" a bit of a buffer to finish its task. +# I have found it sometimes fails otherwise. + +logDate=$(date '+%Y-%m-%dT%TZ') +logFile="/home/rtrp/logs/shutdown-logs.txt" +mainURL="http://ritherdon.abbether.net/api/status/update" # Make sure this is valid. + +getApiUrl () { + case $HOSTNAME in + (factory1) apiURL="${mainURL}/1";; + (factory2) apiURL="${mainURL}/2";; + (factory3) apiURL="${mainURL}/3";; + (gallery1) apiURL="${mainURL}/4";; + (gallery2) apiURL="${mainURL}/5";; + (gallery3) apiURL="${mainURL}/6";; + esac +} + +logStatusChange () { + cat << EOF >> $logFile +$logDate +EOF +} + +logStatusChange +getApiUrl + +curl -X POST --header 'Content-Type: application/json' --header 'Accept: text/html' -d '{"status": "off", "time": "'${logDate}'", "token": "QWERTYuiopasdfghjklzxcvbnm_1234567890"}' "${apiURL}" + +/sbin/shutdown -h now diff --git a/startup.sh b/startup.sh new file mode 100755 index 0000000..9af3fcb --- /dev/null +++ b/startup.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Startup Script +# ==================================================================== +# I wrote this script with the intention of using it to start a +# cron-job -- when the Raspberry Pi is turned on. Before you set the +# cron-job up, you must make sure you have either ran the +# "make-log-files.sh" script or created the appropriate log file and +# folder at the location specified at "logFile" below. +# ==== +# Please note: This script calls another (Python) script at the end of +# it. The code in the Python script (cli_meter.py) is an infinite-loop +# so you will need to kill it manually or turn-off the Raspberry Pi. +# ==== +# I put the "sleep 60" call at the start to reduce any errors +# occurring because a part of the system (I.E. curl) has not finished +# loading. 60 seconds is a little excessive but I wrote this script +# with the expectation of it running on an unmanned Raspberry Pi. So, +# reliable/consistent behaviour is my preference over "fast start-up +# times". + + +# sleep 60 + +logDate=$(date '+%Y-%m-%dT%TZ') +logFile="/home/rtrp/logs/startup-logs.txt" +mainURL="http://ritherdon.abbether.net/api/status/update" # Make sure this is valid. + +getApiUrl () { + case $HOSTNAME in + (factory1) apiURL="${mainURL}/1";; + (factory2) apiURL="${mainURL}/2";; + (factory3) apiURL="${mainURL}/3";; + (gallery1) apiURL="${mainURL}/4";; + (gallery2) apiURL="${mainURL}/5";; + (gallery3) apiURL="${mainURL}/6";; + esac +} + +logStatusChange () { + cat << EOF >> $logFile +$logDate +EOF +} + +logStatusChange +getApiUrl + +curl -S -X POST --header 'Content-Type: application/json' --header 'Accept: text/html' -d '{"status": "on", "time": "'${logDate}'", "token": "QWERTYuiopasdfghjklzxcvbnm_1234567890"}' "${apiURL}" + +python3 /home/rtrp/repos/light-meter/cli_meter.py