Project Duo

https://github.com/szdiy/duo

项目的目标是把 SZDIY Hackspace 的电表接入互联网,可以网上查看空间的用电数据。

Documentation is a love letter that you write to your future self. -- Damian Conway

2017-04-24 星期一

来空间用 USB 转 RS-485 转换器做了一些实验,可以从电表读出一些数据,下面是实验记录。

接线

========================

USB 转 RS-485 T/R+(红黑) <-----> DDS216 电表 485 A(端子 11)

USB 转 RS-485 T/R-(白黄) <-----> DDS216 电表 485 B(端子 12)

电表通讯地址

========================

电表上有一个按钮,按住电表上的按钮,会显示电表的通信地址。

显示表号为: 00130495

在表的正面,也有 NO.130495 这个号码。

根据通信协议,地址是 6 个字节,不够 6 个字节,就补 00,因此,地址变成了: 95 04 13 00 00 00

注意: 低位在前面

通讯参数设置

========================

Baud rate: 2400 bps

Data bits: 8

Stop bits: 1

Parity: Even

下面查询一些我们感兴趣的数据。

正向有功总电能

========================

DL/T645-2007 第 12 页写到

标识码 DI3 DI2 DI1 DI0 =00010000H(数据项) 表示当前正向有功总电能。

数据标识码 DI3 DI2 DI1 DI0 的意义在附录 A.2 (page 26)里有说明

按照要求 DI3 to DI0 每个数字要加上 33H

而且,在组织数据帧时,要按 DI0 DI1 DI2 DI3 的顺序排放

于是,变成 33 33 34 33

PC 发送: 68 95 04 13 00 00 00 68 11 04 33 33 34 33 5E 16

说明:

* 第一个字节 68H 是 帧起始符

* 接下来的 95 04 13 00 00 00 是地址域

* 68H 是再一个 帧起始符

* 然后是一个控制码,这里是 11,表示:读数据 (PDF pg11 5.2.3 控制码 C)

* 接下来的 04H 是 数据域长度 L, 因为我们接下来要发送的 DI3 to DI0 正好是 4 个字节

* 33 33 34 33 就是 数据域 DATA.

* 5E 是 检验码 CS. 它等于从第一个帧起始符开始到校验码之前的所有各字节的模 256 的和,即各字节二进制算术和,不计超过 256 的溢出值。

在这个例子里

(0x68+0x95+0x04+0x13+0x00+0x00+0x00+0x68+0x11+0x04+0x33+0x33+0x34+0x33) % 0x100 = 0x5E

* 结束符 16H

如果数据传输 OK,则电表就会返回数据。

DL/T645-2007 第 14 页 7.1.2 从站正常应答 里作了解释。

(上 1 结算日)正向有功总电能

========================

DI3 to DI0: 00 01 00 01

PC 发送: 68 95 04 13 00 00 00 68 11 04 34 33 34 33 5F 16

电表返回: fe fe fe fe 68 95 04 13 00 00 00 68 91 08 34 33 34 33 bb 44 6c 33 81 16

从后面往前读数据,16H 是结束符,81H 是 Check Sum

我们需要的数据是 33 6C 44 BB

全部减去 33H, 得到

00 39 11 88

因为是 BCD 码表示,因此得到的数据是

3911.88,也就是 3911.88 kWh

如下图所示

每月第 1 结算日

========================

PDF page 80

表A.5

DI3 to DI0: 04 00 0B 01

PC 发送: 68 95 04 13 00 00 00 68 11 04 34 3E 33 37 6D 16

A 相电压

========================

PDF page 48

表A.3

DI3 to DI0: 02 01 01 00

PC 发送: 68 95 04 13 00 00 00 68 11 04 33 34 34 35 61 16

A 相电流

========================

PDF page 48

表A.3

DI3 to DI0: 02 02 01 00

PC 发送: 68 95 04 13 00 00 00 68 11 04 33 34 35 35 62 16

瞬时A有功功率

========================

PDF page 48

表A.3

DI3 to DI0: 02 03 01 00

PC 发送: 68 95 04 13 00 00 00 68 11 04 33 34 36 35 63 16

A相功率因数

========================

PDF page 49

表A.3

DI3 to DI0: 02 06 01 00

PC 发送: 68 95 04 13 00 00 00 68 11 04 33 34 39 35 66 16

零线电流

========================

PDF page 50

表A.3

DI3 to DI0: 02 80 00 01

PC 发送: 68 95 04 13 00 00 00 68 11 04 34 33 B3 35 E0 16

为什么读来是 0?

电网频率

========================

PDF page 50

表A.3

DI3 to DI0: 02 80 00 02

PC 发送: 68 95 04 13 00 00 00 68 11 04 35 33 B3 35 E1 16

读出来的频率是 49.9x Hz

表内温度

========================

PDF page 50

表A.3

DI3 to DI0: 02 80 00 07

PC 发送: 68 95 04 13 00 00 00 68 11 04 3A 33 B3 35 E6 16

返回 fe fe fe fe 68 95 04 13 00 00 00 68 d1 01 35 83 16

d1 说明 从站异常应答帧 (见 page 15)

错别码是 35

时钟电池电压(内部)

========================

PDF page 50

表A.3

DI3 to DI0: 02 80 00 08

PC 发送: 68 95 04 13 00 00 00 68 11 04 3B 33 B3 35 E7 16

返回 fe fe fe fe 68 95 04 13 00 00 00 68 91 06 3b 33 b3 35 9a 36 39 16

36 9A 减去 33H 算出来是 3.67V, 正常 :)

内部电池工作时间

========================

PDF page 50

表A.3

DI3 to DI0: 02 80 00 0A

PC 发送: 68 95 04 13 00 00 00 68 11 04 3D 33 B3 35 E9 16

返回:

fe fe fe fe 68 95 04 13 00 00 00 68 91 08 3d 33 b3 35 43 63 ac 34 f3 16

计算一下(计算日期 2017-04-24):

((1793010/60)/24)/30 = 41.5 个月 = 3 年 5 个月 15 天

电表的购买日期是 2014-04-15

生产日期(ASCII码)

========================

PDF page 79

表A.5

DI3 to DI0: 04 00 04 0C

PC 发送: 68 95 04 13 00 00 00 68 11 04 3F 37 33 37 71 16

返回:fe fe fe fe 68 95 04 13 00 00 00 68 91 0e 3f 37 33 37 69 65 62 66 63 62 65 64 63 65 e7 16

计算出来的日期是 2012/03/26

日期及星期(其中0代表星期天)

========================

PDF page 78

表A.5

DI3 to DI0: 04 00 01 01

PC 发送: 68 95 04 13 00 00 00 68 11 04 34 34 33 37 63 16

返回: 04 13 00 00 00 68 91 08 34 34 33 37 34 3b 38 4a d8 16

我们感兴趣的数据是:34 3b 38 4a

每个数字减去 0x33

算出来的日期和星期是 17 05 08 01

表示 2017 年 5 月 8 日, 周1

时间

========================

PDF page 78

表A.5

DI3 to DI0: 04 00 01 02

PC 发送: 68 95 04 13 00 00 00 68 11 04 35 34 33 37 64 16

返回: fe fe fe fe 68 95 04 13 00 00 00 68 91 07 35 34 33 37 6a 8c 53 30 16

我们感兴趣的数据是: 6a 8c 53

每字节减少 0x33 后得到 20 59 37

表示时间是 20:59:37

2017-05-04 星期四

在华强北买了 5 颗 SP3485EN-L RS-485 Transceiver, 连接到 ESP-12-Q 模块上,准备用 softuart 做通讯实验。

2017-05-08 星期一

实验用 Raspberry Pi + Python + USB-RS485 Adapter 来读电表参数。

Raspberry Pi 里安装 pyserial:

# sudo apt-get install python-serial

发出下面的命令可以读取电表的通信地址:

发送命令: 68 aa aa aa aa aa aa 68 13 00 df 16

返回:

00000000: fe fe fe fe 68 95 04 13 00 00 00 68 93 06 c8 37

00000010: 46 33 33 33 f3 16

95 04 13 00 00 00 就是电表的地址

c8 37 46 33 33 33 也是电表的地址,只不过每个字节被加了 33H

用下面的 python 代码可以成功读出数据

https://github.com/szdiy/duo/blob/master/duo_firmware/read_dlt645.py

pi@raspberrypi ~/projects/duo/duo_firmware $ ./read_dlt645.py 2400 /dev/ttyUSB0

encode_dlt645 hex_str: 68aaaaaaaaaaaa681300df16

decode_dlt645 hex_str: 68950413000000689306c83746333333f316

0 950413000000

-----------------------------------

encode_dlt645 hex_str: 68950413000000681104333333335d16

decode_dlt645 hex_str: 6895041300000068910833333333466373333016

0 403013

total kWh: 4030.13

encode_dlt645 hex_str: 68950413000000681104333433335e16

decode_dlt645 hex_str: 689504130000006891083334333333333333ae16

ping kWh: 0.0

encode_dlt645 hex_str: 68950413000000681104333533335f16

decode_dlt645 hex_str: 6895041300000068910833353333a6945c33ac16

gu kWh: 2961.73

0 4030.13 0.0 2961.73

2017-05-09 星期二

给 python 脚本增加了读时间的功能。

Cron 周期性地执行 python 脚本,命令 crontab -e 对任务进行添加/编辑

用 python httplib 库向服务器写数据。

pi@raspberrypi ~/projects/duo/duo_firmware $ ./read_dlt645.py 2400 /dev/ttyUSB0

encode_dlt645 hex_str: 68aaaaaaaaaaaa681300df16

decode_dlt645 hex_str: 68950413000000689306c83746333333f316

-----------------------------------

encode_dlt645 hex_str: 68950413000000681104333333335d16

decode_dlt645 hex_str: 6895041300000068910833333333736773336116

total kWh: 4034.4

encode_dlt645 hex_str: 68950413000000681104343433376316

decode_dlt645 hex_str: 6895041300000068910834343337353c384ada16

encode_dlt645 hex_str: 68950413000000681104353433376416

decode_dlt645 hex_str: 689504130000006891073534333779374ce316

1494356686.0

https://api.szdiy.org/duo/upload?node=001&total=4034.4&time=1494356686.0

404 Not Found

<html>

<head><title>404 Not Found</title></head>

<body bgcolor="white">

<center><h1>404 Not Found</h1></center>

<hr><center>nginx/1.6.2</center>

</body>

</html>

cron 的配置

*/2 * * * * /home/pi/projects/duo/raspberry-pi/read_dlt645.py 2400 /dev/ttyUSB0 > /tmp/duo-logfile 2>&1

每 2 分钟发一次数据。

重启 cron

# sudo service cron restart

2017-07-23 星期日

上周四晚上发现程序出了问题,时间居然显示为 85:04:6 这表明程序某个地方出了问题。今天又把程序读了一遍,发现只有一个地方多了一个 + 号。把代码重新 clone 到 rpi 上之后,又能上传数据了。这个 bug 是否已经彻底消失了,我不确定。

pi@raspberrypi ~ $ cat /tmp/duo-logfile

encode_dlt645 hex_str: 68aaaaaaaaaaaa681300df16

decode_dlt645 hex_str: 68950413000000689306c83746333333f316

-----------------------------------

encode_dlt645 hex_str: 68950413000000681104333333335d16

decode_dlt645 hex_str: 6895041300000068910833333333b94378338816

total kWh: 4510.86

encode_dlt645 hex_str: 68950413000000681104343433376316

decode_dlt645 hex_str: 689504130000006891083434333733563a4af416

encode_dlt645 hex_str: 68950413000000681104353433376416

decode_dlt645 hex_str: 68950413000000689107353433377967541b16

1500845686.0

https://api.szdiy.org/duo/upload?node=001&total=4510.86&time=1500845686.0

200 OK

OK

这种项目最好能远程访问 Raspberry Pi,杨城提到可以用一个叫 frp 的工具。

2017-07-24 星期一

昨天程序可以正常工作了。但今天早上一早起来发现那个 bug 依然存在!仔细观察,原来问题出在 BCD 码转换到整数时,前置 0 都被去掉了。比如,假如时间是 07:34:06,代码会把它变成一个整数 73406,代码然后又把这个整数变成字符串,前置 0 就这样消失了!

在 StackOverflow 上找了一段代码,quick and dirty 地修好了这个代码。

https://stackoverflow.com/questions/11668969/python-how-to-decode-binary-coded-decimal-bcd

如果代码出了问题,我又要跑到空间来调代码,这样很麻烦。看了资料,frp 要用到一个中间服务器。我目前没有。google 了一下,发现一个叫 ngrok ( https://ngrok.com/ ) 的服务可以解决这个问题。

1. 在 ngrok 网站注册一个帐号。你会得到一个 authtoken

2. 在 raspberry pi 上下载 ngrok 并 unzip

wget -c https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip

unzip ngrok-stable-linux-arm.zip

3. 安装 authtoken

pi@raspberrypi ~/ngrok $ ./ngrok authtoken [自己的 authtoken]

4. ./ngrok tcp 22

记住 ngrok 生成的端口号

5. 在自己的计算机上远程登录 -p 后是 端口号

ssh pi@0.tcp.ngrok.io -p [端口号]

也可以把 ngrok 运行在后台。

ngrok tcp 22 --log=stdout > ngrok.log &

问题:

1. Rasberry Pi 重启后应该重新运行 ngrok

2. ngrok 的 remote_addr (bind the remote TCP port on the given address) 要付费用户才能用。

2017-07-31 星期一

一周过去了,还可以 ssh 到空间的 Raspberry Pi 上,说明 ngrok 的服务还很稳定。就是速度很慢。

2017-09-23 星期六

SZDIYlab0x0 合办了一场 Internet of Thing hackathon(物联网黑客马拉松),我们继续做这个 Project Duo. 前段时间 xyh 把读电表数据的 Raspberry Pi 装成了 NetBSD 7.1 系统。电表的数据上传就中断了。好几年不用 NetBSD, 没想到今天是在 Pi 上遇到它!今天别的成员如 Terryoy 他们在 lab0x0 那里编码,我来白石洲计划把 Python 脚本重新布置到 Pi 上。

NetBSD 上什么软件都没装。

发现日本的 NetBSD 软件包 FTP 访问速度挺快,如:

  • http://ftp7.jp.netbsd.org/pub/pkgsrc/packages/NetBSD/evbarm/6.1/All/
  • http://ftp.jaist.ac.jp/pub//pkgsrc/packages/NetBSD/evbarm/6.1/All/

设置路径

# PATH="/usr/pkg/sbin:$PATH"

设置包路径

# export PKG_PATH="ftp://ftp7.jp.netbsd.org/pub/pkgsrc/packages/NetBSD/evbarm/6.1/All"

# export PATH PKG_PATH

也可用 pkgin 来管理包,安装 pkgin:

# pkg_add -f pkgin

对文件 /usr/pkg/etc/pkgin/repositories.conf 做相应的修改(改成上面的 ftp 地址)。

然后更新仓库

# pkgin update

安装 python

# pkgin install python27

这个过程至少要等待 20 分钟以上,因为某个我不知道的原因,在把数据存入 SD 卡时,速度非常慢!

# pkgin install py27-pip

上面的步骤要花几分钟时间。

安装 pyserial:

# python -m pip install pyserial

也可以用 # pkgin install py27-serial

# /usr/pkg/bin/python2.7 read_dlt645.py 2400 /dev/ttyU0

(NetBSD 里的 USB 转串口, /dev/ttyU0 和 /dev/dtyU0 都能用,我不确定应该用哪个)

有时能成功读取一次串口数据,有时能读两次,然后会出现一个奇怪的错误:

好像和内存 dma 有关。

把代码中的 time.sleep(0.5) 改成 time.sleep(5) 情况有所好转,有一回手动执行命令 10 次也能正常读取

电表数据,但有时也会出错。糟糕的是,一旦出错,后面即使可以打开串口,但也读不出数据。

遇到这种情况,我实在没有办法了。估计是 NetBSD 软件里的问题。

参考

====

1. Making my RPI serial console work, or: fixing a hardware problem in software

http://www.feyrer.de/NetBSD/bx/blosxom.cgi/index.front?-tags=usb

2. Building Stratum 1 clocks with NetBSD

http://anduin.eldar.org/stratum-1/

2017-09-28 星期四

NetBSD 的驱动有问题,那来试试 FreeBSD,安装方法:

https://medium.com/@alexewerlof/installing-freebsd-on-raspberry-pi-using-linux-20f44abb4565

我手上的 rPi 是 Raspberry Pi 1 Model B revision 1.2 with 512MB RAM

下载 img 文件,目前用的是 FreeBSD-11.1-STABLE-arm-armv6-RPI-B-20170925-r323968.img

把 SD 卡插入电脑的 SD 卡读卡器,用 dd 命令很快就能装好系统。

我们需要 python.

先切换到 root 用户(默认密码是 root)

$ su

# pkg install python27

系统会提示 The package management tool is not yet installed on your system.

Do you want to fetch and install it now? [y/N]:

输入 y 即开始安装包管理工具。然后就会开始安装 python. 很快就装好。

然后安装 wget 以方便从 github 上下载代码。

# pkg install wget

# pkg install py27-serial

脚本中 #!/usr/bin/python 改成 #!/usr/local/bin/python2.7

Serial port 文件是 /dev/cuaU0 还是 /dev/ttyU0?

# ./read_dlt645.py 2400 /dev/cuaU0

出现错误

ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:590)

安装 ca_root_nss

# pkg install ca_root_nss

服务器返回

200 OK

OK

# date

显示时间是 9 月 26 日,不对,同步之:

# ntpdate -b pool.ntp.org

上传数据的方式从 HTTP GET 改成了 HTTP POST,服务器返回:

201 Created

{"msg":"ok"}

参考

https://www.codejam.info/2015/05/freebsd-python-and-ssl-certificate-validation.html

freebsd 使用 Ntpdate 和 Ntpd 自动更新系统时间

https://blog.byneil.com/freebsd-%E4%BD%BF%E7%94%A8-ntpdate-%E5%92%8C-ntpd-%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0%E7%B3%BB%E7%BB%9F%E6%97%B6%E9%97%B4/

https://vzaigrin.wordpress.com/2016/10/16/running-freebsd-11-on-raspberry-pi/

2017-10-05 星期四

上周四在 SZDIY 和阿丰一起,配合 Terry 一起成功用 POST 方法上传数据。整个系统连续运行了一周,基本没有问题。网址 http://api.szdiy.org/duo/dashboard/#/

2020-01-02 星期四

在2017年10月7日,社区从白石洲搬到了科苑西小区(现在的地方),电表一直没有装上。今天是每周四的固定聚会日期,我决定提前了一些时间到,把它装上,这个项目就能继续做下去。断电,打孔,固定,接线,很快,电表就装好了。

用 USB-RS485 接到电脑上,跑以前的 python 程序,能读出数据。

接下来要做的事:

  • 用别的硬件(如 ESP8266/ESP32 类)代替 Raspberry Pi,因为 rPi 在这个应用中硬件上显得稍微有点太“重量级”。
  • 把 DL/T645-2007 协议移植到 MCU 中。
  • 服务器端的服务要重新运行起来并在 UI 上做一些细微调整。


Electricity meter mounted in the new space.

电表被重新安装

2020-01-10 星期五

试着用 Racket 语言来写同样的功能。

地址的 byte string:

(define addr (bytes #x95 #x04 #x13 #x00 #x00 #x00))

"读数据"的控制码:

(define ctrl-code-read (bytes #x11))

数据长度 4:

(define lens-four (bytes #x04))

标识码 DI3 DI2 DI1 DI0 =00010000H(数据项) 表示当前正向有功总电能的 byte string:
(define data-tag-total (bytes #x00 #x01 #x00 #x00))

下面的函数对要发送到电表的数据进行编码:
(encode-dlt645 addr ctrl-code lens data-tag)
待确认: 读当前总电能的标识码是什么。

2020-01-17 星期五

今天我尝试用 Racket 语言读串口数据。Racket 有几个 serial port 库,其中一个是 libserialport,另一个是 serial,安装之后在程序前面调用

(require libserialport)

(require serial)

打开串口后,我能用 write-bytes 写数据,但在 (read-bytes n-bytes port) 时却进入了死循环。目前原因未知。还要做更多的相关实验。

想到另一个做这个项目的方法,不管用什么编程语言,这个应用最后只是要 3 个步骤。下面的伪代码(好吧,其实不是 ;-)描述它的工作流程,也就是最后抽象到的程度。

  1. read
  2. post
  3. sleep

如果是用 forth 语言,就是做这 3 个 word 就可以了。