from flask import Flask, render_template, jsonify, request from flask_apscheduler import APScheduler from copy import copy import uuid import time from datetime import datetime import requests app = Flask(__name__) # UUID used for internal calls. internalUUID = str(uuid.uuid4()) # Vitals vitalsHeartrate = None vitalsOxygen = None vitalsBodytemp = None @app.route('/api/vitals/heartrate') def getVitalsHeartrate(): returnArr = [ { 'heartrate': vitalsHeartrate } ] return jsonify(returnArr) @app.route('/api/vitals/heartrate', methods=['POST']) def setVitalsHeartrate(): global vitalsHeartrate json = request.get_json() try: if not authenticate(json['uuid'], '/api/vitals/heartrate'): return 'Forbidden.', 403 vitalsHeartrate = json['heartrate'] except: return 'Incorrect usage.\nUsage: { heartrate: INT, uuid: STRING }\n', 400 return '', 204 @app.route('/api/vitals/oxygen') def getVitalsOxygen(): returnArr = [ { 'oxygen': vitalsOxygen } ] return jsonify(returnArr) @app.route('/api/vitals/oxygen', methods=['POST']) def setVitalsOxygen(): global vitalsOxygen json = request.get_json() try: if not authenticate(json['uuid'], '/api/vitals/oxygen'): return 'Forbidden.', 403 vitalsOxygen = json['oxygen'] except: return 'Incorrect usage.\nUsage: { oxygen: INT }\n', 400 return '', 204 @app.route('/api/vitals/bodytemp') def getVitalsBodytemp(): returnArr = [ { 'bodytemp': vitalsBodytemp } ] return jsonify(returnArr) @app.route('/api/vitals/bodytemp', methods=['POST']) def setVitalsBodytemp(): global vitalsBodytemp json = request.get_json() try: if not authenticate(json['uuid'], '/api/vitals/bodytemp'): return 'Forbidden.', 403 vitalsBodytemp = json['bodytemp'] except: return 'Incorrect usage.\nUsage: { bodytemp: FLOAT }\n', 400 return '', 204 @app.route('/api/vitals') def getVitals(): returnArr = [ { 'heartrate': vitalsHeartrate, 'oxygen': vitalsOxygen, 'bodytemp': vitalsBodytemp } ] return jsonify(returnArr) @app.route('/api/vitals', methods=['POST']) def setVitals(): global vitalsHeartrate global vitalsOxygen global vitalsBodytemp json = request.get_json() try: if not authenticate(json['uuid'], '/api/vitals'): return 'Forbidden.', 403 # This is a bit ugly but its just how I'm checking that everything is there without setting variables if the json is incorrect tempH = json['heartrate'] tempO = json['oxygen'] tempB = json['bodytemp'] vitalsHeartrate = tempH vitalsOxygen = tempO vitalsBodytemp = tempB except: return 'Incorrect usage.\nUsage: { heartrate: INT, oxygen: INT, bodytemp: FLOAT }\n', 400 return '', 204 # Fitness fitnessSteps = None @app.route('/api/fitness/steps') def getSteps(): return getFitness() # This is the same for now. @app.route('/api/fitness/steps', methods=['POST']) def setFitnessSteps(): global fitnessSteps json = request.get_json() try: if not authenticate(json['uuid'], '/api/fitness/steps'): return 'Forbidden.', 403 vitalsBodytemp = json['steps'] except: return 'Incorrect usage.\nUsage: { steps: INT }\n', 400 return '', 204 @app.route('/api/fitness') def getFitness(): returnArr = [ { 'steps': fitnessSteps } ] return jsonify(returnArr) @app.route('/api/fitness', methods=['POST']) def setFitness(): global fitnessSteps json = request.get_json() try: if not authenticate(json['uuid'], '/api/fitness'): return 'Forbidden.', 403 fitnessSteps = json['steps'] except: return 'Incorrect usage.\nUsage: { steps: INT }\n', 400 return '', 204 # Cyberware management cyberware = [] newCyberwareTemplate = { "uuid": None, "name": None, "lastContact": None, "hotpluggable": False, "lastMalfunction": None, "canSet": None, "battery": None, "messages": None } newMessageTemplate = { "title": None, "message": None, "progress": None } # Messages: { Title, Message, Progress } # Title, Message, and Progress are all technically optional. It's up to the frontend to make heads or tails of what's happening. # Typically, if Progress == None: No progress bar, will show up in the Top Message section on NightUI # if Message == None: No message # if Title == None: No title. # # Typical layout for such messages: # _____________________________________________________ # | ICON This is a title! # | ICON This is a message! # | ICON [======= ] # This makes the system aware of a new piece of hardware. # While, for the most part, not required due to the design of this project, # it's handy for error reporting and communication between hardware and # the end user. # # Arguments: { name: STRING, hotpluggable: BOOL, canSet: ARRAY } # Returns: { uuid: INT } # name: A human-readable name # hotpluggable argument: Used for removable modules known as shards. # # uuid: Returns a uuid for the hardware. @app.route('/api/cyberware/add', methods=['POST']) def addCyberware(): global cyberware json = request.get_json() try: tempName = json['name'] tempHotpluggable = json['hotpluggable'] tempCanSet = json['canSet'] tempNewCyberware = copy(newCyberwareTemplate) tempNewCyberware['uuid'] = str(uuid.uuid4()) tempNewCyberware['name'] = tempName tempNewCyberware['lastContact'] = datetime.now() tempNewCyberware['hotpluggable'] = tempHotpluggable tempNewCyberware['canSet'] = tempCanSet cyberware.append(tempNewCyberware) except: return 'Incorrect usage.\nUsage: { name: STRING, hotpluggable: BOOL, canSet: ARRAY }\n', 400 return jsonify([ { "uuid": tempNewCyberware['uuid'] } ]), 200 # Arguments: { uuid: INT } @app.route('/api/cyberware/remove', methods=['POST']) def removeCyberware(): global cyberware json = request.get_json() try: desiredId = json['uuid'] i = 0 for c in cyberware: if c['uuid'] == desiredId: del cyberware[i] return '', 204 i = i + 1 except: return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400 return 'UUID Invalid\n', 400 # Returns: { name: STRING, hotpluggable: STRING lastMalfunction: STRING, battery: INT, messages: ARRAY } # uuid: Unique identifier of the hardware # name: Human-readable name # hotpluggable: Hardware can be removed during runtime. # lastMalfunction: A string with information on the last malfunction. @app.route('/api/cyberware') def getCyberware(): returnArr = [] for c in cyberware: returnArr.append({ 'name': c['name'], 'hotpluggable': c['hotpluggable'], 'lastMalfunction': c['lastMalfunction'], 'battery': c['battery'] }) return jsonify(returnArr), 200 @app.route('/api/cyberware/malfunctions') def getCyberwareMalfunctions(): returnArr = [] for c in cyberware: if c['lastMalfunction'] != None: returnArr.append({ 'name': c['name'], 'lastMalfunction': c['lastMalfunction'] }) resetMalfunctions() return jsonify(returnArr), 200 # Arguments { uuid: STRING, malfunction: STRING } @app.route('/api/cyberware/malfunctions', methods=['POST']) def setCyberwareMalfunction(): json = request.get_json() try: desiredId = json['uuid'] malfunction = json['malfunction'] requestedCyberware = getCyberwareHelper(desiredId) requestedCyberware['lastMalfunction'] = malfunction except: return 'Incorrect usage.\nUsage: { malfunction: STRING, uuid: STRING }\n', 400 @app.route('/api/cyberware/messages') def getCyberwareMessages(): returnArr = [] for c in cyberware: if c['messages'] != None: returnArr.append({ 'name': c['name'], 'messages': c['messages'] }) resetMessages() return jsonify(returnArr), 200 # Arguments { uuid: STRING, message:{ title: STRING, message: STRING, progress: INT } } @app.route('/api/cyberware/messages', methods=['POST']) def setCyberwareMessages(): global cyberware json = request.get_json() try: desiredId = json['uuid'] message = json['message'] # Test message validity testTitle = message["title"] testMessage = message["message"] testProgress = message["progress"] requestedCyberware = getCyberwareHelper(desiredId) if (requestedCyberware != None): if (requestedCyberware["messages"] == None): requestedCyberware["messages"] = [] requestedCyberware["messages"].append(message) return '', 204 except: return "Incorrect usage.\nUsage: { uuid: STRING, message:{ title: STRING, message: STRING, progress: INT } }\n", 400 return "UUID Invalid\n", 400 @app.route('/api/cyberware/get', methods=['POST']) def getCyberwareSpecific(): json = request.get_json() try: desiredId = json['uuid'] requestedCyberware = getCyberwareHelper(desiredId) if (requestedCyberware != None): return jsonify(requestedCyberware), 200 except: return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400 return 'UUID Invalid\n', 400 # Arguments { uuid: STRING } @app.route('/api/cyberware/battery') def getCyberwareBattery(): json = request.get_json() try: desiredId = json['uuid'] requestedCyberware = getCyberwareHelper(desiredId) if (requestedCyberware != None): return jsonify([ { "battery": requestedCyberware['battery'] } ]), 200 except: return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400 return 'UUID Invalid\n', 400 # Arguments { uuid: STRING, battery: INT } @app.route('/api/cyberware/battery', methods=['POST']) def setCyberwareBattery(): global cyberware json = request.get_json() try: desiredId = json['uuid'] battery = json['battery'] requestedCyberware = getCyberwareHelper(desiredId) if (requestedCyberware != None): requestedCyberware['battery'] = battery return '', 204 except: return 'Incorrect usage.\nUsage: { battery: INT, uuid: STRING }\n', 400 return 'UUID Invalid\n', 400 @app.route('/api/cyberware/reset_malfunction') def resetAllCyberwareMalfunction(): resetMalfunctions() # Arguments { uuid: STRING } @app.route('/api/cyberware/reset_malfunction', methods=['POST']) def resetCyberwareMalfunction(): global cyberware json = request.get_json() try: desiredId = json['uuid'] requestedCyberware = getCyberwareHelper(desiredId) if (requestedCyberware != None): requestedCyberware['lastMalfunction'] = None return '', 204 except: return 'Incorrect usage.\nUsage: { uuid: STRING }\n', 400 return 'UUID Invalid\n', 400 def resetMalfunctions(): global cyberware for c in cyberware: c['lastMalfunction'] = None def resetMessages(): global cyberware for c in cyberware: if c['messages'] != None: newMessageList = [] for m in c['messages']: if(m['progress'] != None): # Unless we're FULLY resetting messages, we want to keep messages with a progress bar attached. newMessageList.append(m) if newMessageList == []: c['messages'] = None else: c['messages'] = newMessageList def resetMessagesFull(): global cyberware for c in cyberware: c['messages'] = None def getCyberwareHelper(desiredId): i = 0 for c in cyberware: if c['uuid'] == desiredId: return c break return None # Environment data environmentTemperature = None environmentHumidity = None @app.route('/api/environment') def getEnvironment(): returnArr = [ { 'temperature': environmentTemperature, 'humidity': environmentHumidity } ] return jsonify(returnArr), 200 # Environment//Temperature @app.route('/api/environment/temperature') def getEnvironmentTemperature(): returnArr = [ { 'temperature': environmentTemperature } ] return jsonify(returnArr), 200 @app.route('/api/environment/temperature', methods=['POST']) def setEnvironmentTemperature(): global environmentTemperature json = request.get_json() try: tempTemperature = json['temperature'] environmentTemperature = tempTemperature except: return 'Incorrect usage.\nUsage: { temperature: INT, uuid: STRING }\n', 400 return '', 204 # Environment//Humidity @app.route('/api/environment/humidity') def getEnvironmentHumidity(): returnArr = [ { 'humidity': environmentHumidity } ] return jsonify(returnArr), 200 @app.route('/api/environment/humidity', methods=['POST']) def setEnvironmentHumidity(): global environmentHumidity json = request.get_json() try: tempHumidity = json['humidity'] environmentHumidity = tempHumidity except: return 'Incorrect usage.\nUsage: { humidity: INT, uuid: STRING }\n', 400 return '', 204 # Authentication method # This authorizes the given UUID to determine whether the request is # allowed to set the requested endpoint. # This is ONLY used for PUSH requests currently. def authenticate(uuid, endpoint): # Check for internal UUID if uuid == internalUUID: return True for c in cyberware: # UUID Match if c['uuid'] == uuid: requestedHardware = c c['lastContact'] = datetime.now() # Update last contact break if requestedHardware['canSet'] == None: return False for e in requestedHardware['canSet']: # Endpoint match if e == endpoint: return True return False @app.route('/') def uiindex(): return render_template('index.html') # Maintenance functions # The jank, my oh my valuesToValidate = [ '/api/vitals/heartrate', '/api/vitals/oxygen', '/api/vitals/bodytemp', '/api/fitness/steps', '/api/environment/temperature', '/api/environment/humidity'] baseURL = "http://localhost:5000" # Value invalidation. A value is deemed invalid when there's no hardware attached # that can set it. # This has the potential to become very slow. A better solution is needed. def valueInvalidation(): print("Start value invalidation") # Search for invalid values invalidated = copy(valuesToValidate) for c in cyberware: for value in valuesToValidate: if value in c["canSet"]: invalidated.remove(value) # Value invalidation begins invalidStr = "" for invalidValue in invalidated: # Now this looks stupid, but since the key to post is always supposed to be the same # as the last part of the path, this works. # Does that make sense? I'm tired... key = invalidValue.split('/')[-1] endpointToReset = baseURL + invalidValue requests.post(endpointToReset, json={ key: None, 'uuid': internalUUID }) invalidStr = invalidStr + invalidValue + ", " print("Values invalidated: " + invalidStr) # Device invalidation. A device is deemed invalid after no contact for 15 seconds. timeToInvalid = 15.0 def deviceInvalidation(): cyberwareRemoveEndpoint = baseURL + "/api/cyberware/remove" print("Start device invalidation") now = datetime.now() for c in cyberware: timeSinceContact = (now-c['lastContact']).total_seconds() if timeSinceContact > timeToInvalid: # This hardware has not contacted the server in too long, and it is thus deemed invalid. invalidCyberwareName = c['name'] invalidCyberwareUUID = c['uuid'] print("Cyberware is invalid and will be removed: " + invalidCyberwareName) requests.post(cyberwareRemoveEndpoint, json={ 'uuid': invalidCyberwareUUID }) # APScheduler config class Config: JOBS = [ { "id": "valueInvalidation", "func": "nightserver:valueInvalidation", "trigger": "interval", "seconds": 10, }, { "id": "deviceInvalidation", "func": "nightserver:deviceInvalidation", "trigger": "interval", "seconds": 15 } ] SCHEDULER_API_ENABLED = True # Config Flask and APScheduler app.config.from_object(Config()) scheduler = APScheduler() scheduler.init_app(app) scheduler.start() if __name__ == "__main__": app.run()