2019年

某天接触到某个用Golang实现的程序,不仅体积小,还支持几乎所有种类的CPU,于是下决心学习一下Go这门语言。虽然刚出来的时候就想学,但那时据说有很多坑(比如语法可能会变),就放弃了。现在连被成为垃圾的包管理也有了升级,觉得是时候去学。恰好要把一个二级域名绑定IP的小服务迁移到VPS上,于是干脆用Go重新实现(原来是用Python3)这个服务。

首先要阅读相关教程。初学者教程当然是官方入门教程:
https://tour.go-zh.org/

官方入门教程太简单(毕竟Go本身语法就是简单),还需要阅读其它相关知识:
1)Go搭建一个Web服务器
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/03.2.md
2)golang读取json配置文件
[https://blog.csdn.net/benben_2015/article/details/79134734]
3)文件读写
https://wiki.jikexueyuan.com/project/the-way-to-go/12.2.html
4)go-extend,获取请求的IP的代码
https://github.com/thinkeridea/go-extend/blob/master/exnet/ip.go
5)golang 发送GET和POST示例
https://segmentfault.com/a/1190000013262746
6)GoDNS中dnspod客户端的代码
https://github.com/TimothyYe/godns/blob/master/handler/dnspod/dnspod_handler.go
7)Go语言(golang)的错误(error)处理的推荐方案
https://www.flysnow.org/2019/01/01/golang-error-handle-suggestion.html

这个小服务,就是个web服务。客户端发起带有key和token的请求,此服务会验证有效的授权,然后把对应的二级域名与客户端IP绑定。配置信息以json格式保存在文本文件。客户端IP会记录在对应log文件,以方便每次比较客户端IP是否变化了。每次更新二级域名与IP的绑定,则会记录log。相关文件及代码如下:

配置文件,config.json:

{
"ServIpPort": ":12345"
,"DnsKey": "dnspod的key"
,"DnsToken": "dnspod的token"
,"DomainId": "dnspod的域名id"
,"SubDomainId": {"abc":"dnspod的二级域名id", "efg":"dnspod的二级域名id"}
,"Users": [
        {
                "Key":"client1"
                ,"Token":"aaa123456"
                ,"SubDomains": ["abc"]
        }
        ,{
                "Key":"client2"
                ,"Token":"xxx789012"
                ,"SubDomains": ["efg"]
        }
]
}

小服务的代码,ddnsServ.go:

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "net/url"
    "strings"
    "os"
    "path/filepath"
    "io/ioutil"
    "time"
    "encoding/json"
)

type User struct {
    Key string
    Token string
    SubDomains []string
}

type Config struct {
    ServIpPort string
    DnsKey string
    DnsToken string
    DomainId string
    SubDomainId map[string]string /*key:sub domain name, value:sub domain id*/
    Users []User
}

var (
    curPath string
    ipLogPath string
    historyPath string
    config Config
)

func init() {
    curPath, _ = filepath.Abs(filepath.Dir(os.Args[0])) 
    ipLogPath = curPath + "/ip"
    historyPath = curPath + "/log"
    for _, path := range []string{ipLogPath, historyPath} {
        if err := initPath(path); err != nil {
            fmt.Printf("Init failed. Error info:%s\n", err)
            os.Exit(-1)
            return
        }
    }

    if err := initConfig(curPath + "/config.json"); err != nil {
        fmt.Printf("Init failed. Error info:%s\n", err)
        os.Exit(-1)
        return
    }
}

func initPath(path string) error {
    s, err := os.Stat(path)
    if err == nil && !s.IsDir() {
        return fmt.Errorf("The path is existed, but it is not a directory! Path is:%s", path)
    }
    if err != nil && os.IsNotExist(err) {
        e := os.Mkdir(path, os.ModePerm)
        return e 
    }
    return nil
}

func initConfig(configPath string) error {
    configData, err := ioutil.ReadFile(configPath)
    if err != nil {
        return fmt.Errorf("Failed to read config file: %s! Error info: \n%s", configPath, err)
    }

    err = json.Unmarshal(configData, &config)
    if err != nil {
        return fmt.Errorf("Failed to load config data! Error info: \n%s", err)
    }
    return nil
}

// get client IP address 
func GetClientIP(r *http.Request) string {
    xForwardedFor := r.Header.Get("X-Forwarded-For")
    ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
    if ip != "" {
        return ip
    }

    ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
    if ip != "" {
        return ip
    }

    if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
        return ip
    }

    return ""
}

func GetLogFilePath(logPath string, subDomain string) string {
    return fmt.Sprintf("%s/%s.log", logPath, subDomain)
}

// get the file path which saved ip address of subDomain
func GetIpLog(subDomain string) string {
    path := GetLogFilePath(ipLogPath, subDomain)
    buf, err := ioutil.ReadFile(path)
    if err != nil {
        return ""
    }
    return string(buf)
}

func SaveIpLog(subDomain string, ip string) {
    path := GetLogFilePath(ipLogPath, subDomain)
    ioutil.WriteFile(path, []byte(ip), 0644)
    /*
    err := ioutil.WriteFile(path, []byte(ip), 0644)
    if err != nil {
        panic(err.Error())
    }
    */
}

func SaveHistoryLog(subDomain string, ip string) error {
    path := GetLogFilePath(historyPath, subDomain)
    logFile, err := os.OpenFile(path, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0644)
    if err != nil {
        return fmt.Errorf("Failed to open history log file: %s! Error info: \n%s", path, err)
    }
    defer logFile.Close()

    nowStr := time.Now().Format("2006-01-02 15:04:05")
    log := fmt.Sprintf("%s, ip:%s\n", nowStr, ip)
    logByte := []byte(log)
    n, err := logFile.Write(logByte)
    if err == nil && n < len(logByte) {
        return fmt.Errorf("Failed to save history log file: %s! Error info: \nwrite file failed", path)
    }
    return nil
}

func UpdateDns(subDomain string, ip string) error {
    values := url.Values{}
    values.Add("login_token", config.DnsKey + "," + config.DnsToken)
    values.Add("format", "json")
    values.Add("lang", "en")
    values.Add("error_on_empty", "no")

    values.Add("domain_id", config.DomainId)
    values.Add("record_id", config.SubDomainId[subDomain])
    values.Add("sub_domain", subDomain)
    values.Add("record_type", "A")
    values.Add("record_line", "默认")
    values.Add("value", ip)

    client := &http.Client{}
    req, err := http.NewRequest("POST", "https://dnsapi.cn/Record.Modify", strings.NewReader(values.Encode()))
    if err != nil {
        // handle error
    }

    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Set("Accept", "text/json")
    resp, err := client.Do(req)

    defer resp.Body.Close()

    _, err2 := ioutil.ReadAll(resp.Body)
//    fmt.Println(string(s))
    if err2 != nil {
        return err2
    }
    return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Verify authorization
    var subDomains []string
    key := r.PostFormValue("key") // get key of POST
    token := r.PostFormValue("token") // get token of POST
    for _, user := range config.Users {
        if user.Key == key && user.Token == token {
            subDomains = user.SubDomains
            break
        }
    }
    if subDomains == nil || len(subDomains) <= 0 {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    // get IP
    ip := GetClientIP(r)
    fmt.Fprintf(w, "%s", ip)

    for _, subDomain := range subDomains {
        // get last IP of subDomain
        ipLog := GetIpLog(subDomain) 
        if ip == ipLog {
            // IP is not changed
            continue
        }
    
        // update DNS, bind IP to subDomain
        err := UpdateDns(subDomain, ip)
        if err == nil {
            // update success, save new IP
            SaveIpLog(subDomain, ip)
            SaveHistoryLog(subDomain, ip)
        }
    }
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(config.ServIpPort, nil))
}

关于部署,就用nginx弄个反向代理,指向这个小服务的端口。由于VPS上装了Debian9,可以配置systemd来设置系统服务。

客户端只需发起post请求,把key和token发过来就可以了。以下用curl实现:

#!/bin/sh

#curl命令的参数解析:
# -X 请求的方法。这里用了POST,在HTTPS传输中,数据被加密
# --connect-timeout 连接超时时间,单位:秒
# -m/--max-time 数据传输的最大允许时间,单位:秒
# https://rpi.f...... 请求的URL
# -H/--header 请求头。要设置多个请求头,则设置多个-H参数
# -d/--data 请求数据。
curl -k -X POST --connect-timeout 5 -m 10 https://youdomain.xxx:12345/api/update_dns -H 'cache-control: no-cache' -H 'content-type: application/x-www-form-urlencoded' -d 'key=client1&token=aaa123456'

目前这个小服务工作良好。除了体积有点大(约7MB),其它都挺满意的。

斐讯真是个神奇的公司,凭着其高质量的产品,本来可以好好做个科技公司。其崩盘后,这些优秀的产品给我们带来了一波又一波的惊喜。在某网站的推广下,加上大学同学推坑,终于入手了N1。刷上被清理过的Android系统后,非常适合作为智能机顶盒!主要还是价格不算贵。整理一下相关的资料:

一、相关介绍
N1相关的一切,这个文章都说得很清楚了,我也是跟着上面步骤的来折腾。只是相关的资源都不能很好地下载。
我购买了斐讯N1做电视盒子,发现它远比想象的...
https://pockies.github.io/2019/03/07/phicomm-n1/

这个值得看看是由于N1的功耗很低,就算作为服务器不关机也不用担心电费的问题。
[N1盒子] 闲着无聊测试了下N1的功耗,曲线解决开关机问题
https://www.right.com.cn/forum/thread-322865-1-1.html

二、刷机经历
所需硬件:
1)双公头的USB 2.0线,用来连电脑刷系统。找两根USB线,把两个公头按同颜色的线连接即可。
2)网线,连接路由,用于远程adb操作N1。
3)USB鼠标或键盘,接到N1操作。
4)一台x86CPU的电脑,并且装了Windows的电脑,用来运行刷机工具。

所需软件:
1)webpad的rom,里面的“工具”包含了adb、降级工具、线刷工具等。喜欢这个rom的话,可以刷上,不用下载其它了。
[2018-9-22]斐讯N1 官改v2.2线刷包,精简版,扩展功能可选
https://www.right.com.cn/forum/thread-338759-1-1.html
工具说明:
a)“android-adb-fastboot_1.0.39.7z”,adb和fastboot工具
b)“斐讯T1、N1官方系统降级工具.zip”,官方ROM降级工具
c)“使N1进入线刷模式.zip”,降级后进入线刷模式,其实就是进入bootloader
d)“USB_Burning_Tool_v2.1.6.zip”,Amlogic的线刷工具,只支持windows

2)RUSH固件
[T1] 极限精简斐讯T1/N1 极客开发者强迫症福音6.23/6.24
https://www.right.com.cn/forum/thread-315889-1-1.html
我跟着前面的刷机教程,选择了这个固件。但由于是精简的,几乎应用什么都没有,后面需要折腾。

3)救砖
由于刷ROM中途以为失败了而强制结束,导致刷成砖。最后找到网上的教程,算是比较简单的解决方法,如下:
[N1盒子] 小白折腾了几天刷N1,稍总结下N1刷机和N1救砖,大神略过
https://www.right.com.cn/forum/thread-480477-1-1.html
简单来说是,拆机,短接两个触点,刷入T1固件(RUSH的rom包内有T1 ROM和救砖教程),再刷入所需的ROM

三、使用经验
1)遥控
本来可以买个蓝牙遥控,或者2.4无线遥控。但是本着省钱的原则,接了个有线鼠标上去,应付特殊情况。另外配对了一个蓝牙手柄,基本可以胜任遥控的工作。家里还有个Rapoo 1800 2.4G无线鼠键,可以考虑用上。

2)相关应用

  • 媒体中心:VLC,开源播放器,可以播放各种格式的视频,支持DLNA,支持多语音频等。个人感觉比Kodi好。
  • 横屏工具:还没找到好用无需破解的。由于很多apk都是手机端,默认不支持横屏,需要转个强制横屏工具。
  • 浏览器:无脑推荐Chrome,没考虑操作是否舒适之类,反正能在线追番就可以了。
  • 电视直播:这是个灰色地带。详细关注微信公众号”KUMI分享“,会有不定期推介。
  • 文件管理:ES Explorer,RUSH的ROM自带。支持FTP服务端和客户端,方便局域网内互传文件。
  • 桌面:直接用RUSH自带的那个,很简单,也有一点不方便,没去找其它的,凑合用吧。

3)游戏
N1自带空间不大,不适合玩大型游戏,而且大部分Android游戏都不支持手柄。
a)Minecraft破解版。家里部署了bedrock服务,手机装了正版Minecraft,但是N1没刷GAPPS,所以只能找可以登录的破解版了。跟孩子一起玩,大屏幕,还不错,但是画面略有卡顿。
b)赛车游戏。只是装了个SuperTuxKart来试试,效果还行,手柄还是不如手机的体感操控。
c)游戏机模拟器及游戏。能支持手柄,占用空间小,资源丰富,就只有模拟器了。曾经很期待地装上Dolphin模拟器,以为能玩Wii游戏,但是跑不起来。看来只能考虑旧主机的模拟器。

宽带的80、443端口不能使用了,更新免费的SSL证书(Let’s Encrypt的免费证书)就成问题了。后来找到相关的文章,说是可以通过DNS验证并更新,指向以下官方网址:
User Guide -> Getting certificates (and choosing plugins) -> dns-plugins
https://certbot.eff.org/docs/using.html#dns-plugins

找DNSPod的插件时,发现github上居然有不同的版本(名称却是一样的),因此走了弯路(浪费了一个下午)。最后按照这个的说明,成功更新了证书。
DNSPOD DNS Authenticator plugin for Certbot
https://github.com/SkyLothar/certbot-dns-dnspod/blob/master/README.rst

简单来说,就是
1)去DNSPod.cn申请api授权
2)安装插件

pip install certbot-dns-dnspod

3)生成插件配置文件,例如保存到文件/etc/cetbot-dns-dnspod-credentials.ini。重点是双引号不可缺,token的格式是id和token以逗号分隔

certbot_dns_dnspod:dns_dnspod_email = "DNSPod账户的Email"
certbot_dns_dnspod:dns_dnspod_api_token = "api_id,api_token"

4)配置文件设置权限(只是为了安全,此步可不做)

sudo chmod 600 /etc/cetbot-dns-dnspod-credentials.ini

5)更新证书。xxx.com需要替换为相关域名。

certbot certonly -a certbot-dns-dnspod:dns-dnspod \
  --certbot-dns-dnspod:dns-dnspod-credentials /etc/cetbot-dns-dnspod-credentials.ini \
  -d xxx.com

证书更新成功后,会发现certbot的配置文件(/etc/letsencrypt/renewal/xxx.com.conf)也更新了。

本来一直在用Scrcpy,一个把手机屏幕显示在电脑屏幕的软件。作为一般的操作,可以接受。但是滑动太快,或者玩游戏,会出现马赛克。直到前几天发现了QtScrcpy,据称可以“吃鸡”!今晚终于在Lubuntu上编译出来,玩了下Minecraft,也看了下视频,非常不错~虽然已经在公司用了多天来划水。

详细的介绍,请查看项目的官方介绍。项目地址:https://github.com/barry-ran/QtScrcpy

应用场景:把手机屏幕的内容显示在电脑屏幕,支持 Windows、MAC、Linux 三大系统(其中 Linux 需要自己编译)。可以实现公司电脑上无痕划水(自备无限流量套餐、迷你蓝牙耳机,效果更佳),或者找个烂电脑作为手机伴侣(实现小屏转大屏)。

优点:
1)速度快! 720p可以玩游戏,1080p可以看电影。

2)有熄屏功能。手机画面投影在电脑上,手机屏幕同时可以关掉。

3)有功能完整的操作界面,基本的功能按钮都有,免得打命令。手机的输入可以直接用鼠标和键盘操作(跟 Scrcpy 一样)。

缺点:
1)输入法不能通过键盘选字、不能直接输入符号(这个缺点直接继承 Scrcpy )。直接用蓝牙键盘连手机的话,输入体验会好很多。

2)Linux上需要自己编译。装QtCreator、Android SDK、Android NDK……由于网络的问题,下载相关软件时折腾了一下。编译挺简单的,对于第一次接触QtCreator的我来说,一次编辑成功。对于没有编程经验的人来说,会有难度。但是这种人一般不会使用Linux。另外,QtCreator最好从 https://download.qt.io/archive/qt/ 下载安装包进行安装。

3)窗口模式不能随意调整大小(Scrcpy是可以的),只能固定大小,或者全屏。

4)手机声音不能通过电脑播放。不知道是不是我没找到设置的地方,所以需要配个蓝牙耳机。

在家使用还是有点浪费资源,毕竟要开两个机器来做一件事件。除了玩Minecraft PE,暂时想不到有什么应用场景。但是在公司划水的话,非常实用。

以前一直用的Email客户端是Foxmail,但是某客服邮箱的邮件太多(几千封),一接收,Foxmail就挂了。后来换上Thunderbird就没事了,一直用了几年也没什么问题。但是公司内的同事基本都是用Foxmail,一回复邮件就显示我很异类(Thunderbird回复邮件的默认格式确实也太单调了)。于是找了找解决方案,设置回复邮件的模板。最后虽然不能完全模仿Foxmail的格式,但看着还行,也就这样吧。

首先设置帐户的签名如下,其中·xxx@abc.com·要替换为对应的Email地址:

<hr style="width:210px;height:1px;" color="#b5c4d" size="1" align="left" />
<div style="margin:10px;font-size:10pt">
<div>xxx@abc.com</div>
</div>

然后安装两个“扩展”:ReFwdFormatterSmartTemplate4,并重启Thunder。ReFwdFormatter是用于删除回复邮件时引用原文出现的蓝色竖线。SmartTemplate4就是设置回复邮件的模板。设置步骤如下:

1)打开ReFwdFormatter,取消全部勾选,并只勾选“Remove the '|' prefix from quote in html mail.”,点“Save”按钮保存退出。

2)打开SmartTemplate4,选择对应的帐户。
2.1)点“回复”标签,勾选“将以下模板应用于回复消息”、“替换标准引用头文件”、“使用HTML(例如,bold)”,并在“模板”填入以下内容:

%sig%
<br>
<div style="border:none; border-top:solid #B5C4DF 1.0pt; padding:3.0pt 0cm 0cm 0cm">
<div style="padding-right: 8px; padding-left: 8px; font-size: 12px; font-family: tahoma; color: #000000; background: #efefef; padding-bottom: 8px; padding-top: 8px">
<div><b>发件人:</b> <a href="mailto:%from%" moz-do-not-send="true">%from%</a></div>
<div><b>发送时间:</b> %X:=sent% %Y%-%m%-%d% %H%:%M%:%S%</div>
[[<div><b>收件人:</b> %to(name, bracketMail(angle))%</div>]]
[[<div><b>抄送:</b> %cc(name, bracketMail(angle))%</div>]]
<div><b>主题:</b> %subject%</div>
</div>
</div>

2.2)点“高级” -> “全局设置”。在“邮件内容” -> “顶部换行符数量”,设为“0”。在“高级功能”,取消勾选“插入空格到高亮的光标”、“强制段落模式”。
2.3)点“确定”保存退出。

回复时,除了“收件人”和“抄送”不能定制显示格式,其它都跟Foxmail的一样了。

这星期做了个报表,统计一堆关键词在Solr的搜索结果数量。一开始是计划写Java代码去访问Solr,并获取各个关键词的查询结果数量。后来为了减轻服务器压力,把数据下载到本地并重建了Solr索引。那为什么不用简单快速的JavaScript?

JavaScript的数据请求,可用原生的XMLHttpRequest,jQuery的$.ajax,或者fetch。记得N年前看过吹嘘fetch的文章,于是就选了fetch玩玩。该文章如下:
传统 Ajax 已死,Fetch 永生
https://github.com/camsong/blog/issues/2

说说感受吧:

1)如果需要“同步”请求,需要配合asyncawait使用,里面还要用到function。一下子不适应,$.ajax只需设置async参数。

2)不支持跨域。尝试按教程去设置fetch请求的Header,仍是不行。简单来说,需要服务端设置可跨域相关。那就简单点,把包含代码的HTML文件丢到Solr的本地站点,然后Chrome访问。

3)Promise语法很新鲜。当然,写得不好,也可以很糟糕。

4)关于错误处理,就不写了,本地请求一般不会出错。

总的来说,没想象中那么牛X,也没那么爽。如果面对一般情况,不想写复杂的XMLHttpRequest,也不想引用庞大的jQuery,不用兼容老版浏览器,fetch是个好家伙。例如这个文章提到:
fetch 没有你想象的那么美
http://undefinedblog.com/window-fetch-is-not-as-good-as-you-imagined/

还有Mozilla的参考教程,说得比较详细:
使用 Fetch - Web API 接口参考 | MDN
https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch

近段时间做了几次糯米糍,挺好吃的。也曾拿去孩子的幼儿园做义卖,一下子就卖光了。记录一下制作。

主要材料:
1)淡牛奶,100g
2)椰浆,100g
3)糯米粉,120g
4)玉米淀粉,30g
5)糖粉,50g
6)黄油,15g
这些材料是做糯米糍的皮,大概能做16粒子。做出来的口感偏软。

其它材料:
1)馅料,可选红豆沙、鲜芒果肉等。
2)外皮,可选椰蓉、白芝麻等。如果选白芝麻,要先炒熟。

步骤:
把所有材料倒进碗里搅拌混合,形成没有颗粒的糊状,再去蒸15分钟。如果蒸后还有液体状,那就继续蒸吧。蒸熟后放凉,或者放到冰箱降温,然后就可以用来搓丸子了。

馅料可以按喜好自选。试过红豆沙,因为够甜,很好吃。也试过芒果,但是芒果不够甜,几乎吃出芒果味,可能少放点糖会好点。

包好后,放外皮料滚几下,沾满整个丸子,以免吃的时候粘手。

用Pro Micro做了个摇步器,感觉有点浪费。于是入手了个ATTiny85开发板(国外好像叫Digispark),计划把Pro Micro替换出来。

这个ATTiny85开发板非常迷你和便宜,面积比邮票还小(大约2cm*2.5cm),价格不到7rmb/个。自带Micro USB母口(插上USB就可以烧录程序),除了5v VCC、GND、VIN,还有6个针脚。采用ATTiny85芯片,集成8位CPU,主频最高20MHz,内存512B,闪存8KB等等。更详细的参数,参考以下PDF文档:
https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-2586-AVR-8-bit-Microcontroller-ATtiny25-ATtiny45-ATtiny85_Datasheet.pdf

总体来说这,非常适合细小的项目。但是这货历史有点久远(网上很多资料都是2013年左右),相关资料不好找。踩了一些坑后,终于成功刷入程序并运行。

首先安装 Arduino IDE,版本是1.6以上,然后根据以下文章去配置:
Connecting and Programming Your Digispark
https://digistump.com/wiki/digispark/tutorials/connecting

遇到Linux的问题,主要是需要相关的安装包,可参考这个链接:
Linux Troubleshooting
https://digistump.com/wiki/digispark/tutorials/linuxtroubleshooting

然后,关于编写舵机控制程序,尝试了几个库,最后采用了SoftRcPulseOut并运行成功。参考以下链接:
Beginner Servo
https://digistump.com/board/index.php?topic=1157.0
Digispark (Attiny85) servo tester
http://www.circuitdb.com/?p=1203

最后再说说摇步机。摇步机的原理就是利用舵机进行单摆运动。原来使用手机摇的,现在入手了个二手小米手环2,解放了手机。接线图就不上了,简单描述如下:

ATTiny85 -> 9G舵机(SG90)
5V       -> 红(正极)
GND      -> 棕(负极)
P4       -> 橙(信号)

程序也很简单,舵机先复位到0度,再不断重复从0度转到120度后复位。代码如下:

#include <SoftRcPulseOut.h> 

SoftRcPulseOut myservo;
int pos = 0;
#define NOW  1

void setup() {
  myservo.attach(4); // P4,舵机信号
  myservo.setMaximumPulse(2200);

  for (int i = 0; i < 5; i++) { // 复位到0度
    myservo.write(pos);
    delay(100);
    SoftRcPulseOut::refresh(NOW);
  }
  delay(3000);
  SoftRcPulseOut::refresh(NOW);
} 

void loop() {
  for (pos = 0; pos < 120; pos += 20) {
    myservo.write(pos);
    delay(50);
    SoftRcPulseOut::refresh(NOW);
  }
  
  for (pos = 120; pos >= 0; pos -= 20) {
    myservo.write(pos);
    delay(50);
    SoftRcPulseOut::refresh(NOW);
  }
}


公司网站需要添加导出CSV文件的功能。一开始想用模板文件的方式导出,但是需要读取模板、分析需要替换的标识、格式化字符串、写文件等,太麻烦了。由于是web导出,就想用JSP实现。

首先,需要简单了解CSV文件规范:
1)可以采用UTF-8字符集编码,但是要带BOM,否则Excel打开后,Unicode字符会乱码。

2)一行一条数据,空行会当作一行空数据。同一行的各个数据之间用逗号分隔,每行的数据个数可以不相同。

3)每个数据用双引号括住,可以避免绝大部分的符号问题。比如在双引号里的换行符和逗号不会被解析。唯一需要转义的是双引号,转义符也是双引号。就是双引号里出现的每个双引号,要换成两个双引号。

示例代码:

<%@ page language="java" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" contentType="application/x-download"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<% 
    response.addHeader("Content-Disposition","attachment;filename=ShoppingCart.csv"); // 导出的文件名
    out.print("\ufeff"); // UTF-8的BOM
%>"Name","Sex","Score"
<c:forEach items="${students}" var="student">"${fn:replace(student.name, "\"", "\"\"")}","${student.sex}","${student.score}"
</c:forEach>

简单解析:
1)第一行的trimDirectiveWhitespaces="true",设置自动清除空行。即多个<% %>行,执行完毕后只保留最后一个%>后面的内容,包括换行符。这个清除空行是不完全的,但这样设置最简单。

2)第一行的contentType="application/x-download",设置浏览器可识别的文件类型。这个设置,浏览器打开链接后会自动下载。

3)taglib引入需要用到的标签,例如JSTL或自定义的标签。

4)response.addHeader("Content-Disposition","attachment;filename=ShoppingCart.csv");是设置导出的文件名。

5)out.print("\ufeff");输出UTF-8的BOM。

6)剩下的就是文件内容了。注意换行的问题就可以了。