GoDaddyDDNS

一、 简介/Introduction

这是一个用 GO 语言编写的 DNS 记录更新程序,仅适用于 GoDaddy 域名提供商。程序运行后会将本地网络的出口 IP 更新到指定的 DNS 记录中。

二、 使用方法/Usage

godaddyddns.exe --domain=example.com --key=your_key --secret=your_secret --host=your_sub_domain
  • 如果当前 IP 地址与 DNS 记录中的不一致,则会看到如下提示,说明 DNS 记录已经更新成功
Update dns successfully!

三、相关链接/Link

Github: https://github.com/XingKongSync/GodaddyDDNS

电能表改造并接入HA

一、简介

本次的改造目标是从普通电能表中采集电流、电压、电度等数据,并通过 Wi-Fi 接入智能家居系统中。

DDSU666 是一款单相电能表,适用于家庭环境,可以采集电流、电压等信息,也可以统计电度。大致可分为带通讯版和不带通讯版,本次改造所使用的必须为带通讯的版本。
另外, DDSU666 还分一个带按键的新版,还有一个不带按键的旧版,本例中使用的旧版。

ESP8266 是一款强大的 MCU,支持 Wi-Fi、蓝牙、串口、GPIO 等等。

HomeAssistant 是一个开源的智能家居平台。

大部分代码来自于 liwei19920307 的项目 ESP485,GitHub地址:https://github.com/liwei19920307/ESP485/

关于实现原理、软件配置等,在 ESP485 项目中原作者已经作了非常详尽的解释,在本文中仅做简要提及。本文将侧重于对我在实践中踩过的的坑进行总结,对 ESP485 项目文档中未提及的部分进行补充。

二、差异

在 ESP485 项目中,原作者使用的 ESP32,而本例中使用的是 ESP8266,并且原作者使用的是新款 DDSU666,而本例中使用的是旧款。

三、所需材料

  • DDSU666 电能表(带通讯)
  • RS485 转 TTL 模块
  • ESP8266
  • 220v 转 5v 降压模块
  • TTL 转 USB 模块

四、改造步骤

1、接线

将电能表与 ESP8266 通过 RS485 转 TTL 模块连接起来,将 ESP8266 的 D1 和 D2 分别作为 RX 和 TX(此处与原作者不同)。

之所以使用 ESP8266 的 D1 和 D2 而不直接使用硬件自带的 RX 和 TX ,是因为我在实际测试中,使用 RX 和 TX 时存在似乎可以正常发送数据,但无法接收数据的问题。

另外,新旧款的电能表在接入 220v 家用电时,接线端口也不同。新款的电能表为下端火线入、零线入,上端为火线出、零线出;而旧款的电能表下端为火线入、火线出,上端为零线入、零线出。接线方式一定要注意!我就因为参考了新款电表的接线图而导致家里跳闸!!

2、设置电能表通讯协议

DDSU666 支持的通讯协议有两种,一种是 DLT645,另一种是 Modbus,我们这里需要使用 Modbus 协议。如果很不巧你买到的电能表当前使用的协议不是 Modbus,那么你需要将电能表设置为 Modbus 协议。新款的 DDSU666 似乎可以通过按键来切换通讯协议,而旧款由于没有按键,需要我们通过 RS485 发送指令来切换协议。

切换协议的方法,可以参考此教程,也可以参考电能表的说明书,下载地址:
http://im.chint.com/Upload/File/20210811160117.pdf

简而言之就是,观察电能表的屏幕,屏幕上会轮播很多页,其中一页就是当前电能表所使用的协议类型,如果一页显示了 DLT645,那么紧接着的后两页为电能表的通讯地址,抄下通讯地址备用,紧接着第三页为通讯所使用的波特率,对应关系见下表。

屏幕显示波特率
baud-01200bps
baud-12400bps
baud-24800bps
baud-39600bps

注意!下面所提到的要点是说明书和上文提到的教程中所没有的!感谢 bnnyygy 写一篇帖子,对我帮助很大。

首先将 RS485 转 TTL,再由 TTL 转 USB 连接上电脑,然后打开串口调试工具,设置正确的波特率,8 个数据位,无校验位,2 个停止位,这个地方容易出错,我怀疑不同电能表可能不太一样,如果你使用 2 个停止位没效果的话,可以试试改成 1 个停止位。

参照说明书, DL/T 645-2007 协议切换到 ModBus-RTU 通讯协议的数据帧如下:
FE FE FE FE 68 xx xx xx xx xx xx 68 14 0E 33 33 35 3D 35 33 33 33 33 33 33 33 33 33 CS 16
其中:xx xx xx xx xx xx 为表通讯地址;CS 为校验码。

需要注意的是,在填入通讯地址时,要将之前抄下来的地址反过来写,比如:我的电能表通讯地址是 20 12 01 00 46 41,所以在代入到指令中应当写为 41 46 00 01 12 20

另外,关于校验码的生成,我们需要截取 68 xx xx 到最后一个 33 之间的数据,从而生成校验码。举个例子,我将表的通讯地址代入后,数据帧如下:
FE FE FE FE 68 41 46 00 01 12 20 68 14 0E 33 33 35 3D 35 33 33 33 33 33 33 33 33 33 CS 16
那么,我需要生成校验码的一段为:
68 41 46 00 01 12 20 68 14 0E 33 33 35 3D 35 33 33 33 33 33 33 33 33 33
通过在线生成校验和的工具,计算出的校验和为:
0484
取后一字节,即:84
代入到数据帧中:
FE FE FE FE 68 41 46 00 01 12 20 68 14 0E 33 33 35 3D 35 33 33 33 33 33 33 33 33 33 84 16

建议使用我提供的链接来计算校验和,因为我试过其他的在线生成校验和的网站,有的网站生成出来的结果不正确,应该是算法略有不同。

将数据帧发送出去,此时观察电能表屏幕,应当可以看到通讯协议变为了 Modbus。


下一页出现的数字为 Modbus 协议下的地址,抄下备用,再下一页出现的为波特率,此时波特率可能跟上次的不一样,对应上文中的表格,记下波特率,再下一页是另外的串口参数,类似于 8n2 这样的,注意,这里不同的电能表可能有区别,在上文中提到的教程里,别人家的电能表显示的 8n1,这里的 8 代表 8 个数据位,n 代表无校验位,1 或者 2 代表停止位。

3、烧录程序

大部分内容跟 ESP485 项目中提供的内容相同,需要修改的地方节选如下,对应参数如有不同自行修改:

esphome:
  name: chnt-ddsu666
  platform: ESP8266
  board: nodemcuv2

uart:
  id: esp485_uart
  rx_pin: D1
  tx_pin: D2
  baud_rate: 9600
  data_bits: 8
  parity: NONE
  stop_bits: 2

五、大功告成

通过 IP 地址访问 ESP8266,可以看到数据已经成功采集。

打开 HomeAssistant 的页面,可以看到通知,HA 已经自动发现了新设备,按照提示添加进来即可。

.NET on Docker

一、简介/Introduction

想要在 Linux 上运行 .NET 程序可以使用 .NET Core,但是由于各种各样的原因,我们可能需要运行的是 .NET Framework 程序,如果不想重新编译的话,可以使用 Mono,并且 Mono 官方提供了 Docker 镜像,可以免去我们搭建 Mono 环境的麻烦。

然而,官方的 Docker 镜像给出的使用示例中,需要我们自己编写 Dockerfile,达不到开箱即用的效果。

于是我基于 Mono 官方镜像制作了 MonoRun

二、使用方法/How to use

Docker cli

docker run -d \
    --name=monorun \
    -e APP="/app/yourdotnet.exe" \
    -v /mnt/user/appdata/monorun:/app \
    xingkongsync/monorun

将本机的实际程序所在目录映射到容器中,并通过环境变量传入程序映射后的路径即可

三、相关链接/Link

Github: https://github.com/XingKongSync/MonoRun

DockerHub: https://registry.hub.docker.com/r/xingkongsync/monorun

扫描并记录局域网内设备

简介

最近写了一个记录并统计局域网内设备的服务,每隔30分钟扫描一次局域网,然后把扫描到的数据记录到数据库中,界面中有一个Dashboard,用于展示各个时间段在线设备的总数目。

借助了 InfluxDB、Chronograf和Advanced IP Scanner v2,在这三个软件的基础上进行了集成。软件自身启动的时候会自动启动 InfluxDB 和 Chronograf,并且每隔30分钟会运行一次 Advanced IP Scanner v2,然后把扫描结果存入 InfluxDB 中,通过 Chronograf 进行展示

Github

https://github.com/XingKongSync/LanWatcher

截图

编译说明

Step1.下载依赖软件

  1. InfluxDB
    我使用的版本是 1.7.8,理论上 2.0 以下的版本都可以,2.0 以上的没试过
  2. Chronograf 1.8.10
  3. Advansed IP Scanner v2

Step2.编译

  1. 打开 LanWatcher.sln,此时你可能会注意到工程中少了一些文件,将上一步中下载的软件复制到对应位置
  2. 编译 LanWatcher.UI
  3. 编译出的程序在 LanWatcher.UI\bin\[Debug|Release] 中

Step3.运行

  1. 双击编译后的 LanWatcher.UI.exe
  2. 程序启动后会进入后台运行,双击任务栏托盘图标
  3. 点击左侧的设置按钮
  4. 起始IP结束IP中填入你局域网的IP范围
  5. 配置完成后,点击左侧的仪表盘按钮即可查看当前局域网在线设备数量的图表(如果没有显示出页面,请点击右上角的刷新按钮)

其他

软件每隔半小时扫描一次局域网(按30分钟整点算,如6:00会扫描一次,6:30会扫描一次,以此类推),如果你首次打开时无数据,不必担心

音乐可视化 – 基于ESP32

一、简介

使用ESP32内置的ADC采集电脑输出的音频信号,通过RGB灯带展现出来

最初计划采用外置ADC将音频转为I2S格式传给ESP32,但是遇到了许多问题。 于是转为使用ESP32内置的ADC,虽然也踩了许多坑,但也逐一解决了

GitHub地址:https://github.com/XingKongSync/X.K.MusicESP

二、材料

  • ESP32
  • WS2812
  • Music

三、接线

  • 两条100颗灯珠的WS2812灯带串联,数据线接入ESP32的GPIO12
  • 3.5mm的AUX线,左声道接入ESP32的ADC1_CHANNEL_3,地线接EPS32的GND

四、参考及引用

大部分代码来自于:

其他:

五、踩坑总结

  1. 在通过I2S驱动使用内置ADC时,我读取了长度为512的I2S数据,但是只有前面一半数据可用,不知道为什么
  2. 如果音频线和ESP32没有接地,采集出来的信号中会有杂波
  3. 有的时候音量过大会导致ESP32重启,是因为电压超过了内置ADC的量程了吗?
  4. 接地后,当电脑在没有播放音乐的时候,会出现随机的尖刺,不知道为什么。我使用卡尔曼滤波器 + 星空曼滤波器解决了
  5. 我买的四段式AUX插座,如果把三段式音频插头插到底,会出现左右声道短路的情况,不知道其他AUX插座是不是这样

六、效果演示

https://www.bilibili.com/video/BV1vi4y1x7Pa/

Owin 自托管 支持访问静态文件

公司在弄一个项目,需要用到C#通过Owin自托管启动一个Web服务,并且需要同时提供静态页面和WebAPI

搜索网上Owin自托管的相关文章,都是一个抄一个的,都没有什么深刻的讲解,比如下面这个Owin托管静态文件的例子:

public void Configuration(IAppBuilder appBuilder)
{
    HttpConfiguration config = new HttpConfiguration();
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}",
        defaults: new { id = RouteParameter.Optional }
    );

    appBuilder.Map("wwwroot", map =>
    {
        map.UseStaticFile(new StaticFileMiddlewareOptions()
        {
            RootDirectory = "./wwwroot",
            DefaultFile = "index.html",
            EnableETag = true,
            EnableHtml5LocationMode = true,
            MimeTypeProvider = new MimeTypeProvider(new Dictionary<string, string>
            {
                { ".html", "text/html" },
                { ".htm", "text/html" },
                { ".dtd", "text/xml" },
                { ".xml", "text/xml" },
                { ".ico", "image/x-icon" },
                { ".css", "text/css" },
                { ".js", "application/javascript" },
                { ".json", "application/json" },
                { ".jpg", "image/jpeg" },
                { ".png", "image/png" },
                { ".gif", "image/gif" },
                { ".config", "text/xml" },
                { ".woff2", "application/font-woff2"},
                { ".eot", "application/vnd.ms-fontobject" },
                { ".svg", "image/svg+xml" },
                { ".woff", "font/x-woff" },
                { ".txt", "text/plain" },
                { ".log", "text/plain" }
            })
        });
    });

    appBuilder.UseWebApi(config);
}

首先这个例子有个坑没说清楚,而且还有误导嫌疑,DefaultFile 的路径是相对于网站根路径的!举个例子,如果你把index.html放在wwwroot下,然后程序运行起来之后,你会发现这个DefaultFile压根不起作用,直接访问( http://localhost:9000/wwwroot )会返回404,而直接用完整路径(http://localhost:9000/wwwroot/index.html)就可以访问。

坑之二,RootDirectory可以使用相对路径。但是!!!这个相对路径跟你的程序当前的工作路径无关,它相对的是exe的所在路径。

另外,我们希望静态页面直接能在网页根路径下访问(http://localhost:9000),而不是要在后面再输一长串路径(这个需求应该很常见吧,为什么我百度半天都找不到一个例子!?)

带着思维定势,尝试将Map函数中传入的路径”wwwroot”改为空字符串””之后,网页的确可以在根路径下被访问了,但是WebAPI又访问不到了!

然后又经过一番折腾之后,终于搞明白了,其实没那么复杂,下面直接贴代码

public void Configuration(IAppBuilder appBuilder)
{
    HttpConfiguration config = new HttpConfiguration();
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}",
        defaults: new { id = RouteParameter.Optional }
    );

    appBuilder.UseStaticFile(new StaticFileMiddlewareOptions()
    {
        RootDirectory = "wwwroot",
        DefaultFile = "index.html",
        EnableETag = true,
        EnableHtml5LocationMode = true,
        MimeTypeProvider = new MimeTypeProvider(new Dictionary<string, string>
        {
            { ".html", "text/html" },
            { ".htm", "text/html" },
            { ".dtd", "text/xml" },
            { ".xml", "text/xml" },
            { ".ico", "image/x-icon" },
            { ".css", "text/css" },
            { ".js", "application/javascript" },
            { ".json", "application/json" },
            { ".jpg", "image/jpeg" },
            { ".png", "image/png" },
            { ".gif", "image/gif" },
            { ".config", "text/xml" },
            { ".woff2", "application/font-woff2" },
            { ".eot", "application/vnd.ms-fontobject" },
            { ".svg", "image/svg+xml" },
            { ".woff", "font/x-woff" },
            { ".txt", "text/plain" },
            { ".log", "text/plain" }
        })
    });
    appBuilder.UseWebApi(config);
}

X.K.EzDeploy —— 轻松部署

X.K.EzDeploy

提供图形界面,将任意控制台程序部署为 Windows 服务

开发人员可以将自己编写的一个或多个控制台应用程序通过本工具组织起来,然后连同本工具一起打包,发送给运维人员,方便快速部署

GitHub地址: https://github.com/XingKongSync/X.K.EzDeploy

概述 / Introduction

基于微软的工具 srvany.exe 和 instsrv.exe,提供了图形界面,可以方便地将任意控制台应用添加到系统服务中,或者从系统服务中删除,也可以控制服务的运行和停止

受到 https://wangye.org/blog/archives/42/ 的启发,并且与该作者发布的服务管理工具 SrvanyUI 兼容

为什么要使用此工具 / Advantage

srvany.exe 固然好用,但是其不提供对进程存活与否的检测,举个例子:假设通过 srvany 启动了一个进程 a.exe,运行一段时间后,a.exe 由于崩溃而退出了,此时在系统服务中观察到该服务的状态仍为“正在运行”

本工具除了提供服务的安装功能外,还提供一个守护进程,可以负责将错误的服务运行状态纠正过来,也支持对停止运行的服务进行重启

本工具提供的守护进程也是通过服务来实现的,其作为一个特殊的服务,放置在程序的 Service 文件夹下

用法 / Usage

现将工程编译,在编译的输出文件夹中,可以看到跟 EasyDeploy.exe 同级的有一个 Service 文件夹,请将您在 Service 下新建一个文件夹,并将您的服务放置在那个文件夹下,参考 DemoService 然后,在您的服务文件夹下新建一个名为 ServiceConfig.json 的文本文件

ServiceConfig.json 的格式如下

{
    "ServiceName": "DemoService", //服务唯一标识,建议全英文
    "ExcutableFilePath": "DemoService.exe", //您的控制台程序的可执行文件名
    "Args": "", //您的控制台程序启动时的所需参数
    "DisplayName": "测试服务", //对部署人员友好的服务名称
    "Description": "这个服务会启动一个HTTP服务器,若访问 http://127.0.0.1:8123 成功,则说明服务成功启动" //服务的功能描述
}

若您有多个服务,可以重复上述步骤,然后将整个 EasyDeploy 程序文件夹保持结构完整,发送给运维人员

需要注意的是,建议您在其他服务都安装完成后,再安装 WatchDogService,否则 WatchDogService 无法察觉您所做的更改。

若您先安装了 WatchDogService,您也可以随时卸载 WatchDogService,当您再次安装 WatchDogService时,WatchDogService会自动应用当前已安装服务的变更。

InfluxStreamSharp —— 流式读写InfluxDB

简介 / Introduction

适用于有大量数据持续性写入的业务场景,内置写入队列,按照一定的时间间隔将数据分批写入。建议写入时带入时间戳,这样读取时时间才准确。

当需要读取时,支持重现写入时的场景,举例:若在 t1 时刻写入了数据记录 a1,在 t2 = t1 + 1s 时刻写入了数据 a2,在 t2 + 5 s 时刻又写入了数据 a3,则在读取时,QueryManager会在返回 a1 记录之后1秒返回 a2,再等5秒返回 a3。就好像播放视频录像一样,进行历史再现。此功能内置懒加载功能和缓冲队列,不用担心时间段过长、数据过多造成InfluxDB查询卡死等问题。

当然若不希望使用这种查询方式,可以使用传统的查询模式,即给定时间范围,一次性将所有数据取出来。

读取和写入数据都借鉴了 EntityFramework 的思路,即采用实体对象进行映射,极大地简化了数据库读取、序列化和反序列化的步骤。在构建实体对象时,需要采用特性(Attribute)对字段中的Tag、Value和Timestamp进行标记。

Github

https://github.com/XingKongSync/InfluxStreamSharp

用法 / Usage

static void Main(string[] args)
{
	//Init a InfluxDB writing buffer
	WriteService.Instance.Value.Start();

	//Create a database if not exist
	var influx = InfluxService.Instance.Value;
	influx.InitAsync(
		DB_Url,
		DB_UserName,
		DB_Pwd,
		DB_DbName,
		DB_RetentionHours
		).Wait();

	//write data with buffering
	TestStreamingWrite();
	//Read all data by buffering and timing
	TestStreamingRead();

	Console.ReadKey();
}

static void TestStreamingWrite()
{
	for (int i = 0; i < 10; i++)
	{
		var testModel = new DataModel.Test();
		testModel.DeviceId = i.ToString();
		testModel.x = i;
		testModel.y = i;
		testModel.LocalTime = DateTime.Now.AddMinutes(-1 * i);

		//Convert custom data model to influx model
		var point = ModelTransformer.Convert(testModel);
		//Send the data to the writing queue, the data will be buffered and send to InfluxDB
		WriteService.Instance.Value.Enqueue(point);
	}
}

static void TestStreamingRead()
{
	//Build a query statement
	InfluxQLTemplet templet = new InfluxQLTemplet();
	templet.Measurement = ModelTransformer.GetMeasurement(typeof(DataModel.Test));
	//Add query reqirement
	//templet.WhereEqual("DeviceId", "0");//Only query data which DeviceId equals to 0

	//Construct query manager for streaming read
	QueryManager manager = new QueryManager(DateTime.Now.AddMinutes(-30), DateTime.Now);//Query data within 30 miniutes
	//If you want do muliti queries, please add more QueryTemplet
	manager.AddInfluxQueryTemplet<DataModel.Test>(templet);
	//Handle receveied data
	manager.DataReceived += (object data) =>
	{
		if (data is DataModel.Test t)
		{
			Console.WriteLine($"CurrentPlayTime: {manager.CurrentPlayTime.ToString("yyyy-MM-dd HH:mm:ss")}, id: {t.DeviceId}, x: {t.x}, y: {t.y}");
		}
	};
	//Start query data
	manager.Start();
}

BilibiliUpBlocker —— 根据关键词屏蔽B站视频

简介 / Introduction

一个Chrome插件,可以设定关键词,从而对B站首页的视频进行屏蔽

GitHub地址:https://github.com/XingKongSync/BilibiliUpBlocker

基于Java的InfluxDB查询工具

简介 / Introduction

用Java编写的、用于向InfluxDB查询数据的工具类,无任何第三方依赖,内置HTTP解析器、Json解析器、InfluxDB数据解析器,可以方便集成到任何Java项目中,不会造成依赖冲突。

GitHub地址:https://github.com/XingKongSync/InfluxDB

用法 / Usage

String sql = "SELECT last(ff),at FROM \"2017-05\" WHERE aa = 'C14_L1-9_Ubc' and time>now() - 1h group by time(15s)";
String ip = "192.168.0.121";
int port = 8086;
InfluxDB influxdb = new InfluxDB();
influxdb.setServerIp(ip);
influxdb.setServerPort(port);

InfluxDataAdapter adapter = null;

if (influxdb.Connect()) {
	adapter = influxdb.Query("YZ001", sql);
	influxdb.DisConnect();
}

if (adapter != null) {
	try {
		InfluxResult result = adapter.GetResults().get(0);
		InfluxColumn timeColumn = result.GetColumn("time");
		InfluxColumn ffColumn = result.GetColumn("last");

		if (timeColumn != null && ffColumn != null) {
			InfluxResult.Iterator iter = result.GetIterator();
			while (iter.hasNext()) {
				float ff = iter.GetFloat(ffColumn);
				String time = iter.GetString(timeColumn);
				System.out.println("time: " + time + ", last: " + ff);
				iter.Next();
			}
		}
	} catch (Exception e) {
		System.err.println(e.getMessage());
	}
}