用16M的SD卡启动树莓派,在U盘上安装和运行Linux

树莓派官方的操作系统Raspbian最少需要4G的SDCard,如果你恰好有比较小的SD卡怎么办呢?设计上树莓派只能从SD卡引导启动,但我们可以在SD卡上装一个最小的引导系统,然后把树莓派引导到U盘上启动。SD卡连续运行,读写比较频繁也容易损坏(我已经坏掉3张了。。。),相比之下U盘价格便宜些,读写速度可以比SD卡还高。

制作引导SD卡

这个引导系统只需要16M容量的SD卡,先把SD卡格式化成FAT(windows),然后拷贝下面的文件到根目录。

git clone https://github.com/hugozhu/mini_raspbian_boot

修改 cmdline.txt 中的 root=/dev/mmcblk0p2root=/dev/sda2 以指定用U盘启动

dwc_otg.lpm_enable=0 console=ttyAMA0,115200 kgdboc=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait cgroup_enable=memory

安装操作系统Raspbian到U盘

方法和安装到SD卡一样,在Mac上使用 dd 命令安装,在Windows上可以用win32diskimager:

sudo dd bs=1m if=2015-01-31-raspbian.img of=/dev/disk4 #/dev/disk4

扩展分区

如果你的U盘容量大于4G,那么上一步安装完后,你的U盘只能看到4G空间,大容量的SD卡的这个问题可以通过自带的raspi-config命令来解决。 Linux下可以用fdisk来解决,我的方法是把该U盘插到已经正常启动的树莓派的USB口上,正常识别后,执行sudo fdisk /dev/sda

  1. p后打印出分区表:
/dev/sda1            8192      122879       57344    c  W95 FAT32 (LBA)
/dev/sda2          122880     6399999     3138560   83  Linux
  1. 记住/dev/sda2的起始点122880',然后按d,删除掉二个分区,接着按n重新创建一个分区,起始点就设置为122880`,结束点可以设置为最大

  2. 最后按w保存新的分区表

插上引导SD卡和系统U盘后树莓派就可以愉快的运行起来了,如果U盘的速度足够快,你会发现系统还变快了。

参考文章

  1. http://www.raspberrypi.org/boot-from-a-16mb-sd-card/

用Telegram和树莓派交互

如果你的树莓派不能通过路由器端口转发直接从家庭网络外访问,你还可以用类似聊天App一样的方式来和防火墙后的树莓派轻松交互,通过命令来控制树莓派。

Telegram

image

Telegram是一个跨平台的即时通讯软件,客户端和通讯及加密协议完全公开且开源。官方有正式发布Android,iOS,Mac OS X, Web等客户端版本;Telegram允许多端同时登录。我们在手机上安装Telegram客户端,同时在树莓派上用同一个帐号或另一个帐号也登录Telegram,那么就可以用手机和树莓派聊天了,更高级的一些用法可以是把树莓派帐号加到群聊中,实现类似微软小冰的功能;也可以由树莓派主动向你的手机Push消息实现提醒功能。

安装telegram-cli

Linux的Telegram客户端telegram-cli源代码在 https://github.com/vysheng/tg,按照Readme在树莓派上编译好。telegram-cli支持消息事件对lua脚本中的函数进行回调,支持的函数列表在:https://github.com/vysheng/tg/blob/master/README-LUA,也支持外部程序通过telegram-cli侦听的端口发送交互命令(一次连接只能一个命令)

Lua交互脚本

脚本命名为:tg_raspberrypi.lua

now = os.time()

chat = "树莓派通知" -- telegram的会话名称

safe_commands = {}
safe_commands["uptime"]  = "uptime"
safe_commands["w"]       = "w"
safe_commands["ps"]      = "ps ax"
safe_commands["netstat"] = "netstat -na"
safe_commands["df"]      = "df"
safe_commands["ss"]      = "ss"
safe_commands["free"]    = "free"

function on_msg_receive (msg)
	if msg.out then
	    return
	end
	if msg.text then
		-- mark_read(msg.from.print_name)	
		-- vardump(msg)
		cmd = string.lower(trim(msg.text))
		if cmd == "ping" then
			send_msg (chat, 'pong', ok_cb, false)
		elseif safe_commands[cmd] ~= nil then
			send_msg (chat, exec(safe_commands[cmd]), ok_cb, false)
		end
	end
end

function on_our_id (id)
end
 
function on_secret_chat_created (peer)
end
 
function on_user_update (user)
end
 
function on_chat_update (user)
end
 
function on_get_difference_end ()
end
 
function on_binlog_replay_end ()
end

function exec(cmd)
   local output = ""
   f = assert (io.popen (cmd))
   for line in f:lines() do
     output = output .. "\n" .. line
   end -- for loop
   f:close()
   return output
end	

function trim(s)
  return (s:gsub("^%s*(.-)%s*$", "%1"))
end

function vardump(value, depth, key)
  local linePrefix = ""
  local spaces = ""
  
  if key ~= nil then
    linePrefix = "["..key.."] = "
  end
  
  if depth == nil then
    depth = 0
  else
    depth = depth + 1
    for i=1, depth do spaces = spaces .. "  " end
  end
  
  if type(value) == 'table' then
    mTable = getmetatable(value)
    if mTable == nil then
      print(spaces ..linePrefix.."(table) ")
    else
      print(spaces .."(metatable) ")
        value = mTable
    end		
    for tableKey, tableValue in pairs(value) do
      vardump(tableValue, depth, tableKey)
    end
  elseif type(value)	== 'function' or 
      type(value)	== 'thread' or 
      type(value)	== 'userdata' or
      value		== nil
  then
    print(spaces..tostring(value))
  else
    print(spaces..linePrefix.."("..type(value)..") "..tostring(value))
  end
end

print(exec("uptime"))

启动telegram-cli

第一次启动

第一次启动telegram-cli会需要你输入手机号码,输入短信验证码后登录,成功登录后会在~/.telegram-cli下保存登录信息,后面就不需要再登录了。

[Read More]

用树莓派2代打造智能家庭路由

家里的宽带上下行都有10Mbps了吧?除了可以BT下载外还能让你的移动设备在外的时候也能科学上网。

区别于在海外架设VPN服务:

  • 国内和大部分国外网站都可以直连而不降低速度;不像海外VPN所有流量(国内和国外网站)都要经过海外服务器,速度有一定的延迟
  • 这个方案代理可以使用免费的Goagent服务;或低成本的ssh帐号;而租用海外VPS服务器自建服务或购买VPN帐号费用较高些;
  • 利用的是家里的宽带,只有树莓派的硬件成本,没有主机托管成本;

假设家里的路由器IP地址为:192.168.1.1,树莓派2的IP地址为:192.168.1.3,以下是需要安装和设置步骤。

PPTP和L2TP VPN Server

首先在树莓派上安装和设置VPN服务器,移动设备就可以通过运营商网络连接回家里的树莓派(iPhone和Android都内置了PPTP和L2TP客户端),这样移动设备将以树莓派为路由访问网站,通过一些设置我们可以让树莓派提供科学上网服务。

关于PPTP和L2TP VPN设置和安装可以参考: http://hugozhu.myalert.info/2013/03/01/setup-l2tp-pptp-openvpn-on-ubuntu.html

但在树莓派上安装L2TP时不能直接apt-get install openswan,需要手动下载来安装,原因是因为最新的版本在协议上有些不兼容:

wget http://snapshot.raspbian.org/201403301125/raspbian/pool/main/o/openswan/openswan_2.6.37-3_armhf.deb
sudo dpkg -i openswan_2.6.37-3_armhf.deb

假设VPN服务端的local ip我们设置为192.168.3.1,PPTP客户端IP分配区间为:192.168.3.200~192.168.3.210,L2TP 客户端IP分配区间为:192.168.3.100~192.168.3.110,我们可以通过iptables对IP来源为192.168.3.0/24网段的流量做特殊的处理以达到科学上网的目的。

完成这一步后,需要在路由器上设置端口转发,使得使用运营商网络如移动4G的手机可以通过PPTP或L2TP连到树莓派上。

PPTP需要设置的端口转发 - tcp: 1723

L2TP需要设置的端口转发 - tcp: 50, udp: 500,4500,1701

两种VPN服务相比较:PPTP拨号速度比较快,但是不安全;L2TP有加密,相对安全。

Redsocks2

redsocks2是一个透明TCP代理,其实现使用了libevent库,性能较好,其最大的特点是如果目标IP可以直连则不会转发流量给加密代理,如果IP不能直连(通过连接超时判断)则会将流量转发给加密代理。这样可以将最少的流量转发到代理上,访问一般的国外网站如yahoo.com也不会经过代理而减速,在配置方面则做到了零配置,不需要手工维护网站名单。代理也能支持很多中类型,如socks5, shadowsocks, goagent, http-proxy等,redsocks2安装和配置可以见链接: http://github.com/hugozhu/redsocks

这里我们假设redsocks2的端口使用12345

iptables

使用iptables我们可以将VPN客户端192.168.3.0/24的流量转发到redsocks5的端口12345

sudo iptables -F
sudo iptables -X
sudo iptables -t nat -F
sudo iptables -t nat -X
sudo iptables -t nat -A PREROUTING -s 192.168.3.0/24 -p tcp --dport 80 -j REDIRECT --to-ports 12345 #转发VPN客户端的HTTP流量到端口12345
sudo iptables -t nat -A PREROUTING -s 192.168.3.0/24 -p tcp --dport 443 -j REDIRECT --to-ports 12345 #转发VPN客户端的HTTPS流量到端口12345
sudo iptables -t nat -A POSTROUTING -s 192.168.3.0/24 -o eth0 -j MASQUERADE #转发VPN客户端的TCP流量到网络出口,并进行IP伪装;如果树莓派使用无线网卡则将eth0改成wlan0

DNS加固

上面的设置我们解决了VPN拨号到树莓派的客户端通过redsocks2透明代理分流为直连或通过加密代理连接和访问目标网站,我们还需要解决一下DNS查询被纂改为不存在的IP地址的问题。

[Read More]

零佣金的券商靠什么赚钱?

最近一家叫Robinhood的创业公司推出了零佣金的股票交易服务公测,用户可以通过iOS上的专用App交易。

这家公司的两个创始人斯坦福大学毕业后去了纽约给华尔街的大型股票经济公司做交易系统,做了几年后突然有一天发现券商公司对每一笔交易付出的成本微乎其微,但要收取每个客户多达10美金的佣金。。。于是他们毅然决定辞职,回到硅谷创业去了。

公司打出的口号是就是零佣金,如果你交易的资金来源于自己,那么买入和卖出交易,以及银行转入和转出(不包括电汇)都是免费的。

零佣金对资金不那么充裕的年轻人来说非常有吸引力。那么问题来了,这家公司靠什么赚钱呢?

  1. 交易过股票的人大概都知道,券商会提供融资服务给流动资金不充足的投资者,融资交易(Securities Margin Trading)就是投资者以资金或证券作为质押,向券商借入资金用于证券买卖,并在约定的期限内偿还借款本金和利息。相应的还有融券服务,融券交易是投资者以资金或证券作为质押,向券商借入证券卖出,在约定的期限内,买入相同数量和品种的证券归还券商并支付相应的融券费用。所以Robinhood首先可以通过融资融券服务盈利;

  2. 对于投资者在Robinhood账户里的未购买股票的闲余资金,Robinhood可以通过金融市场获得一部分利息收入;

  3. 提供收费接口给小型的量化和对冲基金;

目前Robinhood已经完成了千万美元的风险投资,现阶段提供颠覆性的交易体验,吸引更多年轻的新投资者,赢得用户的信任显然更为重要。用户多了,盈利就不是问题了。。。

App界面

创始人

参考文档

  1. http://techcrunch.cn/2014/09/25/robinhood-stock-app/
  2. https://robinhoodapp.zendesk.com/hc/en-us/articles/202853769-How-does-Robinhood-make-money-
  3. http://baike.baidu.com/view/431144.htm?fromtitle=融资融券&fromid=1628138&type=syn

使用Gradle生成包含所有依赖库(.jar或.aar)的aar包

Android Library项目中如果使用Android Gradle plugin打aar包,通过maven依赖的库,或者是local依赖的aar都不会包含在生成的aar包里,如果项目是发布一个SDK,为了方便开发者使用,我们倾向于生成一个包含所有依赖库以及.so等文件的aar包。

通过反复研究和测试,以下Gradle脚本能满足需求,如果需要对代码运行ProGuard混淆,则需要使用Gradle 2.1

方法是为项目增加一个sub project(如pkg_project)专门用于打包,该项目中build.gradle内容如下:

apply plugin: 'java'
version = 1.0

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:19.1.0'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile project(':<your_library_project>') //此处填写需要打包的Android Library Project name
}

task sync_jars() << {
	 //把所有依赖的.jar库都拷贝到build/aar/libs下
    copy {
        into buildDir.getPath() +"/aar/libs"
        from configurations.compile.findAll {
            it.getName().endsWith(".jar")
        }
    }
}

task sync_aars(dependsOn:':<your_library_project>:assembleRelease') << {
	 //把所有依赖的.aar库里包含的classes.jar都拷贝到build/aar/libs下,并重命名以不被覆盖
    def jar_name
    def aar_path
    def dest_dir = buildDir.getPath()+"/aar"
    configurations.compile.findAll {
        it.getName().endsWith(".aar")
    }.collect {
        aar_path = it.getPath()
        jar_name = "libs/"+it.getName().replace(".aar",".jar")
        copy {
            from zipTree(aar_path)
            into dest_dir
            include "**/*"
            rename 'classes.jar', jar_name
        }
    }
}

task fataar(dependsOn:[sync_aars, sync_jars]) << {
    task (obfuse_classes_jar, type: proguard.gradle.ProGuardTask) {
    	//build/aar/libs/*.jar混淆后生成build/aar/classes.jar
        configuration "proguard.cfg"
        injars buildDir.getPath()+"/aar/libs"
        outjars buildDir.getPath()+"/aar/classes.jar"
        libraryjars "${System.getProperty('java.home')}/lib/rt.jar"
        libraryjars "${System.getProperty('java.home')}/Contents/Classes/classes.jar"
        libraryjars System.getenv("ANDROID_HOME")+"/platforms/android-19/android.jar"
    }.execute()

    task (gen_aar, type: Zip) {
    	//把生成最终的aar包,注意libs目录需要被排除
        def dest_dir = buildDir.getPath()+"/aar/"
        baseName = "mysdk-all"
        extension = "aar"
        destinationDir = file(buildDir.getPath())
        from dest_dir
        exclude "libs"
    }.execute()
}

最后就可以使用gradlew pkg_project:fataar来打包了

[Read More]

常用Linux命令

拷贝文件

如果想事实看到拷贝的速度可以用rsync -aP

简单手机应用同步协议设计和实现

版本:Ver 0.3

在手机上我们需要持久化应用的一些数据(典型的如本地的设置信息),同时又希望能重装应用或换一台手机登录后能把这些数据再同步回来。业界有SyncMl标准,覆盖的功能很完善,正因为要保证兼容性,开源的实现都较重。如何借鉴这个标准自己来实现一个多端双向同步可扩展的功能呢?

App使用同步协议可以将原本必须在线操作的功能(如:删除一个联系人,修改一个联系人的备注信息)也可以在断网情况下完成。

我们假设一些前提:

  1. 同一时刻只有一端(iPhone,iPad或其他移动设备)能和服务器同步;
  2. 客户端和服务端的时间一致或误差较小;可在长连建立时通过协议记录时间差
  3. 客户端保存全量数据(对于客户端只保存部分数据的情况后面再做讨论);

应用场景

  1. 通讯录同步
  2. 最近联系人
  3. App客户端设置
  4. 最近会话列表
  5. 黑名单
  6. 群设置
  7. 群成员
  8. 用户的一些设置和开关

名词解释

  1. LCID - Local Unique Identifiers, 客户端生成的记录ID,客户端唯一;
  2. GUID - Global Unique Identifiers, 服务端生成的记录ID,全局唯一;
  3. Anchor - 同步锚点,可以使用递增的序列号或时间戳来表示,用来发现两端数据变化的部分;
  4. Session - 同步会话,由客户端发起,Session Id唯一。

客户端表设计

每条记录包含两个同步用的字段:

status - 用来标识记录的状态

Status含义
0本地新增
-1标记删除
1本地更新
9已同步

anchor - 用来记录服务端同步过来的时间戳。

服务端表设计

modified - 记录在服务端的修改时间

双向同步流程示例

1. Client 增加2条记录

idkeyvaluestatusmodifiedanchor
1FooBar010
2HelloWorld020

客户端新增记录时,需要将Status设为0

[Read More]

使用Gradle生成一个App的不同版本,且可以同时安装在一个手机上

背景

开发一个App一般会生成内测版和正式版,甚至还会有不同渠道的版本,不同版本的配置可能会不一样,比如内测版会需要记录完整的日志。

Android手机对于同样的Application Id的App只能安装一个版本,如果我们需要同时安装内测版和正式版,就必须修改其中一个版本的Application Id。

解决方案

Gradle支持buildTypes和productFlavors两种定制方法,这里只介绍通过buildType的解决方案。通过productFlavors则可有效解决渠道包,arm,x86等分平台以及付费版和广告版的打包问题。

修改debug版的包名

配置如下:

android {
    buildTypes {
        release {
            ...
        }

        debug {
            applicationIdSuffix '.debug'
            ...
        }
    }
}

修正资源文件里的包名

如果你的项目里使用了自定义的View,且有自定义的属性时,会需要修正一下xml命名空间里的包名。

android.applicationVariants.all { variant ->
    def buildType = variant.buildType
    def encoding = java.nio.charset.Charset.defaultCharset().toString()
    if (buildType.applicationIdSuffix) {
        def defaultPackageId = variant.packageName.replaceAll(buildType.applicationIdSuffix,'')
        variant.mergeResources.doLast {
            def dir = file("${buildDir}/intermediates/res/${variant.dirName}/layout")
            dir.listFiles().each { f->
                String content = f.getText(encoding)
                content = content.replaceAll("res/"+defaultPackageId, "res/"+variant.packageName)
                f.write(content, encoding)
            }
        }
    }
}

定制APK的应用名称

如果同时安装两个版本,那么最好能从应用名称上来区别一下,一般我们在AndroidManifest.xml中使用String resource来命名,如下:

    <application
    	...
        android:label="@string/app_name" >

build.gradle里增加下面的代码就可以为debug版一个特殊的命名了

android.applicationVariants.all { variant ->
    def buildType = variant.buildType
    def encoding = java.nio.charset.Charset.defaultCharset().toString()
    if (buildType.applicationIdSuffix) {
        def defaultPackageId = variant.packageName.replaceAll(buildType.applicationIdSuffix,'')
        variant.mergeResources.doLast {
            def f = file("${buildDir}/intermediates/res/${variant.dirName}/values/values.xml")
            String content = f.getText(encoding)
            content = content.replaceAll('来往','来往Beta')
            f.write(content,encoding)
        }
    }
}

修改ContentProvider Authority

如果你的应用里还提供ContentProvider的话,如下:

[Read More]