Castle Valley Mill is an 18th century gristmill located on the bank of the Neshaminy Creek in Doylestown, Pennsylvania USA. The doors opened in 1730 and was in operation until the early 1900s. Around this time, flour milling was becoming more of an industrial process and flour production was moving to the western united states, aided by the steam and gas driven engines that could power large processes without the need for a water turbine or water wheel. This change in manufacturing made our mill obsolete at the time and the doors were closed because of this. That was until 2012 when the mill was reopened and restored to a working capacity.
Throughout the last seven years, Castle Valley Mill has been restored to clean tons of grain at a time and mill thousands of pounds of flour a week. Unlike modern flour facilities, we are using wooden machinery from the late 1800s to do the majority of the work around the mill. Powered by pulleys and flat belts, our machinery completes a wide variety of tasks from cleaning the grain of impurities to sifting flour of course bran particles. Because the majority of our machines are flat belt driven, electric power is a scarce utility in the mill. Currently, we use three motors to run twelve machines in various places throughout our building. While this may add to the charm of our operation, it can cause problems for the staff who have to monitor this complex system.
One of the largest challenges we face on a daily basis has to do with the lack of feedback received from the machinery. To ensure that the system is operating properly, a trained employee has to walk throughout the building about every 10 minutes. Visually inspecting the factory system for grain backups and machine breakdowns, while easy to do, it is not reliable as a malfunction can cause hours of lost production time if left unnoticed. This is because when one machine breaks down, all the machinery behind it backs up with grain and starts to fail as in a cascading system. To alleviate this issue, a network of sensors was proposed that would monitor the system and send data to a central server that can be accessed from a computer interface to visualize the data.
The first version of devices for the network were based on an Arduino Nano and NRF24I01 radio device. Built to read ambient temperature throughout the building, measure the current (load) certain motors are experiencing, and how much space is left in a grain bin. Version 1 was successfully installed and is currently working with six devices. In the end, these devices were not very useful as assembly time could vary from a half hour to an hour for one device and the Arduino program was awkward to modify. Concerns involving a lack of security for messages sent with the RF24 radio were a large factor in abandoning Version 1 and starting Version 2.
Sensor network Version 2 started as a system of sensor modules that could be plugged into a WiFi connected device, recognizing the type of sensor connected and reporting data at specified intervals. Due to my limited knowledge at the time, I couldn't get this project working to design specifications. The level shifting circuit I created did not work properly and the sensors only had a 25% chance of correctly transmitting data to the ATMega328p microprocessor. These devices were intended to be battery-powered and, at completion of the project, were found to last less than one day while powered with four AA batteries. While this version is considered a failure, I was able to learn from the mistakes and fix them for version 3.
After re-evaluating what the network should do, goals were created to guide the design process to create a useful device. Those goals were to:
· Connect devices to a central Python server
· Operate devices with 3 AA batteries and achieve a 60-day lifespan
· Display data collected by the server in a formatted web page
· Store collected long term data in a database
· Allow easy configuration of devices without flashing them with new software
Main Device and SensorsAfter 16 weeks of development, Sensor Network Version 3 can be considered a success. Two boards were created to handle 3 different sensor applications. These boards can be split into two areas, the core circuitry to operate the ESP8266 and the sensor circuitry to do all sensor functions.
The core circuitry revolves around an ESP8266-12E, used for aggregating the sensor information and posting that data to a Python network on the same WiFi network. Using an LM3671-3.3 switching regulator to provide 3.3v power, the core circuitry will only draw 3mA when the ESP is in deep sleep mode. Three buttons are provided for resetting and putting the device into program or configuration mode. The ESP can also read the voltage of the battery pack, using a voltage divider connected to the ESP's built-in ADC.
There are currently three use cases for these devices installed in the mill. The valve monitor sensor reads the state of a switch valve (bidirectional valve to direct grain one of two ways), the shaft RPM monitor measure rotation speed of a line shaft, and the bearing temperature monitor measures the temperature of a 1900's line shaft bearings to detect heat generated by friction. The bearing temperature monitor and shaft rotation monitor are integrated into the same device so a line shaft's bearing temperatures and rotation can be measured by a single device.
The bearing temperature monitor uses an ATtiny84 to aggregate data from two Adafruit MAX31855 thermocouple devices. The bearings this would be applied to are called 'Babbit Bearings' and they use a mix of lead and tin in a iron casing to act as a low friction surface for the steel shaft to rotate in. Because there are no roller bearings, there will always be friction that generates heat. When these bearings are run for a while they can get very warm and could start a fire.
DDRA |= (1 << thrmPower); //pinMode(thrmPower, OUTPUT);
PORTA |= (1 << thrmPower); //digitalWrite(thrmPower, HIGH);
double tempR = 0.0, tempL = 0.0;
float ambTemp = thermocoupleL.readInternal();
ambTemp *= 9.0; ambTemp /= 5.0; ambTemp += 32;
boolean second = digitalRead(secondThrm);
tempL = thermocoupleL.readFarenheit();
if (second == true)
tempR = thermocoupleR.readFarenheit();
PORTA &= ~(1 << thrmPower); //digitalWrite(thrmPower, LOW);
pinMode(thrmPower, INPUT);
After the ATtiny wakes from sleep mode, it powers the thermocouple devices and takes a measurement of the left and right thermocouple. If the second thermocouple select pin is not HIGH (LOW by default) then the ATtiny will only read the thermocouple in the left position (shown in picture). Then we turn off our thermocouple device power off because they are not needed again until the next cycle begins.
DDRA |= (1 << espReset); //set pinmode Output
PORTA &= ~(1 << espReset); //then ground the reset of ESP
_delay_ms(5);
PORTA |= (1 << espReset); //Make High to end reset
pinMode(espReset, INPUT); //Change our espReset pin to Input, more efficient for battery ops
_delay_ms(100);
if (!isnan(tempL)) {
espCom.print("1:");espCom.print(tempL);
if (!isnan(tempR) and second) {
espCom.print("|2:");espCom.print(tempR);
}
}
bool interv = false;
int sleepIntervalL = 0,sleepIntervalR = 0,finInt;
if((tempR-ambTemp) >= 0 && (tempR-ambTemp) < 100){
sleepIntervalR = 5+(50-((tempR-ambTemp)/2.0));
interv = true;
}
if((tempR-ambTemp) >= 0 && (tempR-ambTemp) < 100){
sleepIntervalL = 5+(50-((tempR-ambTemp)/2.0));
interv = true;
}
if(sleepIntervalL > sleepIntervalR){
finInt = sleepIntervalR;
}else if(sleepIntervalR > sleepIntervalL){
finInt = sleepIntervalL;
}else{
finInt = 55;
}
for (int j = 0; j < finInt; j ++) {
myWatchdogEnable (0b100001); // 8 seconds
}
Next, we pull the ESP reset LOW and then HIGH to reset the ESP. The information is sent over a serial line to the ESP and now we can put the ATtiny back to sleep. To track a hot bearing, if the measured temp - ambient temp is greater than zero, we want to check that bearing more often depending on how hot the bearing is. This value is then entered into the sleep duration of the ATtiny, causing a room temperature bearing to be read every 440 seconds and a bearing that is 30 degrees warmer than room temperature being read every 320 seconds.
The shaft speed monitor uses an ATtiny84 and a reed switch to measure shaft rotation by finding the difference between recorded 'hits' or the HIGH state of the reed switch. This is useful because if a line shaft stops moving, the cleaning or milling process would stop and the grain would back up to machines further up the process. A shaft will also rotate slower depending on the amount of resistance the machines connected to it are creating. For instance, if a bucket elevator used to move grain from the first floor to the third floor was to be jammed with grain (stop moving), this would cause some resistance on the line shaft causing it to slow down.
volatile unsigned long recordedHits[4] = {0,0,0,0}; //past 10 data points
float avgHit = 10.0; //combined average of recordedHits
unsigned long lastAvg = 0; //last time we averaged recordedHits
unsigned long lastESPcom = 0; //last time we sent message to ESP
boolean wasHighLast = false; //keeps track of last read
byte pos = 0;
unsigned long lastHit = 0;
void loop() {
unsigned long currentMillis = millis(); //get current time
bool state = digitalRead(trigPin); //check reed switch state
if(state == 1 and state != wasHighLast){ //if state is active
recordedHits[pos] = currentMillis-lastHit;
lastHit = currentMillis;
pos += 1;
if(pos==4)
pos = 0;
}
After a hit is registered, the interval between the new hit and last hit is recorded (millis()-lastHitTime) in a 4-position array.
if((currentMillis-lastAvg) > 1000){ //Timer for averaging recordedHits
avgHit = 0.0; //reset our count
byte zeroVal = 0; //counts how many 0s in dataset (meant to prevent weird average on startup)
for(byte i=0; i<4; i++){ //traverse our recordedHits array
avgHit += ((float(recordedHits[i])/100.0)); //add difference between two hits to count
if(recordedHits[i]/100.0 == 0)
zeroVal++;
}
avgHit = (avgHit/float(4-zeroVal)); //calculate average count/datapoints in array
avgHit = (60.0/avgHit); //convert to rpm
lastAvg = currentMillis; //reset our timer
}
//time is only 15 seconds for testing purposes
if((currentMillis-lastESPcom) > 5000){ //Timer for sending data to esp
sendESPmessage(avgHit);
lastESPcom = currentMillis; //reset our timer
}
if((currentMillis-lastHit) > 6000){ //send attiny into sleep mode after 5 times the average of hits are counted (if spinning 60 rpm, turns off in 5 seconds)
sendESPmessage(0.0); //tell esp shaft has stopped spinning
forceSleep(); //go to sleep
}
wasHighLast = state;
}
At specific times in the ATtiny's run cycle, the average of the hit intervals is taken and sent, via serial line, to the ESP controller in the same way the bearing temp code sends a message. Finally we send a value 0.0 to the ESP if there hasn't been a registered hit in six seconds of running, then we call a forceSleep() function to put the ATtiny to sleep until a new hit is registered.
The valve monitor detects the position of the paddle within a bidirectional valve. By detecting which position the paddle is in, we can see which path the grain is moving through our system. If we are about to start cleaning grain, the server will show us where the grain will end up. If the system shows the grain is going into a bin we do not want it to go into, we can flip the valves to direct it to our storage bin and we can watch the server interface display the new route the grain is traveling. The last case to watch for is if the paddle is floating (not to one side), in which case the server should throw an error. If the paddle is floating, grain has the potential to go through both outputs of the valve, contaminating whatever grain is further down the spouting. Just imagine wheat mixed into corn because the valve was floating.
When the paddle position changes, the combination of logic shown above causes the ESP to be reset. This feature was designed to update the server as soon as the paddle changes, giving us the most up to date information. After the state has been changed and the ESP has been reset, the ESP device will read the state of the reed switches with digitalRead() and the states are posted to the server.
System OverviewThe server is meant to run as a local entity on an existing WiFi network. By loading the Python application onto a Raspberry Pi you can build a local server to handle device requests.
Devices have an HTTP Post and Get method, to post information as well as to receive information from the server. The device formats a JSON string with new sensor data and information about the device consisting of the power voltage, network security key and all the sensor information generated during the device cycle.
The Python server uses Flask to handle HTTP requests as well as to serve web pages with sensor data to a user. If the sensor data received is consistently off by a constant value, the server can be given values to modify the data as well as allow the user to choose the operation [+, -, *, /, %] to modify that data. A temperature sensor off by ten degrees can have ten added to the value before the value is saved as a new entry of 'sensordata' in the database. Whenever a user tries to access a web page hosted by our server, the data for that page is generated from various database entries and displayed with Flask templates formatted to display the data dynamically.
ESP8266 ProgramA device cycle can be summed up in hree main parts: collecting the data, formatting the data as a JSON string, and posting the JSON string to a webpage. After these tasks are completed, the device is expected to go into deep sleep until the device is electronically reset or the sleep time interval expires.
All of the code shown below can be found in the ESP8266 ATtiny program listed under the 'Code' section.
long start = millis();
bool gotSensorRead = false;
String stosend = "";
while(millis()-start < 1000){
while(Serial.available() > 0){
byte nb = Serial.read();
Serial.print((nb));
if(nb != byte('~')){
stosend += char(nb);
}else{
start = -1000;
gotSensorRead = true;
}
}
}
To acquire our data, the ESP8266 ATtiny program will read a message from the ATtiny connected via a serial line. This works because the ATtiny will pull the ESP reset LOW and then HIGH again before it sends a message, forcing the ESP to reset and begin reading the message sent from the ATtiny. The message is stored in a string where it will be broken into components later.
At this point, the program reads in values stored in EEPROM like the connected network SSID and Password, in addition to the URL we want to post to and other important data our devices need to function. Not shown is a method of setting the ESP device in Access Point mode (with APmode button) so we can display a configuration web page to a user logged in to the access point. This functions to give the end user a way to configure a device without needing to flash it with a new program. When the WiFi credentials change, someone will have to update the password on all of the devices but this is done with a web form shown below. After the user submits the form, the new information is saved into EEPROM and our device can continue its regular cycle.
StaticJsonBuffer<200> messageBuffer; //Create a JSON buffer
JsonObject& message = messageBuffer.createObject(); //Creat a JSON object we can add our keys/values into
message["id"] = eepromData.deviceID;
message["network_key"] = eepromData.flaskKey; //Create a JSON Key called "key" with value secretFlaskKey
message["battery_type"] = "1";
message["battery_level"] = ((analogRead(A0)/1023.0)*11.5);
JsonArray& sensors = message.createNestedArray("sensors");
JsonObject& sensor1 = sensors.createNestedObject(); //Creat a JSON object we can add our keys/values into
bool sec = false;
String firstData = "";
char sensor1Num = 255;
String secondData = "";
char sensor2Num = 255;
int stage = 0;
for(int i = 0; i < stosend.length()-1; i++){
if(stosend[i] == ':'){
stage++;
}
if(stosend[i] == '|'){
sec = true;
stage++;
}
if(stage == 0){
sensor1Num = stosend[i];
}else if(stage == 1){
firstData += stosend[i];
}else if(stage == 2){
sensor2Num = stosend[i];
}else if(stage == 3){
secondData += stosend[i];
}else{
Serial.println(stosend[i]);
}
}
sensor1["id"] = sensor1Num;
if(sensor1Num == '1' or sensor1Num == '2')
sensor1["type"] = "bearing_temp";
else if(sensor1Num == '3')
sensor1["type"] = "shaft_rpm";
else
sensor1["type"] = "null";
sensor1["data"] = firstData;
if(!secondData.equals("")){
JsonObject& sensor2 = sensors.createNestedObject(); //Creat a JSON object we can add our keys/values into
sensor2["id"] = sensor2Num;
if(sensor2Num == '1' or sensor2Num == '2')
sensor2["type"] = "bearing_temp";
else if(sensor2Num == '3')
sensor2["type"] = "shaft_rpm";
else
sensor2["type"] = "null";
sensor2["data"] = secondData;
}
char payload[200]; //Create a char array...
message.printTo(payload); //Then print the message object to the char array so we can send it as plaintext over wifi
Using the ArduinoJSON library, we can create JSON strings with nested JSON arrays and objects. We start by making a JSON buffer with a max length of 200 characters. Then we create a JSON object to hold higher level data that occurs only once (like the device ID or the voltage of the battery). Because there are multiple sensors connected to our device, we need to create a nested JSON array to hold new nested JSON objects, which will store the data of each sensor. Most of the logic shown is to break the received string, from the ATtiny, into the components sensorID and sensor data. A string with value "1:30|3:40" would be split into sensor 1 with value 30 and sensor 3 with value 40. Finally, we make a new char array 'payload' and print the JSON string to the char array so we can transmit this message.
if (WiFi.status() == WL_CONNECTED) { //Check if we are connected to wifi, if we are then continue with loop
Serial.println("Wifi Connected");
HTTPClient http; //Create HTTPClient object (HTTPClient handles our POST and GET requests)
String sAddress = String(eepromData.postURL); //Make the server address out of components (our URLs should look like this "http://192.168.1.250:5000/server/update"
Serial.println(sAddress);
http.begin(sAddress); //Begin sending content to URL
http.addHeader("Content-Type", "text/plain"); //Make a POST header
int mStatus = http.POST(String(payload)); //POST our message (as a char array) to server, get error response code (200 == success, 404 or 500 == error)
String returnpayload = http.getString(); //Get response from server (not necessary if server doesnt return a message)
http.end(); //Close connection
Serial.println(mStatus);
}
To transmit our new char array storing our JSON string, we can POST this to a web page using the ESP8266HTTPClient library. First, we check that we are connected to WiFi and if we are, we create an instance of the HTTP client (called http). Next we start our HTTP client instance with http.begin(addr), making sure to include the address we want to post our data to as the argument 'addr'. Add a header to our POST so the server knows how to process it, then post the data with http.POST(data). This will return an error code as an integer, so if we receive 200 then the form was successfully posted to the server. If we receive a 404, 500 or -1 then our POST failed and no data was transmitted. Finally, we have to end our connection to the server with http.end().
ESP.deepSleep(int(eepromData.timeToWait * 60) * 1000000);
The final step in the device cycle is to put the ESP back into deep sleep mode. In this case, the user has the ability to set the time in between sleep cycles (in minutes). Remember that deep sleep is measured in microseconds, so I had to convert minutes to microseconds to get an accurate sleep period.
Python Server ProgramConsidering this project is supposed to be a network of devices which report to a central server, the best way to explain how the server works is to follow how a device message would be handled.
This starts with a POST to the server. The Flask method for handling devices trying to post to our server ('handle_device') is called and the received message is decoded and checked for the correct security key. Once the device is validated, the server looks in its database for the existing device and sensors entries contained in the JSON string sent by the device. If a device or sensor does not exist, the server will make new entries in its database for the device or sensor. Then the data received from our device is either modified with values to offset the measured value or the data is directly added to the database as a new 'sensordata' entry.
Because this project uses Flask, the 'flask_sqlalchemy' module can be used to develop an SQL-based database without the need to write explicit SQL code. When developing the sqlAlchemy database, the programmer is creating 'models' of what a database entry would look like. Let's look at the model for a Device:
class Device(db.Model):
__bind_key__ = 'network'
id = db.Column(db.Integer, primary_key=True)
assigned_id = db.Column(db.String(10), unique=True, nullable=False)
title = db.Column(db.String(30), unique=False, nullable=True)
mill_floor = db.Column(db.Integer, unique=False, nullable=True)
battery_type = db.Column(db.Integer, unique=False, nullable=False)
battery_data = db.relationship('BatteryData', backref='device', lazy=True)
sensors = db.relationship('Sensor', backref='device', lazy=False)
Because there are multiple databases associated with my program, the device model is bound to the 'network' database to keep all device and sensor data together. The db.Column refers to a column in our database table and each column is a different item that holds some kind of data. For example, a column is created for the assigned_id of a device that stores a string of 10 chars. Because we cannot have multiple devices with the same assigned_id, the unique identifier is set to TRUE. A device will always have an assigned id, so we can use the nullable identifier set to false to make sure this happens. A relationship can be made between entries by using the.relationship() method. It takes arguments for the child model associated with the parent model, a backref which is the parent model and a lazy argument to load the data of the relationship when the parent entry is viewed.
def __init__(self,assigned_id,battery_type,title=None,mill_floor=None): #initialize our new device
self.assigned_id = assigned_id
self.title = title
self.mill_floor = mill_floor
self.battery_type = battery_type
@classmethod
def create(cls, **kw): #method to create new device and store it in our database
obj = cls(**kw)
db.session.add(obj)
db.session.commit()
def remove(passed_device_id): #removes device with given device assigned_id (also removes all sensors, battery data, sensor events related to device)
element = Device.query.filter_by(assigned_id=passed_device_id).first()
for j in element.battery_data:
db.session.delete(j)
for sens in element.sensors:
for j in sens.sensor_data:
db.session.delete(j)
for j in sens.events:
db.session.delete(j)
db.session.delete(sens)
db.session.delete(element)
db.session.commit()
Functions were made to handle, create, and remove database entries so entries can be made or destroyed with one command. To create an entry, the create() function is called with arguments passed in for the device's assigned_id, title, floor device is located, and battery reading method identifier (code to keep track how the device measures battery voltage). To remove an entry from the database, an assigned_id of the device to be removed is passed into the remove() function. The function then deletes all database entries that are children of our device entry. After the children entries are removed, the device entry can be removed. The session.commit() function is used to finalize any edits made to the database and should be called after creating, removing or modifying any database entry.
A user, connected to the same WiFi network as our server, should be able to view the data from the server database easily and clearly. This means HTML web pages have to be populated with data generated from the server. To do this, a function is called before the page is displayed which creates a JSON string with specific data pulled from the database.
@app.route("/floor/first", methods=["GET"])
def serve_floor_first():
blocks_and_groups = '{"block":[{"title":"valve1","id":"111115|1"}],"group":[{"table_title":"1st Floor Bearing Temperatures","sensor_type":"bearing_temp"},{"table_title":"1st Floor Line Shaft RPM","sensor_type":"shaft_rpm"},{"table_title":"1st Floor Valves","sensor_type":"valve"},{"table_title":"1st Floor Devices Battery Level","sensor_type":"dev|battery_level"},{"table_title":"1st Floor Devices Last Online","sensor_type":"dev|last_online"}]}' #{"title":"","id":""} {"table_title":"","sensor_type":""}
data = compile_into_BandG(1,blocks_and_groups)
log.logger.debug(data)
return render_template('floorblank.html', data=data)
The above code is the Flask 'route' to the web page that displays all the devices on the first floor of the building. Groups and blocks are used to group data together into tables or display a single sensor in a clear and formatted way respectively. This is necessary because the server will only generate the JSON string of devices on the first floor and it will format them to be displayed according to the group and block criteria. Flask routes always end with a return statement that passes data to the user's web browser. The render_template method will pass our 'floorblank.html' file to the user with data (called 'data'), displaying a formatted HTML page populated with data.
{% extends "website.html" %}
{% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/floorcss.css') }}">
<div class="flex-container-main">
{% for group in data["groups"] %}
<div class="flex-container-item-group">
<h3><u>{{group["title"]}}</u></h3>
<table>
<tr>
<th>Device</th>
<th>Data</th>
</tr>
{% for group_element in group["elements"] %}
{% if group_element["type"] == "battery" %}
<tr id="row|battery|{{group_element['id']}}">
<td>{{group_element["title"]}} [{{group_element["id"]}}]</td>
<td id="data|battery|{{group_element['id']}}">{{group_element["data"]}}</td>
{% elif group_element["type"] == "timestamp" %}
<tr id="row|timestamp|{{group_element['id']}}">
<td>{{group_element["title"]}} [{{group_element["id"]}}]</td>
<td id="data|timestamp|{{group_element['id']}}">{{group_element["data"]}}</td>
{% else %}
<tr id="row|{{group['sensor_type']}}|{{group_element['id']}}">
<td>{{group_element["title"]}} [{{group_element["id"]}}]</td>
<td id="data|{{group['sensor_type']}}|{{group_element['id']}}">{{group_element["data"]}}</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
</div>
{% endblock %}
Flask uses Jinja2 to handle generating a page dynamically. Normally, you can't use a for loop in HTML code but, with Jinja2 templating, we can loop through data to populate tables and div elements with data passed from the Python server. To generate a table from our JSON data, a for loop is created to traverse through each table and then each element. As the code moves through each element of the JSON data, an id is generated for each table row and the element's data is passed into the text area of the table. Now our page will display all the data as HTML text that can be overwritten with a javascript function since we know the id of any given text element.
Now that the web page is correctly displaying data to the user, we need a way of updating that data without refreshing the page. Refreshing the page every few seconds will work, but the server can crash due to having too many web page requests at once. The better option is to use web sockets, using Flask_socketio, and push our new data to the web page directly. For example, the header of the page has a field for alerts and for the time. Rather than updating this time every second with a page refresh, the server uses this code (running in a different thread) to push new data to a javascript script running on the web page.
def update_header(alerts):
alert_list = ""
counter = 0
for alert in alerts:
alert_list += f'{alert}'
counter = counter + 1
if counter != len(alert):
alert_list += ','
time = datetime.now().strftime("%b %d, %I:%M:%S")
socketio.emit('updateheader', {'time':f'{time}', 'alerts':[f'{alert_list}']}, broadcast=True, namespace="/all")
By using the socketio.emit() function, we can trigger the socketio javascript, listening for an 'updateheader' argument, and pass our new data to the web page.
var socket = io.connect('http://' + document.domain + ':' + location.port + '/all');
socket.on('connect', function() {
socket.emit('connect', {data: 'I\'m connected!'});
});
socket.on("updateheader", function(data){
var timetext = document.getElementById("masthead_time");
timetext.innerHTML = data['time'];
});
The page has a variable called socket that holds the function to be triggered when 'updateheader' is called from the server. This code updates the text of the clock by getting the element by document ID (the id we call our text with id="") allowing the clock to be updated without refreshing the page.
A configuration menu was added to this project to give whoever has to maintain the database an easy way of adding and removing devices/sensors as well as to create sensor events for certain sensors. This means all the network configuration can be done through a web browser, allowing anyone to be able to update different device or sensor configurations (if they have the correct login credentials).
ConclusionThis project is not finished by any means, but the system is working in a usable capacity. The devices are done and the program for the ESP devices is working well enough to be finished.
To finish the project completely, the sensor event system needs to be finalized to allow users to set thresholds for sensor data that trigger a response if they are exceeded. The HTML code needs to be fixed to display better on mobile platforms so a user can use their smart phone to check the server.
Credits
Jeremy McGinnis
Alexander Buyser
Matthew Mangapit
Justin Cruz
Comments