Compare commits
29 Commits
farm_alarm
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b7e734ac10 | |||
| 3b3b6583d9 | |||
| e6342d9c93 | |||
|
|
68eb5c24f9 | ||
|
|
a1cf3a40a3 | ||
|
|
3e01c0b7f7 | ||
|
|
82ea273c0d | ||
| a5ae635e7d | |||
| 106b774098 | |||
| 048bc89b01 | |||
| 434c3a352e | |||
|
|
620c69d1a0 | ||
|
|
f6e9d79e4b | ||
|
|
aba33781ef | ||
|
|
947a9c6a43 | ||
|
|
81652c8c64 | ||
|
|
e86ef0ba3a | ||
| 8c236609e8 | |||
|
|
72a4b877a9 | ||
| 546fc65842 | |||
|
|
53cd8264e8 | ||
|
|
977fb6c809 | ||
|
|
41ed24589b | ||
|
|
38a9bd83b9 | ||
|
|
d1e216b0fd | ||
|
|
a06a22857d | ||
| 411419c375 | |||
|
|
070a1071c9 | ||
| 8db2054f05 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -60,3 +60,8 @@ target/
|
||||
|
||||
# meteor
|
||||
app.tar.gz
|
||||
.idea/
|
||||
*.o
|
||||
*.a
|
||||
*.out
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -6,3 +6,15 @@ TFM consists of following components:
|
||||
* TFM Controller - daemon running on embedded platform for reading the sensors, executing commands in offline and online mode
|
||||
|
||||
|
||||
# Deploying to server:
|
||||
|
||||
1. ssh to agrar.zoblak.com
|
||||
2. do meteor build . on dev computer
|
||||
3. cp app.tar.gz to zoblak@agrar.zoblak.com:/home/zoblak/
|
||||
4. sudo su
|
||||
5. cd
|
||||
6. npm install -g node-gyp # for bcrypt to build correctly
|
||||
7. npm install -g node-pre-gyp # for bcrypt to build correctly
|
||||
6. ./deploy.sh
|
||||
|
||||
|
||||
|
||||
19
apiv2/Makefile
Normal file
19
apiv2/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
CC = gcc
|
||||
CFLAGS = -I. -I./include -I/opt/homebrew/include -I/usr/include -Wall $(shell pkg-config --cflags jansson)
|
||||
SOURCES = src/main.c src/database.c mongoose.c sqlite3.c
|
||||
OBJECTS = $(SOURCES:.c=.o)
|
||||
TARGET = api_server
|
||||
LIBS = -lpthread -ldl $(shell pkg-config --libs jansson)
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJECTS)
|
||||
$(CC) $(CFLAGS) $(OBJECTS) -o $@ $(LIBS)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f $(OBJECTS) $(TARGET)
|
||||
BIN
apiv2/api_server
Executable file
BIN
apiv2/api_server
Executable file
Binary file not shown.
BIN
apiv2/database.db
Normal file
BIN
apiv2/database.db
Normal file
Binary file not shown.
9
apiv2/include/database.h
Normal file
9
apiv2/include/database.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef DATABASE_H
|
||||
#define DATABASE_H
|
||||
|
||||
int db_init();
|
||||
char* db_query(const char *query);
|
||||
int db_insert(const char *value);
|
||||
void db_close();
|
||||
|
||||
#endif
|
||||
19019
apiv2/mongoose.c
Normal file
19019
apiv2/mongoose.c
Normal file
File diff suppressed because it is too large
Load Diff
3166
apiv2/mongoose.h
Normal file
3166
apiv2/mongoose.h
Normal file
File diff suppressed because it is too large
Load Diff
257679
apiv2/sqlite3.c
Normal file
257679
apiv2/sqlite3.c
Normal file
File diff suppressed because it is too large
Load Diff
13425
apiv2/sqlite3.h
Normal file
13425
apiv2/sqlite3.h
Normal file
File diff suppressed because it is too large
Load Diff
88
apiv2/src/database.c
Normal file
88
apiv2/src/database.c
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "database.h"
|
||||
#include <sqlite3.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <jansson.h>
|
||||
|
||||
static sqlite3 *db;
|
||||
|
||||
int db_init() {
|
||||
int rc = sqlite3_open("database.db", &db);
|
||||
if (rc) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Initialize tables if necessary
|
||||
const char *sql = "CREATE TABLE IF NOT EXISTS data (id INTEGER PRIMARY KEY, value TEXT);";
|
||||
char *errmsg = 0;
|
||||
rc = sqlite3_exec(db, sql, 0, 0, &errmsg);
|
||||
if (rc != SQLITE_OK) {
|
||||
sqlite3_free(errmsg);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
char* db_query(const char *query) {
|
||||
// Prepare SQL statement
|
||||
const char *sql = "SELECT * FROM data WHERE value LIKE ?;";
|
||||
sqlite3_stmt *stmt;
|
||||
char *response = NULL;
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return strdup("{\"error\": \"Failed to prepare statement\"}");
|
||||
}
|
||||
|
||||
// Bind the query parameter
|
||||
sqlite3_bind_text(stmt, 1, query, -1, SQLITE_TRANSIENT);
|
||||
|
||||
// Build JSON response
|
||||
json_t *json_arr = json_array();
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
json_t *json_obj = json_object();
|
||||
int id = sqlite3_column_int(stmt, 0);
|
||||
const unsigned char *value = sqlite3_column_text(stmt, 1);
|
||||
|
||||
json_object_set_new(json_obj, "id", json_integer(id));
|
||||
json_object_set_new(json_obj, "value", json_string((const char *)value));
|
||||
|
||||
json_array_append_new(json_arr, json_obj);
|
||||
}
|
||||
|
||||
char *json_str = json_dumps(json_arr, JSON_INDENT(2));
|
||||
response = strdup(json_str);
|
||||
|
||||
// Cleanup
|
||||
free(json_str);
|
||||
json_decref(json_arr);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
int db_insert(const char *value) {
|
||||
const char *sql = "INSERT INTO data (value) VALUES (?);";
|
||||
sqlite3_stmt *stmt;
|
||||
int rc;
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Bind the value parameter
|
||||
sqlite3_bind_text(stmt, 1, value, -1, SQLITE_TRANSIENT);
|
||||
|
||||
// Execute the statement
|
||||
rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void db_close() {
|
||||
sqlite3_close(db);
|
||||
}
|
||||
112
apiv2/src/main.c
Normal file
112
apiv2/src/main.c
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "mongoose.h"
|
||||
#include "database.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <jansson.h>
|
||||
|
||||
static const char *s_http_port = "127.0.0.1:8001";
|
||||
static const char *s_root_dir = "."; // Serve current directory
|
||||
|
||||
static void handle_get(struct mg_connection *c, struct mg_http_message *hm) {
|
||||
char query[256] = {0};
|
||||
|
||||
// Parse query parameter 'q'
|
||||
mg_http_get_var(&hm->query, "q", query, sizeof(query));
|
||||
|
||||
char *response = db_query(query);
|
||||
|
||||
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", response);
|
||||
|
||||
free(response);
|
||||
}
|
||||
|
||||
static void handle_post(struct mg_connection *c, struct mg_http_message *hm) {
|
||||
char *response;
|
||||
int result;
|
||||
|
||||
// Get the JSON data from the body
|
||||
char *body = malloc(hm->body.len + 1);
|
||||
memcpy(body, hm->body.buf, hm->body.len);
|
||||
body[hm->body.len] = '\0';
|
||||
|
||||
// Parse JSON data
|
||||
json_error_t error;
|
||||
json_t *root = json_loads(body, 0, &error);
|
||||
free(body);
|
||||
|
||||
if (!root) {
|
||||
mg_http_reply(c, 400, "Content-Type: text/plain\r\n", "Invalid JSON data: %s", error.text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract data from JSON
|
||||
const char *value = json_string_value(json_object_get(root, "value"));
|
||||
|
||||
if (!value) {
|
||||
mg_http_reply(c, 400, "Content-Type: text/plain\r\n", "Missing 'value' in JSON data");
|
||||
json_decref(root);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store data in the database
|
||||
result = db_insert(value);
|
||||
|
||||
if (result == 0) {
|
||||
mg_http_reply(c, 200, "Content-Type: text/plain\r\n", "Data stored successfully");
|
||||
} else {
|
||||
mg_http_reply(c, 500, "Content-Type: text/plain\r\n", "Failed to store data");
|
||||
}
|
||||
|
||||
json_decref(root);
|
||||
}
|
||||
|
||||
static void handle_api_call(struct mg_connection *c, struct mg_http_message *hm) {
|
||||
if (mg_strcasecmp(hm->method, mg_str("GET")) == 0) {
|
||||
handle_get(c, hm);
|
||||
} else if (mg_strcasecmp(hm->method, mg_str("POST")) == 0) {
|
||||
handle_post(c, hm);
|
||||
} else {
|
||||
mg_http_reply(c, 405, "Content-Type: text/plain\r\n", "Method Not Allowed");
|
||||
}
|
||||
}
|
||||
|
||||
static void fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||
if (ev == MG_EV_HTTP_MSG) {
|
||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||
|
||||
if (mg_match(hm->uri, mg_str("/api"), NULL)) {
|
||||
handle_api_call(c, hm);
|
||||
} else {
|
||||
struct mg_http_serve_opts opts = {.root_dir = s_root_dir};
|
||||
mg_http_serve_dir(c, hm, &opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
struct mg_mgr mgr;
|
||||
|
||||
mg_mgr_init(&mgr);
|
||||
|
||||
// Initialize the database
|
||||
if (db_init() != 0) {
|
||||
printf("Failed to initialize database\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Starting RESTful API on port %s\n", s_http_port);
|
||||
|
||||
// Start listening for connections
|
||||
if (mg_http_listen(&mgr, s_http_port, fn, NULL) == NULL) {
|
||||
printf("Failed to create listener\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
mg_mgr_poll(&mgr, 1000);
|
||||
}
|
||||
|
||||
mg_mgr_free(&mgr);
|
||||
db_close();
|
||||
return 0;
|
||||
}
|
||||
1
apiv2g/go.mod
Normal file
1
apiv2g/go.mod
Normal file
@@ -0,0 +1 @@
|
||||
module apiv2g
|
||||
25
apiv2g/main.go
Normal file
25
apiv2g/main.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//TIP To run your code, right-click the code and select <b>Run</b>. Alternatively, click
|
||||
// the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.
|
||||
|
||||
func main() {
|
||||
//TIP Press <shortcut actionId="ShowIntentionActions"/> when your caret is at the underlined or highlighted text
|
||||
// to see how GoLand suggests fixing it.
|
||||
s := "gopher"
|
||||
fmt.Println("Hello and welcome, %s!", s)
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
//TIP You can try debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
|
||||
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>. To start your debugging session,
|
||||
// right-click your code in the editor and select the <b>Debug</b> option.
|
||||
fmt.Println("i =", 100/i)
|
||||
}
|
||||
}
|
||||
|
||||
//TIP See GoLand help at <a href="https://www.jetbrains.com/help/go/">jetbrains.com/help/go/</a>.
|
||||
// Also, you can try interactive lessons for GoLand by selecting 'Help | Learn IDE Features' from the main menu.
|
||||
1220
apiv2r/Cargo.lock
generated
Normal file
1220
apiv2r/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
apiv2r/Cargo.toml
Normal file
29
apiv2r/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
[package]
|
||||
name = "zoblak"
|
||||
version = "2.0.0"
|
||||
authors = ["Senad Uka <senad@uka.life>"]
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0"
|
||||
actix-files = "0.6"
|
||||
rusqlite = "0.29.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = "0.9"
|
||||
speculate = "0.1"
|
||||
parking_lot = { version = "0.12", features = ["nightly"] }
|
||||
|
||||
[dependencies.rocket_contrib]
|
||||
version = "0.4"
|
||||
default-features = false
|
||||
features = ["json", "diesel_postgres_pool"]
|
||||
|
||||
[features]
|
||||
default = [edition2024]
|
||||
|
||||
|
||||
@@ -30,3 +30,5 @@ standard-minifier-css
|
||||
standard-minifier-js
|
||||
iron:router
|
||||
accolver:twilio-meteor
|
||||
chart:chart
|
||||
reactive-var
|
||||
|
||||
@@ -15,6 +15,7 @@ boilerplate-generator@1.0.7
|
||||
caching-compiler@1.0.3
|
||||
caching-html-compiler@1.0.5
|
||||
callback-hook@1.0.7
|
||||
chart:chart@1.0.1-beta.4
|
||||
check@1.1.3
|
||||
coffeescript@1.0.16
|
||||
ddp@1.2.4
|
||||
|
||||
@@ -7,10 +7,28 @@
|
||||
<strong class="bg-danger">{{ pretty_reasons alarmReasons }}</strong>
|
||||
<img src="/images/alarm.gif" class="img-responsive center-block" id="alarm_image" />
|
||||
<button id="stop_alarm" class="btn btn-danger"> <i class="fa fa-ban"></i> Prekini </button>
|
||||
|
||||
{{/if}}
|
||||
{{#if alarmStopped}}
|
||||
<strong class="bg-danger">{{ pretty_reasons alarmReasons }}</strong>
|
||||
<br /><br />
|
||||
<div class="bg-warning"> <strong>Alarm prekinut!</strong> Nadzor će se nastaviti kada se temperatura vrati u zadani raspon.
|
||||
</div>
|
||||
<br /><br />
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{#with last_reading}}
|
||||
<div class="huge_text"> {{ all_temperatures }}</div>
|
||||
<div class="bigger_text centeredtable">
|
||||
<table>
|
||||
{{#each temperature in all_temperatures }}
|
||||
<tr>
|
||||
<td>{{temperature.name}}:</td>
|
||||
<td> {{temperature.value}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<div>{{pretty_time created_at}}</div>
|
||||
{{/with}}
|
||||
<button id="run_alarm_settings" class="btn btn-default"> <i class="fa fa-wrench"></i> Podešavanje </button>
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
function sensor_data_collection() {
|
||||
var controllerId = Session.get('controller_id');
|
||||
return SensorData.find({
|
||||
controllerId: controllerId
|
||||
}, {
|
||||
sort: {
|
||||
created_at: -1
|
||||
},
|
||||
limit: 3
|
||||
});
|
||||
}
|
||||
|
||||
function last_sensor_reading() {
|
||||
var controller = Session.get('controller_id');
|
||||
var result = null;
|
||||
if (controller) {
|
||||
result = sensor_data_collection();
|
||||
}
|
||||
if (result && result.count() > 0) {
|
||||
return result.fetch()[0];
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
Template.alarm.helpers({
|
||||
last_reading: last_sensor_reading,
|
||||
last_reading: Meteor.zoblak.client.last_sensor_reading,
|
||||
state: function() {
|
||||
return Meteor.zoblak.client.controller_state().state;
|
||||
},
|
||||
@@ -32,11 +9,15 @@ Template.alarm.helpers({
|
||||
return moment(time).format("DD.MM.YYYY, HH:mm")
|
||||
},
|
||||
all_temperatures: function() {
|
||||
var result = "";
|
||||
var temperatures = last_sensor_reading().temperatures;
|
||||
var result = [];
|
||||
var names = Meteor.zoblak.client.controller_state().config['sensorNames'] || {};
|
||||
var temperatures = Meteor.zoblak.client.last_sensor_reading().temperatures;
|
||||
|
||||
for (var i in temperatures) {
|
||||
result += '' + parseFloat(temperatures[i]).toFixed(1) + ' °C ';
|
||||
var temperature = parseFloat(temperatures[i]).toFixed(1);
|
||||
var temperatureLabel = (Meteor.zoblak.shared.valid_temperature(temperature)) ? temperature : "XX.X";
|
||||
var name = names[i] || "Senzor " + i.toString();
|
||||
result.push({ name: name, value: temperatureLabel + ' °C ' });
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
@@ -71,9 +71,36 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<h3>Senzori: </h3>
|
||||
<table class="table">
|
||||
{{#each sensor in sensors }}
|
||||
<tr>
|
||||
<td>
|
||||
{{ pretty_temperature sensor.value }}
|
||||
</td>
|
||||
<td>
|
||||
<label class="switch">
|
||||
<input type="checkbox" checked={{sensor.on}} class="sensor_switch" />
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="text" placeholder="prostorija" value={{sensor.name}} class="sensor_name" maxlength="7" />
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button id="cancel_settings" class="btn btn-danger" name="cancel_settings" data-dismiss="modal">Odgodi</button>
|
||||
|
||||
<button id="save_settings" class="btn btn-default" name="save_settings" data-dismiss="modal">Zapamti</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -26,12 +26,36 @@ Template.alarm_settings.helpers({
|
||||
var result = config()[property];
|
||||
console.log('returning', result);
|
||||
return result;
|
||||
},
|
||||
|
||||
pretty_temperature: function(temperature) {
|
||||
var temperatureLabel = (Meteor.zoblak.shared.valid_temperature(temperature)) ? temperature : "XX.X";
|
||||
return '' + temperatureLabel + ' °C ';
|
||||
},
|
||||
|
||||
sensors: function() {
|
||||
var temperatures = Meteor.zoblak.client.last_sensor_reading().temperatures;
|
||||
var enabled = config()['sensorsEnabled'] || {};
|
||||
var sensorNames = config()['sensorNames'] || {};
|
||||
var sensors = [];
|
||||
for (var index in temperatures) {
|
||||
var is_on = (index in enabled) ? enabled[index] : true; // on by default
|
||||
var name = (index in sensorNames) ? sensorNames[index] : null; // no name by default
|
||||
var value = parseFloat(temperatures[index])
|
||||
sensors.push({
|
||||
value: value,
|
||||
on: is_on,
|
||||
name: name
|
||||
})
|
||||
}
|
||||
return sensors;
|
||||
}
|
||||
});
|
||||
|
||||
Template.alarm_settings.events({
|
||||
'click #save_settings': function() {
|
||||
var controller_id = Meteor.zoblak.client.controller_state().controller_id;
|
||||
1
|
||||
var instance = Template.instance();
|
||||
var minTemperature = instance.$('#min_temperature').val();
|
||||
var maxTemperature = instance.$('#max_temperature').val();
|
||||
@@ -42,6 +66,25 @@ Template.alarm_settings.events({
|
||||
var sms3 = instance.$('#sms3').val();
|
||||
var sms4 = instance.$('#sms4').val();
|
||||
|
||||
Meteor.call('saveAlarmSettings', controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, [sms1, sms2, sms3, sms4]);
|
||||
var sensorSwitches = instance.$('.sensor_switch');
|
||||
var enabled = {};
|
||||
|
||||
sensorSwitches.each( function(index,element) {
|
||||
enabled[index] = instance.$(element).is(':checked');
|
||||
} );
|
||||
|
||||
console.log("Enabled: ", enabled);
|
||||
|
||||
var names = {};
|
||||
|
||||
var sensorNames = instance.$('.sensor_name');
|
||||
|
||||
sensorNames.each( function(index,element) {
|
||||
names[index] = instance.$(element).val();
|
||||
} );
|
||||
|
||||
console.log("Names ", names);
|
||||
|
||||
Meteor.call('saveAlarmSettings', controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, [sms1, sms2, sms3, sms4] ,enabled, names);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// at the beginning
|
||||
Session.set("templateName", "start");
|
||||
Session.set("templateName", "alarm");
|
||||
|
||||
Template.body.helpers({
|
||||
template_name: function() {
|
||||
|
||||
@@ -18,6 +18,27 @@
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.bigger_text {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
div.centeredtable
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.centeredtable table
|
||||
{
|
||||
border-collapse: separate;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
border-spacing: 10px;
|
||||
}
|
||||
|
||||
div.centeredtable table, div.centeredtable td{
|
||||
border: 1px solid #eeeeee;
|
||||
}
|
||||
|
||||
@media all and (orientation: portrait) {
|
||||
#bucket_image {
|
||||
width: 90%;
|
||||
@@ -58,3 +79,70 @@
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {display:none;}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(26px);
|
||||
-ms-transform: translateX(26px);
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
#chartHorizontalScroll {
|
||||
width: 100%;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
white-space:nowrap;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<template name="log">
|
||||
|
||||
<div id="chartHorizontalScroll">
|
||||
<canvas id="temperatureChart"></canvas>
|
||||
</div>
|
||||
<!--
|
||||
<div class="hello">
|
||||
|
||||
|
||||
<ul class="no-bullets">
|
||||
{{#each sensorDataCollection}}
|
||||
{{> sensorData}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
-->
|
||||
|
||||
</template>
|
||||
|
||||
@@ -4,7 +4,7 @@ function sensor_data_collection() {
|
||||
controllerId: controllerId
|
||||
}, {
|
||||
sort: {
|
||||
created_at: -1
|
||||
created_at: 1
|
||||
},
|
||||
limit: 100
|
||||
});
|
||||
@@ -20,10 +20,194 @@ Template.log.events({
|
||||
}
|
||||
});
|
||||
|
||||
var tempHolder = {};
|
||||
|
||||
var createChart = function() {
|
||||
|
||||
var self = tempHolder;
|
||||
self.labels = [];
|
||||
self.temperatures = [];
|
||||
Tracker.autorun(function() {
|
||||
var sdc = sensor_data_collection();
|
||||
|
||||
sdc.forEach(function(sensorReading) {
|
||||
if (sensorReading.temperatures) {
|
||||
self.labels.push(moment(sensorReading.lastBoxContact).format('ddd DD.MM. HH:mm:ss'));
|
||||
for (var i = 0; i < sensorReading.temperatures.length; i++) {
|
||||
if (!self.temperatures[i]) {
|
||||
self.temperatures[i] = [];
|
||||
}
|
||||
self.temperatures[i].push(sensorReading.temperatures[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if(self.temperatures.length <= 0) { return; } /// there are no temperatures
|
||||
|
||||
|
||||
// Get the context of the canvas element we want to select
|
||||
var chartCanvas = document.getElementById("temperatureChart");
|
||||
var lengthOfTemperatures = self.temperatures[0].length * 40 + 10;
|
||||
chartCanvas.width = (window.innerWidth > lengthOfTemperatures) ? window.innerWidth : lengthOfTemperatures;
|
||||
var heightOfElementsBefore = chartCanvas.getBoundingClientRect().top;
|
||||
chartCanvas.height = window.innerHeight - heightOfElementsBefore;
|
||||
var ctx = chartCanvas.getContext("2d");
|
||||
|
||||
|
||||
|
||||
|
||||
// Set the options
|
||||
var options = {
|
||||
|
||||
///Boolean - Whether grid lines are shown across the chart
|
||||
scaleShowGridLines: true,
|
||||
|
||||
//String - Colour of the grid lines
|
||||
scaleGridLineColor: "rgba(0,0,0,.05)",
|
||||
|
||||
//Number - Width of the grid lines
|
||||
scaleGridLineWidth: 1,
|
||||
|
||||
//Boolean - Whether to show horizontal lines (except X axis)
|
||||
scaleShowHorizontalLines: true,
|
||||
|
||||
//Boolean - Whether to show vertical lines (except Y axis)
|
||||
scaleShowVerticalLines: true,
|
||||
|
||||
//Boolean - Whether the line is curved between points
|
||||
bezierCurve: true,
|
||||
|
||||
//Number - Tension of the bezier curve between points
|
||||
bezierCurveTension: 0.4,
|
||||
|
||||
//Boolean - Whether to show a dot for each point
|
||||
pointDot: true,
|
||||
|
||||
//Number - Radius of each point dot in pixels
|
||||
pointDotRadius: 4,
|
||||
|
||||
//Number - Pixel width of point dot stroke
|
||||
pointDotStrokeWidth: 1,
|
||||
|
||||
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
|
||||
pointHitDetectionRadius: 20,
|
||||
|
||||
//Boolean - Whether to show a stroke for datasets
|
||||
datasetStroke: true,
|
||||
|
||||
//Number - Pixel width of dataset stroke
|
||||
datasetStrokeWidth: 2,
|
||||
|
||||
//Boolean - Whether to fill the dataset with a colour
|
||||
datasetFill: true,
|
||||
|
||||
//String - A legend template
|
||||
legendTemplate: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%> aaa </ul>"
|
||||
|
||||
};
|
||||
|
||||
var random = function() {
|
||||
return Math.random() * (100 - 1) + 1;
|
||||
}
|
||||
|
||||
var colours = [{ // blue
|
||||
fillColor: "rgba(151,187,205,0.2)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(151,187,205,0.8)"
|
||||
}, { // red
|
||||
fillColor: "rgba(247,70,74,0.2)",
|
||||
strokeColor: "rgba(247,70,74,1)",
|
||||
pointColor: "rgba(247,70,74,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(247,70,74,0.8)"
|
||||
}, { // green
|
||||
fillColor: "rgba(70,191,189,0.2)",
|
||||
strokeColor: "rgba(70,191,189,1)",
|
||||
pointColor: "rgba(70,191,189,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(70,191,189,0.8)"
|
||||
}, { // light grey
|
||||
fillColor: "rgba(220,220,220,0.2)",
|
||||
strokeColor: "rgba(220,220,220,1)",
|
||||
pointColor: "rgba(220,220,220,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(220,220,220,0.8)"
|
||||
},
|
||||
|
||||
{ // yellow
|
||||
fillColor: "rgba(253,180,92,0.2)",
|
||||
strokeColor: "rgba(253,180,92,1)",
|
||||
pointColor: "rgba(253,180,92,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(253,180,92,0.8)"
|
||||
}, { // grey
|
||||
fillColor: "rgba(148,159,177,0.2)",
|
||||
strokeColor: "rgba(148,159,177,1)",
|
||||
pointColor: "rgba(148,159,177,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(148,159,177,0.8)"
|
||||
}, { // dark grey
|
||||
fillColor: "rgba(77,83,96,0.2)",
|
||||
strokeColor: "rgba(77,83,96,1)",
|
||||
pointColor: "rgba(77,83,96,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(77,83,96,1)"
|
||||
}
|
||||
];
|
||||
var datasets = [];
|
||||
|
||||
for (var i = 0; i < self.temperatures.length; i++) {
|
||||
// repeat colors once you run out
|
||||
var colour = colours[i % colours.length];
|
||||
var dataset = Object.assign({
|
||||
label: "Senzor #",
|
||||
data: self.temperatures[i]
|
||||
}, colour);
|
||||
datasets.push(dataset);
|
||||
}
|
||||
|
||||
// Set the data
|
||||
var data = {
|
||||
labels: self.labels,
|
||||
datasets: datasets
|
||||
};
|
||||
|
||||
// draw the charts
|
||||
var myLineChart = new Chart(ctx).Line(data, options);
|
||||
|
||||
|
||||
};
|
||||
|
||||
Template.log.onRendered(function() {
|
||||
createChart();
|
||||
});
|
||||
|
||||
Template.log.resized = function() {
|
||||
createChart();
|
||||
return Session.get('resize');
|
||||
}
|
||||
|
||||
Template.log.orientation = function() {
|
||||
createChart();
|
||||
return Session.get('orientation');
|
||||
}
|
||||
|
||||
|
||||
Template.sensorData.helpers({
|
||||
created_at_formatted: function() {
|
||||
return moment(this.created_at).format(/*"DD.MM.YYYY, */ " (HH:mm)")
|
||||
return moment(this.created_at).format( /*"DD.MM.YYYY, */ " (HH:mm)")
|
||||
},
|
||||
|
||||
all_temperatures: function(temperatures) {
|
||||
var result = '';
|
||||
if (temperatures.length > 0) {
|
||||
|
||||
@@ -1,34 +1,16 @@
|
||||
Tracker.autorun(function() {
|
||||
var id = Session.get('controller_id');
|
||||
Session.set("orientation", new Date());
|
||||
Session.set("resize", new Date());
|
||||
if (id) {
|
||||
Meteor.subscribe("sensor_data", id);
|
||||
Meteor.subscribe("controller_state", id);
|
||||
Meteor.subscribe('pictures', id);
|
||||
}
|
||||
window.addEventListener('orientationchange', function(){
|
||||
Session.set("orientation", new Date());
|
||||
});
|
||||
window.addEventListener('resize', function(){
|
||||
Session.set("resize", new Date());
|
||||
});
|
||||
});
|
||||
|
||||
function safeRoute(route) {
|
||||
return function () {
|
||||
var controllerId = this.params.query.controller_id;
|
||||
|
||||
if(controllerId) {
|
||||
Session.setPersistent('controller_id', controllerId);
|
||||
Session.setPersistent('hide_controller_selection', true);
|
||||
} else {
|
||||
Session.setPersistent('hide_controller_selection', false);
|
||||
}
|
||||
console.log('go ', route);
|
||||
if (Meteor.zoblak.client.accessible(route)) {
|
||||
Session.set('templateName', route);
|
||||
} else {
|
||||
Session.set('templateName', 'no_access')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Router.route('/', safeRoute('start'));
|
||||
Router.route('/alarm', safeRoute('alarm'));
|
||||
Router.route('/log', safeRoute('log'));
|
||||
Router.route('/surveillance', safeRoute('surveillance'));
|
||||
Router.route('/weather', safeRoute('weather'));
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if accessible 'log'}}
|
||||
<li role="presentation" class="{{ class_for 'log' }}"><a class="clickable">Danas</a></li>
|
||||
<li role="presentation" class="{{ class_for 'log' }}"><a class="clickable">Pregled</a></li>
|
||||
{{/if}}
|
||||
|
||||
{{#if accessible 'surveillance'}}
|
||||
|
||||
@@ -34,7 +34,7 @@ function saveParamsAndGo(where) {
|
||||
|
||||
Template.tabs.events({
|
||||
'click .start': function() {
|
||||
saveParamsAndGo('/');
|
||||
saveParamsAndGo('/water');
|
||||
},
|
||||
'click .weather': function() {
|
||||
saveParamsAndGo('/weather');
|
||||
|
||||
@@ -24,11 +24,43 @@ Meteor.zoblak.client = {
|
||||
if (!controller.features) return false;
|
||||
|
||||
return controller.features[feature] === true;
|
||||
},
|
||||
|
||||
sensor_data_collection: function() {
|
||||
var controllerId = Session.get('controller_id');
|
||||
return SensorData.find({
|
||||
controllerId: controllerId
|
||||
}, {
|
||||
sort: {
|
||||
created_at: -1
|
||||
},
|
||||
limit: 3
|
||||
});
|
||||
},
|
||||
|
||||
last_sensor_reading: function() {
|
||||
var controller = Session.get('controller_id');
|
||||
var result = null;
|
||||
if (controller) {
|
||||
result = Meteor.zoblak.client.sensor_data_collection();
|
||||
}
|
||||
if (result && result.count() > 0) {
|
||||
return result.fetch()[0];
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.zoblak.shared = {
|
||||
valid_temperature: function(value) {
|
||||
return (parseFloat(value) > -40 && parseFloat(value) < 50);
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.zoblak.server = {
|
||||
controller_state: function(controller_id) {
|
||||
|
||||
var result = {}
|
||||
if (controller_id) {
|
||||
result = ControllerState.findOne({
|
||||
@@ -36,9 +68,30 @@ Meteor.zoblak.server = {
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Asked for ", controller_id, " got ", result);
|
||||
|
||||
if (!result) {
|
||||
result = {}
|
||||
};
|
||||
return result;
|
||||
},
|
||||
|
||||
on_all_controllers: function(whatToDo) {
|
||||
var ids = ControllerState.find({}, {
|
||||
fields: {
|
||||
'controller_id': 1
|
||||
}
|
||||
}).map(function(cid) {
|
||||
return cid.controller_id
|
||||
});
|
||||
|
||||
for (var index in ids) {
|
||||
var controller_id = ids[index];
|
||||
try {
|
||||
whatToDo(controller_id);
|
||||
} catch (err) {
|
||||
console.log('Cannot call ', whatToDo, controller_id, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
app/router.js
Normal file
26
app/router.js
Normal file
@@ -0,0 +1,26 @@
|
||||
function safeRoute(route) {
|
||||
return function () {
|
||||
var controllerId = this.params.query.controller_id;
|
||||
|
||||
if(controllerId) {
|
||||
Session.setPersistent('controller_id', controllerId);
|
||||
Session.setPersistent('hide_controller_selection', true);
|
||||
} else {
|
||||
Session.setPersistent('hide_controller_selection', false);
|
||||
}
|
||||
console.log('go ', route);
|
||||
if (Meteor.zoblak.client.accessible(route)) {
|
||||
Session.set('templateName', route);
|
||||
} else {
|
||||
Session.set('templateName', 'no_access')
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Router.route('/', safeRoute('alarm'));
|
||||
Router.route('/water', safeRoute('start'));
|
||||
Router.route('/alarm', safeRoute('alarm'));
|
||||
Router.route('/log', safeRoute('log'));
|
||||
Router.route('/surveillance', safeRoute('surveillance'));
|
||||
Router.route('/weather', safeRoute('weather'));
|
||||
@@ -9,6 +9,7 @@ Api.addRoute('sensorData', {
|
||||
authRequired: false
|
||||
}, {
|
||||
post: function() {
|
||||
console.log("Sensordata ", this.bodyParams);
|
||||
reactToSensorData(this.bodyParams);
|
||||
var sensorObject = {
|
||||
temperatureValue: parseFloat(this.bodyParams.temperatureValue),
|
||||
@@ -63,8 +64,10 @@ Api.addRoute('alarm/:id/phonePing', {
|
||||
|
||||
|
||||
reactToSensorData = function(nextSensorReading) {
|
||||
console.log("reacting to sensor");
|
||||
|
||||
var controllerId = nextSensorReading.controllerId;
|
||||
|
||||
console.log("reacting to sensor ", nextSensorReading);
|
||||
var stateObject = stateOrDefault(controllerId);
|
||||
var state = stateObject.state;
|
||||
var config = stateObject.config;
|
||||
@@ -201,8 +204,8 @@ function stateOrDefault(id) {
|
||||
manualInflow: true
|
||||
},
|
||||
features: {
|
||||
start: true,
|
||||
weather: true,
|
||||
start: false,
|
||||
weather: false,
|
||||
surveillance: true,
|
||||
log: true,
|
||||
alarm: true
|
||||
|
||||
@@ -143,7 +143,7 @@ function saveControllerConfig(controller_id, time, days, manualInflow) {
|
||||
});
|
||||
}
|
||||
|
||||
function saveAlarmSettings(controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, smsNumbers) {
|
||||
function saveAlarmSettings(controller_id, minTemperature, maxTemperature, timeoutBox, timeoutPhone, smsNumbers, sensorsEnabled, sensorNames) {
|
||||
var state = Meteor.zoblak.server.controller_state(controller_id);
|
||||
ControllerState.update(state._id, {
|
||||
'$set': {
|
||||
@@ -155,9 +155,12 @@ function saveAlarmSettings(controller_id, minTemperature, maxTemperature, timeou
|
||||
'config.sms1': smsNumbers[0],
|
||||
'config.sms2': smsNumbers[1],
|
||||
'config.sms3': smsNumbers[2],
|
||||
'config.sms4': smsNumbers[3]
|
||||
'config.sms4': smsNumbers[3],
|
||||
'config.sensorsEnabled': sensorsEnabled,
|
||||
'config.sensorNames': sensorNames
|
||||
}
|
||||
});
|
||||
|
||||
var jobName = "automatic_alarm_" + controller_id;
|
||||
|
||||
SyncedCron.remove(jobName);
|
||||
@@ -185,6 +188,7 @@ reactToAlarmData = function(controller_id) {
|
||||
var state = Meteor.zoblak.server.controller_state(controller_id);
|
||||
var config = state.config;
|
||||
|
||||
|
||||
var minTemperature = function(temperatures) {
|
||||
// if it gets a lot colder than absolute zero
|
||||
// we will have more problems than the bug in this code
|
||||
@@ -196,7 +200,7 @@ reactToAlarmData = function(controller_id) {
|
||||
}
|
||||
}
|
||||
return minimal;
|
||||
}
|
||||
};
|
||||
|
||||
var maxTemperature = function(temperatures) {
|
||||
// obviously - hell is not supported in this version
|
||||
@@ -208,11 +212,18 @@ reactToAlarmData = function(controller_id) {
|
||||
}
|
||||
}
|
||||
return maximal;
|
||||
}
|
||||
};
|
||||
|
||||
var tooCold = config.minTemperature && (minTemperature(reading.temperatures) < config.minTemperature);
|
||||
var temperatures = (reading.temperatures || []).filter(function(temperature, index) {
|
||||
var is_on = (index in config.sensorsEnabled)?config.sensorsEnabled[index]: true;
|
||||
return Meteor.zoblak.shared.valid_temperature(temperature) && is_on;
|
||||
});
|
||||
|
||||
var tooHot = config.maxTemperature && (maxTemperature(reading.temperatures) > config.maxTemperature);
|
||||
console.log("Konfiguracija: ", controller_id, config);
|
||||
|
||||
var tooCold = config.minTemperature && (minTemperature(temperatures) < config.minTemperature);
|
||||
|
||||
var tooHot = config.maxTemperature && (maxTemperature(temperatures) > config.maxTemperature);
|
||||
|
||||
var minutesSinceLastBoxContact = reading.lastBoxContact ? moment(new Date()).diff(moment(reading.lastBoxContact), 'minutes') : -1;
|
||||
var boxSilent = config.timeoutBox && minutesSinceLastBoxContact > config.timeoutBox;
|
||||
|
||||
@@ -2,6 +2,22 @@ if (Meteor.isServer) {
|
||||
Meteor.startup(function() {
|
||||
// code to run on server at startup
|
||||
SyncedCron.start();
|
||||
|
||||
Meteor.zoblak.server.on_all_controllers(function(controller_id) {
|
||||
if(!controller_id) { return; }; // protects from null controller_id
|
||||
var jobName = "automatic_alarm_" + controller_id;
|
||||
|
||||
SyncedCron.remove(jobName);
|
||||
SyncedCron.add({
|
||||
name: jobName,
|
||||
schedule: function(parser) {
|
||||
return parser.text('every 10 seconds');
|
||||
},
|
||||
job: function() {
|
||||
reactToAlarmData(controller_id);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,7 +39,7 @@ if (Meteor.isServer) {
|
||||
tankLevel2: this.bodyParams.tankLevel2,
|
||||
tankLevel3: this.bodyParams.tankLevel3,
|
||||
tankLevel4: this.bodyParams.tankLevel4,
|
||||
tankFull: this.bodyParams.tankFull,
|
||||
tankFull: this.bodyParams.tankFull,
|
||||
startPumpingAt: this.bodyParams.startPumpingAt,
|
||||
stopPumpingAt: this.bodyParams.stopPumpingAt,
|
||||
owner: this.bodyParams.owner,
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
## Installation
|
||||
|
||||
0.
|
||||
```
|
||||
mkdir projects
|
||||
git clone https://github.com/senaduka/tfm
|
||||
```
|
||||
1. Go to every subdirectory in drivers directory and follow instructions about installation of drivers
|
||||
```
|
||||
/home/pi/projects/tfm/controller/drivers/adafruit# cat README.md
|
||||
```
|
||||
2. edit controller/config/__init__.py and set your controller ID to unique number
|
||||
3. configure cron to run controller.py every 15 minutes as a superuser:
|
||||
|
||||
@@ -104,3 +112,38 @@ as multiple cameras need to be combined and avconv does not support the appropri
|
||||
|
||||
this is being used in /controller/config/__init__.py PICTURE_COMMAND when executing the "convert" command while combining multiple images
|
||||
```
|
||||
|
||||
13. set up fixed IP addresses (rpi3):
|
||||
```
|
||||
Edit /etc/dhcpcd.conf as follows:-
|
||||
|
||||
Here is an example which configures a static address, routes and dns.
|
||||
interface eth0
|
||||
static ip_address=192.168.1.6/24
|
||||
static routers=192.168.1.1
|
||||
static domain_name_servers=192.168.1.1
|
||||
|
||||
interface wlan0
|
||||
static ip_address=192.168.1.7/24
|
||||
static routers=192.168.1.1
|
||||
static domain_name_servers=192.168.1.1
|
||||
```
|
||||
|
||||
14. set up w1 thermometer sensing
|
||||
```
|
||||
DS18B20
|
||||
W1-thermometer sensing on RPI3
|
||||
|
||||
wiring schema: https://cdn-learn.adafruit.com/downloads/pdf/adafruits-raspberry-pi-lesson-11-ds18b20-temperature-sensing.pdf (firrt schema - I used GPIO BCN #17)
|
||||
edit /boot/config.txt - at the end insert (rpi2 doesn't need the gpiopin, but rpi3 does! see for more info: http://raspberrypi.stackexchange.com/questions/37157/ds18b20-no-longer-working, rpi would need only: dtoverlay=w1-gpio):
|
||||
dtoverlay=w1-gpio-pullup,gpiopin=17
|
||||
|
||||
reboot
|
||||
issue the following commands, the last directory has a variable name, based on the sensor's serial number!
|
||||
sudo modprobe w1-gpio
|
||||
sudo modprobe w1-therm
|
||||
cd /sys/bus/w1/devices
|
||||
ls
|
||||
cd 28-xxxx (change this to match what serial number pops up)
|
||||
cat w1_slave
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ try:
|
||||
except:
|
||||
print("onewire thermo error:", sys.exc_info()[0])
|
||||
|
||||
# Un-comment the line below to convert the temperature to Fahrenheit.
|
||||
# Un-comment the line below to convert the temperature to Fahrenheiq
|
||||
# temperature = temperature * 9/5.0 + 32
|
||||
|
||||
print 'Temp={0:0.1f}*C'.format(temperature)
|
||||
|
||||
Binary file not shown.
171
shelly/syncscript.js
Normal file
171
shelly/syncscript.js
Normal file
@@ -0,0 +1,171 @@
|
||||
const CONTROLLER_ID="550";
|
||||
|
||||
const senzori = [
|
||||
"7c:c6:b6:74:a4:be",
|
||||
"7c:c6:b6:75:a1:21"
|
||||
];
|
||||
|
||||
const temperature = ["22.0", "22.0"];
|
||||
const humidity = ["50", "50"];
|
||||
|
||||
|
||||
|
||||
let ALLTERCO_MFD_ID_STR = "0ba9";
|
||||
let BTHOME_SVC_ID_STR = "fcd2";
|
||||
|
||||
let ALLTERCO_MFD_ID = JSON.parse("0x" + ALLTERCO_MFD_ID_STR);
|
||||
let BTHOME_SVC_ID = JSON.parse("0x" + BTHOME_SVC_ID_STR);
|
||||
|
||||
let SCAN_DURATION = BLE.Scanner.INFINITE_SCAN;
|
||||
|
||||
let uint8 = 0;
|
||||
let int8 = 1;
|
||||
let uint16 = 2;
|
||||
let int16 = 3;
|
||||
let uint24 = 4;
|
||||
let int24 = 5;
|
||||
|
||||
function getByteSize(type) {
|
||||
if (type === uint8 || type === int8) return 1;
|
||||
if (type === uint16 || type === int16) return 2;
|
||||
if (type === uint24 || type === int24) return 3;
|
||||
//impossible as advertisements are much smaller;
|
||||
return 255;
|
||||
}
|
||||
|
||||
let BTH = [];
|
||||
BTH[0x00] = { n: "pid", t: uint8 };
|
||||
BTH[0x01] = { n: "Battery", t: uint8, u: "%" };
|
||||
BTH[0x3a] = { n: "Button", t: uint8 };
|
||||
BTH[0x2e] = { n: "Humidity", t: uint8 };
|
||||
BTH[0x45] = { n: "Temperature", t: int16, f: 0.1 };
|
||||
|
||||
|
||||
let BTHomeDecoder = {
|
||||
utoi: function (num, bitsz) {
|
||||
let mask = 1 << (bitsz - 1);
|
||||
return num & mask ? num - (1 << bitsz) : num;
|
||||
},
|
||||
getUInt8: function (buffer) {
|
||||
return buffer.at(0);
|
||||
},
|
||||
getInt8: function (buffer) {
|
||||
return this.utoi(this.getUInt8(buffer), 8);
|
||||
},
|
||||
getUInt16LE: function (buffer) {
|
||||
return 0xffff & ((buffer.at(1) << 8) | buffer.at(0));
|
||||
},
|
||||
getInt16LE: function (buffer) {
|
||||
return this.utoi(this.getUInt16LE(buffer), 16);
|
||||
},
|
||||
getUInt24LE: function (buffer) {
|
||||
return (
|
||||
0x00ffffff & ((buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0))
|
||||
);
|
||||
},
|
||||
getInt24LE: function (buffer) {
|
||||
return this.utoi(this.getUInt24LE(buffer), 24);
|
||||
},
|
||||
getBufValue: function (type, buffer) {
|
||||
if (buffer.length < getByteSize(type)) return null;
|
||||
let res = null;
|
||||
if (type === uint8) res = this.getUInt8(buffer);
|
||||
if (type === int8) res = this.getInt8(buffer);
|
||||
if (type === uint16) res = this.getUInt16LE(buffer);
|
||||
if (type === int16) res = this.getInt16LE(buffer);
|
||||
if (type === uint24) res = this.getUInt24LE(buffer);
|
||||
if (type === int24) res = this.getInt24LE(buffer);
|
||||
return res;
|
||||
},
|
||||
unpack: function (buffer) {
|
||||
// beacons might not provide BTH service data
|
||||
if (typeof buffer !== "string" || buffer.length === 0) return null;
|
||||
let result = {};
|
||||
let _dib = buffer.at(0);
|
||||
result["encryption"] = _dib & 0x1 ? true : false;
|
||||
result["BTHome_version"] = _dib >> 5;
|
||||
if (result["BTHome_version"] !== 2) return null;
|
||||
//Can not handle encrypted data
|
||||
if (result["encryption"]) return result;
|
||||
buffer = buffer.slice(1);
|
||||
|
||||
let _bth;
|
||||
let _value;
|
||||
|
||||
while (buffer.length > 0) {
|
||||
_bth = BTH[buffer.at(0)];
|
||||
if (typeof(_bth) === "undefined") {
|
||||
console.log("BTH: unknown type");
|
||||
break;
|
||||
}
|
||||
|
||||
buffer = buffer.slice(1);
|
||||
_value = this.getBufValue(_bth.t, buffer);
|
||||
|
||||
if (_value === null) break;
|
||||
if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
|
||||
result[_bth.n] = _value;
|
||||
buffer = buffer.slice(getByteSize(_bth.t));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
let ShellyBLUParser = {
|
||||
getData: function (res) {
|
||||
let result = BTHomeDecoder.unpack(res.service_data[BTHOME_SVC_ID_STR]);
|
||||
result.addr = res.addr;
|
||||
result.rssi = res.rssi;
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
function sendToZoblak() {
|
||||
const url = "http://agrar.zoblak.com/api/v1.0/sensorData";
|
||||
const owner = "Controller: " + CONTROLLER_ID;
|
||||
const payload = {
|
||||
owner: owner,
|
||||
temperatureValue: temperature[0],
|
||||
humidityValue: humidity[0],
|
||||
controllerId: CONTROLLER_ID,
|
||||
temperatures: temperature
|
||||
};
|
||||
|
||||
Shelly.call(
|
||||
"HTTP.POST", {
|
||||
"url": url,
|
||||
"body": JSON.stringify(payload)
|
||||
},
|
||||
function(result) {
|
||||
if (result) {
|
||||
print("server says: ", result.code);
|
||||
}
|
||||
else {
|
||||
print("There is no server response present. Check connection!")
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function handleScanResult(event, result) {
|
||||
if(event === BLE.Scanner.SCAN_RESULT) {
|
||||
for (var i in senzori) {
|
||||
if(result.addr === senzori[i]) {
|
||||
let data = ShellyBLUParser.getData(result);
|
||||
if(data.Temperature && data.Humidity) {
|
||||
temperature[i] = data.Temperature.toString();
|
||||
humidity[i] = data.Humidity.toString();
|
||||
sendToZoblak();
|
||||
} else {
|
||||
console.log("Nesto nije uredu");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//BLE.Scanner.Subscribe(handleScanResult);
|
||||
BLE.Scanner.Start({ duration_ms: SCAN_DURATION, active: false }, handleScanResult);
|
||||
Reference in New Issue
Block a user