diff --git a/app/client/startup.js b/app/client/startup.js index 1a3518a..e4d6914 100644 --- a/app/client/startup.js +++ b/app/client/startup.js @@ -2,7 +2,7 @@ Tracker.autorun(function () { var id = Session.get('controller_id'); if (id) { Meteor.subscribe("sensor_data", id); - var hamo = Meteor.subscribe("controller_state", id); - console.log(hamo); + Meteor.subscribe("controller_state", id); + Meteor.subscribe('pictures', id); } }); diff --git a/app/client/state_details.html b/app/client/state_details.html index 070059d..b818f86 100644 --- a/app/client/state_details.html +++ b/app/client/state_details.html @@ -12,6 +12,7 @@
Otpusni ventil: {{pretty_valve state.out_valve }}
Ulazni ventil/pumpa: {{pretty_valve state.in_valve }}
+
Dobavlja sliku: {{picture_requested state }}
Zadnja komunikacija: {{ last_communication_time }}
Zadnje zaljevanje: {{ last_out_valve_open }} diff --git a/app/client/state_details.js b/app/client/state_details.js index 17b3e26..35706ba 100644 --- a/app/client/state_details.js +++ b/app/client/state_details.js @@ -9,10 +9,13 @@ Template.state_details.helpers({ return moment(this.time).fromNow(); }, last_out_valve_open: function() { - return moment(this.significantEvents.lastOutValveOpen).fromNow(); + return ""; }, last_in_valve_open: function() { - return moment(this.significantEvents.lastInValveOpen).fromNow(); + return ""; + }, + picture_requested: function(state) { + return (state.picture_requested === 'true') ? 'DA' : 'NE'; } }); diff --git a/app/client/surveillance.html b/app/client/surveillance.html new file mode 100644 index 0000000..2750fa9 --- /dev/null +++ b/app/client/surveillance.html @@ -0,0 +1,15 @@ + diff --git a/app/client/surveillance.js b/app/client/surveillance.js new file mode 100644 index 0000000..d61bd35 --- /dev/null +++ b/app/client/surveillance.js @@ -0,0 +1,49 @@ +function controller_state() { + var controllerId = Session.get('controller_id'); + result = ControllerState.findOne({}); + if (!result) { + result = {} + }; + return result; +}; + +function picture() { + var controllerId = Session.get('controller_id'); + result = Picture.findOne({ + controller_id: controllerId + }); + console.log("rez je", result); + if (!result) { + result = {} + }; + return result; +}; + + + +Template.surveillance.helpers({ + controller_state: controller_state, + picture_src: function() { + var picture_base64 = picture().picture_base64; + var picture_src = '/images/noImage.png'; + if (picture_base64) { + picture_src = 'data:image/jpeg;charset=utf-8;base64,' + picture_base64; + } + return picture_src; + }, + picture_time: function() { + var picture_entry = picture(); + if (picture_entry) { + return moment(picture_entry.time).fromNow(); + } else { + return "Nikad!"; + } + } +}); + +Template.surveillance.events({ + 'click #request_new_picture': function() { + var controller_id = Session.get('controller_id'); + Meteor.call('requestNewPicture', controller_id) + } +}); diff --git a/app/client/tabs.html b/app/client/tabs.html index 036106b..accaac7 100644 --- a/app/client/tabs.html +++ b/app/client/tabs.html @@ -4,8 +4,8 @@ + - diff --git a/app/client/tabs.js b/app/client/tabs.js index 0de0d4b..ff1bd86 100644 --- a/app/client/tabs.js +++ b/app/client/tabs.js @@ -26,10 +26,12 @@ Template.tabs.events({ 'click .log': function() { Session.set('templateName', 'log'); }, + 'click .surveillance': function() { + Session.set('templateName', 'surveillance'); + }, 'click .settings': function() { Session.set('templateName', 'settings'); }, - 'click #switch': function() { var instance = Template.instance(); controller_id = instance.$('#controller').val(); diff --git a/app/common/collections.js b/app/common/collections.js index c45a563..84c7465 100644 --- a/app/common/collections.js +++ b/app/common/collections.js @@ -1,3 +1,4 @@ SensorData = new Mongo.Collection("sensorData"); ControllerState = new Mongo.Collection("controller_states"); +Picture = new Mongo.Collection("pictures"); diff --git a/app/public/images/noImage.png b/app/public/images/noImage.png new file mode 100644 index 0000000..757a9bf Binary files /dev/null and b/app/public/images/noImage.png differ diff --git a/app/server/api.js b/app/server/api.js index 7f654d9..ed6b715 100644 --- a/app/server/api.js +++ b/app/server/api.js @@ -10,7 +10,7 @@ Api.addRoute('sensorData', { }, { post: function() { reactToSensorData(this.bodyParams); - SensorData.insert({ + var sensorObject = { temperatureValue: parseFloat(this.bodyParams.temperatureValue), humidityValue: parseFloat(this.bodyParams.humidityValue), tankLevel0: this.bodyParams.tankLevel0, @@ -24,7 +24,8 @@ Api.addRoute('sensorData', { owner: this.bodyParams.owner, controllerId: this.bodyParams.controllerId, created_at: new Date() - }); + }; + SensorData.insert(sensorObject); return []; } }); @@ -47,11 +48,11 @@ reactToSensorData = function(nextSensorReading) { var shouldStartAutomaticPumping = (!state.in_valve || state.in_valve === 'closed') && ((parseInt(nextSensorReading.tankFull) === 0) && startPumpingLevelReached && (state.out_valve === 'closed' || state.out_valve === 'closing')); - console.log("State: ", state); + console.log("State: ", state); var shouldStartPumping = shouldStartAutomaticPumping && config && !config.manualInflow; - console.log("shouldStartAutomaticPumping: ", shouldStartAutomaticPumping); + console.log("shouldStartAutomaticPumping: ", shouldStartAutomaticPumping); console.log("shouldStartPumping: ", shouldStartPumping); @@ -75,11 +76,11 @@ reactToSensorData = function(nextSensorReading) { else if (nextSensorReading.stopPumpingAt == 'TANKLEVEL4') stopPumpingLevelReached = (parseInt(nextSensorReading.tankLevel4) === 1) else stopPumpingLevelReached = false; - var shouldStopAutomaticPumping = (state.in_valve === 'open' || state.in_valve === 'opening') && (parseInt(nextSensorReading.tankFull) === 1 || stopPumpingLevelReached || state.out_valve === 'open' || state.out_valve === 'opening'); + var shouldStopAutomaticPumping = (state.in_valve === 'open' || state.in_valve === 'opening') && (parseInt(nextSensorReading.tankFull) === 1 || stopPumpingLevelReached || state.out_valve === 'open' || state.out_valve === 'opening'); - var shouldStopPumping = shouldStopAutomaticPumping && config && !config.manualInflow; + var shouldStopPumping = shouldStopAutomaticPumping && config && !config.manualInflow; - console.log("shouldStopPumping: ", shouldStopPumping); + console.log("shouldStopPumping: ", shouldStopPumping); if (shouldStopPumping) { @@ -106,6 +107,7 @@ Api.addRoute('state/:id', { '$set': { 'state.out_valve': this.bodyParams.out_valve, 'state.in_valve': this.bodyParams.in_valve, + 'state.picture_requested': this.bodyParams.picture_requested, 'time': new Date(), 'set_by': 'client' } @@ -116,10 +118,35 @@ Api.addRoute('state/:id', { } }); +Api.addRoute('picture/:id', { + authRequired: false +}, { + post: function() { + console.log("setting picture", this.bodyParams); + return Picture.upsert({ + controller_id: this.urlParams.id + }, { + '$set': { + 'picture_base64': this.bodyParams.picture_base64, + 'time': new Date() + } + }); + } +}); + +function base64_to_buffer(base64string) { + var buf; + if (typeof Buffer.from === "function") { + buf = Buffer.from(base64string, 'base64'); + } else { + buf = new Buffer(base64string, 'base64'); + } + return buf; +} function stateOrDefault(id) { var stateEntry = ControllerState.findOne({ - controller_id: id, + controller_id: id }); if (stateEntry === undefined) { diff --git a/app/server/methods.js b/app/server/methods.js index 16d71cb..4459f77 100644 --- a/app/server/methods.js +++ b/app/server/methods.js @@ -52,6 +52,17 @@ function setInValveTo(controller_id, nextState) { } +function requestNewPicture(controller_id) { + var state = controller_state(controller_id); + ControllerState.update(state._id, { + '$set': { + 'state.picture_requested': 'true', + 'time': new Date(), + 'set_by': 'server' + } + }); +}; + function openInValve(controller_id) { var state = controller_state(controller_id); var config = state.config; @@ -174,5 +185,6 @@ Meteor.methods({ openInValve: openInValve, closeInValve: closeInValve, clearLog: clearLog, - saveControllerConfig: saveControllerConfig + saveControllerConfig: saveControllerConfig, + requestNewPicture: requestNewPicture }); diff --git a/app/server/publications.js b/app/server/publications.js index cd14e21..30ae624 100644 --- a/app/server/publications.js +++ b/app/server/publications.js @@ -16,3 +16,11 @@ Meteor.publish("controller_state", function(controllerId) { controller_id: controllerId }); }); + + +// This code only runs on the server +Meteor.publish("pictures", function(controllerId) { + return Picture.find({ + controller_id: controllerId + }); +}); diff --git a/controller/README.md b/controller/README.md index 78dbd22..64f17b9 100644 --- a/controller/README.md +++ b/controller/README.md @@ -11,6 +11,8 @@ crontab -e -u root #enter these lines */5 * * * * cd /home/pi/projects/tfm/controller && sh activity.sh */30 * * * * /usr/bin/python /home/pi/projects/tfm/controller/network_check.py +*/1 * * * * /usr/bin/python /home/pi/projects/tfm/controller/camera_capture.py +*/1 * * * * /usr/bin/python /home/pi/projects/tfm/controller/camera_send.py ``` 4. add following lines at the end of /etc/rc.local @@ -19,3 +21,20 @@ crontab -e -u root python /home/pi/projects/tfm/controller/lockdown.py python /home/pi/projects/tfm/controller/dweet.py ``` + +5. create directory +``` +mkdir -p /mnt/zoblakdata +``` + +6. add following line to /etc/fstab for ramdisk +``` +tmpfs /mnt/zoblakdata tmpfs nodev,nosuid,size=100M 0 0 +``` + +7. install packages + +``` +sudo apt-get update +sudo apt-get install libav-tools +``` diff --git a/controller/camera_capture.py b/controller/camera_capture.py new file mode 100644 index 0000000..ff590d5 --- /dev/null +++ b/controller/camera_capture.py @@ -0,0 +1,3 @@ +import drivers.camera as camera + +camera.capture_picture() diff --git a/controller/camera_send.py b/controller/camera_send.py new file mode 100644 index 0000000..afc0b9a --- /dev/null +++ b/controller/camera_send.py @@ -0,0 +1,16 @@ +import drivers.camera as camera +import requests +import config + +picture_base64 = camera.get_transfer_picture_base64() +if picture_base64 is not None: + camera.remove_transfer_picture() + controller_id = config.CONTROLLER_ID + owner = "Controller: %s" % controller_id + + response = requests.post(config.PICTURE_URL + '/' + controller_id, json={ + "picture_base64": picture_base64 + }) + print response + +print picture_base64 is not None diff --git a/controller/config/copy__init__.py.example b/controller/config/copy__init__.py.example index 52ec8d4..244d6d9 100644 --- a/controller/config/copy__init__.py.example +++ b/controller/config/copy__init__.py.example @@ -1,5 +1,3 @@ - - GPIO_PIN_DHT = 4 # BCM SENSORDATA_URL = 'http://agrar.zoblak.com/api/v1.0/sensorData' GPIO_PIN_TANKLEVEL0 = 5 # BCM @@ -14,4 +12,8 @@ START_PUMPING_AT = 'TANKFULL' # start pumping when this level = 0 STOP_PUMPING_AT = 'TANKFULL' # stop pumping when this level = 1 API_BASE_URL = 'http://agrar.zoblak.com/api/v1.0' CONTROLLER_ID = '120' # every controller must have a different one -STATE_FILE = '/var/run/controller_state' +STATE_FILE = '/mnt/zoblakdata/controller_state' +PICTURE_TRANSFER_FILE='/mnt/zoblakdata/picture_transfer_ready.jpg' +PICTURE_INPUT_FILE='/mnt/zoblakdata/picture.jpg' # must match file in PICTURE_COMMAND +PICTURE_COMMAND="""avconv -i rtsp://192.168.5.10:554//user=admin_password=_channel=1_stream=0.sdp -i /home/pi/projects/tfm/controller/zoblakLogo.png -filter_complex "[0:v]drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf:fontsize=30:box=1:boxcolor=black@0.75:text='%d\.%m\.%Y\. %H\:%M\:%S':fontcolor=white@0.8: x=10: y=10[text]; [text][1:v]overlay=main_w-overlay_w-5:5 [filtered]" -map "[filtered]" -f image2 -vframes 1 /mnt/zoblakdata/picture.jpg""" # filename must match PICTURE_INPUT_FILE path +PICTURE_URL = 'http://agrar.zoblak.com/api/v1.0/picture/' diff --git a/controller/drivers/__init__.py b/controller/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/controller/drivers/camera/__init__.py b/controller/drivers/camera/__init__.py new file mode 100644 index 0000000..3fc347a --- /dev/null +++ b/controller/drivers/camera/__init__.py @@ -0,0 +1,28 @@ +import config +import os +import base64; +from shutil import copyfile +import sys + +def capture_picture(): + os.system(config.PICTURE_COMMAND) + +def get_transfer_picture_base64(): + try: + with open(config.PICTURE_TRANSFER_FILE, "rb") as image_file: + return base64.b64encode(image_file.read()) + except: + print("Unexpected error:", sys.exc_info()[0]) + return None + +def remove_transfer_picture(): + try: + os.remove(config.PICTURE_TRANSFER_FILE) + except: + print("Error removing: ", config.PICTURE_TRANSFER_FILE ) + +def make_transfer_picture(): + try: + copyfile(config.PICTURE_INPUT_FILE, config.PICTURE_TRANSFER_FILE) + except: + print("Error copying: ", config.PICTURE_INPUT_FILE, config.PICTURE_TRANSFER_FILE ) diff --git a/controller/sensors.py b/controller/sensors.py index 9c8ff9d..70ea964 100644 --- a/controller/sensors.py +++ b/controller/sensors.py @@ -7,27 +7,27 @@ import RPi.GPIO as GPIO # Try to read the state of GPIO_PIN_TANKLEVELx and GPIO_PIN_TANKFULL GPIO.setmode(GPIO.BCM) -GPIO.setup(config.GPIO_PIN_TANKLEVEL0, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) -GPIO.setup(config.GPIO_PIN_TANKLEVEL1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) -GPIO.setup(config.GPIO_PIN_TANKLEVEL2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) -GPIO.setup(config.GPIO_PIN_TANKLEVEL3, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) +#GPIO.setup(config.GPIO_PIN_TANKLEVEL0, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) +#GPIO.setup(config.GPIO_PIN_TANKLEVEL1, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) +#GPIO.setup(config.GPIO_PIN_TANKLEVEL2, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) +#GPIO.setup(config.GPIO_PIN_TANKLEVEL3, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) GPIO.setup(config.GPIO_PIN_TANKLEVEL4, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) GPIO.setup(config.GPIO_PIN_TANKFULL, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) # tank sensor has inverse logic - 0 when it is full 1 when it is not # so we are inverting its value here -tankLevel0 = (GPIO.input(config.GPIO_PIN_TANKLEVEL0) == GPIO.LOW) -tankLevel1 = (GPIO.input(config.GPIO_PIN_TANKLEVEL1) == GPIO.LOW) -tankLevel2 = (GPIO.input(config.GPIO_PIN_TANKLEVEL2) == GPIO.LOW) -tankLevel3 = (GPIO.input(config.GPIO_PIN_TANKLEVEL3) == GPIO.LOW) +# tankLevel0 = (GPIO.input(config.GPIO_PIN_TANKLEVEL0) == GPIO.LOW) +# tankLevel1 = (GPIO.input(config.GPIO_PIN_TANKLEVEL1) == GPIO.LOW) +# tankLevel2 = (GPIO.input(config.GPIO_PIN_TANKLEVEL2) == GPIO.LOW) +# tankLevel3 = (GPIO.input(config.GPIO_PIN_TANKLEVEL3) == GPIO.LOW) tankLevel4 = (GPIO.input(config.GPIO_PIN_TANKLEVEL4) == GPIO.LOW) tankFull = (GPIO.input(config.GPIO_PIN_TANKFULL) == GPIO.LOW) GPIO.cleanup() -print 'Bacva Level0: {}'.format(tankLevel0) -print 'Bacva Level1: {}'.format(tankLevel1) -print 'Bacva Level2: {}'.format(tankLevel2) -print 'Bacva Level3: {}'.format(tankLevel3) +# print 'Bacva Level0: {}'.format(tankLevel0) +# print 'Bacva Level1: {}'.format(tankLevel1) +# print 'Bacva Level2: {}'.format(tankLevel2) +# print 'Bacva Level3: {}'.format(tankLevel3) print 'Bacva Level4: {}'.format(tankLevel4) print 'Bacva puna: {}'.format(tankFull) @@ -42,6 +42,7 @@ stopPumpingAt = config.STOP_PUMPING_AT # to 15 times to get a sensor reading (waiting 2 seconds between each retry). humidity, temperature = Adafruit_DHT.read_retry(SENSOR_TYPE, config.GPIO_PIN_DHT) + # Un-comment the line below to convert the temperature to Fahrenheit. # temperature = temperature * 9/5.0 + 32 @@ -50,7 +51,13 @@ humidity, temperature = Adafruit_DHT.read_retry(SENSOR_TYPE, config.GPIO_PIN_DHT # guarantee the timing of calls to read the sensor). # If this happens try again! if tankFull is not None: - response = requests.post(config.SENSORDATA_URL, json={"owner": owner, "temperatureValue": temperature, "humidityValue":humidity, "tankLevel0": "1" if tankLevel0 else "0","tankLevel1": "1" if tankLevel1 else "0","tankLevel2": "1" if tankLevel2 else "0","tankLevel3": "1" if tankLevel3 else "0", "tankLevel4": "1" if tankLevel4 else "0", "tankFull": "1" if tankFull else "0", + response = requests.post(config.SENSORDATA_URL, json={"owner": owner, "temperatureValue": temperature, "humidityValue":humidity, + # "tankLevel0": "1" if tankLevel0 else "0", + # "tankLevel1": "1" if tankLevel1 else "0", + # "tankLevel2": "1" if tankLevel2 else "0", + # "tankLevel3": "1" if tankLevel3 else "0", + "tankLevel4": "1" if tankLevel4 else "0", + "tankFull": "1" if tankFull else "0", "startPumpingAt": startPumpingAt,"stopPumpingAt": stopPumpingAt,"controllerId": controller_id }) print 'Temp={0:0.1f}*C'.format(temperature) diff --git a/controller/state/__init__.py b/controller/state/__init__.py index 1c5ffb1..d1a50d5 100644 --- a/controller/state/__init__.py +++ b/controller/state/__init__.py @@ -37,7 +37,10 @@ def sync(): changer = Changer(local_state, server_state) current_state = changer.process_change() + print "posting :" + repr(current_state) + local.data = current_state server.post_state(current_state) + local.save() except: print " panicking safely ! " safely_panic() diff --git a/controller/state/changer.py b/controller/state/changer.py index 3c6f4f0..f2b7714 100644 --- a/controller/state/changer.py +++ b/controller/state/changer.py @@ -1,5 +1,6 @@ import RPi.GPIO as GPIO import config +from drivers.camera import make_transfer_picture class Changer(object): @@ -45,6 +46,8 @@ class Changer(object): if in_valve_change is not None: in_valve_change() + self.fulfill_picture_request() + return self.local_state def open_in_valve(self): @@ -63,6 +66,12 @@ class Changer(object): GPIO.output(config.GPIO_PIN_OUT_VALVE, GPIO.LOW) self.local_state['out_valve'] = 'closed' + def fulfill_picture_request(self): + if self.remote_state['picture_requested'] == 'true': + make_transfer_picture() + self.local_state['picture_requested'] = 'false' + + def validate_states(self): if self.local_state is None or self.remote_state is None: raise ClassNotReadyException("Both local and remote states must be present!") diff --git a/controller/state/file.py b/controller/state/file.py index 1172205..9bbe285 100644 --- a/controller/state/file.py +++ b/controller/state/file.py @@ -9,7 +9,8 @@ class File(object): self.filename = filename def present(self): - os.path.isfile(self.filename) + return os.path.isfile(self.filename) + def load(self): if self.filename is None: diff --git a/controller/zoblakLogo.png b/controller/zoblakLogo.png new file mode 100644 index 0000000..27112ff Binary files /dev/null and b/controller/zoblakLogo.png differ