分类目录归档:Uncategorized

Build a thermal control fan by Go

Github

Why

I just bought a Asus router that sometimes it is overheat like 80C. For cooling, I bought a pair of USB powered fan and it works well, however, it’s waste of energy while CPU is idle. So I think it’s a good idea to use USB control a relay that switch on fan while CPU is hot and turn it off while it’s idle.

Here is the circle diagram, pretty simple.

bad USB driver

I bought a usb hid relay from Taobao( Chinese version of Ebay) for only $2.5

USB HID relay

Putting them together.

But when I try to coding, the vendor didn’t has any kind of opensource code nor protocol to drive this relay 🙁

WTF

After painstacking search on Google, I found a project named USB Relay HID, which is same device and implment by … well … C

I love Golang, so why not try it in Golang?

Lucky Serge Zaitsev has already written a pure USB HID driver, which only support Linux and it’s fine to me.

First, I use my Mac to do the cross compile the example code from the usb hid driver project, and scp to my router and it works!

env GOARM=5 GOARCH=arm GOOS=linux go build example/main.go

Some note, although RT-AC68U is based on ARMv7 but it don’t have FPU, so don’t try to use GOARM=7 or you will get this error:

Illegal instruction

Now we had done with compiling, let’s take a look at the driver code. Fortunately the code is simple and easy to understand, here is the core control code

if ( relaynum < 0 && (-relaynum) <= 8 ) {
mask = 0xFF;
cmd2 = 0;
if (is_on) {
cmd1 = 0xFE;
maskval = (unsigned char)( (1U << (-relaynum)) - 1 );
} else {
cmd1 = 0xFC;
maskval = 0;
}
} else {
if ( relaynum <= 0 || relaynum > 8 ) {
printerr("Relay number must be 1-8\n");
return 1;
}
mask = (unsigned char)(1U << (relaynum-1));
cmd2 = (unsigned char)relaynum;
if (is_on) {
cmd1 = 0xFF;
maskval = mask;
} else {
cmd1 = 0xFD;
maskval = 0;
}
}

After I copied the code, it just not working, what could possibly gone wrong?

I found some definition of USB requests at USB in nutshell Chapter 6 , for server engineer like me, I think this is a good analogy below.

Node in the networking -> Device
Application -> Interface
protocol port -> Endpoint

I tried to capture the USB package by Wireshark to see the diffrenece between USB HID RELAY and I own driver.
tcpdump -i usbmon -w cap.pcap

Turns out there is one byte less in the good control request. You can see that at the Data Fragment.

One more thing. Status from relay, I tried to send GetReport request, nothing came back.
I used tcpdump and Wireshark, again.

Non Standrad USB request

Well, this USB relay vendor using a non stadrad request for the reporting feature. So I have to construct a non standrad request too.

var dev *Relay
dev.Ctrl(0xa0, 0x01, 3<<8, 0x0, buf, 1000)

Compile, execute it, and it works!

Golang Control USB Relay

3分钟AC68U打造成Time Machine

最近为了100M光纤,一咬牙买了台好点的华硕AC68U路由器。
结果发现这货竟然可以当作Time Machine用,原来想过用树莓派做的,结果发现传输速度太慢,只有仅仅1Mb/s,希望这次的AC68U能给力点。

因为常见的fat32不支持4G以上的文件,NTFS系统苹果又没有自带的,Linux支持也不好,所以我就准备用AC68U格式化磁盘。

首先是登入路由器,“系统管理 -> 系统设置”

把ssh选项打开,把自己的公钥,注意是公钥!文件名以.pub结尾的!把里面的内容粘贴到上图的黑框中,然后应用设置。

接着登入路由器

ssh admin@192.168.1.1

注意是用户名是admin,不是root

把移动硬盘接上。格式化成ext3,为啥是sda,因为我只接上了一个磁盘。

mkfs.ext3 /dev/sda

等格式化完成后,就可以愉快地使用咯,点击“USB 相关应用 -> Time Machine“,开启并选定我们刚才格式化好的磁盘。

打开Mac里的Time Machine,注意这里链接时需要的账号密码是路由器的管理员账号密码 ( = =)

这样就开始自动备份了~

Go零消耗debug log技巧

tL;DR, 本文末尾提供零消耗的日志代码,最高性能提升60000%。

看到题目,有人肯定会问,官方的log模块不好么?
Debug Log一般很长,在生产环境还输出的话,也很难找。
再者,log的消耗是比较大的,特别是需要打印行号时。

https://golang.org/src/log/log.go#L158

if l.flag&(Lshortfile|Llongfile) != 0 {
// Release lock while getting caller info - it's expensive.
l.mu.Unlock()
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
l.mu.Lock()
}

因为需要调用runtime.Caller,这样性能就有较多的损耗。
简单的benchmark,可以发现慢50%。

func BenchmarkWithLine(b *testing.B) {
logger := log.New(ioutil.Discard, "", log.Llongfile|log.LstdFlags)
tf := strings.Repeat("abcde", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Print(tf)
}
}
// BenchmarkWithLine-4 500000 2806 ns/op
// BenchmarkWithoutLine-4 1000000 1754 ns/op

虽然,log的性能不差,仅需要1us就能进行一次,但如果在代码中有大量的debug日志,这个损耗累积起来,那也是相当惊人的了。

那么,在生产环境,能不能不执行log语句呢?
可以的,例如

const Dev = false
func BenchmarkConst(b *testing.B) {
logger := log.New(ioutil.Discard, "", log.LstdFlags)
tf := strings.Repeat("abcde", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if Dev {
logger.Print(tf)
}
}
}
// BenchmarkConst-4 2000000000 0.29 ns/op

go tool objdump查看生成的二进制文件

 log_test.go:36 0x4efc32 48890424 MOVQ AX, 0(SP)
log_test.go:36 0x4efc36 e815d4fbff CALL testing.(*B).ResetTimer(SB)
log_test.go:36 0x4efc3b 488b842480000000 MOVQ 0x80(SP), AX
log_test.go:36 0x4efc43 31c9 XORL CX, CX
log_test.go:38 0x4efc45 eb03 JMP 0x4efc4a
log_test.go:38 0x4efc47 48ffc1 INCQ CX
log_test.go:38 0x4efc4a 488b90f0000000 MOVQ 0xf0(AX), DX
log_test.go:38 0x4efc51 4839d1 CMPQ DX, CX
log_test.go:38 0x4efc54 7cf1 JL 0x4efc47
log_test.go:43 0x4efc56 488b6c2470 MOVQ 0x70(SP), BP
log_test.go:43 0x4efc5b 4883c478 ADDQ $0x78, SP
log_test.go:43 0x4efc5f c3 RET

可以看出,ResetTimer之后,仅仅是跑了个空的for循环,这是因为编译器发现if语句永远不成立,所以不编译这一段了(如果Dev是var值,那么还是会对比一下,而不是没有语句生成),不过这个方法需要每次debug时都要改代码。不想改代码可以用go build -ldflags -X方法, 但这个仅仅支持字符串,特别麻烦。

所以有没有更好的解决方案呢?有的,使用build tags
下面是例子,一共三个文件:

log.go

package main
func main() {
Debug("it's expensive")
if Dev {
fmt.Println("we are in develop mode")
}
}

log_debug.go

//+build debug
package main
import (
"fmt"
)
const Dev = true
func Debug(a ...interface{}) {
fmt.Println(a...)
}

log_release.go

//+build !debug
package main
const Dev = false
func Debug(a ...interface{}) {}

debug和release最大的差别就在文件头的//+build !debug, 意思是告诉编译器,如果有debug这个tags,那么编译的时候就略过这个文件。
比如你运行go build -tags "debug" && ./main就会输出,不设定的话,就什么都不输出。

再用go tool objdump 查看生成的可执行文件,跟之前的if Dev效果相同,压根不生成语句。这样就不用每次都改代码了来debug了,是不是很赞啊?

对于Debug函数,由于Go的函数是first class,所以Call function不可避免,不过性能损失基本上为零了。

package main
import "testing"
var a = strings.Repeat("abcde", 1024)
func BenchmarkDebug(b *testing.B) {
for i := 0; i < b.N; i++ {
Debug(a)
}
}
go test -bench=.
BenchmarkDebug-4 500000000 3.27 ns/op
go test -bench=. -tags debug
BenchmarkDebug-4 10000000 146 ns/op

总结一下,如果极度要求性能,尽量使用if Dev这种判断模式,如果要求不高,可以使用Debug函数的方法。

Yaml To Go

项目地址:Yaml-To-Go

最近工作需要把yaml配置改成Go的对象,我知道有个json-to-go,但是没有发现yaml-to-go,所以就自己搞了一套。

其实原理很简单,主要是把原来的jsonToGo函数里的解析函数全部替换成yaml的解析器。
虽然简单,但是实际上移植的时候,发现js的库太复杂了……npm 不想安装,怎么办?

Heavy NPM

可以看项目里的dist文件夹,里面会有有编译好的文件。
这样就可以用老办法,直接用script标签载入了。

两行开启Go http quic

QUIC,简单来说,就是使用UDP的传输协议,根据Google自己的报告,速度可以加快30%。
主要优点有:

1. 快速建立链接(不用3次握手和TLS4次握手)
2. 多路复用
3. 改进的流控
4. 快速SSL/TLS握手
5. 适合移动用户访问

quic-layer

这么好的性能,当然要赶紧用Go试试看。

https://github.com/lucas-clemente/quic-go

示例中的代码也很简单。

http.Handle("/", http.FileServer(http.Dir(wwwDir)))
h2quic.ListenAndServeQUIC("localhost:4242", "/path/to/cert/chain.pem", "/path/to/privkey.pem", nil)

不过在实践里,还是碰到了2个坑。

TLS配置

因为我的服务是一个http.Handler, 所以quic需要重新配置TLSconfig,否则就会报错。
下面是示例代码

quic := &h2quic.Server{Server: server}
quic.TLSConfig = &tls.Config{}
quic.TLSConfig.GetCertificate = getCertificate
pln, err := net.ListenPacket("udp", cfg.Listen)
if err != nil {
log.Fatal(err)
}
log.Print("listen quic on udp:%s", cfg.Listen)
go quic.Serve(pln)

HEADER设置

成功启用后,Chrome中的SPDY插件并没有出现绿色的标志,还是继续使用HTTP2,经过查找后,发现Google在自家的header中添加了

 writer.ResponseWriter.Header().Add("alt-svc", `quic=":443"; ma=2592000; v="38,37,36"`)

其中

  • ma是过期时间,单位是秒
  • v是指支持的quic版本
  • alt-svc是alternative-service的缩写
  • quic中是quic的端口,我指定了443

最后通过在chrome地址栏中输入

chrome://net-internals/#quic

quic-demo