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 + +[![Documentation Status](https://readthedocs.org/projects/terahz/badge/?version=latest)](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. + +![pinout](imgs/raspi-pinout.png) + +## 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 logo +# 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 + + + +
+

TeraHz

+ +
+
+ +

+

+

Spectrogram

+
+

Spectral readings

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BandWavelength [nm]Irradiance [μW/cm²]
A410 nm---
B435 nm---
C460 nm---
D485 nm---
E510 nm---
F535 nm---
G560 nm---
H585 nm---
R610 nm---
I645 nm---
S680 nm---
J705 nm---
T730 nm---
U760 nm---
V810 nm---
W860 nm---
K900 nm---
L940 nm---
+
+

Lux and UV readings

+ + + + + + + + + + + + + + + + + + + + + + + +
ParameterValue
Illuminance [lx]---
UVA irradiance [μW/cm²]---
UVB irradiance [μW/cm²]---
UVA/UVB average [μW/cm²]---
+
+ + + + + + + + + + + + + + diff --git a/frontend/stylesheet.css b/frontend/stylesheet.css new file mode 100644 index 0000000..fb4556b --- /dev/null +++ b/frontend/stylesheet.css @@ -0,0 +1,69 @@ +body { + background: rgb(2,0,36); + background: linear-gradient(138deg, rgba(2,0,36,1) 0%, rgba(5,5,209,1) 51%, rgba(0,212,255,1) 100%); + color: white; +} +#A { + background-color: #7e00db; + color: white; +} + +#B { + background-color: #2300ff; + color: white; +} + +#C { + background-color: #007bff; + color: white; +} + +#D { + background-color: #00eaff; +} + +#E { + background-color: #00ff00; +} + +#F { + background-color: #70ff00; +} + +#G { + background-color: #c3ff00; +} + +#H { + background-color: #ffef00; +} + +#R { + background-color: #ff9b00; +} + +#I, #S { + background-color: #ff0000; +} + +#J { + background-color: #f60000; +} + +#T { + background-color: #c80000; + color: white; +} + +#U { + background-color: #8d0000; + color: white; +} + +.table-secondary { + color: black; +} + +.flot-tick-label { + fill: white; +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..b791f94 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,9 @@ +site_name: TeraHz Documentation +theme: readthedocs +nav: +- Start page: 'index.md' +- Advanced guides: + - Build guide: 'build.md' + - Developer's guide: 'dev-guide.md' + - Electrical connections: 'electrical.md' + - Latest dependencies: 'dependencies.md' diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 0000000..05494f1 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,3 @@ +version: 2 +mkdocs: + configuration: mkdocs.yml diff --git a/sensor_docs/AS7265_CommandSet 12V1_0.xlsx b/sensor_docs/AS7265_CommandSet 12V1_0.xlsx new file mode 100644 index 0000000..6b30aa6 Binary files /dev/null and b/sensor_docs/AS7265_CommandSet 12V1_0.xlsx differ diff --git a/sensor_docs/AS7265x_DS000612_1-00.pdf b/sensor_docs/AS7265x_DS000612_1-00.pdf new file mode 100644 index 0000000..6778c55 Binary files /dev/null and b/sensor_docs/AS7265x_DS000612_1-00.pdf differ diff --git a/sensor_docs/PoUSB12 user manual.pdf b/sensor_docs/PoUSB12 user manual.pdf new file mode 100644 index 0000000..0fbfa82 Binary files /dev/null and b/sensor_docs/PoUSB12 user manual.pdf differ diff --git a/sensor_docs/demo.epgz b/sensor_docs/demo.epgz new file mode 100644 index 0000000..dc49227 Binary files /dev/null and b/sensor_docs/demo.epgz differ diff --git a/sensor_docs/demo.odt b/sensor_docs/demo.odt new file mode 100644 index 0000000..caa5dd4 Binary files /dev/null and b/sensor_docs/demo.odt differ diff --git a/sensor_docs/demo.svg b/sensor_docs/demo.svg new file mode 100644 index 0000000..db027cc --- /dev/null +++ b/sensor_docs/demo.svg @@ -0,0 +1,131 @@ +379,35918.95,23.933333333333334#FFFFFFFF#000000FF1|'Comic Sans MS'|normal|normal|12px|none#000000FF1,1 + + + 248,2481600,1600,ref://c549d0e3af32443d81f8de3a764ef050.pngfalse#FFFFFF00#000000FF0| + + + + + + + + + + + + + + false100,0falseRaspberry Pi 3 B+#000000FF'Comic Sans MS'|normal|normal|18px|none0,0 + + Raspberry Pi 3 B+ + 200,15010,10#FFFFFFFF#000000FF1|AS7265X +Spektrometerski senzor za IR in vidno svetlobo'Comic Sans MS'|normal|normal|12px|none#000000FF1,1 + + AS7265XSpektrometerski senzor za IRin vidno svetlobo + 200,15010,10#FFFFFFFF#000000FF1|VEML6075 +Senzor UV svetlobe'Comic Sans MS'|normal|normal|12px|none#000000FF1,1 + + VEML6075Senzor UV svetlobe + 200,15010,10#FFFFFFFF#000000FF1|APDS-9301 +Merilec osvetljenosti +(lux-meter)'Comic Sans MS'|normal|normal|12px|none#000000FF1,1 + + APDS-9301Merilec osvetljenosti(lux-meter) + 200,15010,10#FFFFFFFF#000000FF1|2600 mAh Powerbank +Napaja spektrometer'Comic Sans MS'|normal|normal|12px|none#000000FF1,1 + + 2600 mAh PowerbankNapaja spektrometer + -64,92189.5,112truetruecurvyfalse#1B3280FF2|"Liberation Sans",Arial,sans-serif|normal|normal|13px|none#000000FF1,1 + + + + + + + + + + + 76,3163.8,21.066666666666663#FFFFFFFF#000000FF1|I2C +Vodilo'Comic Sans MS'|normal|normal|12px|none#000000FF1,1 + + I2CVodilo + -249,-290-223,-207truetruecurvyfalse#1B3280FF2|"Liberation Sans",Arial,sans-serif|normal|normal|13px|none#000000FF1,1 + + + + + + + + + + + -12,-13115,-216truetruecurvyfalse#1B3280FF2|"Liberation Sans",Arial,sans-serif|normal|normal|13px|none#000000FF1,1 + + + + + + + + + + + -48,-13816,-208truetruecurvyfalse#1B3280FF2|"Liberation Sans",Arial,sans-serif|normal|normal|13px|none#000000FF1,1 + + + + + + + + + + + 200,0false<span xmlns="http://www.w3.org/1999/xhtml" style="font-family: &quot;Comic Sans MS&quot;;">Računalnik na tiskanem vezju (SBC = single board computer)<br xmlns="http://www.w3.org/1999/xhtml" />Opravlja prevzem in obdelavo podatkov s senzorja, in jih preda telefonu, ki upravlja spektrmeter</span>"Liberation Sans",Arial,sans-serif|normal|normal|13px|none#000000FF + + + +
Računalnik na tiskanem vezju (SBC = single board computer)
Opravlja prevzem in obdelavo podatkov s senzorja, in jih preda telefonu, ki upravlja spektrmeter
+
+
-93,-108-53,-4truetruecurvyfalse#1B3280FF2|"Liberation Sans",Arial,sans-serif|normal|normal|13px|none#000000FF1,1 + + + + + + + + + + + 195,276318,450,ref://7214557d80314d2ea0269f3a449d0000.pngfalse#FFFFFF00#000000FF0| + + + + + + + + + + + + + + 0,0282,-4truetruecurvyfalse#1B3280FF4|3,3"Liberation Sans",Arial,sans-serif|normal|normal|13px|none#000000FF1,1 + + + + + + + + + + + false100,0falseMobilni telefon preko WiFi povezave upravlja spektrometer#000000FF'Comic Sans MS'|normal|normal|12px|none0,0 + + Mobilni telefon preko WiFi povezave upravlja spektrometer +
\ No newline at end of file diff --git a/sensor_docs/predstavitev.odt b/sensor_docs/predstavitev.odt new file mode 100644 index 0000000..3d8e1f0 Binary files /dev/null and b/sensor_docs/predstavitev.odt differ diff --git a/sensor_docs/spektralni_odziv.ods b/sensor_docs/spektralni_odziv.ods new file mode 100644 index 0000000..6d675d8 Binary files /dev/null and b/sensor_docs/spektralni_odziv.ods differ diff --git a/sensor_docs/spektralni_odziv.pdf b/sensor_docs/spektralni_odziv.pdf new file mode 100644 index 0000000..7f87e87 Binary files /dev/null and b/sensor_docs/spektralni_odziv.pdf differ diff --git a/sensor_docs/sysprep.md b/sensor_docs/sysprep.md new file mode 100644 index 0000000..631e85e --- /dev/null +++ b/sensor_docs/sysprep.md @@ -0,0 +1 @@ +# sysprep - How to prepare a Raspberry Pi 3 B+ to run TeraHz diff --git a/sensor_docs/untitled_page.png b/sensor_docs/untitled_page.png new file mode 100644 index 0000000..5f8b95c Binary files /dev/null and b/sensor_docs/untitled_page.png differ diff --git a/utils/getcdata.py b/utils/getcdata.py new file mode 100644 index 0000000..e87706e --- /dev/null +++ b/utils/getcdata.py @@ -0,0 +1,45 @@ +# getcdata.py - fetch the calibrated data from the AS7265x module +# All code in this file is licensed under the ISC license, provided in LICENSE.txt + +import serial as ser +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import time +#global variables +uartpath = '/dev/ttyUSB0' +uartbaud = 115200 +uarttout = 5 + +wl = [410, 435, 460, 485, 510, 535, 560, 585, 610, 645, 680, 705, 730, 760, 810, 860, 900, 940] +responseorder = [i for i in 'RSTUVWGHIJKLABCDEF'] # works, do NOT touch! +realorder = [i for i in 'ABCDEFGHRISJTUVWKL'] + + +print('getcdata') +print('This utility is part of the TeraHz project') + +wavelens = pd.Series(realorder) + + +plt.ion() +win = plt.figure() +spectrum=win.add_subplot(111) + + +with ser.Serial(uartpath, uartbaud, timeout=uarttout) as sensor: + while True: + sensor.write(b'ATCDATA\n') + rawresp = sensor.readline().decode() + # parses, calculates and saves the data + response = pd.Series([float(i)/35.0 for i in rawresp[:-3].split(',')], index=responseorder) + data = pd.DataFrame(response, index=realorder, columns = ['uW/cm^2']) # puts data into a DataFrame + data.insert(0, 'wavelenght', wl) #inserts a legend + print(data) + spectrum.cla() + spectrum.plot(data['wavelenght'], data['uW/cm^2']) + spectrum.set_xlabel('Valovna dolžina') + spectrum.set_ylabel('uW/cm2') + win.canvas.draw() + + time.sleep(0.1) diff --git a/utils/smbus-test.py b/utils/smbus-test.py new file mode 100644 index 0000000..1fba5c1 --- /dev/null +++ b/utils/smbus-test.py @@ -0,0 +1,12 @@ +import smbus2 + +bus = smbus2.SMBus(1) + +result = bus.read_byte_data(0x39, 0x8a) +print('LUX Meter ID = {}'.format(result)) + +result = bus.read_word_data(0x10, 0x0c) +print('UV sensor ID = {}'.format(result)) + +result = bus.read_word_data(0x39, 0xec) +print('LUX chan 0 = {}'.format(result)) diff --git a/utils/tinter-demo.py b/utils/tinter-demo.py new file mode 100644 index 0000000..5c2a035 --- /dev/null +++ b/utils/tinter-demo.py @@ -0,0 +1,22 @@ +from serial import Serial +import tkinter as tk +import pandas as pd + +from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) +from matplotlib.backend_bases import key_press_handler +from matplotlib.figure import Figure + +uartpath = '/dev/ttyUSB0' +uartbaud = 115200 +uarttout = 5 + +wl = [410, 435, 460, 485, 510, 535, 560, 585, 610, 645, 680, 705, 730, 760, 810, 860, 900, 940] +responseorder = [i for i in 'RSTUVWGHIJKLABCDEF'] # works, do NOT touch! +realorder = [i for i in 'ABCDEFGHRISJTUVWKL'] + +root = tk.Tk() +root.wm_title('TeraHz Demo') + +fig = Figure(figsize=(5, 4), dpi=100) +plot = fig.add_subplot(111) +canvas = FigureCanvasTkAgg(fig, master=root)