Table of contents
- Setup and reading sensor values
- Using iSDIO while reading the SD card
- Sending measurements from Arduino to Raspberry Pi
- Visualizing on Raspberry Pi — completion
Finally, the last installment.
We receive data sent from the Arduino on the Raspberry Pi
and visualize it with JavaScript.
The PHP and JavaScript created here are on GitHub.
To recap, the full communication sequence:

A data collection API accepts incoming transmissions from the Arduino.
Data is written to log files, which are then read by the JavaScript visualization page on a tablet.
Running a Web API on Raspberry Pi
Set up a Web API using Apache and PHP — just install the packages.You can find plenty of guides online, so I'll keep this brief.
pi@raspberrypi ~ $ sudo apt-get install apache2
pi@raspberrypi ~ $ sudo apt-get install php
pi@raspberrypi ~ $ sudo systemctl start apache2
After installation, files placed under /var/www/html/ are accessible via HTTP.Data Collection API
Receives data from the Arduino over HTTP and writes it to a log file.Strictly speaking, file locking for concurrent access and log rotation for oversized files should be considered —
but with only a few Arduinos, "good enough" is fine for now.
Key design decisions:
- Arduino can only read elapsed time since boot, so the Raspberry Pi's system clock is used for log timestamps.
- Separate log files per location ID.
- Skip log entries when the Arduino sends invalid (all-zero) values.
First, create the log directory:
pi@raspberrypi ~ $ mkdir /var/www/html/data/
pi@raspberrypi ~ $ chmod 777 /var/www/html/data/
Save the following PHP as /var/www/html/api.php.
When the Arduino sends data to "http://{IP}/api.php",
the values are appended to /var/www/data/log_{locationID}.txt.
<?php
// HTTP headers
header('Content-type:text/plain');
// Log file path: "log_{locationID}.txt"
define('LOG_PREFIX','./data/log_');
define('LOG_SUFFIX','.txt');
// Use Raspberry Pi's system time as timestamp
date_default_timezone_set('Asia/Tokyo');
$timestamp = date('Y/m/d H:i:s');
$id = 0;
$h = 0;
$t = 0;
// Get HTTP GET parameters
// filter_var strips non-numeric values
if ( isset($_GET['ID']) ){
$tmp = filter_var($_GET['ID'], FILTER_VALIDATE_INT);
if ( $tmp ) { $id = $tmp; }
}
if ( isset($_GET['H']) ){
$tmp = filter_var($_GET['H'], FILTER_VALIDATE_FLOAT);
if ( $tmp ){ $h = $tmp; }
}
if ( isset($_GET['T']) ){
$tmp = filter_var($_GET['T'], FILTER_VALIDATE_FLOAT);
if ( $tmp ){ $t = $tmp; }
}
// If all params are still default (0), something's wrong — skip the log
if ( $h == 0 && $t == 0 && $id == 0 ){ echo 'NG'; return; }
// Format one log line
$log_msg = $timestamp.','.$h.','.$t."\n";
// Write (create file if new, append if exists)
$log_file = LOG_PREFIX.$id.LOG_SUFFIX;
$fp = ( file_exists($log_file) )
? fopen($log_file,'a')
: fopen($log_file,'w');
fwrite($fp,$log_msg);
fclose($fp);
?>OK
Display Page
The display page was also built quickly without much abstraction — just enough to work.For the chart I used the bubble chart from Chart.js.
There's not much documentation on the bubble chart specifically, but it works the same as other chart types.
Start with the HTML wrapper — it just loads JavaScript and creates a canvas.
All rendering happens inside "mychart.js".
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.3.0/Chart.min.js"></script>
<script src="mychart.js"></script>
<title>Temperature</title>
</head>
<body onLoad=main() >
<div style="margin:60px; width:80%">
<canvas id="myChart" style="width: 100%; height: auto;"></canvas>
</div>
</body>
</html>
Now for mychart.js, which contains the bubble chart logic.
The main() function (called by onLoad) builds the list of log files and passes it to the rendering function.
setInterval refreshes the chart every 10 minutes.
readyChart() defines the static parts of the chart — axes, labels, colors.
See the Chart.js documentation for details.
Everything is hardcoded, so adding more locations requires editing the code.
function main() {
var filePath = [
'./data/log_1.txt?'+new Date().getTime(),
'./data/log_2.txt?'+new Date().getTime(),
'./data/log_3.txt?'+new Date().getTime(),
'./data/log_4.txt?'+new Date().getTime(),
'./data/log_5.txt?'+new Date().getTime(),
'./data/log_6.txt?'+new Date().getTime()
];
var myChart = readyChart();
updateChart(filePath, myChart,0);
setInterval(updateChart(filePath,myChart),60000);
}
function readyChart() {
var data = [{},{},{},{},{},{}] ;
var ctx = document.getElementById("myChart").getContext("2d");
var myChart = new Chart(ctx, {
type: 'bubble',
data: {
datasets: [
{
label: "Living Room",
data: data[0],
borderColor: '#FF0000',
borderWidth: 3,
showLine: true
},
// ... (additional rooms omitted) ...
{
label: "Bathroom Storage",
data: data[5],
borderColor: '#666666',
borderWidth: 3,
showLine: true
}
]
},
options: {
scales: {
xAxes: [{
scaleLabel: {
display: true,
labelString: 'Temperature [°C]'
}
}],
yAxes: [{
scaleLabel: {
display: true,
labelString: 'Humidity [%]'
}
}]
}
}
});
return myChart;
}
Next, the chart update logic.
Log files are loaded one at a time and converted to JSON format.
This could have been simpler using PHP to output JSON-P, loaded as a script tag —
but I went with JavaScript CSV parsing so let's stick with it.
Sequential XMLHttpRequest calls: to avoid parallel async redraws corrupting the display,
each log file is loaded in the onLoad callback of the previous XMLHttpRequest (recursive).
If one request fails, subsequent logs won't load...
One chart-specific note: using raw data point count as bubble radius means rooms with longer measurement periods get huge bubbles.
To normalize, I round values to integers and divide by the maximum count of any single measurement value,
capping the maximum bubble size.
function updateChart(filePath, myChart, idx) {
if ( idx >= filePath.length ){ return; }
var req = new XMLHttpRequest();
req.open("GET", filePath[idx], true);
req.onload = function() {
var data = convertData(csv2Array(req.responseText));
myChart.data.datasets[idx].data = data;
myChart.update();
updateChart(filePath, myChart, idx+1);
}
req.send(null);
}
function convertData(source) {
var data = [];
var max = 1;
var buf = {};
for (var row in source) {
y = String(parseInt(parseFloat(source[row][1]) + 0.5));
x = String(parseInt(parseFloat(source[row][2]) + 0.5));
if ( buf[x] == undefined ){
buf[x] = {};
buf[x][y] = 1;
} else if ( buf[x][y] == undefined ){
buf[x][y] = 1;
}else{ buf[x][y] ++; }
if ( buf[x][y] > max ){ max = buf[x][y]; }
};
for (var i in buf) {
for (var j in buf[i]) {
data.push( { x:i, y:j, r:20*buf[i][j]/max } );
}
}
return data;
}
function csv2Array(str) {
var csvData = [];
var lines = str.split("\n");
for (var i = 0; i < lines.length; ++i) {
var cells = lines[i].split(",");
csvData.push(cells);
}
return csvData;
}
Complete
The Raspberry Pi side is hacky in places but fully working.Open the display page on an iPad and it looks like this.
The bedroom was measured only at night so it's cold.
The kids' room turned out more humid than the bathroom.
No comments:
Post a Comment