diff --git a/..gitignore.swp b/..gitignore.swp
new file mode 100644
index 0000000..5a6994a
Binary files /dev/null and b/..gitignore.swp differ
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..52d1498
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,129 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..d8f0ced
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,2 @@
+[MASTER]
+disable = c0103
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..c4b02bb
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,14 @@
+Copyright (c) 2018, 2019, Kristjan Komloši, Jakob Kosec, Juš Dolžan,
+TeraHz development team
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bdb52fc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,23 @@
+# TeraHz
+
+[](https://terahz.readthedocs.io/en/latest/?badge=latest)
+
+TeraHz is a low-cost spectrometer based on a Raspberry Pi 3 or 3 B+ and three sensors:
+ + [__AS7265x__](https://www.tindie.com/products/onehorse/compact-as7265x-spectrometer/)
+ is a 18 channel spectrometer chipset that provides the device with spectral data
+ + [__VEML6075__](https://www.sparkfun.com/products/15089) is an
+ UVA/UVB sensor
+ + [__APDS-9301__](https://www.sparkfun.com/products/14350) is a calibrated illuminance (lux) meter that provides the device with reliable readings
+
+## Why?
+Because people and institutions could use an affordable and accurate light-analysing device that is also portable, easy to use and simple to assemble. TeraHz was started as an answer to our high school not being able to afford a commercially available solution. One TeraHz spectrometer costs around 150$ in parts, which makes it a competitive alternative to other solutions on the market today.
+
+## Development team
+Copyright 2018, 2019
+
+- Kristjan "cls-02" Komloši (electronics, sensor drivers, backend)
+- Jakob "D3m1j4ck" Kosec (frontend)
+
+
+I would also like to thank Juš "ANormalPerson" Dolžan, who decided to leave the
+team, but helped me a lot with backend development.
diff --git a/backend/app.py b/backend/app.py
new file mode 100644
index 0000000..ad3fedf
--- /dev/null
+++ b/backend/app.py
@@ -0,0 +1,17 @@
+# app.py - main backend program
+'''Main TeraHz backend program'''
+# All code in this file is licensed under the ISC license, provided in LICENSE.txt
+from flask import Flask
+import flask
+import sensors
+
+app = Flask(__name__)
+@app.route('/data')
+def sendData():
+ '''Responder function for /data route'''
+ s = sensors.Spectrometer(path='/dev/serial0', baudrate=115200, tout=1)
+ u = sensors.UVSensor()
+ l = sensors.LxMeter()
+ response = flask.jsonify([s.getData(), l.getData(), u.getABI()])
+ response.headers.add('Access-Control-Allow-Origin', '*')
+ return response
diff --git a/backend/run.sh b/backend/run.sh
new file mode 100755
index 0000000..7664abb
--- /dev/null
+++ b/backend/run.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+# run.sh - run the backend server
+cd `dirname $0`
+sudo gunicorn app:app -b 0.0.0.0:5000 &
diff --git a/backend/sample.db b/backend/sample.db
new file mode 100644
index 0000000..30ef50e
Binary files /dev/null and b/backend/sample.db differ
diff --git a/backend/sensors.py b/backend/sensors.py
new file mode 100644
index 0000000..a979c85
--- /dev/null
+++ b/backend/sensors.py
@@ -0,0 +1,257 @@
+# sensors.py - a module for interfacing to the sensors
+'''Module for interfacing with TeraHz sensors'''
+# Copyright 2019 Kristjan Komloši
+# All code in this file is licensed under the ISC license, provided in LICENSE.txt
+import serial as ser
+import pandas as pd
+import smbus2
+
+class Spectrometer:
+ '''Class representing the AS7265X specrometer'''
+ def initializeSensor(self):
+ '''confirm the sensor is responding and proceed\
+ with spectrometer initialization'''
+ try:
+ rstring = 'undefined' # just need it set to a value
+ self.setParameters({'gain': 0})
+ self.serialObject.write(b'AT\n')
+ rstring = self.serialObject.readline().decode()
+ if rstring == 'undefined':
+ raise Exception # sensor didn't respond
+ if rstring == 'OK':
+ pass # handshake passed
+ if rstring == 'ERROR':
+ raise Exception # sensor is in error state
+ except:
+ raise Exception(
+ 'An exception ocurred when performing spectrometer handshake')
+
+ def setParameters(self, parameters):
+ '''applies the parameters like LED light and gain to the spectrometer'''
+ try:
+ if 'it_time' in parameters:
+ it_time = int(parameters['it_time'])
+ if it_time <= 0:
+ it_time = 1
+ self.serialObject.write(
+ 'ATINTTIME={}\n'.format(str(it_time)).encode())
+ self.serialObject.readline()
+
+ if 'gain' in parameters:
+ gain = int(parameters['gain'])
+ if gain < 0 or gain > 3:
+ gain = 1
+ self.serialObject.write('ATGAIN={}\n'.format(gain).encode())
+ self.serialObject.readline()
+
+ if 'led' in parameters:
+ led = bool(parameters['led'])
+ if led:
+ led = 1
+ else:
+ led = 0
+ self.serialObject.write('ATLED3={}\n'.format(led).encode())
+ self.serialObject.readline()
+ except:
+ raise Exception(
+ 'An exception occured during spectrometer initialization')
+
+ def getData(self):
+ '''Returns spectral data in a pandas DataFrame.'''
+ try:
+ self.serialObject.write(b'ATCDATA\n')
+ rawresp = self.serialObject.readline().decode()
+ except:
+ raise Exception(
+ 'An exception occurred when polling for spectrometer data')
+ else:
+ responseorder = [i for i in 'RSTUVWGHIJKLABCDEF']
+ realorder = [i for i in 'ABCDEFGHRISJTUVWKL']
+ response = pd.Series(
+ [float(i) / 35.0 for i in rawresp[:-3].split(',')], index=responseorder)
+ return pd.DataFrame(response, index=realorder, columns=['uW/cm^2']).to_dict()['uW/cm^2']
+
+ def __init__(self, path='/dev/ttyUSB0', baudrate=115200, tout=1):
+ self.path = path
+ self.baudrate = baudrate
+ self.timeout = 1
+ try:
+ self.serialObject = ser.Serial(path, baudrate, timeout=tout)
+ except:
+ raise Exception(
+ 'An exception occured when opening the serial port at {}'.format(path))
+ else:
+ self.initializeSensor()
+
+
+class LxMeter:
+ '''Class representing the APDS-9301 digital photometer.'''
+ def __init__(self, busNumber=1, addr=0x39):
+ self.addr = addr
+ try:
+ # initialize the SMBus interface
+ self.bus = smbus2.SMBus(busNumber)
+ except:
+ raise Exception(
+ 'An exception occured opening the SMBus {}'.format(self.bus))
+
+ try:
+ self.bus.write_byte_data(
+ self.addr, 0xa0, 0x03) # enable the sensor
+ self.setGain(16)
+ except:
+ raise Exception('An exception occured when enabling lux meter')
+
+ def setGain(self, gain):
+ '''Set the sensor gain. Either 1 or 16.'''
+ if gain == 1:
+ try:
+ temp = self.bus.read_byte_data(self.addr, 0xa1)
+ self.bus.write_byte_data(self.addr, 0xa1, 0xef & temp)
+ except:
+ raise Exception(
+ 'An exception occured when setting lux meter gain')
+ if gain == 16:
+ try:
+ temp = self.bus.read_byte_data(self.addr, 0xa1)
+ self.bus.write_byte_data(self.addr, 0xa1, 0x10 | temp)
+ except:
+ raise Exception(
+ 'An exception occured when setting lux meter gain')
+ else:
+ raise Exception('Invalid gain')
+
+ def getGain(self):
+ '''Get the gain from the sensor.'''
+ try:
+ if self.bus.read_byte_data(self.addr, 0xa1) & 0x10 == 0x10:
+ return 16
+ if self.bus.read_byte_data(self.addr, 0xa1) & 0x10 == 0x00:
+ return 1
+ raise Exception('An error occured when getting lux meter gain')
+ # Under normal conditions, this raise is unreachable.
+ except:
+ raise Exception('An error occured when getting lux meter gain')
+
+ def setIntTime(self, time):
+ '''Set the lux sensor integration time. 0 to including 2'''
+ if time < 0 or time > 2:
+ raise Exception('Invalid integration time')
+ try:
+ temp = self.bus.read_byte_data(self.addr, 0xa1)
+ self.bus.write_byte_data(self.addr, 0xa1, (temp & 0xfc) | time)
+ except:
+ raise Exception(
+ 'An exception occured setting lux integration time')
+
+ def getIntTime(self):
+ '''Get the lux sensor integration time.'''
+ try:
+ return self.bus.read_byte_data(self.addr, 0xa1) & 0x03
+ except:
+ raise Exception(
+ 'An exception occured getting lux integration time')
+
+ def getData(self):
+ '''return the calculated lux value'''
+ try:
+ chA = self.bus.read_word_data(self.addr, 0xac)
+ chB = self.bus.read_word_data(self.addr, 0xae)
+ except:
+ raise Exception('An exception occured fetching lux channels')
+
+ # scary computations ahead! refer to the apds-9301 datasheet!
+ if chB / chA <= 0.5 and chB / chA > 0:
+ lux = 0.0304 * chA - 0.062 * chA * (chB / chA)**1.4
+ elif chB / chA <= 0.61 and chB / chA > 0.5:
+ lux = 0.0224 * chA - 0.031 * chB
+ elif chB / chA <= 0.8 and chB / chA > 0.61:
+ lux = 0.0128 * chA - 0.0153 * chB
+ elif chB / chA <= 1.3 and chB / chA > 0.8:
+ lux = 0.00146 * chA - 0.00112 * chB
+ else:
+ lux = 0
+ return lux
+
+
+class UVSensor:
+ '''Class representing VEML6075 UVA/B meter'''
+ def __init__(self, bus=1, addr=0x10):
+ self.addr = addr
+ try:
+ self.bus = smbus2.SMBus(bus)
+ except:
+ raise Exception(
+ 'An exception occured opening SMBus {}'.format(bus))
+
+ try:
+ # enable the sensor and set the integration time
+ self.bus.write_byte_data(self.addr, 0x00, 0b00010000)
+ except:
+ raise Exception(
+ 'An exception occured when initalizing the UV Sensor')
+
+ def getABI(self):
+ '''Calculates the UVA and UVB irradiances,
+ along with UV index. Returns [a,b,i]'''
+
+ try:
+ # read the raw UVA, UVB and compensation values from the sensor
+ aRaw = self.bus.read_word_data(self.addr, 0x07)
+ bRaw = self.bus.read_word_data(self.addr, 0x09)
+ c1 = self.bus.read_word_data(self.addr, 0x0a)
+ c2 = self.bus.read_word_data(self.addr, 0x0b)
+ except:
+ raise Exception('An exception occured when fetching raw UV data')
+ # scary computations ahead! refer to Vishay app note 84339 and Sparkfun
+ # VEML6075 documentation.
+
+ # compensate for visible and IR noise
+ aCorr = aRaw - 2.22 * c1 - 1.33 * c2
+ bCorr = bRaw - 2.95 * c1 - 1.74 * c2
+
+ # convert values into irradiances
+ a = aCorr * 1.06
+ b = bCorr * 0.48
+
+ # zero out negative results (readings with no uv)
+ if a < 0:
+ a = 0
+ if b < 0:
+ b = 0
+ # last, calculate the UV index
+ i = (a + b) / 2
+
+ return [a, b, i]
+
+ def getA(self):
+ '''Returns UVA value. A getABI() wrapper.'''
+ return self.getABI()[0]
+
+ def getB(self):
+ '''Returns UVB value. A getABI() wrapper.'''
+ return self.getABI()[1]
+
+ def getI(self):
+ '''Returns UV index. A getABI() wrapper.'''
+ return self.getABI()[2]
+
+ def on(self):
+ '''Turns the UV sensor on after shutdown.'''
+ try:
+ # write the default value for power on
+ # no configurable params = no bitmask
+ self.bus.write_byte_data(self.addr, 0x00, 0x10)
+ except:
+ raise Exception(
+ 'An exception occured when turning the UV sensor on')
+
+ def off(self):
+ '''Shuts the UV sensor down.'''
+ try:
+ # write the default value + the shutdown bit
+ # no configurable params = no bitmask
+ self.bus.write_byte_data(self.addr, 0x00, 0x11)
+ except:
+ raise Exception(
+ 'An exception occured when shutting the UV sensor down')
diff --git a/backend/storage.py b/backend/storage.py
new file mode 100644
index 0000000..6072d45
--- /dev/null
+++ b/backend/storage.py
@@ -0,0 +1,37 @@
+# storage.py - storage backend for TeraHz
+'''TeraHz storage backend'''
+# Copyright Kristjan Komloši 2019
+# All code in this file is licensed under the ISC license,
+# provided in LICENSE.txt
+
+
+import sqlite3
+class jsonStorage:
+ '''Class for simple sqlite3 database of JSON entries'''
+ def __init__(self, dbFile):
+ '''Storage object constructor. Argument is filename'''
+ self.db = sqlite3.connect(dbFile)
+
+ def listJSONs(self):
+ '''Returns a list of all existing entries.'''
+ c = self.db.cursor()
+ c.execute('SELECT * FROM storage')
+ result = c.fetchall()
+ c.close()
+ return result
+
+ def storeJSON(self, jsonString, comment):
+ '''Stores a JSON entry along with a timestamp and a comment.'''
+ c = self.db.cursor()
+ c.execute(('INSERT INTO storage VALUES (datetime'
+ '(\'now\', \'localtime\'), ?, ?)'), (comment, jsonString))
+ c.close()
+ self.db.commit()
+
+ def retrieveJSON(self, timestamp):
+ '''Retrieves a JSON entry. Takes a timestamp string'''
+ c = self.db.cursor()
+ c.execute('SELECT * FROM storage WHERE timestamp = ?', (timestamp,))
+ result = c.fetchall()
+ c.close()
+ return result
diff --git a/backend/terahz.conf b/backend/terahz.conf
new file mode 100644
index 0000000..e69de29
diff --git a/backend/terahz.fcgi b/backend/terahz.fcgi
new file mode 100644
index 0000000..52ef4f3
--- /dev/null
+++ b/backend/terahz.fcgi
@@ -0,0 +1,7 @@
+#!/usr/bin/python3
+# Minimal flup configuration for Flask
+from flup.server.fcgi import WSGIServer
+from app import app
+
+if __name__ == '__main__':
+ WSGIServer(app, bindAddress='/var/www/api/terahz.sock').run()
diff --git a/docs/build.md b/docs/build.md
new file mode 100644
index 0000000..a5b56df
--- /dev/null
+++ b/docs/build.md
@@ -0,0 +1,9 @@
+# TeraHz build guide
+In its early development phase, TeraHz was hard and time-consuming to compile and install.
+This is not case now, as the more optimized DietPi Linux distribution allows
+better performance and simpler configuration than formerly used Raspbian.
+
+## Downloading the preconfigured image
+DietPi needs some initial configuration to support TeraHz. To shorten the process,
+Preconfigured SD card images are available for download under the release tab in
+the Github repository
diff --git a/docs/dependencies.md b/docs/dependencies.md
new file mode 100644
index 0000000..e7e0aab
--- /dev/null
+++ b/docs/dependencies.md
@@ -0,0 +1,26 @@
+# Development-stable dependencies
+The current development version of TeraHz has been verified to work with:
+
+ - Raspbian Stretch (9)
+ - Python 3.6.8 (built from source code and altinstall'd)
+ - Module versions (direct `pip3.6 list` output):
+
+```
+ Package Version
+ --------------- ---------
+ Click 7.0
+ Flask 1.0.3
+ itsdangerous 1.1.0
+ Jinja2 2.10.1
+ MarkupSafe 1.1.1
+ numpy 1.16.4
+ pandas 0.24.2
+ pip 18.1
+ pyserial 3.4
+ python-dateutil 2.8.0
+ pytz 2019.1
+ setuptools 40.6.2
+ six 1.12.0
+ smbus 1.1.post2
+ Werkzeug 0.15.4
+```
diff --git a/docs/dev-guide.md b/docs/dev-guide.md
new file mode 100644
index 0000000..5420236
--- /dev/null
+++ b/docs/dev-guide.md
@@ -0,0 +1,95 @@
+# TeraHz developer's guide
+This document explains how TeraHz works. It's a good starting point for developers
+and an interesting read for the curious.
+
+# Hardware
+TeraHz was developed on and for the Raspberry Pi 3 Model B+. Compatibility with
+other Raspberries can probably be achieved by tweaking the device paths in the
+`app.py` file, but isn't confirmed at this point. Theoretically, 3 Model B and
+Zero W should work out of the box, but models without Wi-Fi will need an
+external Wi-Fi adapter if Wi-Fi functionality is desired. The practicality of
+compiling Python on the first generation of Raspberry Pis is also very
+questionable.
+
+Sensors required for operation are:
++ AS7265x
++ VEML6075
++ APDS-9301
+
+They provide the spectrometry data, UV data and illuminance data, respectively.
+They all support I2C, AS7265x supports UART in addition.
+
+The sensors leech power from the GPIO connector, thus eliminating the need for a
+separate power supply. The necessary power for the whole system is delivered through
+the Raspberry's USB port. This also allows for considerable versatility, as it
+enables the resulting device to be either wall-powered or battery-powered.
+In a portable configuration, I used a one-cell power bank, which allowed for
+about 45 minutes of continuous operation.
+
+## AS7265x chipset
+_[Datasheet][1ds] [Buy breakout board][1]_
+
+This chipset supports either I2C or UART. Because transferring large amounts of
+data over I2C is rather cumbersome, TeraHz uses AS7265x in UART mode.
+
+This chipset consists of three rather small surface-mounted chips and requires
+an EEPROM. To lower the complexity of assembly for the end-user, I recommend
+using a breakout board.
+
+The serial UART connection operates at 115200 baud, which seems to be the
+standard for most recent embedded peripherals. As with most serial hardware,
+the TxD and RxD lines must be crossed over when connecting to the processor.
+
+Communication with the sensor is simple and clear through AT commands. There's
+a lot of them, all documented inside the datasheet, but the most important one
+is `ATGETCDATA`, which returns the calibrated spectral data from the sensors.
+
+The data is returned in the form of a comma-separated list of floating point
+values, ending with a newline. The order is alphabetical, which is __different
+from wavelength order__. See the datasheet for more information.
+
+## VEML6075
+_[Datasheet][2ds] [Buy breakout board][2]_
+
+This chip communicates through I2C and provides TeraHz with UVA and UVB
+irradiance readings. It's not an ideal chip for this task, as it's been marked
+End-of-Life by Vishay and it'll have to be replaced with a better one in future
+hardware versions of TeraHz.
+
+The chip resides at the I2C address `0x10`. There's not a lot of communication
+required: at initialization, the integration time has to be set and after that,
+the sensor is ready to go.
+
+16-bit UV values lie in two two-byte registers, `0x07` for UVA and `0x09` for
+UVB. For correct result conversion, there are also two correction registers,
+UVCOMP_1 and 2, located at `0x0A` and `0x0B`, respectively.
+
+To convert these four values into irradiances, they must be multiplied by
+certain constants, somewhat loosely defined in the sensor datasheet. Keep in
+mind that the way of computing the "irradiance" is very much experimentally
+derived, and even Vishay's tech support doesn't know how exactly to calculate
+the irradiance.
+
+## APDS-9301
+_[Datasheet][3ds] [Buy breakout board][3]_
+This chip measures illuminance in luxes and like the VEML6075, connects through
+I2C. Unlike the VEML6075, this chip is very good at its job, providing accurate
+and fast results without undefined mathematics or required calibration.
+
+At power-on, it needs to be enabled and the sensor gain set to the high setting,
+as the formula for Lux calculation is only defined for that setting. This
+initialization is handled by the sensors module.
+
+The lux reading is derived from two channels, descriptively called CH0 and CH1,
+residing in respective 16-bit registers at addresses `0xAC` and `0xAE`. After a
+successful read of both data registers, the lux value can be derived using the
+formula in the sensor's datasheet.
+
+
+
+[1]: https://www.tindie.com/products/onehorse/compact-as7265x-spectrometer/
+[2]: https://www.sparkfun.com/products/15089
+[3]: https://www.sparkfun.com/products/14350
+[1ds]: sensor-docs/AS7265x.pdf
+[2ds]: sensor-docs/veml6075.pdf
+[3ds]: sensor-docs/APDS-9301.pdf
diff --git a/docs/electrical.md b/docs/electrical.md
new file mode 100644
index 0000000..efde45b
--- /dev/null
+++ b/docs/electrical.md
@@ -0,0 +1,55 @@
+# TeraHz Electrical Guide
+This section briefly explains the neccessary electrical connections between the
+Raspberry Pi and the sensors you'll need to make to ensure correct and safe
+operation.
+
+As mentioned before, TeraHz requires 3 sensors to operate. The simpler UVA/UVB
+sensor and the ambient light analyzer connect to the Raspberry's SMBus (I2C)
+bus, while the spectrometer connects via high-speed UART.
+
+
+
+## PCBs vs breakout boards & jumpers
+The Raspberry Pi GPIO port includes enough power pins to require only jumper
+cables to connect the sensors to the Raspberry Pi. However, this is not a great
+idea. During development, jumper cables have repeatedly been proven to be an
+unreliable nuisance, and their absolute lack of rigidity helped me fry one of my
+development Raspberry Pis. For this reason, I wholeheartedly recommend using a
+simple PCB to route the connections from the Pi to the sensors. At this time,
+there is no official TeraHz PCB, but it shall be announced and included in the
+project when basic testing will be done.
+
+GPIO can be routed to the PCB with a standard old IDE disk cable, and terminated
+with another 40-pin connector at the PCB. Sensor breakouts should be mounted
+<<<<<<< HEAD
+through standard 0.1" connectors, male on the sensor breakout and female on the
+PCB. A shitty add-on header and a shitty add-on header v1.69bis can't hurt, either.
+=======
+through standard 0.1" connectors, male on the sensor brakout and female on the
+PCB. A shitty addon header and a shitty addon header v1.69bis can't hurt, either.
+>>>>>>> fd1f07d40dace3e003e49377d4771de53f8bdeb8
+
+## SMBus sensors
+SMBus is a well-defined version of the well-known I2C bus, widely used
+in computer motherboards for low-band bandwidth communication with various ICs,
+especially sensors and power-supply related devices. This bus is broken out on
+the Raspberry Pi GPIO port as the "I2C1" bus (see picture).
+
+Pins are familiarly marked as SDA and SCL, the same as with classic I2C. They
+connect to the SDA and SCL pins on the VEML6075 and APDS-9301 sensor.
+
+## UART sensor
+<<<<<<< HEAD
+Spectral sensor attaches through the UART port on the Raspberry pi (see picture).
+=======
+Spectrometry sensor attaches through the UART port on the Raspberry pi (see picture).
+>>>>>>> fd1f07d40dace3e003e49377d4771de53f8bdeb8
+
+The Tx and Rx lines must cross over, connecting the sensor's Tx line to the
+computer's Rx line and vice versa.
+
+## Power supply
+As the sensors require only a small amount of power, they can be powered directly from the Raspberry Pi itself, leeching power from the 3.3V lines.
+
+## Ground
+There's not a lot to say here, connect sensor GND to Pi's GND.
diff --git a/docs/imgs/logo-sq.png b/docs/imgs/logo-sq.png
new file mode 100755
index 0000000..97a8df6
Binary files /dev/null and b/docs/imgs/logo-sq.png differ
diff --git a/docs/imgs/raspi-config/1.png b/docs/imgs/raspi-config/1.png
new file mode 100644
index 0000000..70e0d4c
Binary files /dev/null and b/docs/imgs/raspi-config/1.png differ
diff --git a/docs/imgs/raspi-config/10.png b/docs/imgs/raspi-config/10.png
new file mode 100644
index 0000000..565ca86
Binary files /dev/null and b/docs/imgs/raspi-config/10.png differ
diff --git a/docs/imgs/raspi-config/11.png b/docs/imgs/raspi-config/11.png
new file mode 100644
index 0000000..0af2616
Binary files /dev/null and b/docs/imgs/raspi-config/11.png differ
diff --git a/docs/imgs/raspi-config/12.png b/docs/imgs/raspi-config/12.png
new file mode 100644
index 0000000..0f8a3ad
Binary files /dev/null and b/docs/imgs/raspi-config/12.png differ
diff --git a/docs/imgs/raspi-config/14.png b/docs/imgs/raspi-config/14.png
new file mode 100644
index 0000000..a2f0505
Binary files /dev/null and b/docs/imgs/raspi-config/14.png differ
diff --git a/docs/imgs/raspi-config/15.png b/docs/imgs/raspi-config/15.png
new file mode 100644
index 0000000..d4fd3b2
Binary files /dev/null and b/docs/imgs/raspi-config/15.png differ
diff --git a/docs/imgs/raspi-config/16.png b/docs/imgs/raspi-config/16.png
new file mode 100644
index 0000000..d4fd3b2
Binary files /dev/null and b/docs/imgs/raspi-config/16.png differ
diff --git a/docs/imgs/raspi-config/17.png b/docs/imgs/raspi-config/17.png
new file mode 100644
index 0000000..99c4e94
Binary files /dev/null and b/docs/imgs/raspi-config/17.png differ
diff --git a/docs/imgs/raspi-config/18.png b/docs/imgs/raspi-config/18.png
new file mode 100644
index 0000000..a14bcc8
Binary files /dev/null and b/docs/imgs/raspi-config/18.png differ
diff --git a/docs/imgs/raspi-config/19.png b/docs/imgs/raspi-config/19.png
new file mode 100644
index 0000000..93065d4
Binary files /dev/null and b/docs/imgs/raspi-config/19.png differ
diff --git a/docs/imgs/raspi-config/2.png b/docs/imgs/raspi-config/2.png
new file mode 100644
index 0000000..1da167f
Binary files /dev/null and b/docs/imgs/raspi-config/2.png differ
diff --git a/docs/imgs/raspi-config/3.png b/docs/imgs/raspi-config/3.png
new file mode 100644
index 0000000..75d4876
Binary files /dev/null and b/docs/imgs/raspi-config/3.png differ
diff --git a/docs/imgs/raspi-config/4.png b/docs/imgs/raspi-config/4.png
new file mode 100644
index 0000000..00bcfb1
Binary files /dev/null and b/docs/imgs/raspi-config/4.png differ
diff --git a/docs/imgs/raspi-config/5.png b/docs/imgs/raspi-config/5.png
new file mode 100644
index 0000000..74d255a
Binary files /dev/null and b/docs/imgs/raspi-config/5.png differ
diff --git a/docs/imgs/raspi-config/6.png b/docs/imgs/raspi-config/6.png
new file mode 100644
index 0000000..60571de
Binary files /dev/null and b/docs/imgs/raspi-config/6.png differ
diff --git a/docs/imgs/raspi-config/7.png b/docs/imgs/raspi-config/7.png
new file mode 100644
index 0000000..c7f5b6f
Binary files /dev/null and b/docs/imgs/raspi-config/7.png differ
diff --git a/docs/imgs/raspi-config/8.png b/docs/imgs/raspi-config/8.png
new file mode 100644
index 0000000..1647cb9
Binary files /dev/null and b/docs/imgs/raspi-config/8.png differ
diff --git a/docs/imgs/raspi-config/9.png b/docs/imgs/raspi-config/9.png
new file mode 100644
index 0000000..5ad2ae1
Binary files /dev/null and b/docs/imgs/raspi-config/9.png differ
diff --git a/docs/imgs/raspi-pinout.png b/docs/imgs/raspi-pinout.png
new file mode 100644
index 0000000..f657d35
Binary files /dev/null and b/docs/imgs/raspi-pinout.png differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..d18e29f
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,3 @@
+
+# TeraHz documentation - index
+This is the starting point of TeraHz documentation.
diff --git a/docs/sensor-docs/APDS-9301.pdf b/docs/sensor-docs/APDS-9301.pdf
new file mode 100644
index 0000000..a8bbe43
Binary files /dev/null and b/docs/sensor-docs/APDS-9301.pdf differ
diff --git a/docs/sensor-docs/AS7265x.pdf b/docs/sensor-docs/AS7265x.pdf
new file mode 100644
index 0000000..6778c55
Binary files /dev/null and b/docs/sensor-docs/AS7265x.pdf differ
diff --git a/docs/sensor-docs/veml6075.pdf b/docs/sensor-docs/veml6075.pdf
new file mode 100644
index 0000000..1253f33
Binary files /dev/null and b/docs/sensor-docs/veml6075.pdf differ
diff --git a/etcs/dnsmasq.conf b/etcs/dnsmasq.conf
new file mode 100644
index 0000000..335be3b
--- /dev/null
+++ b/etcs/dnsmasq.conf
@@ -0,0 +1,3 @@
+interface=wlan0
+ dhcp-range=192.168.1.10,192.168.1.100,255.255.255.0,24h
+ address=/terahz.site/192.168.1.1
diff --git a/etcs/hostapd/edit_ssid.sh b/etcs/hostapd/edit_ssid.sh
new file mode 100755
index 0000000..1e6bc89
--- /dev/null
+++ b/etcs/hostapd/edit_ssid.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+# edit_ssid.sh - edits hostapd.conf and sets a MAC address-based SSID
+cd `dirname $0`
+ssid=`ip link | awk '/wlan0/ {getline; print $2}' | awk -v FS=':' '{printf("TeraHz_%s%s%s\n", $4, $5, $6)}'`
+sed "/ssid=.*/s/ssid=.*/ssid=$ssid/" hostapd.conf > newconf
+mv newconf hostapd.conf
diff --git a/etcs/hostapd/hostapd.conf b/etcs/hostapd/hostapd.conf
new file mode 100644
index 0000000..07bd7dd
--- /dev/null
+++ b/etcs/hostapd/hostapd.conf
@@ -0,0 +1,9 @@
+interface=wlan0
+hw_mode=g
+channel=8
+wpa=2
+wpa_key_mgmt=WPA-PSK
+wpa_pairwise=TKIP
+rsn_pairwise=CCMP
+ssid=TeraHz
+wpa_passphrase=terahertz
diff --git a/etcs/install.sh b/etcs/install.sh
new file mode 100755
index 0000000..d3c1304
--- /dev/null
+++ b/etcs/install.sh
@@ -0,0 +1,18 @@
+# install.sh - install TeraHz onto a Raspbian or DietPi installation
+apt -y update
+apt -y full-upgrade
+apt install -y python3 python3-pip lighttpd dnsmasq hostapd libatlas-base-dev
+pip3 install numpy pandas flask smbus2 pyserial gunicorn
+
+cp -R hostapd/ /etc
+chmod +rx /etc/hostapd/edit_ssid.sh
+cp dnsmasq.conf /etc
+cp rc.local /etc
+cp interfaces-terahz /etc/network/interfaces.d/
+
+cp -R ../frontend/* /var/www/html
+mkdir -p /usr/local/lib/terahz
+cp -R ../backend/* /usr/local/lib/terahz
+
+systemctl unmask dnsmasq hostapd lighttpd
+systemctl enable dnsmasq hostapd lighttpd
diff --git a/etcs/interfaces-terahz b/etcs/interfaces-terahz
new file mode 100644
index 0000000..4370c25
--- /dev/null
+++ b/etcs/interfaces-terahz
@@ -0,0 +1,3 @@
+iface wlan0 inet static
+ address 192.168.1.1
+ netmask 255.255.255.0
diff --git a/etcs/lighttpd/lighttpd.conf b/etcs/lighttpd/lighttpd.conf
new file mode 100644
index 0000000..80e0191
--- /dev/null
+++ b/etcs/lighttpd/lighttpd.conf
@@ -0,0 +1,27 @@
+server.modules = (
+ "mod_access",
+ "mod_alias",
+ "mod_compress",
+ "mod_redirect"
+)
+
+server.document-root = "/var/www/html"
+server.upload-dirs = ( "/var/cache/lighttpd/uploads" )
+server.errorlog = "/var/log/lighttpd/error.log"
+server.pid-file = "/var/run/lighttpd.pid"
+server.username = "www-data"
+server.groupname = "www-data"
+server.port = 80
+
+
+index-file.names = ( "index.php", "index.html", "index.lighttpd.html" )
+url.access-deny = ( "~", ".inc" )
+static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
+
+compress.cache-dir = "/var/cache/lighttpd/compress/"
+compress.filetype = ( "application/javascript", "text/css", "text/html", "text/plain" )
+
+# default listening port for IPv6 falls back to the IPv4 port
+include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
+include_shell "/usr/share/lighttpd/create-mime.assign.pl"
+include_shell "/usr/share/lighttpd/include-conf-enabled.pl"
diff --git a/etcs/rc.local b/etcs/rc.local
new file mode 100644
index 0000000..f44e1e5
--- /dev/null
+++ b/etcs/rc.local
@@ -0,0 +1,16 @@
+#!/bin/sh -e
+#
+# rc.local
+#
+# This script is executed at the end of each multiuser runlevel.
+# Make sure that the script will "exit 0" on success or any other
+# value on error.
+#
+# In order to enable or disable this script just change the execution
+# bits.
+#
+# By default this script does nothing.
+/etc/hostapd/edit_ssid.sh &
+/usr/local/lib/terahz/run.sh &
+sleep 3
+service hostapd restart # restart hostapd to prevent a weird startup race condition
diff --git a/frontend/frontend.js b/frontend/frontend.js
new file mode 100644
index 0000000..d3b422c
--- /dev/null
+++ b/frontend/frontend.js
@@ -0,0 +1,46 @@
+// All code in this file is licensed under the ISC license, provided in LICENSE.txt
+$('#update').click(function () {
+ updateData();
+});
+// jQuery event binder
+
+function updateData () {
+ const url = 'http://' + window.location.hostname + ':5000/data';
+ $.ajax({ // spawn an AJAX request
+ url: url,
+ success: function (data, status) {
+ console.log(data);
+ graphSpectralData(data[0], 0);
+ fillTableData(data);
+ },
+ timeout: 2500 // this should be a pretty sane timeout
+ });
+}
+
+function graphSpectralData (obj, dom) {
+ // graph spectral data in obj into dom
+ var graphPoints = [];
+ var graphXTicks = [];
+
+ Object.keys(obj).forEach((element, index) => {
+ graphPoints.push([index, obj[element]]); // build array of points
+ graphXTicks.push([index, element]); // build array of axis labels
+ });
+ // console.log(graphPoints);
+ const options = {
+ grid: {color: 'white'},
+ xaxis: {ticks: graphXTicks}
+ };
+ $.plot('#graph', [graphPoints], options);
+ // flot expects an array of arrays (lines) of 2-element arrays (points)
+}
+
+function fillTableData (obj) {
+ // fill the obj data into HTML tables
+ Object.keys(obj[0])
+ .forEach((element) => { $('#' + element).text(obj[0][element]); });
+ $('#lx').text(obj[1]);
+ $('#uva').text(obj[2][0]);
+ $('#uvb').text(obj[2][1]);
+ $('#uvi').text(obj[2][2]);
+}
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..924da3d
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,162 @@
+
+
+
+
TeraHz+
+| Band | +Wavelength [nm] | +Irradiance [μW/cm²] | +
|---|---|---|
| A | +410 nm | +--- | +
| B | +435 nm | +--- | +
| C | +460 nm | +--- | +
| D | +485 nm | +--- | +
| E | +510 nm | +--- | +
| F | +535 nm | +--- | +
| G | +560 nm | +--- | +
| H | +585 nm | +--- | +
| R | +610 nm | +--- | +
| I | +645 nm | +--- | +
| S | +680 nm | +--- | +
| J | +705 nm | +--- | +
| T | +730 nm | +--- | +
| U | +760 nm | +--- | +
| V | +810 nm | +--- | +
| W | +860 nm | +--- | +
| K | +900 nm | +--- | +
| L | +940 nm | +--- | +
| Parameter | +Value | +
|---|---|
| Illuminance [lx] | +--- | +
| UVA irradiance [μW/cm²] | +--- | +
| UVB irradiance [μW/cm²] | +--- | +
| UVA/UVB average [μW/cm²] | +--- | +