diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 0000000..ccc4eb2 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,16 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore + +node_modules diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a0b80b --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +<<<<<<< HEAD +# Created by .ignore support plugin (hsz.mobi) +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen +/CustomEstimator/modules/ensemble_modules/trainer_from_storage/misc/exported_model/ +/CustomEstimator/modules/ensemble_modules/trainer_from_storage/hyperparam_original.yaml +/CustomEstimator/CLI_commands/ +#/CustomEstimator/gaugeReader.ipynb +/CustomEstimator/data/ +/CustomEstimator/.ipynb_checkpoints/ +/CustomEstimator/modules/primary_models_modules/logs/exported_model/ +/CustomEstimator/modules/primary_models_modules/logs/models_ensemble/ +/CustomEstimator/modules/primary_models_modules/logs/primary_models_original/ +/CustomEstimator/modules/primary_models_modules/logs/temporary_latest_streaming_image/ +/CustomEstimator/modules/primary_models_modules/logs/temporary_models/ +/CustomEstimator/modules/primary_models_modules/logs/TensorBoard/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi-NAN/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_10/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_9_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_1/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_7_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_5_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_12/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_3/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_14/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_2_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_8/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_11_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_13_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_11/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_14_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_2/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_8_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_0/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_6_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_4_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_6/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_13/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_4/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_15/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_9/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_1_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_3_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_7/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_10_5/ +/CustomEstimator/modules/primary_models_modules/logs/models/psi_12_5/ +/CustomEstimator/modules/ensemble_modules/trainer_from_memory/ +/CustomEstimator/modules/ensemble_modules/ensemble/ +/CustomEstimator/modules/ensemble_modules/trainer_from_storage/bucket_create.sh +/CustomEstimator/modules/ensemble_modules/trainer_from_storage/bucket_data.sh +/CustomEstimator/modules/ensemble_modules/trainer_from_storage/bucket_misc.sh +/CustomEstimator/modules/ensemble_modules/trainer_from_storage/predict_base64_locally.py +/CustomEstimator/modules/ensemble_modules/trainer_from_storage/train_test_local.sh +/CustomEstimator/modules/ensemble_modules/trainer_local/ +/CustomEstimator/customEstimator.py +/CustomEstimator/DeepGauge_ML-Demo.ipynb +/CustomEstimator/train_ensemble.py +/CustomEstimator/train_gcp.sh +/CustomEstimator/train_local.sh +/CustomEstimator/trial.py +/CustomEstimator/README.md +/CustomEstimator/prediction_online.py +/CustomEstimator/prediction_batch.py +/CustomEstimator/logs/ +||||||| merged common ancestors +.idea/ +*.pyc +======= +google-cloud-vision-api/* +test-shadow-removal/* +>>>>>>> master + diff --git a/.idea/DeepGauge-ML-Demo.iml b/.idea/DeepGauge-ML-Demo.iml deleted file mode 100644 index 364d559..0000000 --- a/.idea/DeepGauge-ML-Demo.iml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 9b9fb6c..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/libraries/R_User_Library.xml b/.idea/libraries/R_User_Library.xml deleted file mode 100644 index 71f5ff7..0000000 --- a/.idea/libraries/R_User_Library.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 3a80394..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index d44ced0..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml deleted file mode 100644 index 71a5330..0000000 --- a/.idea/workspace.xml +++ /dev/null @@ -1,1005 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - = - optimize_and_save_logs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + +Gauge Reading on Live Stream + + + + + + + + + + + + + + + + + + + + Gauge Reader + + + + + + + + + + + + + + + diff --git a/FlaskApp_DeepGauge/templates/predict.html b/FlaskApp_DeepGauge/templates/predict.html new file mode 100644 index 0000000..ef60e2f --- /dev/null +++ b/FlaskApp_DeepGauge/templates/predict.html @@ -0,0 +1,31 @@ + + + + + Gauge Reader + + + + + + +

Gauge Reading

+
+ + +

{{ image_file_name }}

+ +

Predicted Label: {{ label }}

+

Accuracy : {{ accuracy }}

+ Back to Home +
+ +
+ + > +
+ + + + + diff --git a/FlaskApp_DeepGauge/uploads/.DS_Store b/FlaskApp_DeepGauge/uploads/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/FlaskApp_DeepGauge/uploads/.DS_Store differ diff --git a/FlaskApp_DeepGauge/uploads/gauge_rotated_10.jpg b/FlaskApp_DeepGauge/uploads/gauge_rotated_10.jpg new file mode 100644 index 0000000..4205b17 Binary files /dev/null and b/FlaskApp_DeepGauge/uploads/gauge_rotated_10.jpg differ diff --git a/modules/ensemble_modules/trainer_from_storage/data/ImageEveryUnit/psi_1/gauge_scale_11.jpg b/FlaskApp_DeepGauge/uploads/gauge_scale_11.jpg similarity index 100% rename from modules/ensemble_modules/trainer_from_storage/data/ImageEveryUnit/psi_1/gauge_scale_11.jpg rename to FlaskApp_DeepGauge/uploads/gauge_scale_11.jpg diff --git a/modules/ensemble_modules/trainer_from_storage/data/ImageEveryUnit/psi_1/gauge_scale_2.jpg b/FlaskApp_DeepGauge/uploads/gauge_scale_2.jpg similarity index 100% rename from modules/ensemble_modules/trainer_from_storage/data/ImageEveryUnit/psi_1/gauge_scale_2.jpg rename to FlaskApp_DeepGauge/uploads/gauge_scale_2.jpg diff --git a/modules/ensemble_modules/trainer_from_storage/data/ImageEveryUnit/psi_1/gauge_scale_20.jpg b/FlaskApp_DeepGauge/uploads/gauge_scale_20.jpg similarity index 100% rename from modules/ensemble_modules/trainer_from_storage/data/ImageEveryUnit/psi_1/gauge_scale_20.jpg rename to FlaskApp_DeepGauge/uploads/gauge_scale_20.jpg diff --git a/FlaskApp_DeepGauge/uploads/gauge_scale_6.jpg b/FlaskApp_DeepGauge/uploads/gauge_scale_6.jpg new file mode 100644 index 0000000..9112685 Binary files /dev/null and b/FlaskApp_DeepGauge/uploads/gauge_scale_6.jpg differ diff --git a/README.md b/README.md index 3649fed..1858f35 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# gaugeReader +# DeepGauge + +In this project an ensembling machine learning (ML) model is trained and used to predict pressure levels from a manual gauge' images. The steps taken to train the model and implement predictions are explained in the gaugeReader jupyter notebook. To use this notebook, first install Python 3.6 and jupyter, and then run the following command in a terminal to clone the repository and open the notebook. + +1- run 'git clone https://github.com/oci-labs/DeepGauge-ML-Demo.git' + +2- go to the repository folder in a terminal + +3- run 'jupyter notebook' + +4- in the jupyter console, go to CustomEstimator folder and open gaugeReader.ipynb + +5- run the whole document to install the required Python packages, download the images, and run the code blocks + + + +![DeepGauge](./assets/DeepGauge.jpg "GCP DeepGauge") diff --git a/app-engine/.gcloudignore b/app-engine/.gcloudignore new file mode 100644 index 0000000..60a5f00 --- /dev/null +++ b/app-engine/.gcloudignore @@ -0,0 +1,14 @@ +# This file specifies files that are *not* uploaded to Google Cloud Platform +# using gcloud. It follows the same syntax as .gitignore, with the addition of +# "#!include" directives (which insert the entries of the given .gitignore-style +# file at that point). +# +# For more information, run: +# $ gcloud topic gcloudignore +# +.gcloudignore +# If you would like to upload your .git directory, .gitignore file or files +# from your .gitignore file, remove the corresponding line +# below: +.git +.gitignore diff --git a/app-engine/README.md b/app-engine/README.md new file mode 100644 index 0000000..0f6b2e2 --- /dev/null +++ b/app-engine/README.md @@ -0,0 +1,120 @@ +``` +$ gcloud projects list +``` + +``` +gcloud config set project ocideepgauge +``` +## Running Locally +Then set environment variables before starting your application: + +``` +export GOOGLE_CLOUD_PROJECT=[your-project-id] +export PUBSUB_VERIFICATION_TOKEN=[your-verification-token] +export PUBSUB_TOPIC=[your-topic] +python main.py +``` + +Application specific + +``` +export GOOGLE_CLOUD_PROJECT=ocideepgauge +export PUBSUB_VERIFICATION_TOKEN=7fSDp7HI29 +export PUBSUB_TOPIC=flowers-prediction +python3 main.py +``` + +### Simulating push notifications +The application can send messages locally, but it is not able to receive push messages locally. You can, however, simulate a push message by making an HTTP request to the local push notification endpoint. There is an included sample_message.json. You can use curl to POST this: +``` +$ curl -i --data @sample_message.json "http://localhost:8080/pubsub/push?token=7fSDp7HI29" +``` + +Deploy AppEngine into Project +``` +gcloud app deploy --version pre-prod-1 --project MY_PROJECT +gcloud app deploy --version pre-prod-1 --project ocideepgauge +``` +Browse the AppEngine Project +``` +$ gcloud app browse -s deep-gauge +``` +View any logs +``` + $ gcloud app logs tail +``` +Create an isolated Python environment in a directory external to your project and activate it: +``` +virtualenv env +source env/bin/activate +``` +Navigate to your project directory and install dependencies: +``` +cd YOUR_PROJECT +pip install -r requirements.txt +``` +Run the application: +``` +python main.py +``` +In your web browser, enter the following address: +``` +http://localhost:8080 +``` +# Swagger, SQLLite, SQLAlchemy +## Install packages for database +``` +pip3 install flask +pip3 install connexion +pip3 install flask_marshmallow +pip3 install connexion[swagger-ui] +pip3 install google-cloud-pubsub +pip3 install google-cloud-storage +pip3 install google-resumable-media +pip3 install marshmallow-sqlalchemy + +``` +# Models +Define a schema that represents that data. + +``` +User + id + user_name + display_name + company + thumbnail + updated + +Device + id + id_user + name + image + bucket + type + location + prediction + frame_rate + refresh_rate + notes + high_threshold + low_threshold + updated + +Setting + id + id_user + type + frame_rate + refresh_rate + updated + +Reading + id + id_device + prediction + accuracy + body + timestamp +``` diff --git a/app-engine/app.yaml b/app-engine/app.yaml new file mode 100644 index 0000000..cdd9440 --- /dev/null +++ b/app-engine/app.yaml @@ -0,0 +1,17 @@ +runtime: python37 +service: default + +env_variables: + PUBSUB_TOPIC: gauge-prediction + PUBSUB_VERIFICATION_TOKEN: 7fSDp7HI29 +handlers: + # This configures Google App Engine to serve the files in the app's static + # directory. +- url: /static + static_dir: static + + # This handler routes all requests not caught above to your main app. It is + # required when static routes are defined, but can be omitted (along with + # the entire handlers section) when there are no static files defined. +- url: /.* + script: auto diff --git a/app-engine/build_database.py b/app-engine/build_database.py new file mode 100644 index 0000000..952feb1 --- /dev/null +++ b/app-engine/build_database.py @@ -0,0 +1,59 @@ +import os +from config import db +from models import Setting, Reading, Device, User, Person + +# Delete database file if it exists currently +if os.path.exists("deepgauge.db"): + os.remove("deepgauge.db") + +# Create the database +db.create_all() + +# Data to initialize database with +DEVICES = [ + { + "id_user":1, + "name":"Device One", + "image":"https://placehold.it/282x282/", + "bucket":"ocideepgauge", + "type":"Camera", + "location":"St. Louis", + "frame_rate":5, + "refresh_rate":60, + "notes":"General notes and information about Camera One", + "high_threshold":10, + "low_threshold":5 + }, + { + "id_user":1, + "name":"Device Two", + "image":"https://placehold.it/282x282/", + "bucket":"ocideepgauge", + "type":"Camera", + "location":"St. Louis", + "frame_rate":10, + "refresh_rate":120, + "notes":"General notes and information about Camera One", + "high_threshold":20, + "low_threshold":10 + } +] + +# iterate over the PEOPLE structure and populate the database +for device in DEVICES: + d = Device( + id_user=device.get("id_user"), + name=device.get("name"), + image=device.get("image"), + bucket=device.get("bucket"), + type=device.get("type"), + location=device.get("location"), + frame_rate=device.get("frame_rate"), + refresh_rate=device.get("refresh_rate"), + notes=device.get("notes"), + high_threshold=device.get("high_threshold"), + low_threshold=device.get("low_threshold") + ) + db.session.add(d) + +db.session.commit() diff --git a/app-engine/config.py b/app-engine/config.py new file mode 100644 index 0000000..99b7c71 --- /dev/null +++ b/app-engine/config.py @@ -0,0 +1,35 @@ +import os +import connexion +from flask_sqlalchemy import SQLAlchemy +from flask_marshmallow import Marshmallow + +basedir = os.path.abspath(os.path.dirname(__file__)) + +# Create the connexion application instance +connex_app = connexion.App(__name__, specification_dir=basedir) + +# Get the underlying Flask app instance +app = connex_app.app + +# Build the Sqlite ULR for SqlAlchemy +# sqlite_url = "sqlite:////" + os.path.join(basedir, "deepgauge.db") +sqlite_url = "sqlite:///:memory:" + + + +# Configure the SqlAlchemy part of the app instance +app.config["SQLALCHEMY_ECHO"] = True +app.config["SQLALCHEMY_DATABASE_URI"] = sqlite_url +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +# Configure the following environment variables via app.yaml +# This is used in the push request handler to veirfy that the request came from +# pubsub and originated from a trusted source. +app.config['PUBSUB_VERIFICATION_TOKEN'] = os.environ['PUBSUB_VERIFICATION_TOKEN'] +app.config['PUBSUB_TOPIC'] = os.environ['PUBSUB_TOPIC'] +app.config['PROJECT'] = os.environ['GOOGLE_CLOUD_PROJECT'] + +# Create the SqlAlchemy db instance +db = SQLAlchemy(app) + +# Initialize Marshmallow +ma = Marshmallow(app) diff --git a/app-engine/deepgauge.db b/app-engine/deepgauge.db new file mode 100644 index 0000000..0b3df2b Binary files /dev/null and b/app-engine/deepgauge.db differ diff --git a/app-engine/dev/sample_message.json b/app-engine/dev/sample_message.json new file mode 100644 index 0000000..5b6d29f --- /dev/null +++ b/app-engine/dev/sample_message.json @@ -0,0 +1,5 @@ +{ + "message": { + "data": "W3sicHJlZGljdGlvbiI6IDEsICJrZXkiOiAiMCIsICJzY29yZXMiOiBbMC4zNzM4ODA4MzMzODczNzQ5LCAwLjQ3NTczMzY2NzYxMjA3NTgsIDUuMTk4NDkyMDkwMTMwNDU4ZS0wNiwgMC4xNTAzNjQwNTYyMjk1OTEzNywgMy45ODAwMDI5Mjk4OTMwODk1ZS0wNiwgMS4yMjMwMzQ2MzI4NTc4ODllLTA1XX1d" + } +} diff --git a/app-engine/device.py b/app-engine/device.py new file mode 100644 index 0000000..d6fa362 --- /dev/null +++ b/app-engine/device.py @@ -0,0 +1,146 @@ +""" +This is the people module and supports all the REST actions for the +people data +""" + +from flask import make_response, abort +from config import db +from models import Device, DeviceSchema +from datetime import datetime + +def read_all(): + """ + This function responds to a request for /api/device + with the complete lists of users + + :return: json string of list of devices + """ + # Create the list of people from our data + query = Device.query.order_by(Device.id_user).all() + + # Serialize the data for the response + schema = DeviceSchema(many=True) + data = schema.dump(query).data + return data + + +def read_one(id_device): + """ + This function responds to a request for /api/device/{id_user} + with one matching user from settings + + :param id_user: Id of user to find + :return: user matching id + """ + # Get the person requested + query = Device.query.filter(Device.id == id_device).one_or_none() + + # Did we find a person? + if query is not None: + + # Serialize the data for the response + schema = DeviceSchema() + data = schema.dump(query).data + return data + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Device not found for Id: {id_user}".format(id_user=id_user), + ) + + +def create(device): + """ + This function creates a new entry in the device structure + based on the passed in user id data + + :param user_name: user name to create in user structure + :param display_name: display name for the user + :param company: the user company + :param thumbnail: string url to the thumbnail image + + :return: 201 on success, 406 on default exists + """ + # Create a person instance using the schema and the passed in person + schema = DeviceSchema() + devices = schema.load(device, session=db.session).data + + # Add the person to the database + db.session.add(devices) + db.session.commit() + + # Serialize and return the newly created person in the response + data = schema.dump(devices).data + + return data, 201 + + + +def update(id_device, device): + """ + This function updates an existing user in the structure + + :param id_device: id of the device to update in the default structure + :param device: device to update + :return: updated device structure + """ + # Get the person requested from the db into session + update_device = Device.query.filter(Device.id == id_device).one_or_none() + + # Did we find a device? + if update_device is not None: + update_device.updated = datetime.utcnow() + update_device.bucket = device['bucket'] + update_device.frame_rate = device['frame_rate'] + update_device.high_threshold = device['high_threshold'] + update_device.id_user = device['id_user'] + update_device.image = device['image'] + update_device.location = device['location'] + update_device.low_threshold = device['low_threshold'] + update_device.name = device['name'] + update_device.notes = device['notes'] + update_device.prediction = device['prediction'] + update_device.refresh_rate = device['refresh_rate'] + update_device.type = device['type'] + + db.session.commit() + + schema = DeviceSchema() + data = schema.dump(update_device).data + + return data, 200 + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Device not found for Id: {id_device}".format(id_device=id_device), + ) + + +def delete(id_device): + """ + This function deletes a user from the default structure + + :param user_name: Id of the user to delete + :return: 200 on successful delete, 404 if not found + """ + # Get the person requested + delete = Device.query.filter(Device.id == id_device).one_or_none() + + # Did we find a person? + if delete is not None: + db.session.delete(delete) + db.session.commit() + return make_response( + "Device {id_device} deleted".format(id_device=id_device), 200 + ) + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Device not found for Id: {id_device}".format(id_device=id_device), + ) diff --git a/app-engine/lib/GCSObjectStreamUpload.py b/app-engine/lib/GCSObjectStreamUpload.py new file mode 100644 index 0000000..d73c32c --- /dev/null +++ b/app-engine/lib/GCSObjectStreamUpload.py @@ -0,0 +1,77 @@ +from google.auth.transport.requests import AuthorizedSession +from google.resumable_media import requests, common +from google.cloud import storage + +class GCSObjectStreamUpload(object): + def __init__( + self, + client: storage.Client, + bucket_name: str, + blob_name: str, + chunk_size: int=256 * 1024 + ): + self._client = client + self._bucket = self._client.bucket(bucket_name) + self._blob = self._bucket.blob(blob_name) + + self._buffer = b'' + self._buffer_size = 0 + self._chunk_size = chunk_size + self._read = 0 + + self._transport = AuthorizedSession( + credentials=self._client._credentials + ) + self._request = None # type: requests.ResumableUpload + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, *_): + if exc_type is None: + self.stop() + + def start(self): + url = ( + f'https://www.googleapis.com/upload/storage/v1/b/' + f'{self._bucket.name}/o?uploadType=resumable' + ) + self._request = requests.ResumableUpload( + upload_url=url, chunk_size=self._chunk_size + ) + self._request.initiate( + transport=self._transport, + content_type='application/octet-stream', + stream=self, + stream_final=False, + metadata={'name': self._blob.name}, + ) + + def stop(self): + self._request.transmit_next_chunk(self._transport) + + def write(self, data: bytes) -> int: + data_len = len(data) + self._buffer_size += data_len + self._buffer += data + del data + while self._buffer_size >= self._chunk_size: + try: + self._request.transmit_next_chunk(self._transport) + except common.InvalidResponse: + self._request.recover(self._transport) + return data_len + + def read(self, chunk_size: int) -> bytes: + # I'm not good with efficient no-copy buffering so if this is + # wrong or there's a better way to do this let me know! :-) + to_read = min(chunk_size, self._buffer_size) + memview = memoryview(self._buffer) + self._buffer = memview[to_read:].tobytes() + self._read += to_read + self._buffer_size -= to_read + return memview[:to_read].tobytes() + + def tell(self) -> int: + return self._read diff --git a/app-engine/main.py b/app-engine/main.py new file mode 100644 index 0000000..688370e --- /dev/null +++ b/app-engine/main.py @@ -0,0 +1,240 @@ +# [START gae_python37_render_template] +from flask import Flask, Response, request, json, render_template, current_app, redirect +from google.cloud import pubsub_v1, storage +from lib.GCSObjectStreamUpload import GCSObjectStreamUpload +import base64, json, logging, os +from config import db, app, connex_app +from models import * +from datetime import datetime + +# Read the swagger.yml file to configure the endpoints +connex_app.add_api("swagger.yml") + +def make_database(): + # Delete database file if it exists currently + # Keep for running the database locally + # if os.path.exists("deepgauge.db"): + # os.remove("deepgauge.db") + + # Create the database + db.create_all() + + # Data to initialize database with + d = Device( + id_user = 1, + name = "Device One", + image = "https://storage.googleapis.com/ocideepgauge-images/gauge_7.png", + bucket = "ocideepgauge", + type = "Gauge", + location = "St. Louis", + prediction = "PSI 7", + frame_rate = 5, + refresh_rate = 60, + notes = "General notes and information about Camera One", + high_threshold = 10, + low_threshold = 5 + ) + + u = User( + user_name = "Technician", + display_name = "Technician Name", + company = "Technicians Company", + thumbnail = "https://jobs.centurylink.com/sites/century-link/images/sp-technician-img.jpg" + ) + + r = Reading( + id_device = 1, + prediction = "psi 8", + accuracy = "89%", + body = "[{}]" + ) + + db.session.add(u) + db.session.add(d) + db.session.add(r) + + db.session.commit() + return True + +# Flask lets you create a test request to initialize an app. +with app.test_request_context(): + make_database() + +@app.route('/') +def root(): + query = Device.query.order_by(Device.id_user).all() + + # Serialize the data for the response + schema = DeviceSchema(many=True) + data = schema.dump(query).data + + for idx, d in enumerate(data): + date_time_obj = datetime.strptime(data[idx]['updated'], '%Y-%m-%dT%H:%M:%S.%f%z') + data[idx]['updated'] = date_time_obj.strftime('%B %d, %Y, %H:%M:%S') + + return render_template('dashboard.html', devices=data) + + +@app.route('/upload', methods=['GET', 'POST']) +def upload(): + if request.method == 'POST': + file = request.files['file'] + client = storage.Client() + bucket = 'ocideepgauge-images' #TODO: Make this a global variable + + # Create a new Device entry + schema = DeviceSchema() + device = Device( + id_user = 1, #TODO request this from the Flask auth session - not implemented + name = "", + image = 'https://storage.googleapis.com/{0}/{1}'.format(bucket, file.filename), + bucket = "gs://ocideepgauge-images", + type = "gauge", + prediction = "", + location = "", #TODO detect or update value from geo service + frame_rate = 15, #TODO change to defaults set in the database + refresh_rate = 30, #TODO change to defaults set in the database + notes = "", + high_threshold = 0, + low_threshold = 0 + ) + + # Add the device to the database + db.session.add(device) + db.session.commit() + + # Serialize and return the newly created person in the response + data = schema.dump(device).data + + # Upload the image to Google Storage bucket + with GCSObjectStreamUpload(client=client, bucket_name=bucket, blob_name=file.filename) as s: + s.write(file.read()) + + my_bucket = client.get_bucket(bucket) + blob = my_bucket.blob(file.filename) + blob.metadata = {'device_id':data['id'],'type':'gauge'} + blob.patch() + + # Redirect to the device page. + return redirect("/device/setting/{}".format(data['id']), code=302) + + +@app.route('/setting') +def setting(): + ## TODO Add query to local database to get defaults + return render_template('setting.html') + +@app.route('/user') +def user(): + user = User.query.filter(User.id == 1).one_or_none() + + # Serialize the data for the response + schema = UserSchema() + data = schema.dump(user).data + + return render_template('user.html', user=data) + +@app.route('/device/new') +def new_device(): + return render_template('new_device.html') + +@app.route('/device/') +def one_device(device_id): + query = Device.query.filter(Device.id == device_id).one_or_none() + if query is not None: + + # Serialize the data for the response + schema = DeviceSchema() + data = schema.dump(query).data + + # Otherwise, nope, didn't find that person + else: + data = [] + + query_reading = Reading.query.filter(Reading.id_device == device_id).one_or_none() + if query_reading is not None: + + # Serialize the data for the response + schema = ReadingSchema() + reading = schema.dump(query_reading).data + + # Otherwise, nope, didn't find that person + else: + reading = [] + + return render_template('one_device.html', device=data, reading=reading) + +@app.route('/device/setting/') +def show_device_setting(device_id): + query = Device.query.filter(Device.id == device_id).one_or_none() + + # Did we find a person? + if query is not None: + + # Serialize the data for the response + schema = DeviceSchema() + data = schema.dump(query).data + + # Otherwise, nope, didn't find that person + else: + data = [] + + return render_template('setting_device.html', device=data) + +# [START push] +@app.route('/pubsub/push', methods=['POST']) +def pubsub_push(): + if (request.args.get('token', '') != + current_app.config['PUBSUB_VERIFICATION_TOKEN']): + return 'Invalid request', 400 + + envelope = json.loads(request.get_data().decode('utf-8')) + payload = base64.b64decode(envelope['message']['data']) + + payload_json = payload.decode('utf8').replace("'", '"') + payload_data = json.loads(payload_json) + for d in payload_data: + for cl in d['class_label']: + prediction = cl + for ci in d['class_ids']: + acc = d['probabilities'][ci]*100 + + schema = ReadingSchema() + reading = Reading( + id_device = envelope['message']['attributes']['device'], + prediction = prediction, + accuracy = acc, + body = payload_json + ) + # Add to the database + db.session.add(reading) + db.session.commit() + + # Update the Device model with the prediction + id_device = envelope['message']['attributes']['device'] + update_device = Device.query.filter(Device.id == id_device).one_or_none() + + if update_device is not None: + update_device.prediction = prediction.replace("_"," ").upper() + db.session.commit() + + # Serialize and return the newly created person in the response + data = schema.dump(reading).data + + # Returning any 2xx status indicates successful receipt of the message. + return 'OK', 200 +# [END push] + + +@app.errorhandler(500) +def server_error(e): + logging.exception('An error occurred during a request.') + return """ + An internal error occurred:
{}
+ See logs for full stacktrace. + """.format(e), 500 + + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) +# [START gae_python37_render_template] diff --git a/app-engine/models.py b/app-engine/models.py new file mode 100644 index 0000000..3533f00 --- /dev/null +++ b/app-engine/models.py @@ -0,0 +1,85 @@ +from datetime import datetime +from config import db, ma + +class Person(db.Model): + __tablename__ = "person" + person_id = db.Column(db.Integer, primary_key=True) + lname = db.Column(db.String(32)) + fname = db.Column(db.String(32)) + timestamp = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + +class PersonSchema(ma.ModelSchema): + class Meta: + model = Person + sqla_session = db.session + +class Setting(db.Model): + __tablename__ = "setting" + id = db.Column(db.Integer, primary_key=True) + id_user = db.Column(db.Integer) + type = db.Column(db.String(32)) + frame_rate = db.Column(db.String(32)) + refresh_rate = db.Column(db.String(32)) + updated = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) +class SettingSchema(ma.ModelSchema): + class Meta: + model = Setting + sqla_session = db.session +### +class Reading(db.Model): + __tablename__ = "reading" + id = db.Column(db.Integer, primary_key=True) + id_device = db.Column(db.Integer) + prediction = db.Column(db.String(64)) + accuracy = db.Column(db.String(64)) + body = db.Column(db.String(128)) + timestamp = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) +class ReadingSchema(ma.ModelSchema): + class Meta: + model = Reading + sqla_session = db.session + +### +class Device(db.Model): + __tablename__ = "device" + id = db.Column(db.Integer, primary_key=True) + id_user = db.Column(db.Integer) + name = db.Column(db.String(32)) + image = db.Column(db.String(32)) + bucket = db.Column(db.String(32)) + type = db.Column(db.String(32)) + location = db.Column(db.String(32)) + prediction = db.Column(db.String(32)) + frame_rate = db.Column(db.String(32)) + refresh_rate = db.Column(db.String(32)) + notes = db.Column(db.String(64)) + high_threshold = db.Column(db.Integer) + low_threshold = db.Column(db.Integer) + updated = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) +class DeviceSchema(ma.ModelSchema): + class Meta: + model = Device + sqla_session = db.session +### +class User(db.Model): + __tablename__ = "user" + id = db.Column(db.Integer, primary_key=True) + user_name = db.Column(db.String(32)) + display_name = db.Column(db.String(32)) + company = db.Column(db.String(32)) + thumbnail = db.Column(db.String(32)) + updated = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) +class UserSchema(ma.ModelSchema): + class Meta: + model = User + sqla_session = db.session diff --git a/app-engine/people.py b/app-engine/people.py new file mode 100644 index 0000000..9168a93 --- /dev/null +++ b/app-engine/people.py @@ -0,0 +1,160 @@ +""" +This is the people module and supports all the REST actions for the +people data +""" + +from flask import make_response, abort +from config import db +from models import Person, PersonSchema + + +def read_all(): + """ + This function responds to a request for /api/people + with the complete lists of people + + :return: json string of list of people + """ + # Create the list of people from our data + people = Person.query.order_by(Person.lname).all() + + # Serialize the data for the response + person_schema = PersonSchema(many=True) + data = person_schema.dump(people).data + return data + + +def read_one(person_id): + """ + This function responds to a request for /api/people/{person_id} + with one matching person from people + + :param person_id: Id of person to find + :return: person matching id + """ + # Get the person requested + person = Person.query.filter(Person.person_id == person_id).one_or_none() + + # Did we find a person? + if person is not None: + + # Serialize the data for the response + person_schema = PersonSchema() + data = person_schema.dump(person).data + return data + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Person not found for Id: {person_id}".format(person_id=person_id), + ) + + +def create(person): + """ + This function creates a new person in the people structure + based on the passed in person data + + :param person: person to create in people structure + :return: 201 on success, 406 on person exists + """ + fname = person.get("fname") + lname = person.get("lname") + + existing_person = ( + Person.query.filter(Person.fname == fname) + .filter(Person.lname == lname) + .one_or_none() + ) + + # Can we insert this person? + if existing_person is None: + + # Create a person instance using the schema and the passed in person + schema = PersonSchema() + new_person = schema.load(person, session=db.session).data + + # Add the person to the database + db.session.add(new_person) + db.session.commit() + + # Serialize and return the newly created person in the response + data = schema.dump(new_person).data + + return data, 201 + + # Otherwise, nope, person exists already + else: + abort( + 409, + "Person {fname} {lname} exists already".format( + fname=fname, lname=lname + ), + ) + + +def update(person_id, person): + """ + This function updates an existing person in the people structure + + :param person_id: Id of the person to update in the people structure + :param person: person to update + :return: updated person structure + """ + # Get the person requested from the db into session + update_person = Person.query.filter( + Person.person_id == person_id + ).one_or_none() + + # Did we find a person? + if update_person is not None: + + # turn the passed in person into a db object + schema = PersonSchema() + update = schema.load(person, session=db.session).data + + # Set the id to the person we want to update + update.id = update_person.person_id + + # merge the new object into the old and commit it to the db + db.session.merge(update) + db.session.commit() + + # return updated person in the response + data = schema.dump(update_person).data + + return data, 200 + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Person not found for Id: {person_id}".format(person_id=person_id), + ) + + +def delete(person_id): + """ + This function deletes a person from the people structure + + :param person_id: Id of the person to delete + :return: 200 on successful delete, 404 if not found + """ + # Get the person requested + person = Person.query.filter(Person.person_id == person_id).one_or_none() + + # Did we find a person? + if person is not None: + db.session.delete(person) + db.session.commit() + return make_response( + "Person {person_id} deleted".format(person_id=person_id), 200 + ) + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Person not found for Id: {person_id}".format(person_id=person_id), + ) diff --git a/app-engine/reading.py b/app-engine/reading.py new file mode 100644 index 0000000..3a7b846 --- /dev/null +++ b/app-engine/reading.py @@ -0,0 +1,148 @@ +# Reading +# id +# id_device +# prediction +# accuracy +# body +# timestamp + +""" +This is the people module and supports all the REST actions for the +people data +""" + +from flask import make_response, abort +from config import db +from models import Reading, ReadingSchema + + +def read_all(): + """ + This function responds to a request for /api/user + with the complete lists of users + + :return: json string of list of users + """ + # Create the list of people from our data + users = Reading.query.order_by(Reading.id_device).all() + + # Serialize the data for the response + schema = ReadingSchema(many=True) + data = schema.dump(users).data + return data + + +def read_many(id_device): + """ + This function responds to a request for /api/reading/{id_device} + with matching readings from device id + + :param id_device: Id of device to find + :return: readings matching id + """ + # Get the person requested + read = Reading.query.filter(Reading.id_device == id_device).all() + + # Did we find a person? + if read is not None: + + # Serialize the data for the response + schema = ReadingSchema(many=True) + data = schema.dump(read).data + return data, 201 + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Readings not found for Id: {id_device}".format(id_device=id_device), + ) + + +def create(reading): + """ + This function creates a new entry in the default structure + based on the passed in user id data + + :param id_device: individual device identification + :param prediction: the predicted label + :param accuracy: the accuracy of the predictions + :param body: the reccommendation response body + + :return: 201 on success + """ + # Create a person instance using the schema and the passed in person + schema = ReadingSchema() + readings = schema.load(reading, session=db.session).data + readings.id_device = reading.get('id_device') + # Add to the database + db.session.add(readings) + db.session.commit() + + # Serialize and return the newly created person in the response + data = schema.dump(reading).data + + return data, 201 + + +def update(reading_id, reading): + """ + This function updates an existing reading in the structure + + :param id: id of the reading to update in the default structure + :param user: user to update + :return: updated default structure + """ + # Get the person requested from the db into session + update = Reading.query.filter( + Reading.id == reading_id + ).one_or_none() + + # Did we find a user? + if update is not None: + + # turn the passed in person into a db object + schema = ReadingSchema() + updates = schema.load(reading, session=db.session).data + + # merge the new object into the old and commit it to the db + db.session.merge(updates) + db.session.commit() + + # return updated person in the response + data = schema.dump(updates).data + + return data, 200 + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Reading not found for Id: {reading_id}".format(reading_id=reading_id), + ) + + +def delete(reading_id): + """ + This function deletes a reading from the default structure + + :param reading_id: Id of the reading to delete + :return: 200 on successful delete, 404 if not found + """ + # Get the row requested + row = Reading.query.filter(Reading.id == reading_id).one_or_none() + + # Did we find a person? + if row is not None: + db.session.delete(row) + db.session.commit() + return make_response( + "Id {reading_id} deleted".format(reading_id=reading_id), 200 + ) + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Reading not found for Id: {reading_id}".format(reading_id=reading_id) + ) diff --git a/app-engine/requirements.txt b/app-engine/requirements.txt new file mode 100644 index 0000000..f2a5d37 --- /dev/null +++ b/app-engine/requirements.txt @@ -0,0 +1,39 @@ +cachetools==3.0.0 +certifi==2018.10.15 +chardet==3.0.4 +Click==7.0 +clickclick==1.2.2 +connexion==2.0.2 +Flask==1.0.2 +flask-marshmallow==0.9.0 +google-api-core==1.5.2 +google-auth==1.6.1 +google-cloud-core==0.28.1 +google-cloud-pubsub==0.38.0 +google-cloud-storage==1.13.0 +google-resumable-media==0.3.1 +googleapis-common-protos==1.5.5 +grpc-google-iam-v1==0.11.4 +grpcio==1.16.1 +idna==2.7 +inflection==0.3.1 +itsdangerous==1.1.0 +Jinja2==2.10 +jsonschema==2.6.0 +MarkupSafe==1.1.0 +marshmallow==2.16.3 +marshmallow-sqlalchemy==0.15.0 +openapi-spec-validator==0.2.4 +protobuf==3.6.1 +pyasn1==0.4.4 +pyasn1-modules==0.2.2 +pytz==2018.7 +PyYAML==3.13 +requests==2.20.1 +rsa==4.0 +six==1.11.0 +SQLAlchemy==1.2.14 +swagger-ui-bundle==0.0.2 +urllib3==1.24.1 +Werkzeug==0.14.1 +flask_sqlalchemy==2.3.2 diff --git a/app-engine/setting.py b/app-engine/setting.py new file mode 100644 index 0000000..609ae5c --- /dev/null +++ b/app-engine/setting.py @@ -0,0 +1,164 @@ +""" +This is the people module and supports all the REST actions for the +people data +""" + +from flask import make_response, abort +from config import db +from models import Setting, SettingSchema + + +def read_all(): + """ + This function responds to a request for /api/setting + with the complete lists of users settings + + :return: json string of list of default settings + """ + # Create the list of people from our data + users = Setting.query.order_by(Setting.id_user).all() + + # Serialize the data for the response + settings_schema = SettingSchema(many=True) + data = settings_schema.dump(users).data + return data + + +def read_one(id_user): + """ + This function responds to a request for /api/setting/{id_user} + with one matching user from settings + + :param id_user: Id of user to find + :return: user matching id + """ + # Get the person requested + settings = Setting.query.filter(Setting.id_user == id_user).one_or_none() + + # Did we find a person? + if settings is not None: + + # Serialize the data for the response + setting_schema = SettingSchema() + data = setting_schema.dump(settings).data + return data + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Settings not found for Id: {id_user}".format(id_user=id_user), + ) + + +def create(setting): + """ + This function creates a new entry in the default structure + based on the passed in user id data + + :param default: person to create in people structure + :param default.id_user: user id to create the default structure + :param default.type: default type to display for the user + :param default.frame_rate: default frame rate of the device + :param default.refresh_rate: refresh rate of the device + :param default.updated: timestamp of the last update + :return: 201 on success, 406 on default exists + """ + + id_user = setting.get("id_user") + type = setting.get("type") + + existing_setting = ( + Setting.query.filter(Setting.id_user == id_user) + .one_or_none() + ) + + # Can we insert this person? + if existing_setting is None: + + # Create a person instance using the schema and the passed in person + schema = SettingSchema() + new_setting = schema.load(setting, session=db.session).data + + # Add the person to the database + db.session.add(new_setting) + db.session.commit() + + # Serialize and return the newly created person in the response + data = schema.dump(new_setting).data + return data, 201 + + # # Otherwise, nope, person exists already + # else: + # abort( + # 409, + # "Setting {id_user} exists already".format( + # id_user=id_user + # ), + # ) + + +def update(id_user, default): + """ + This function updates an existing setting in the default structure + + :param id_user: id of the user to update in the default structure + :param setting: setting to update + :return: updated default structure + """ + # Get the person requested from the db into session + update_setting = Setting.query.filter( + Setting.id_user == id_user + ).one_or_none() + + # Did we find a user? + if update_setting is not None: + + # turn the passed in person into a db object + schema = SettingSchema() + update = schema.load(setting, session=db.session).data + + # Set the id to the person we want to update + update.id = update_setting.id_user + + # merge the new object into the old and commit it to the db + db.session.merge(update) + db.session.commit() + + # return updated person in the response + data = schema.dump(update_setting).data + + return data, 200 + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Setting not found for Id: {id_user}".format(id_user=id_user), + ) + + +def delete(id_user): + """ + This function deletes a user from the default structure + + :param id_user: Id of the user to delete + :return: 200 on successful delete, 404 if not found + """ + # Get the person requested + setting = Setting.query.filter(Setting.id_user == id_user).one_or_none() + + # Did we find a person? + if setting is not None: + db.session.delete(setting) + db.session.commit() + return make_response( + "Setting {id_user} deleted".format(id_user=id_user), 200 + ) + + # Otherwise, nope, didn't find that person + else: + abort( + 404, + "Setting not found for Id: {id_user}".format(id_user=id_user), + ) diff --git a/app-engine/static/chart-script.js b/app-engine/static/chart-script.js new file mode 100644 index 0000000..07c1b92 --- /dev/null +++ b/app-engine/static/chart-script.js @@ -0,0 +1,201 @@ +window.chartColors = { + red: 'rgb(255, 99, 132)', + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(75, 192, 192)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)' +}; + +(function(global) { + var Months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + + var COLORS = [ + '#4dc9f6', + '#f67019', + '#f53794', + '#537bc4', + '#acc236', + '#166a8f', + '#00a950', + '#58595b', + '#8549ba' + ]; + + var Samples = global.Samples || (global.Samples = {}); + var Color = global.Color; + + Samples.utils = { + // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ + srand: function(seed) { + this._seed = seed; + }, + + rand: function(min, max) { + var seed = this._seed; + min = min === undefined ? 0 : min; + max = max === undefined ? 1 : max; + this._seed = (seed * 9301 + 49297) % 233280; + return min + (this._seed / 233280) * (max - min); + }, + + numbers: function(config) { + var cfg = config || {}; + var min = cfg.min || 0; + var max = cfg.max || 1; + var from = cfg.from || []; + var count = cfg.count || 8; + var decimals = cfg.decimals || 8; + var continuity = cfg.continuity || 1; + var dfactor = Math.pow(10, decimals) || 0; + var data = []; + var i, value; + + for (i = 0; i < count; ++i) { + value = (from[i] || 0) + this.rand(min, max); + if (this.rand() <= continuity) { + data.push(Math.round(dfactor * value) / dfactor); + } else { + data.push(null); + } + } + + return data; + }, + + labels: function(config) { + var cfg = config || {}; + var min = cfg.min || 0; + var max = cfg.max || 30; + var count = cfg.count || 8; + var step = (max - min) / count; + var decimals = cfg.decimals || 8; + var dfactor = Math.pow(10, decimals) || 0; + var prefix = cfg.prefix || ''; + var values = []; + var i; + + for (i = min; i < max; i += step) { + values.push(prefix + Math.round(dfactor * i) / dfactor); + } + + return values; + }, + + months: function(config) { + var cfg = config || {}; + var count = cfg.count || 12; + var section = cfg.section; + var values = []; + var i, value; + + for (i = 0; i < count; ++i) { + value = Months[Math.ceil(i) % 12]; + values.push(value.substring(0, section)); + } + + return values; + }, + + color: function(index) { + return COLORS[index % COLORS.length]; + }, + + transparentize: function(color, opacity) { + var alpha = opacity === undefined ? 0.5 : 1 - opacity; + return Color(color).alpha(alpha).rgbString(); + } + }; + + // INITIALIZATION + + Samples.utils.srand(Date.now()); + + +}(this)); + + + +$(document).ready(function() { + + function randomNumber(min, max) { + return Math.random() * (max - min) + min; + } + + function randomBar(date, lastClose) { + var open = randomNumber(lastClose * 0.95, lastClose * 1.05); + var close = randomNumber(open * 0.95, open * 1.05); + return { + t: date.valueOf(), + y: close + }; + } + + var dateFormat = 'MMMM DD YYYY'; + var date = moment('April 01 2017', dateFormat); + var data = [randomBar(date, 30)]; + var labels = [date]; + while (data.length < 10) { + date = date.clone().add(1, 'd'); + if (date.isoWeekday() <= 5) { + data.push(randomBar(date, data[data.length - 1].y)); + labels.push(date); + } + } + + var ctx = document.getElementById('myChart').getContext('2d'); + ctx.canvas.height = 100; + + var color = Chart.helpers.color; + var cfg = { + type: 'bar', + data: { + labels: labels, + datasets: [{ + label: 'November', + backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(), + borderColor: window.chartColors.red, + data: data, + type: 'line', + pointRadius: 0, + fill: false, + lineTension: 0, + borderWidth: 2 + }] + }, + options: { + scales: { + xAxes: [{ + type: 'time', + distribution: 'series', + ticks: { + source: 'labels' + } + }], + yAxes: [{ + scaleLabel: { + display: true, + labelString: 'Pressure - PSI' + } + }] + } + } + }; + var chart = new Chart(ctx, cfg); + + +}); diff --git a/app-engine/static/img/oci.png b/app-engine/static/img/oci.png new file mode 100644 index 0000000..ef7cd30 Binary files /dev/null and b/app-engine/static/img/oci.png differ diff --git a/app-engine/static/script.js b/app-engine/static/script.js new file mode 100644 index 0000000..627a1a3 --- /dev/null +++ b/app-engine/static/script.js @@ -0,0 +1,5 @@ +$(document).ready(function() { + $('#sidebarCollapse').on('click', function() { + $('#sidebar').toggleClass('active'); + }); +}); diff --git a/app-engine/static/style.css b/app-engine/static/style.css new file mode 100644 index 0000000..75f426d --- /dev/null +++ b/app-engine/static/style.css @@ -0,0 +1,263 @@ +/* + DEMO STYLE +*/ + +@import "https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700"; +body { + font-family: 'Poppins', sans-serif; + background: #fafafa; +} + +p { + font-family: 'Poppins', sans-serif; + font-size: 1.1em; + font-weight: 300; + line-height: 1.7em; + color: #999; +} + +a, a:hover, a:focus { + color: inherit; + text-decoration: none; + transition: all 0.3s; +} + +.navbar { + padding: 15px 10px; + background: #fff; + border: none; + border-radius: 0; + margin-bottom: 40px; + box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.1); +} + +.navbar-btn { + box-shadow: none; + outline: none !important; + border: none; +} + +.line { + width: 100%; + height: 1px; + border-bottom: 1px dashed #ddd; + margin: 40px 0; +} + +i, span { + display: inline-block; +} + +/* --------------------------------------------------- + SIDEBAR STYLE +----------------------------------------------------- */ + +.wrapper { + display: flex; + align-items: stretch; +} + +#sidebar { + min-width: 250px; + max-width: 250px; + background: #72c8d5; + color: #fff; + transition: all 0.3s; +} + +#sidebar.active { + min-width: 80px; + max-width: 80px; + text-align: center; +} + +#sidebar.active .sidebar-header h3, #sidebar.active .CTAs { + display: none; +} + +#sidebar.active .sidebar-header strong { + display: block; +} + +#logo { + filter: brightness(0) invert(1); + width: 32px; +} + +#sidebar ul li a { + text-align: right; +} + +#sidebar.active ul li a { + padding: 20px 10px; + text-align: center; + font-size: 1.5em; +} + +#sidebar.active ul li a i { + font-size: 1.5em; + float: none; + margin-left: 0px; +} + +#sidebar.active ul ul a { + padding: 10px !important; +} + +#sidebar.active ul li a span { + display: none; +} + +#sidebar.active .dropdown-toggle::after { + top: auto; + bottom: 10px; + right: 50%; + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%); +} + +#sidebar .sidebar-header { + padding: 20px; + background: #72c8d5; + height: 80px; +} + +#sidebar .sidebar-header strong { + display: none; + font-size: 1.8em; +} + +#sidebar ul.components { + padding: 20px 0; + border-top: 1px solid #47748b; + clear: both; +} + +#sidebar ul li a { + padding: 1em; + font-size: 1.5em; + display: block; + line-height: 1em; +} + +#sidebar ul li a:hover { + color: #72c8d5; + background: #fff; +} + +#sidebar ul li a i { + margin-left: 10px; + float: right; +} + +#sidebar ul li.active>a, a[aria-expanded="true"] { + color: #fff; + background: #2559a7; +} + +a[data-toggle="collapse"] { + position: relative; +} + +.dropdown-toggle::after { + display: block; + position: absolute; + top: 50%; + right: 20px; + transform: translateY(-50%); +} + +ul ul a { + font-size: 0.9em !important; + padding-left: 30px !important; + background: #72c8d5; +} + +ul.CTAs { + padding: 20px; +} + +ul.CTAs a { + text-align: center; + font-size: 0.9em !important; + display: block; + border-radius: 5px; + margin-bottom: 5px; +} + +a.download { + background: #fff; + color: #72c8d5; +} + +a.article, a.article:hover { + background: #2559a7 !important; + color: #fff !important; +} + +/* --------------------------------------------------- + CONTENT STYLE +----------------------------------------------------- */ + +#content { + width: 100%; + padding: 20px; + min-height: 100vh; + transition: all 0.3s; +} + +/* --------------------------------------------------- + MEDIAQUERIES +----------------------------------------------------- */ + +@media (max-width: 768px) { + #sidebar { + min-width: 80px; + max-width: 80px; + text-align: center; + } + .dropdown-toggle::after { + top: auto; + bottom: 10px; + right: 50%; + -webkit-transform: translateX(50%); + -ms-transform: translateX(50%); + transform: translateX(50%); + } + #sidebar.active { + margin-left: 0 !important; + } + #sidebar .sidebar-header h3, #sidebar .CTAs { + display: none; + } + #sidebar .sidebar-header strong { + display: block; + } + #sidebar ul li a { + padding: 20px 10px; + text-align: center; + } + #sidebar ul li a span { + font-size: 0.50em; + } + #sidebar ul li a i { + margin-right: 0; + display: block; + } + #sidebar ul ul a { + padding: 10px !important; + } + #sidebar ul li a i { + font-size: 1.3em; + margin-left: 0; + text-align: center; + float: none; + } + #sidebar { + margin-left: 0; + } + #sidebarCollapse span { + display: none; + } +} diff --git a/app-engine/swagger.yml b/app-engine/swagger.yml new file mode 100644 index 0000000..bfd2029 --- /dev/null +++ b/app-engine/swagger.yml @@ -0,0 +1,1018 @@ +swagger: "2.0" +info: + description: This is the swagger file that goes with our server code + version: "1.0.0" + title: DeepGauge +consumes: + - application/json +produces: + - application/json + +basePath: /api + +# Paths supported by the server application +paths: + /setting: + get: + operationId: setting.read_all + tags: + - Setting + summary: Read the entire set of settings, sorted by user id + description: Read the entire set of settings, sorted by user id + responses: + 200: + description: Successfully read settings set operation + schema: + type: array + items: + properties: + id_user: + type: integer + description: Id of the user + type: + type: string + description: type of the setting + frame_rate: + type: string + description: Setting frame rate + refresh_rate: + type: string + description: Setting refresh rate + updated: + type: string + description: Creation/Update timestamp of the setting + post: + operationId: setting.create + tags: + - Setting + summary: Create a setting + description: Create a new setting + parameters: + - name: setting + in: body + description: Setting to create + required: True + schema: + type: object + properties: + id_user: + type: integer + description: Id of the user + type: + type: string + description: type of the setting + frame_rate: + type: string + description: Setting frame rate + refresh_rate: + type: string + description: Setting refresh rate + + responses: + 201: + description: Successfully created setting + schema: + properties: + id_user: + type: integer + description: Id of the user + type: + type: string + description: type of the setting + frame_rate: + type: string + description: Setting frame rate + refresh_rate: + type: string + description: Setting refresh rate + updated: + type: string + description: Creation/Update timestamp of the default + /setting/{id_user}: + get: + operationId: setting.read_one + tags: + - Setting + summary: Read one setting + description: Read one setting + parameters: + - name: id_user + in: path + description: id of the user to get + type: integer + required: True + responses: + 200: + description: Successfully read setting from user data operation + schema: + type: object + properties: + id_user: + type: integer + description: Id of the user + type: + type: string + description: type of the setting + frame_rate: + type: string + description: Setting frame rate + refresh_rate: + type: string + description: Setting refresh rate + updated: + type: string + description: Creation/Update timestamp of the setting + + put: + operationId: setting.update + tags: + - Setting + summary: Update a setting + description: Update a setting + parameters: + - name: id_user + in: path + description: Id the user to update + type: integer + required: True + - name: setting + in: body + schema: + type: object + properties: + id_user: + type: integer + description: Id of the user + type: + type: string + description: type of the setting + frame_rate: + type: string + description: Setting frame rate + refresh_rate: + type: string + description: Setting refresh rate + updated: + type: string + description: Creation/Update timestamp of the default + responses: + 200: + description: Successfully updated person + schema: + properties: + id_user: + type: integer + description: Id of the user + type: + type: string + description: type of the setting + frame_rate: + type: string + description: Setting frame rate + refresh_rate: + type: string + description: Setting refresh rate + updated: + type: string + description: Creation/Update timestamp of the default + + delete: + operationId: setting.delete + tags: + - Setting + summary: Delete a setting from the setting list + description: Delete a setting + parameters: + - name: id_user + in: path + type: integer + description: Id of the user to delete + required: true + responses: + 200: + description: Successfully deleted a person + /user: + get: + operationId: user.read_all + tags: + - User + summary: Read the entire set of users, sorted by user name + description: Read the entire set of settings, sorted by user name + responses: + 200: + description: Successfully read users set operation + schema: + type: array + items: + properties: + user_name: + type: integer + description: Id of the user + display_name: + type: string + description: the display name + company: + type: string + description: Users Company + thumbnail: + type: string + description: Url of the thumbnail + updated: + type: string + description: Creation/Update timestamp of the setting + post: + operationId: user.create + tags: + - User + summary: Create a user + description: Create a new user + parameters: + - name: user + in: body + description: Setting to create + required: True + schema: + type: object + properties: + user_name: + type: string + description: username of the user + display_name: + type: string + description: the display name + company: + type: string + description: Users Company + thumbnail: + type: string + description: Url of the thumbnail + responses: + 201: + description: Successfully created user + schema: + properties: + user_name: + type: integer + description: Id of the user + display_name: + type: string + description: the display name + company: + type: string + description: Users Company + thumbnail: + type: string + description: Url of the thumbnail + /user/{user_name}: + get: + operationId: user.read_one + tags: + - User + summary: Read one user + description: Read one user + parameters: + - name: user_name + in: path + description: id of the user to get + type: string + required: True + responses: + 200: + description: Successfully read setting from user data operation + schema: + type: object + properties: + user_name: + type: string + description: Id of the user + display_name: + type: string + description: the display name + company: + type: string + description: Users Company + thumbnail: + type: string + description: Url of the thumbnail + + put: + operationId: user.update + tags: + - User + summary: Update a user + description: Update a user + parameters: + - name: user_name + in: path + description: Id the user to update + type: string + required: True + - name: user + in: body + schema: + type: object + properties: + user_name: + type: string + description: Id of the user + display_name: + type: string + description: the display name + company: + type: string + description: Users Company + thumbnail: + type: string + description: Url of the thumbnail + responses: + 200: + description: Successfully updated person + schema: + properties: + user_name: + type: string + description: Id of the user + display_name: + type: string + description: the display name + company: + type: string + description: Users Company + thumbnail: + type: string + description: Url of the thumbnail + + delete: + operationId: user.delete + tags: + - User + summary: Delete a user from the users list + description: Delete a user + parameters: + - name: user_name + in: path + type: string + description: name of the user to delete + required: true + responses: + 200: + description: Successfully deleted a person + + /device: + get: + operationId: device.read_all + tags: + - Device + summary: Read the entire set of devices, sorted by user id + description: Read the entire set of devices, sorted by user id + responses: + 200: + description: Successfully read devices set operation + schema: + type: array + items: + properties: + id_user: + type: integer + description: Id of the user + name: + type: string + description: the device name + image: + type: string + description: the device image + bucket: + type: string + description: Bucket URL of the device + type: + type: string + description: The type of device + location: + type: string + description: The location of the device + prediction: + type: string + description: The location of the device + frame_rate: + type: string + description: The frame rate of the device + refresh_rate: + type: string + description: The refresh rate of the device + notes: + type: string + description: The notes for device + high_threshold: + type: integer + description: The high threshold for device + low_threshold: + type: integer + description: The low threshold for device + updated: + type: string + description: Creation/Update timestamp of the setting + + post: + operationId: device.create + tags: + - Device + summary: Create a device + description: Create a new device + parameters: + - name: device + in: body + description: Device to create + required: True + schema: + type: object + properties: + id_user: + type: integer + description: Id of the user + name: + type: string + description: the device name + image: + type: string + description: the device image + bucket: + type: string + description: Bucket URL of the device + type: + type: string + description: The type of device + location: + type: string + description: The location of the device + prediction: + type: string + description: The location of the device + frame_rate: + type: string + description: The frame rate of the device + refresh_rate: + type: string + description: The refresh rate of the device + notes: + type: string + description: The notes for device + high_threshold: + type: integer + description: The high threshold for device + low_threshold: + type: integer + description: The low threshold for device + + responses: + 201: + description: Successfully created device + schema: + properties: + id_user: + type: integer + description: Id of the user + name: + type: string + description: the device name + image: + type: string + description: the device image + bucket: + type: string + description: Bucket URL of the device + type: + type: string + description: The type of device + location: + type: string + description: The location of the device + prediction: + type: string + description: The location of the device + frame_rate: + type: string + description: The frame rate of the device + refresh_rate: + type: string + description: The refresh rate of the device + notes: + type: string + description: The notes for device + high_threshold: + type: integer + description: The high threshold for device + low_threshold: + type: integer + description: The low threshold for device + updated: + type: string + description: Creation/Update timestamp of the setting + /device/{id_device}: + get: + operationId: device.read_one + tags: + - Device + summary: Read one device + description: Read one device + parameters: + - name: id_device + in: path + description: id of the device to get + type: integer + required: True + responses: + 200: + description: Successfully read device from read operation + schema: + type: object + properties: + id_user: + type: integer + description: Id of the user + name: + type: string + description: the device name + image: + type: string + description: the device image + bucket: + type: string + description: Bucket URL of the device + type: + type: string + description: The type of device + location: + type: string + description: The location of the device + prediction: + type: string + description: The location of the device + frame_rate: + type: string + description: The frame rate of the device + refresh_rate: + type: string + description: The refresh rate of the device + notes: + type: string + description: The notes for device + high_threshold: + type: integer + description: The high threshold for device + low_threshold: + type: integer + description: The low threshold for device + updated: + type: string + description: Creation/Update timestamp of the setting + + put: + operationId: device.update + tags: + - Device + summary: Update a device + description: Update a device + parameters: + - name: id_device + in: path + description: Id the device to update + type: integer + required: True + - name: device + in: body + schema: + type: object + properties: + id_user: + type: integer + description: Id of the user + name: + type: string + description: the device name + image: + type: string + description: the device image + bucket: + type: string + description: Bucket URL of the device + type: + type: string + description: The type of device + location: + type: string + description: The location of the device + prediction: + type: string + description: The location of the device + frame_rate: + type: string + description: The frame rate of the device + refresh_rate: + type: string + description: The refresh rate of the device + notes: + type: string + description: The notes for device + high_threshold: + type: integer + description: The high threshold for device + low_threshold: + type: integer + description: The low threshold for device + updated: + type: string + description: Creation/Update timestamp of the setting + responses: + 200: + description: Successfully updated device + schema: + properties: + id_user: + type: integer + description: Id of the user + name: + type: string + description: the device name + image: + type: string + description: the device image + bucket: + type: string + description: Bucket URL of the device + type: + type: string + description: The type of device + location: + type: string + description: The location of the device + prediction: + type: string + description: The location of the device + frame_rate: + type: string + description: The frame rate of the device + refresh_rate: + type: string + description: The refresh rate of the device + notes: + type: string + description: The notes for device + high_threshold: + type: integer + description: The high threshold for device + low_threshold: + type: integer + description: The low threshold for device + updated: + type: string + description: Creation/Update timestamp of the setting + + delete: + operationId: device.delete + tags: + - Device + summary: Delete a device from the list + description: Delete a device + parameters: + - name: id_device + in: path + type: integer + description: id of the device to delete + required: true + responses: + 200: + description: Successfully deleted a person + /reading: + get: + operationId: reading.read_all + tags: + - Reading + summary: Read the entire set of readings, sorted by device id + description: Read the entire set of readings, sorted by device id + responses: + 200: + description: Successfully read readings set operation + schema: + type: array + items: + properties: + id_device: + type: integer + description: individual device identification + prediction: + type: string + description: the predicted label + accuracy: + type: string + description: the accuracy of the predictions + body: + type: string + description: the reccommendation response body + timestamp: + type: string + description: Creation/Update timestamp of the setting + + post: + operationId: reading.create + tags: + - Reading + summary: Create a reading + description: Create a new reading + parameters: + - name: reading + in: body + description: Reading to create + required: True + schema: + type: object + properties: + id_device: + type: integer + description: individual device identification + prediction: + type: string + description: the predicted label + accuracy: + type: string + description: the accuracy of the predictions + body: + type: string + description: the reccommendation response body + responses: + 201: + description: Successfully created reading + schema: + properties: + id_device: + type: integer + description: reading id + id_device: + type: integer + description: individual device identification + prediction: + type: string + description: the predicted label + accuracy: + type: string + description: the accuracy of the predictions + body: + type: string + description: the reccommendation response body + timestamp: + type: string + description: Creation/Update timestamp of the setting + /reading/{id_device}: + get: + operationId: reading.read_many + tags: + - Reading + summary: Read readings from one device + description: Read readings from one device + parameters: + - name: id_device + in: path + description: id of the device to get + type: integer + required: True + responses: + 200: + description: Successfully read reading from device id data operation + schema: + type: object + properties: + id_device: + type: integer + description: individual device identification + prediction: + type: string + description: the predicted label + accuracy: + type: string + description: the accuracy of the predictions + body: + type: string + description: the reccommendation response body + timestamp: + type: string + description: Creation/Update timestamp of the setting + /reading/{reading_id}: + put: + operationId: reading.update + tags: + - Reading + summary: Update a reading + description: Update a reading + parameters: + - name: reading_id + in: path + description: Id the reading to update + type: integer + required: True + - name: reading + in: body + schema: + type: object + properties: + id_device: + type: integer + description: individual device identification + prediction: + type: string + description: the predicted label + accuracy: + type: string + description: the accuracy of the predictions + body: + type: string + description: the reccommendation response body + timestamp: + type: string + description: Creation/Update timestamp of the setting + responses: + 200: + description: Successfully updated reading + schema: + properties: + id_device: + type: integer + description: individual device identification + prediction: + type: string + description: the predicted label + accuracy: + type: string + description: the accuracy of the predictions + body: + type: string + description: the reccommendation response body + timestamp: + type: string + description: Creation/Update timestamp of the setting + delete: + operationId: reading.delete + tags: + - Reading + summary: Delete a reading from the list + description: Delete a reading + parameters: + - name: reading_id + in: path + type: integer + description: id of the reading to delete + required: true + responses: + 200: + description: Successfully deleted a reading + /people: + get: + operationId: people.read_all + tags: + - People + summary: Read the entire set of people, sorted by last name + description: Read the entire set of people, sorted by last name + responses: + 200: + description: Successfully read people set operation + schema: + type: array + items: + properties: + person_id: + type: string + description: Id of the person + fname: + type: string + description: First name of the person + lname: + type: string + description: Last name of the person + timestamp: + type: string + description: Creation/Update timestamp of the person + + post: + operationId: people.create + tags: + - People + summary: Create a person + description: Create a new person + parameters: + - name: person + in: body + description: Person to create + required: True + schema: + type: object + properties: + fname: + type: string + description: First name of person to create + lname: + type: string + description: Last name of person to create + responses: + 201: + description: Successfully created person + schema: + properties: + person_id: + type: string + description: Id of the person + fname: + type: string + description: First name of the person + lname: + type: string + description: Last name of the person + timestamp: + type: string + description: Creation/Update timestamp of the person record + + /people/{person_id}: + get: + operationId: people.read_one + tags: + - People + summary: Read one person + description: Read one person + parameters: + - name: person_id + in: path + description: Id of the person to get + type: integer + required: True + responses: + 200: + description: Successfully read person from people data operation + schema: + type: object + properties: + person_id: + type: string + description: Id of the person + fname: + type: string + description: First name of the person + lname: + type: string + description: Last name of the person + timestamp: + type: string + description: Creation/Update timestamp of the person record + + put: + operationId: people.update + tags: + - People + summary: Update a person + description: Update a person + parameters: + - name: person_id + in: path + description: Id the person to update + type: integer + required: True + - name: person + in: body + schema: + type: object + properties: + fname: + type: string + description: First name of the person + lname: + type: string + description: Last name of the person + responses: + 200: + description: Successfully updated person + schema: + properties: + person_id: + type: string + description: Id of the person in the database + fname: + type: string + description: First name of the person + lname: + type: string + description: Last name of the person + timestamp: + type: string + description: Creation/Update timestamp of the person record + + delete: + operationId: people.delete + tags: + - People + summary: Delete a person from the people list + description: Delete a person + parameters: + - name: person_id + in: path + type: integer + description: Id of the person to delete + required: true + responses: + 200: + description: Successfully deleted a person diff --git a/app-engine/templates/base.html b/app-engine/templates/base.html new file mode 100644 index 0000000..ca08bcd --- /dev/null +++ b/app-engine/templates/base.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + OCI | Deep Gauge + + + + + + +
+ + + + + {% block content %}{% endblock %} + + + + + + + + + {% block script %}{% endblock %} + + diff --git a/app-engine/templates/dashboard.html b/app-engine/templates/dashboard.html new file mode 100644 index 0000000..bb35ed1 --- /dev/null +++ b/app-engine/templates/dashboard.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+ {% if devices %} + {% for device in devices %} +
+
+ {% if device.image %} + Card image cap + {% endif %} +
+
Device {{ device.id }}
+

+ Prediction + {{ device.prediction }} +

+ Updated: {{ device.updated }} +
+ View Device +
+
+
+ {% endfor %} + {% else %} +

No Devices :(

+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/app-engine/templates/new_device.html b/app-engine/templates/new_device.html new file mode 100644 index 0000000..6cd6035 --- /dev/null +++ b/app-engine/templates/new_device.html @@ -0,0 +1,100 @@ +{% extends 'base.html' %} +{% block content %} + +
+ + +
+
+
+

Add New Device

+ + + + +
+
+
+
+ + + + + + +