Last active
May 16, 2018 07:05
-
-
Save NeoCat/73f8ed86db468dd005a2 to your computer and use it in GitHub Desktop.
スマートメーターからの電力をBP35A1を使って取得・Webブラウザ上でリアルタイムにグラフ描画 (詳細: http://d.hatena.ne.jp/NeoCat/20160117/1453021993 )
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | |
<title>Power</title> | |
<script type="text/javascript" src="http://code.jquery.com/jquery-2.2.0.min.js"></script> | |
<script src="https://code.highcharts.com/highcharts.js"></script> | |
<script src="https://code.highcharts.com/highcharts-more.js"></script> | |
<script src="https://code.highcharts.com/modules/exporting.js"></script> | |
<style type="text/css"> | |
</style> | |
<script type="text/javascript"> | |
var websocket_url = 'ws://127.0.0.1:5000'; | |
var chart2, chart3; | |
var daily = []; | |
$(function() { | |
$(document).ready(function () { | |
Highcharts.setOptions({global: { useUTC: false }}); | |
var conf = { | |
chart: { | |
type: 'area', | |
animation: Highcharts.svg, | |
events: { load: function(){ load(this); } }, | |
}, | |
credits: { enabled: false }, | |
title: { text: '' }, | |
xAxis: { | |
type: 'datetime', | |
tickPixelInterval: 50, | |
labels: { rotation: -45 } | |
}, | |
yAxis: { | |
title: { | |
text: 'Power [W]' | |
}, | |
max: 3000, | |
min: 0, | |
}, | |
tooltip: { | |
formatter: function () { | |
return '<b>' + this.series.name + '</b><br/>' + | |
Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' + | |
Math.round(this.y) + " W"; | |
} | |
}, | |
legend: { enabled: false }, | |
exporting: { enabled: false }, | |
series: [{ | |
name: 'Power', | |
data: [], | |
color: 'null', | |
marker: { enabled: false }, | |
fillColor: { | |
linearGradient: {x1: 0, y1: 0, x2: 0, y2: 1}, | |
stops: [ | |
[0.0, '#ff0000'], | |
[0.3, '#dddd00'], | |
[0.5, '#00ff00'], | |
[0.8, '#00ffff'], | |
[1.0, '#0000ff'], | |
] | |
}, | |
}], | |
}; | |
$('#container').highcharts(conf); | |
conf.chart.type = 'column'; | |
conf.chart.events.load = function(){ chart2 = this; }; | |
conf.series[0].color = 'null'; | |
$('#container2').highcharts(conf); | |
conf.chart.type = 'area'; | |
conf.xAxis.labels.rotation = 0; | |
conf.xAxis.tickPixelInterval = 100; | |
conf.yAxis.tickPixelInterval = 20; | |
var day = 24*60*60*1000; | |
var tz = new Date('1/1/1').getTime() % day; | |
for (var i = 0; i < 4; i++) { | |
var date = new Date().getTime() - i*day; | |
date -= (date - tz) % day; | |
conf.xAxis.min = date; | |
conf.xAxis.max = date + day; | |
conf.chart.events.load = function(){ daily[i] = this; }; | |
$('#day' + i).highcharts(conf); | |
} | |
$('#container3').highcharts({ | |
chart: { | |
type: 'gauge', | |
plotBackgroundColor: null, | |
plotBackgroundImage: null, | |
plotBorderWidth: 0, | |
plotShadow: false, | |
spacingTop: -100, | |
spacingLeft: 0, | |
spacingRight: 0, | |
spacingBottom: -50, | |
animation: { duration: 2500 }, | |
events: { load: function(){ chart3 = this; } }, | |
}, | |
title: { text: 'Power' }, | |
pane: { | |
center: ['50%', '75%'], | |
startAngle: -90, | |
endAngle: 90, | |
background: [{ | |
shape: 'arc', | |
backgroundColor: { | |
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, | |
stops: [ | |
[0, '#FFF'], | |
[1, '#333'] | |
] | |
}, | |
borderWidth: 0, | |
outerRadius: '109%' | |
}, { | |
shape: 'arc', | |
backgroundColor: { | |
linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, | |
stops: [ | |
[0, '#333'], | |
[1, '#FFF'] | |
] | |
}, | |
borderWidth: 1, | |
outerRadius: '107%' | |
}, { | |
// default background | |
shape: 'arc', | |
}, { | |
shape: 'arc', | |
backgroundColor: '#DDD', | |
borderWidth: 0, | |
outerRadius: '105%', | |
innerRadius: '103%' | |
}] | |
}, | |
yAxis: { | |
min: -150, | |
max: 3150, | |
minorTickInterval: 'auto', | |
minorTickWidth: 1, | |
minorTickLength: 10, | |
minorTickPosition: 'inside', | |
minorTickColor: '#666', | |
tickPixelInterval: 30, | |
tickWidth: 2, | |
tickPosition: 'inside', | |
tickLength: 10, | |
tickColor: '#666', | |
labels: { | |
step: 2, | |
rotation: 'auto' | |
}, | |
title: { | |
text: 'W' | |
}, | |
plotBands: [{ | |
from: 0, | |
to: 1500, | |
color: '#55BF3B' // green | |
}, { | |
from: 1500, | |
to: 2000, | |
color: '#DDDF0D' // yellow | |
}, { | |
from: 2000, | |
to: 3000, | |
color: '#DF5353' // red | |
}] | |
}, | |
series: [{ | |
name: 'Power', | |
dataLabels: { x: 0, y: -40 }, | |
data: [0], | |
tooltip: { | |
valueSuffix: ' W' | |
} | |
}] | |
}); | |
}); | |
}); | |
function color(val) { | |
if (val < 500) return '#0000ff'; | |
if (val < 1000) return '#00ffff'; | |
if (val < 1500) return '#00ff00'; | |
if (val < 2000) return '#dddd00'; | |
return '#ff0000'; | |
} | |
var realtime = false, shift = false, shift2 = false; | |
var nr = 0, nr2 = 0; | |
var last_min = -1, sum = 0, per_min = 0; | |
function add_to_chart2(time, power) { | |
if (++nr2 == 30) | |
shift2 = true; | |
chart2.series[0].addPoint( | |
{color: color(power), | |
x: parseInt(time) * 1000, y: power}, | |
false, shift2); | |
last_min = Math.floor(parseInt(time) / 60); | |
} | |
function load(chart) { | |
var ws = new WebSocket(websocket_url); | |
ws.onopen = function() { | |
console.log('ws opened'); | |
} | |
ws.onmessage = function(message) { | |
var data = JSON.parse(message.data); | |
if (data.error) { | |
alert(data.error); | |
} else if (data.realtime == "start") { | |
realtime = true; | |
chart2.redraw(); | |
} else if (data instanceof Array) { | |
var hist = data.map(function(d) { | |
return {x: parseInt(d.time)*1000, y: d.power} | |
}); | |
for (var i = 0; i < 4; i++) | |
daily[i].series[0].setData(hist); | |
} else if (data.time) { | |
if (realtime) { | |
if (++nr == 25) | |
shift = true; | |
chart.series[0].addPoint( | |
[parseInt(data.time) * 1000, data.power], true, shift); | |
chart3.series[0].points[0].update(data.power); | |
min = Math.floor(parseInt(data.time) / 60); | |
if (last_min < min) { | |
add_to_chart2(min * 60, sum / per_min); | |
chart2.redraw(); | |
sum = per_min = 0; | |
} | |
sum += data.power; | |
per_min += 1; | |
} else { | |
add_to_chart2(data.time, data.power); | |
} | |
} | |
} | |
ws.onerror = function() { | |
alert('Error!'); | |
} | |
ws.onclose = function() { | |
alert('Connection closed!'); | |
} | |
} | |
</script> | |
</head> | |
<body> | |
<div id="daily" style="width: 30%; height: 500px; margin: 0 auto; float: left;"> | |
<div id="day3" style="width: 100%; height: 24%; margin: 0 auto;"></div> | |
<div id="day2" style="width: 100%; height: 24%; margin: 0 auto;"></div> | |
<div id="day1" style="width: 100%; height: 24%; margin: 0 auto;"></div> | |
<div id="day0" style="width: 100%; height: 24%; margin: 0 auto;"></div> | |
</div> | |
<div id="container2" style="width: 30%; height: 500px; margin: 0 auto; float: left;"></div> | |
<div id="container" style="width: 30%; height: 500px; margin: 0 auto; float: left;"></div> | |
<div id="container3" style="width: 400px; height: 200px; margin: 0 auto; float: left;"></div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/local/bin/node | |
var fs = require('fs'); | |
var ws = require('websocket.io'); | |
var net = require('net'); | |
var server = ws.listen(5000, function () { | |
console.log('Server running'); | |
}); | |
var clients = []; | |
function send_obj(obj, client) { | |
var str = JSON.stringify(obj); | |
console.log(str); | |
if (client) | |
client.send(str); | |
else for (var c in clients) | |
clients[c].send(str); | |
} | |
var pow_sock_rt; | |
function open_pow_sock(client) { | |
var pow_sock = net.createConnection("/tmp/power.sock"); | |
if (!client) pow_sock_rt = pow_sock; | |
pow_sock.setEncoding('utf8'); | |
pow_sock.on('connect', function() { | |
console.log("connected to power socket"); | |
}); | |
pow_sock.on('data', function(d) { | |
if (d.match(/^error:\s+(.*)/)) { | |
send_obj({'error':RegExp.$1}, client); | |
return; | |
} | |
var data = d.replace(/\n$/,'').split(/\n/); | |
for (var i in data) { | |
if (data[i] == '') { | |
send_obj({'realtime': 'start'}, client); | |
continue; | |
} | |
var dat = data[i].split(/\s+/); | |
var time = dat[0]; | |
var power = parseInt(dat[1]); | |
send_obj({'time': time, 'power': power}, client); | |
} | |
}); | |
pow_sock.on('close', function(err) { | |
console.log('socket closed'); | |
if (!client) { | |
for (var c in clients) | |
clients[c].close(); | |
clients = []; | |
} | |
}); | |
pow_sock.on('error', function(err){ | |
console.log(err); | |
console.log(err.stack); | |
}); | |
} | |
// クライアントからの接続イベントを処理 | |
server.on('connection', function(client) { | |
console.log('connection start'); | |
var fd = fs.openSync('/tmp/power.log', 'r'); | |
var size = fs.fstatSync(fd).size; | |
var read_size = 18*6*24*4; // 末尾の概ね4日分のデータのみ読み込む | |
var offset = Math.max(0, size - read_size); | |
var buffer = new Buffer(read_size); | |
fs.readSync(fd, buffer, 0, read_size, offset); | |
fs.closeSync(fd); | |
var hist = buffer.toString('ascii'); | |
hist = hist.replace(/^.*?\n|\n$/, '').split(/\n/).map((s)=>{ | |
var dat = s.split(/\s+/); | |
return {'time': dat[0], 'power': parseInt(dat[1])}; | |
}); | |
send_obj(hist, client); | |
if (!clients.length) | |
open_pow_sock(); // open RT event sock | |
else | |
open_pow_sock(client); | |
clients.push(client); | |
// クライアントからのメッセージ受信イベントを処理 | |
client.on('message', function(request) { | |
}); | |
// クライアントが切断したときの処理 | |
client.on('disconnect', function(){ | |
console.log('connection disconnect'); | |
}); | |
// 通信がクローズしたときの処理 | |
client.on('close', function(){ | |
console.log('connection close'); | |
var nclients = []; | |
for (var c in clients) | |
if (clients[c] != client) | |
nclients.push(clients[c]); | |
clients = nclients; | |
if (!clients.length) { | |
pow_sock_rt.end(); | |
pow_sock_rt.destroy(); | |
pow_sock_rt = null; | |
} | |
}); | |
// エラーが発生した場合 | |
client.on('error', function(err){ | |
console.log(err); | |
console.log(err.stack); | |
client.close(); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdio.h> | |
#include <string.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
#include <fcntl.h> | |
#include <poll.h> | |
#include <termios.h> | |
#include <signal.h> | |
#include <time.h> | |
#include <sys/socket.h> | |
#include <sys/un.h> | |
#include <pthread.h> | |
//----------------------------------------------------------- | |
// Configurations | |
//----------------------------------------------------------- | |
// B-route ID and Password (PANA authentication) | |
#define ID "0000XXXXXXXXXXXXXXXXXXXXXXXXXXXX" | |
#define PASSWORD "XXXXXXXXXXXX" | |
// Serial | |
#define BP35A1_path "/dev/ttyS1" | |
#define BP35A1_baud 115200 | |
// Socket | |
#define SOCK_PATH "/tmp/power.sock" | |
// Log file name | |
#define LOG_FILENAME "/tmp/power.log" | |
//----------------------------------------------------------- | |
// data trasnfer buffer | |
//----------------------------------------------------------- | |
const int bufsize = 512; //122; | |
char buf[bufsize]; | |
const int size = bufsize - 1; | |
//----------------------------------------------------------- | |
// debug message utilities | |
//----------------------------------------------------------- | |
//#define DEBUG | |
#define error0(fmt) fprintf(stderr, fmt "\n") | |
#define error1(fmt, str) fprintf(stderr, fmt "%s\n", str) | |
#define error(fmt, ...) fprintf(stderr, fmt "\n", __VA_ARGS__) | |
#define log0(fmt) fprintf(stderr, fmt "\n") | |
#define log1(fmt, str) fprintf(stderr, fmt "%s\n", str) | |
#define log(fmt, ...) fprintf(stderr, fmt "\n", __VA_ARGS__) | |
#ifdef DEBUG | |
#define debug0(fmt, ...) fprintf(stderr, fmt "\n") | |
#define debug1(fmt, str) fprintf(stderr, fmt "%s\n", str) | |
#define debug(fmt, ...) fprintf(stderr, fmt "\n", __VA_ARGS__) | |
#else | |
#define debug0(fmt) | |
#define debug1(fmt, str) | |
#define debug(fmt, ...) | |
#endif | |
//----------------------------------------------------------- | |
// BP35A1 communication class | |
//----------------------------------------------------------- | |
class BP35A1 { | |
int fd; | |
unsigned short tid; | |
bool found; | |
bool connected; | |
char channel[3]; | |
char panid[5]; | |
char addr[17]; | |
char ipv6_addr[40]; | |
public: | |
BP35A1(const char *path, int baud) : tid(0), found(0), connected(0) { | |
fd = open(path, O_RDWR); | |
struct termios tio; | |
tcgetattr(fd, &tio); | |
cfsetispeed(&tio, baud); | |
cfsetospeed(&tio, baud); | |
tcflush(fd, TCIFLUSH); | |
tcsetattr(fd, TCSANOW, &tio); | |
} | |
void listen() { | |
} | |
ssize_t write(const char *buf, size_t len) { | |
return ::write(fd, buf, len); | |
} | |
void write_ln(const char *cmd, const char *arg) { | |
if (cmd) | |
write(cmd, strlen(cmd)); | |
if (arg) | |
write(arg, strlen(arg)); | |
write("\r", 1); | |
if (cmd) debug1("> ", cmd); | |
if (arg) debug1("> ", arg); | |
} | |
int read() { | |
char d; | |
if (::read(fd, &d, 1) <= 0) | |
return -1; | |
return d; | |
} | |
int read_sync(int maxsec=-1) { | |
char ret; | |
struct pollfd p = {fd, POLLIN, 0}; | |
while (poll(&p, 1, maxsec < 0 ? -1 : maxsec*1000) == 1) { | |
if (::read(fd, &ret, 1) < 1) | |
break; | |
if (ret == '\n') | |
continue; | |
return (int)(unsigned int)ret; | |
} | |
debug0("timeout"); | |
return -1; | |
} | |
int readline(char *resbuf, int size, int maxsec=-1, bool need_flush=true) { | |
for (int p = 0; p < size; p++) { | |
resbuf[p] = read_sync(maxsec); | |
if (resbuf[p] == '\r') { | |
resbuf[p] = 0; | |
debug1("< ", resbuf); | |
return p; | |
} | |
if ((signed char)resbuf[p] <= 0) { | |
resbuf[p] = 0; | |
return -1; | |
} | |
} | |
debug1("<.. ", resbuf); | |
if (need_flush) | |
flush(); | |
return size; | |
} | |
void flush() { | |
int ret; | |
do { | |
ret = read_sync(); | |
} while (ret != '\r' && ret > 0); | |
} | |
int hex(char c) { | |
if (c >= '0' && c <= '9') return c - '0'; | |
if (c >= 'A' && c <= 'F') return c - 'A' + 10; | |
if (c >= 'a' && c <= 'f') return c - 'a' + 10; | |
return -1; | |
} | |
bool startswith(const char *str, const char *search) { | |
while (*search) | |
if (*(search++) != *(str++)) | |
return false; | |
return true; | |
} | |
int recv_udp(char *resbuf, int size, int len) { | |
unsigned short port, psize; | |
if (size < 7+40+40+5+5+17+2+5) { | |
error0("buffer is too short to handle ERXUDP!"); | |
flush(); | |
return -1; | |
} | |
int offset = 7+40+40+5; | |
port = (hex(resbuf[offset+0]) << 12) | | |
(hex(resbuf[offset+1]) << 8) | | |
(hex(resbuf[offset+2]) << 4) | | |
(hex(resbuf[offset+3])); | |
offset = 7+40+40+5+5+17+2; | |
psize = (hex(resbuf[offset+0]) << 12) | | |
(hex(resbuf[offset+1]) << 8) | | |
(hex(resbuf[offset+2]) << 4) | | |
(hex(resbuf[offset+3])); | |
log("UDP (port:%d size:%d): ", port, psize); | |
if (len > 7+40+40+5+5+17+2+5) | |
log1(" ", resbuf+7+40+40+5+5+17+2+5); | |
do { | |
len = readline(resbuf, size, -1, false); | |
if (len < 0) | |
break; | |
log1(" ", resbuf); | |
} while (size == len); | |
return psize; | |
} | |
enum { | |
WAIT_UDP = 0x01, | |
WAIT_OK = 0x02, | |
HANDLE_PAN = 0x04, | |
}; | |
int wait_response(char *events, int elen, unsigned int flags, const char* cmd, | |
bool &ok, int maxsec, char *resbuf, int size) { | |
for (;;) { | |
int len = readline(resbuf, size>121?121:size, maxsec, false); | |
if (len == 0 || (len == 1 && resbuf[0] == 0)) | |
continue; | |
if (len < 0) | |
return len; | |
if (startswith(resbuf, "ERXUDP")) { | |
int ret = recv_udp(resbuf, size, len); | |
if (flags & WAIT_UDP) | |
return ret; | |
continue; | |
} | |
if (len == size) | |
flush(); | |
if ((cmd && startswith(resbuf, cmd)) || startswith(resbuf, "SK")) { | |
continue; | |
} | |
if (startswith(resbuf, "OK")) { | |
ok = true; | |
if (flags & WAIT_OK) | |
return 0; | |
continue; | |
} | |
if (startswith(resbuf, "FAIL")) { | |
error1("command failed -> ", resbuf); | |
return -1; | |
} | |
if (startswith(resbuf, "EVER")) { | |
log1("Version: ", resbuf + 5); | |
continue; | |
} | |
if (startswith(resbuf, "EVENT")) { | |
char id = (hex(resbuf[6]) << 4) | hex(resbuf[7]); | |
log("Event %x", id); | |
for (int i = 0; i < elen; i++) | |
if (id == events[i]) | |
return id; | |
continue; | |
} | |
if (startswith(resbuf, "EPANDESC")) { | |
if (!(flags & HANDLE_PAN)) | |
continue; | |
found = true; | |
readline(resbuf, size); // " Channel:xx" | |
strcpy(channel, resbuf + 10); | |
readline(resbuf, size); // " Channel Page:xx" | |
readline(resbuf, size); // " Pan ID:xxxx" | |
strcpy(panid, resbuf + 9); | |
readline(resbuf, size); // " Addr:xxxxxxxxxxxxxxxx" | |
strcpy(addr, resbuf + 7); | |
readline(resbuf, size); // " LQI:xx" | |
char lqi[3] = {resbuf[6],resbuf[7],0}; | |
readline(resbuf, size); // " PairID:xxxx" | |
log("Pan ID %s (Channel:%s Addr:%s LQI:%s) found!", | |
panid, channel, addr, lqi); | |
continue; | |
} | |
error1("unknown response -> ", resbuf); | |
} | |
} | |
int wait_udp(char *resbuf, int size, int maxsec, bool &ok) { | |
if (size < 121) { | |
error0("buffer size is too short!"); | |
return -1; | |
} | |
return wait_response(NULL, 0, WAIT_UDP, NULL, ok, maxsec, resbuf, size); | |
} | |
int wait_event(char *events, int elen, char *resbuf, int size) { | |
bool ok = false; | |
return wait_response(events, elen, HANDLE_PAN, NULL, ok, -1, resbuf, size); | |
} | |
int exec_command(const char *cmd, const char *arg, char *resbuf, int size) { | |
write_ln(cmd, arg); | |
bool ok = false; | |
return wait_response(NULL, 0, WAIT_OK, cmd, ok, -1, resbuf, size); | |
} | |
bool setup(const char *id, const char *password, char *buf, int size) { | |
listen(); | |
disconnect(buf, size); | |
if (exec_command("SKVER", NULL, buf, size) < 0) | |
return false; | |
if (exec_command("ROPT", NULL, buf, size) < 0) | |
return false; | |
if (buf[5] == '0') { // "OK 00" | |
log0("enabling ASCII dump mode"); | |
if (exec_command("WOPT 01", NULL, buf, size) < 0) | |
return false; | |
} | |
if (exec_command("SKSETPWD C " , password, buf, size)) // set password | |
return false; | |
if (exec_command("SKSETRBID ", id, buf, size)) // set RBID | |
return false; | |
return true; | |
} | |
void active_scan(char *buf, int size) { | |
log0("Scanning PAN ..."); | |
if (exec_command("SKSCAN 2 FFFFFFFF 6", NULL, buf, size)) { // active scan | |
sleep(2); | |
return; | |
} | |
char events[] = {0x22}; | |
wait_event(events, 1, buf, size); | |
if (!found) { | |
error0("PAN not found"); | |
sleep(2); | |
} | |
} | |
bool connect(char *buf, int size) { | |
rescan: | |
while (!found) | |
active_scan(buf, size); | |
if (exec_command("SKSREG S2 ", channel, buf, size)) | |
return false; | |
if (exec_command("SKSREG S3 ", panid, buf, size)) | |
return false; | |
write_ln("SKLL64 ", addr); | |
readline(buf, size); // echo | |
if (readline(ipv6_addr, 40) != 39) { | |
error1("invalid ipv6 address: ", ipv6_addr); | |
return false; | |
} | |
log1("Connecting to ", ipv6_addr); | |
if (exec_command("SKJOIN ", ipv6_addr, buf, size)) | |
return false; | |
char events[] = {0x24, 0x25}; | |
if (wait_event(events, 2, buf, size) != 0x25) { | |
error0("connection failed"); | |
found = false; | |
return false; | |
} | |
log1("Connected to ", ipv6_addr); | |
connected = true; | |
bool ok = false; | |
wait_udp(buf, size, 3, ok); // wait instance announce | |
return true; | |
} | |
void disconnect(char *buf, int size) { | |
exec_command("SKTERM", NULL, buf, size); // disconnect forcively | |
sleep(1); | |
} | |
bool epc_get(unsigned char epc, char *buf, int size) { | |
bool ok = false; | |
write("SKSENDTO 1 ", 11); | |
write(ipv6_addr, 39); | |
char msg[] = " 0E1A 1 000E \x10\x81\x00\x01\x05\xFF\x01\x02\x88\x01\x62\x01\xe7\x00"; | |
tid++; | |
msg[15] = tid >> 8; | |
msg[16] = tid & 0xff; | |
msg[25] = epc; | |
write(msg, sizeof(msg)-1); | |
for (;;) { | |
int ret = wait_udp(buf, size, 5, ok); | |
if (ret < 0) { | |
error1("wait_udp failed! -> ", buf); | |
return false; | |
} | |
if (ret < 14) { | |
error1("response error: ", buf); | |
return false; | |
} | |
unsigned short rtid = (hex(buf[4]) << 12) | (hex(buf[5]) << 8) | | |
(hex(buf[6]) << 4) | hex(buf[7]); | |
if (rtid != tid) | |
continue; | |
if (buf[20] == '5' && buf[21] == '2') { | |
error0(" ** Get EPC not supported"); | |
return false; | |
} | |
if (buf[20] != '7' || buf[21] != '2') { | |
error1("unknown message received: ", buf); | |
return false; | |
} | |
epc = (hex(buf[24]) << 4) | hex(buf[25]); | |
log(" ** EPC %x = ", epc); | |
log1(" ", buf+28); | |
if (!ok) | |
exec_command(NULL, NULL, buf, size); | |
return true; | |
} | |
} | |
void epc_set(unsigned char epc, char *data, int len, char *buf, int size) { | |
bool ok; | |
write("SKSENDTO 1 ", 11); | |
write(ipv6_addr, 39); | |
char msg[64] = " 0E1A 1 000E \x10\x81\x00\x01\x05\xFF\x01\x02\x88\x01\x61\x01\xe7"; | |
msg[15] = tid >> 8; | |
msg[16] = tid & 0xff; | |
tid++; | |
msg[25] = epc; | |
msg[26] = len; | |
memcpy(msg+27, data, len); | |
sprintf(msg + 8, "%04X", len+14); | |
msg[12] = ' '; | |
write(msg, len+27); | |
int ret = wait_udp(buf, size, 3, ok); | |
if (ret < 0) { | |
error1("wait_udp failed! -> ", buf); | |
return; | |
} | |
if (ret < 14) { | |
error1("response error: ", buf); | |
return; | |
} | |
if (buf[20] == '5' && buf[21] == '1') { | |
error0(" ** Set EPC not supported"); | |
return; | |
} | |
if (buf[20] != '7' || buf[21] != '1') { | |
error1("unknown message received: ", buf); | |
return; | |
} | |
log(" ** EPC %x =>", epc); | |
log1(" ", buf+28); | |
if (!ok) | |
exec_command(NULL, NULL, buf, size); | |
} | |
}; | |
//----------------------------------------------------------- | |
// Main routine | |
//----------------------------------------------------------- | |
BP35A1 wisun(BP35A1_path, BP35A1_baud); // BP35A1 instance | |
char punit = 0; | |
const int HIST_MAX_PER_MINUTE = 60; | |
const int HIST_MIN_MAX = 60; | |
struct PowerLog { | |
time_t time; | |
int power; | |
} history[2][HIST_MAX_PER_MINUTE], history_min[HIST_MIN_MAX]; | |
int hist_side = 0; | |
int hist_pos = 0, hist_pos2 = 0; | |
int hist_min_pos = 0; | |
int last_min = -1; | |
FILE * volatile client = NULL; | |
void client_close(int sig) { | |
if (!client) | |
return; | |
fclose(client); | |
client = NULL; | |
} | |
void record_power(int power) { | |
time_t t = time(NULL); | |
if (client) { | |
fprintf(client, "%lld %d\n", (unsigned long long)t, power); | |
if (fflush(client) < 0) { | |
client_close(0); | |
} | |
} | |
if (t/60 != last_min) { | |
if (last_min != -1) { | |
int sum = 0; | |
for (int i = 0; i < hist_pos; i++) | |
sum += history[hist_side][i].power; | |
history_min[hist_min_pos].time = t - t%60; | |
history_min[hist_min_pos++].power = sum/hist_pos; | |
hist_min_pos %= HIST_MIN_MAX; | |
hist_side = 1 - hist_side; | |
hist_pos2 = hist_pos; | |
hist_pos = 0; | |
} | |
last_min = t/60; | |
if (last_min % 10 == 0) { | |
FILE *f = fopen(LOG_FILENAME, "a"); | |
if (!f) { | |
printf("failed to open "LOG_FILENAME"\n"); | |
} else { | |
int sum = 0, n = hist_min_pos > 10 ? 10 : hist_min_pos; | |
if (n > 0) { | |
for (int i = 1; i <= n; i++) | |
sum += history_min[hist_min_pos - i].power; | |
fprintf(f, "%lld %d\n", (unsigned long long)(t - t%60), sum/n); | |
fclose(f); | |
} | |
} | |
} | |
} | |
if (hist_pos >= HIST_MAX_PER_MINUTE) | |
return; | |
history[hist_side][hist_pos].time = t; | |
history[hist_side][hist_pos++].power = power; | |
} | |
int sock_read() { | |
int len; | |
char buf[256]; | |
struct sockaddr_un addr = {AF_UNIX, SOCK_PATH}; | |
socklen_t addrlen = sizeof(addr); | |
int sock = socket(AF_UNIX, SOCK_STREAM, 0); | |
if (sock < 0) { | |
perror("socket"); | |
return 1; | |
} | |
if (connect(sock, (struct sockaddr*)&addr, addrlen) < 0) { | |
perror("connect"); | |
return 1; | |
} | |
for (;;) { | |
len = read(sock, buf, sizeof(buf)); | |
if (len <= 0) | |
break; | |
int ret = write(1, buf, len); | |
} | |
return 0; | |
} | |
int sock = -1; | |
void quit(int sig) { | |
close(sock); | |
unlink(SOCK_PATH); | |
wisun.disconnect(buf, size); | |
exit(0); | |
} | |
void *wait_client(void *arg) { | |
int fd = (int)(long)arg; | |
char dummy; | |
while (read(fd, &dummy, 1) > 0); // wait close | |
client_close(0); | |
return NULL; | |
} | |
void *sock_thread(void *arg) { | |
int fd; | |
struct sockaddr_un addr = {AF_UNIX, SOCK_PATH}; | |
socklen_t addrlen = sizeof(addr); | |
sock = socket(AF_UNIX, SOCK_STREAM, 0); | |
if (sock < 0) { | |
perror("socket"); | |
exit(1); | |
} | |
if (bind(sock, (struct sockaddr*)&addr, addrlen) < 0) { | |
perror("bind"); | |
exit(1); | |
} | |
signal(SIGPIPE, client_close); | |
signal(SIGTERM, quit); | |
signal(SIGINT, quit); | |
listen(sock, 1); | |
while ((fd = accept(sock, (struct sockaddr*)&addr, &addrlen)) >= 0) { | |
FILE *f = fdopen(fd, "w"); | |
if (!f) { | |
close(fd); | |
continue; | |
} | |
for (int i = 0; i < HIST_MIN_MAX; i++) { | |
int p = (hist_min_pos + i) % HIST_MIN_MAX; | |
if (history_min[p].time) | |
fprintf(f, "%lld %d\n", | |
(unsigned long long)history_min[p].time, | |
history_min[p].power); | |
} | |
fprintf(f, "\n"); | |
for (int i = 0; i < hist_pos2; i++) | |
fprintf(f, "%lld %d\n", | |
(unsigned long long)history[1-hist_side][i].time, | |
history[1-hist_side][i].power); | |
for (int i = 0; i < hist_pos; i++) | |
fprintf(f, "%lld %d\n", | |
(unsigned long long)history[hist_side][i].time, | |
history[hist_side][i].power); | |
fflush(f); | |
if (client) { | |
fclose(f); | |
continue; | |
} | |
client = f; | |
pthread_t wait_th; | |
pthread_create(&wait_th, NULL, wait_client, (void*)(long)fd); | |
} | |
return NULL; | |
} | |
void setup() { | |
buf[size] = 0; | |
if (!wisun.setup(ID, PASSWORD, buf, size)) { | |
error0("setup failed"); | |
exit(1); | |
} | |
while (!wisun.connect(buf, size)) | |
sleep(5); // reconnect on error after 5 sec | |
// check if the smart meter is in running state (EPC 0x80 = 0x30) | |
char val = 0; | |
if (wisun.epc_get(0x80, buf, size)) | |
val = (wisun.hex(buf[28]) << 4) | wisun.hex(buf[29]); | |
if (val != 0x30) | |
error0("SmartMeter is not working!"); | |
// check if the smart meter is not in error state (EPC 0x88 = 0x42) | |
val = 0; | |
if (wisun.epc_get(0x88, buf, size)) | |
val = (wisun.hex(buf[28]) << 4) | wisun.hex(buf[29]); | |
if (val != 0x42) | |
error0("SmartMeter is in error state!"); | |
#if 0 | |
// get unit of integral power | |
if (wisun.epc_get(0xe1, buf, size)) | |
punit = (wisun.hex(buf[28]) << 4) | wisun.hex(buf[29]); | |
// get history | |
char data = 0x01; | |
wisun.epc_set(0xe5, &data, 1, buf, size); | |
wisun.epc_get(0xe2, buf, size); | |
data = 0x00; | |
wisun.epc_set(0xe5, &data, 1, buf, size); | |
wisun.epc_get(0xe2, buf, size); | |
// get integral power | |
wisun.epc_get(0xea, buf, size); | |
#endif | |
} | |
void loop() { | |
static int err_cnt = 0; | |
log0("getting EPC 0xe7 ..."); | |
bool ret = wisun.epc_get(0xe7, buf, size); | |
if (ret) { | |
// convert hex string into unsigned long | |
unsigned long val = 0; | |
for (int i = 28; buf[i]; i++) { | |
val <<= 4; | |
val |= wisun.hex(buf[i]); | |
} | |
// print immediate power value to Serial | |
printf("Power = %lu Watt\n", val); | |
record_power((int)val); | |
err_cnt = 0; | |
} else { | |
err_cnt++; | |
error0("EPC get failed"); | |
if (err_cnt >= 5) { | |
err_cnt = 0; | |
log0("reconnecting ..."); | |
wisun.disconnect(buf, size); | |
wisun.connect(buf, size); | |
} | |
} | |
bool ok; | |
wisun.wait_udp(buf, size, 3, ok); // wait 3 sec | |
} | |
int main(int argc, char *argv[]) | |
{ | |
if (argc > 1 && strcmp(argv[1], "-c") == 0) { | |
return sock_read(); | |
} | |
pthread_t th; | |
pthread_create(&th, NULL, sock_thread, NULL); | |
setup(); | |
for (;;) | |
loop(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment