<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Soul Of Free Loop &#187; 技术</title>
	<atom:link href="https://zohead.com/archives/category/technology/feed" rel="self" type="application/rss+xml" />
	<link>https://zohead.com</link>
	<description>Uranus Zhou&#039;s Blog</description>
	<lastBuildDate>Sat, 19 Jul 2025 15:42:46 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.8</generator>
	<item>
		<title>使用 Lade 云开发平台部署容器应用</title>
		<link>https://zohead.com/archives/lade-cloud/</link>
		<comments>https://zohead.com/archives/lade-cloud/#comments</comments>
		<pubDate>Mon, 28 Apr 2025 15:35:39 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[Lade]]></category>
		<category><![CDATA[Miniflux]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[云平台]]></category>
		<category><![CDATA[容器]]></category>
		<category><![CDATA[数据库]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1948</guid>
		<description><![CDATA[关于 Lade 云开发平台 最近了解到一个比较新的 Lade 云开发平台，Lade 是专为开发者设计的云端平台，可以帮助开发者在云端部署测试应用，并提供基础的免费应用额度，还有多处数据中心服务器可供选择，还默认直接支持 HTTPS 域名以方便连接访问容器应用。 Lade 开发平台默认支持以下编程语言： Go Node.js PHP Python Ruby 以及以下这些常用的数据库： MariaDB Memcached MongoDB MySQL PostgreSQL Redis 对于其它编程语言，Lade 还支持以 Dockerfile 形式运行测试应用容器。 首先可以在 Lade 官方网站注 [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="about-lade-cloud">关于 Lade 云开发平台</h2>
<p>最近了解到一个比较新的 <a href="https://www.lade.io/">Lade</a> 云开发平台，Lade 是专为开发者设计的云端平台，可以帮助开发者在云端部署测试应用，并提供基础的免费应用额度，还有多处数据中心服务器可供选择，还默认直接支持 HTTPS 域名以方便连接访问容器应用。</p>
<p>Lade 开发平台默认支持以下编程语言：</p>
<ul>
<li>Go</li>
<li>Node.js</li>
<li>PHP</li>
<li>Python</li>
<li>Ruby</li>
</ul>
<p>以及以下这些常用的数据库：</p>
<ul>
<li>MariaDB</li>
<li>Memcached</li>
<li>MongoDB</li>
<li>MySQL</li>
<li>PostgreSQL</li>
<li>Redis</li>
</ul>
<p>对于其它编程语言，Lade 还支持以 Dockerfile 形式运行测试应用容器。</p>
<p>首先可以在 Lade 官方网站注册账户，然后需要绑定并验证 GitHub 账号后才能创建部署容器应用。</p>
<h2 id="lade-cli">Lade CLI 部署应用</h2>
<p>Lade 网站目前没有提供任何容器应用的部署和管理的功能，容器应用的部署和管理都需要使用 <a href="https://www.lade.io/docs/platform/cli">Lade CLI</a> 工具来完成。</p>
<p>这里以 Linux 系统为例进行简单的介绍，按照页面介绍来安装 Lade CLI：</p>
<pre class="brush: bash; title: ; notranslate">
~# curl -L https://github.com/lade-io/lade/releases/latest/download/lade-linux-amd64.tar.gz | tar xz
~# sudo mv lade /usr/local/bin
</pre>
<p>首先使用 CLI 登录 Lade 账户：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade login
Enter your Lade credentials:
? Username or email: zohead
? Password: 
Logged in as zohead*
</pre>
<p>输入用户名和密码登录成功之后就可以进行创建部署容器应用了，这次我准备先部署一个 <a href="https://miniflux.app/">Miniflux</a> 应用来试试效果。</p>
<p>Miniflux 是一个极简的 RSS 阅读器应用，基于 Go 语言开发，使用 Postgres 数据库保存数据，与其它 RSS 阅读器应用相比功能和界面都很简洁，不过关键还支持抓取 RSS 文章全文、全文检索等功能。</p>
<p>Lade 的收费方式可以参考官网 <a href="https://www.lade.io/pricing">Pricing</a> 页面，并有基础的免费额度，当然免费的额度也是随时有可能取消的：</p>
<ul>
<li>应用：
<ul>
<li>128 MB 内存；</li>
<li>1 CPU；</li>
</ul>
</li>
<li>数据库：
<ul>
<li>128 MB 内存；</li>
<li>1 GB 存储空间；</li>
</ul>
</li>
<li>磁盘：
<ul>
<li>1 GB 磁盘。</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>备注：</strong></p>
<p>2025-06-07 开始 Lade 平台正式取消了免费额度，目前最便宜的 128 MB 内存 / 1 CPU 的应用价格为每月 $1.25，大家自行酌情选择合适的容器收费配置计划。</p>
</blockquote>
<h3 id="app-container">应用容器</h3>
<p>我们首先使用 <code>lade apps create</code> 命令创建新的应用容器，按照提示输入应用容器名称，并选择容器配置和区域即可完成创建，这里就选择免费配置计划和日本东京区域：</p>
<blockquote>
<p><strong>提示：</strong></p>
<ul>
<li>容器名称非常关键，后面很多命令都需要用到，请谨慎输入；</li>
<li>请根据实际应用程序对内存和 CPU 的要求，选择合适的应用配置计划；</li>
<li>免费额度随时有可能取消，如果使用免费配置计划，请注意备份应用数据。</li>
</ul>
</blockquote>
<pre class="brush: bash; title: ; notranslate">
~# lade apps create
? App Name: miniflux
? Plan: 128mb
? Region: Tokyo
</pre>
<p>创建成功之后，可以运行 <code>lade apps list</code> 命令查看容器列表：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade apps list
NAME        PLAN     CREATED           STATUS
miniflux    128mb    43 minutes ago    running
</pre>
<p>另外使用 <code>lade apps show</code> 命令可以查看具体某个应用容器的详细信息和状态，下方输出数据最后的 <code>Web URL</code> 就是 Lade 云开发平台为容器应用自动分配的 HTTPS 访问域名：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade apps show miniflux
Owner:        XXX@XXX.com
Processes:    web: 0/1
Plan:         128mb
Region:       Tokyo
Status:       running
Web URL:      miniflux-zohead.ladeapp.com
</pre>
<p>接着我们就需要在容器上部署实际的应用了，对于 Miniflux 可以从 GitHub 检出最新 v2 版本的代码：</p>
<pre class="brush: bash; title: ; notranslate">
~# mkdir miniflux
~# cd miniflux
~/miniflux# git clone --depth 1 https://github.com/miniflux/v2.git
</pre>
<p>然后使用 <code>lade deploy</code> 命令进行容器应用部署：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade deploy --app miniflux
time=&quot;2025-04-24T20:28:59+08:00&quot; level=error msg=&quot;Can't add file internal/reader/readability/testdata to tar: archive/tar: unknown file mode ?rw-rw-rw-&quot;
Sending build context to Docker daemon  4.563MB
Step 1/11 : FROM golang:1.23.0
1.23.0: Pulling from library/golang
Successfully built e7ae4ac3677b
Successfully tagged registry.lade.io/zohead/miniflux:build-1njzwaheu4
Pushing miniflux build to registry
Build finished use &quot;lade logs -a miniflux -f&quot; to view app logs
</pre>
<p>容器应用部署成功之后，我们可以使用 <code>lade ps</code> 命令确认容器内的进程是否运行正常，可以看到 Lade 部署生成的 <code>miniflux.app</code> 应用程序进程正在运行：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade ps -a miniflux
NAME     PLAN     STARTED         COMMAND
web.1    128mb    14 hours ago    miniflux.app
</pre>
<p>我们还可以使用 <code>lade run</code> 命令实现在应用容器中运行需要的命令，这里就直接运行一个 Bash Shell，运行自定义命令也需要指定一个配置计划，这里还是使用免费配置计划：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade run &quot;bash&quot; -a miniflux -p 128mb
</pre>
<p>容器内的 Bash Shell 启动成功之后，我们可以查看并确认 Miniflux 应用信息，对于 Miniflux 这种 Go 语言应用，Lade 默认部署生成的应用程序在 <code>/go/bin</code> 目录下：</p>
<pre class="brush: bash; title: ; notranslate">
~$ ls -l /go/bin/miniflux.app
-rwxr-xr-x 1 web web 19411204 Apr 24 12:30 /go/bin/miniflux.app
~$ miniflux.app -i
Version: dev
Commit: HEAD
Build Date: undefined
Go Version: go1.23.0
Compiler: gc
Arch: amd64
OS: linux
</pre>
<p>另外根据上面部署命令输出中的提示信息，使用 <code>lade logs -a miniflux -f</code> 命令可以跟踪显示容器应用的运行日志，实际上就是显示容器内应用程序的标准输出和错误输出：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade logs -a miniflux -f
web.1 | level=INFO msg=&quot;The default value for DATABASE_URL is used&quot;
web.1 | dial tcp [::1]:5432: connect: connection refused
</pre>
<p>由于还没有为 Miniflux 应用配置数据库，会出现类似上面的报错信息。</p>
<h3 id="database-addon">数据库扩展</h3>
<p>接着我们需要使用 <code>lade addons create</code> 命令为容器应用配置数据库扩展，下面的 <code>lade addons create postgres</code> 命令即为创建 PostgreSQL 数据库，我仍然使用免费配置计划，并选择和应用相同位置的日本东京区域，以加快数据库访问速度：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade addons create postgres
? Addon Name: miniflux-postgres
? Plan: 128mb
? Region: Tokyo
? Version: 17
? Public: No
</pre>
<p>简单说明一下命令参数：</p>
<ul>
<li><code>Addon Name</code> 为数据库扩展名称，后续扩展管理相关命令也会用到；</li>
<li><code>Version</code> 为数据库版本，<code>lade</code> 命令会列出支持的数据库版本；</li>
<li><code>Public</code> 表示数据库是否允许外部公共访问，我只需要 PostgreSQL 数据库被 Miniflux 应用访问，并不需要公共访问，这里就选 <strong>No</strong> 了。</li>
</ul>
<p>创建成功之后，就可以使用 <code>lade addons show</code> 命令查看数据库扩展的详细信息和状态了：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade addons show miniflux-postgres
Owner:        XXX@XXX.com
Service:      PostgreSQL
Plan:         128mb
Region:       Tokyo
Version:      17
Public:       No
Status:       running
Addon URI:    postgresql://USERNAME:PASSWORD@tyo-sw1.lade.io:30066/XXXX?sslmode=require
</pre>
<p>最后的 <code>Addon URI</code> 就是最关键的数据库连接地址，我们需要为应用容器配置数据库连接地址，地址中包含数据库服务器地址、用户名、密码、数据库名称等数据。</p>
<p>确认数据库运行正常之后，就可以将 PostgreSQL 数据库扩展附加到 Miniflux 应用容器：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade addons attach miniflux-postgres --app miniflux
? Env Name: POSTGRES_URL
</pre>
<p>然后使用 <code>lade env list</code> 命令检查应用容器的环境变量：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade env list -a miniflux
DATABASE_URL=postgresql://USERNAME:PASSWORD@tyo-sw1.lade.io:30066/XXXX?sslmode=require
POSTGRES_URL=postgresql://USERNAME:PASSWORD@tyo-sw1.lade.io:30066/XXXX?sslmode=require
</pre>
<p>可以看到数据库扩展附加之后，会自动为应用容器配置数据库环境变量，包含通用的 <code>DATABASE_URL</code> 和 PostgreSQL 专用的 <code>POSTGRES_URL</code> 环境变量。</p>
<h2 id="miniflux-app-issue">Miniflux 应用容器问题</h2>
<p>我原以为按照上面的步骤为 Miniflux 应用容器配置好 PostgreSQL 数据库扩展之后，Miniflux 应用就可以正常运行了，而实际上容器仍然有报错：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade logs -a miniflux -f
web.1 | the database schema is not up to date: current=v0 expected=v108
</pre>
<p>参考 Miniflux 项目 <a href="https://github.com/miniflux/v2/issues/1680">You must run the SQL migrations, the database schema is not up to date: current=v0 expected=v62</a> Issue 的说明，对于类似 Docker 容器的环境 Miniflux 需要开启 <code>RUN_MIGRATIONS</code> 环境变量。</p>
<p>我们可以使用 <code>lade env set</code> 命令为容器配置或修改环境变量：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade env set RUN_MIGRATIONS=1 -a miniflux
</pre>
<p>稍等一会容器应用自动重启后，就可以看到 Miniflux 应用已经正常运行了：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade logs -a miniflux -f
web.1 | level=INFO msg=&quot;Running database migrations&quot; current_version=0 latest_version=108 driver=postgresql
web.1 | level=INFO msg=&quot;Starting HTTP server&quot; listen_address=:3000
</pre>
<p>现在我们其实就已经可以通过 Lade 云平台为容器应用分配的域名访问 Miniflux Web 管理界面了，当然还是需要用 HTTPS 访问的：</p>
<p><img src="https://images.weserv.nl/?url=https://res.cloudinary.com/digwht2y0/image/upload/v1752420850/lade-miniflux.png" alt="Miniflux Web 管理界面"></p>
<p>不过由于我们还没有为 Miniflux 配置管理员账户，还不能直接登录。</p>
<p>这个时候就需要借助 <code>lade run</code> 命令在应用容器中运行命令创建管理员账户了，下面输出中的 <code>miniflux.app</code> 程序是在容器内运行的：</p>
<pre class="brush: bash; title: ; notranslate">
~$ miniflux.app -create-admin
Enter Username: admin
Enter Password:
level=INFO msg=&quot;Created new admin user&quot; username=admin user_id=1
</pre>
<p>管理员账户创建成功之后，我们就可以通过容器域名访问 Miniflux Web 管理界面进行 RSS 源的配置和订阅管理了。</p>
<p>当然我们也可以开始就通过指定环境变量让 Miniflux 容器自动创建管理员账户，与上面的 <code>RUN_MIGRATIONS</code> 环境变量配合起来就类似：</p>
<pre class="brush: bash; title: ; notranslate">
~# lade env set RUN_MIGRATIONS=1 CREATE_ADMIN=1 ADMIN_USERNAME=admin ADMIN_PASSWORD=password -a miniflux
</pre>
<p>这样通过环境变量指定管理员账户名称和密码也很方便。</p>
<h2 id="summary ">总结</h2>
<p>本文简单介绍了如何使用 Lade 云开发平台的 <code>lade</code> CLI 命令部署配置容器应用，我部署的 Miniflux RSS 阅读器容器应用也运行了一段时间，Lade 云平台还是比较稳定的，只是没有国内的数据中心，访问速度倒是一般。</p>
<p>最后 Lade 云平台的开发者已经确认，目前提供的免费额度仅供开发人员测试使用，免费额度后续随时有可能取消，希望大家不要滥用，如果觉得 Lade 好用也可以考虑付费使用哦，祝大家玩的开心。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/lade-cloud/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>使用kbpbot实现Keybase静态文件托管</title>
		<link>https://zohead.com/archives/kbpbot-keybase/</link>
		<comments>https://zohead.com/archives/kbpbot-keybase/#comments</comments>
		<pubDate>Sat, 22 Jul 2023 14:13:19 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[网络技术]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[KBFS]]></category>
		<category><![CDATA[kbpbot]]></category>
		<category><![CDATA[Keybase]]></category>
		<category><![CDATA[rsync]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1884</guid>
		<description><![CDATA[关于 Keybase.pub 我之前写过一篇介绍 Keybase 加密网络服务体验 的文章，里面提到 Keybase 对于用户的公共目录提供了 Keybase.pub 这个静态文件托管网站，方便其它用户访问 Keybase KBFS 里保存的公共文件。 所以之前我都会把我的 MDwiki 知识库 也同步更新到我的 Keybase 下的 wiki 公共目录，这样就可以把我的 nocwat.keybase.pub 作为一个备用的 Wiki 网站，如果我的 VPS 服务器无法访问，也可以通过 Keybase.pub 访问 Wiki 知识库。 然而最近我才后知后觉地发现原来 Keybase 从今年 3 [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="about-keybase-pub">关于 Keybase.pub</h2>
<p>我之前写过一篇介绍 <a href="https://zohead.com/archives/keybase/">Keybase 加密网络服务体验</a> 的文章，里面提到 Keybase 对于用户的公共目录提供了 Keybase.pub 这个静态文件托管网站，方便其它用户访问 Keybase KBFS 里保存的公共文件。</p>
<p>所以之前我都会把我的 <a href="https://mdwiki.zohead.com/">MDwiki 知识库</a> 也同步更新到我的 Keybase 下的 <strong>wiki</strong> 公共目录，这样就可以把我的 <code>nocwat.keybase.pub</code> 作为一个备用的 Wiki 网站，如果我的 VPS 服务器无法访问，也可以通过 Keybase.pub 访问 Wiki 知识库。</p>
<p>然而最近我才后知后觉地发现原来 Keybase 从今年 3 月份开始就已经关闭了 Keybase.pub 静态文件托管服务，理由则是使用这项服务的用户数量太少，因此也就需要考虑新的 Keybase 静态文件托管方案了。</p>
<h2 id="kbpbot">kbpbot</h2>
<p>我稍微看了一下 Keybase Book 中关于 <a href="https://book.keybase.io/sites">Kebyase Sites</a> 的介绍文章，里面关于 Keybase.pub 的介绍还没有更新，不过还好目前 Keybase 提供的 <a href="https://keybase.io/kbpbot">kbpbot</a> 机器人还是可以正常使用的。</p>
<p>kbpbot 机器人原本是为了支持用户使用自定义域名发布网站而开发的，为自定义域名配置正确的 <strong>CNAME</strong> 和 <strong>TXT</strong> 记录之后，访问域名时 kbpbot 就可以读取指定的 Keybase 目录中的文件，然后将静态文件内容返回给访问者。</p>
<p>由此也可以看出，kbpbot 也必须有访问对应的 Keybase 目录的权限，自定义域名静态文件托管才能正常工作。得益于 Keybase KBFS 文件系统的设计，公共目录、私有目录、团队目录都可以给 kbpbot 赋予访问权限：</p>
<blockquote>
<p><strong>提示</strong></p>
<p>KBFS 文件系统中的所有文件都会自动签名加密，如果需要了解更多关于 KBFS 的使用方式，可以参考 Keybase 官方的 <a href="https://book.keybase.io/docs/files">Introducing the Keybase Filesystem</a> 这篇文章。</p>
</blockquote>
<ul>
<li>
<p><strong>公共目录</strong>：</p>
<p>kbpbot 默认就可以访问用户的所有公共目录，不需要专门配置，例如我的 Wiki 知识库的 KBFS 路径就是：<code>/keybase/public/nocwat/wiki</code>；</p>
</li>
<li>
<p><strong>私有目录</strong>：</p>
<p>用户只需要在私有目录下建立一个自己和 kbpbot 能访问的子目录，这里多个 Keybase 用户名使用逗号隔开即可，例如：<code>/keybase/private/yourname,kbpbot/XXX</code>；</p>
</li>
<li>
<p><strong>团队目录</strong>：</p>
<p>团队目录则更加简单，将 kbpbot 作为读者加入你的团队，然后使用团队目录路径，例如：<code>/keybase/team/yourteam/XXX</code>。</p>
</li>
</ul>
<p>然后是最重要的自定义域名配置，按照 Keybase Sites 文章里介绍的方式添加对应的 DNS 记录即可。例如我的 MDwiki 知识库使用的是 <a href="https://wiki.vp8.win/">wiki.vp8.win</a> 自定义域名，为 kbpbot 添加如下 DNS 记录：</p>
<ul>
<li>
<p><strong>CNAME</strong> 记录：</p>
<p>名称为 <code>wiki</code>，记录值为：<code>kbp.keybaseapi.com</code>；</p>
</li>
<li>
<p><strong>TXT</strong> 记录：</p>
<p>名称为 <code>_keybase_pages.wiki</code>，记录值为：<code>kbp=/keybase/public/nocwat/wiki</code>。</p>
</li>
</ul>
<p>其中比较关键的就是 <strong>TXT</strong> 记录了，<code>kbp=XXX</code> 记录值就是用来告诉 kbpbot 机器人，用户访问此域名时需要访问什么 KBFS 目录。如果你用的是私有目录，把 <strong>TXT</strong> 记录值换成类似于 <code>kbp=/keybase/private/yourname,kbpbot/XXX</code> 就可以了。</p>
<p>域名配置完成生效之后，就可以通过自定义域名访问你的静态站点了。由于 kbpbot 还支持自动申请和更新 Let’s Encrypt HTTPS 证书，用户访问站点时将会自动使用 HTTPS 访问，完全不需要维护 HTTPS 证书之类的，这一点还是非常方便的。</p>
<p>另外 kbpbot 甚至还支持使用 Keybase Git 代码仓库来访问静态文件，只是不支持私有代码仓库（只有自己才能访问私有 Git 仓库），需要使用团队仓库，同样也要将 kbpbot 作为读者加入你的团队。</p>
<p>例如团队名称还是上面的 <code>yourteam</code>，如果建立一个 <code>teamwiki</code> 仓库，就可以使用 <code>keybase://team/yourteam/teamwiki.git</code> 这种地址来 clone 仓库了，然后存放一些静态文件内容并提交。</p>
<p>最后我们把 Git 仓库对应的域名 <strong>TXT</strong> 记录值修改为：<code>kbp=git@keybase:team/yourteam/teamwiki</code>，用户就可以通过域名 HTTPS 访问静态站点了。</p>
<h2 id="sync-kbfs">同步 KBFS</h2>
<p>我的 Wiki 主站是放在目前博客的 VPS 服务器上运行的，整个 Wiki 知识库目录通过 <a href="https://www.resilio.com/individuals/">Resilio Sync</a> 与本地的 Windows / Linux 电脑进行同步，本地使用 Markdown 编辑器修改 Wiki 内容后就可以自动更新到 VPS 服务器上。</p>
<p>Keybase KBFS 对应的 Wiki 目录，我则是直接在 Linux 上使用 <code>rsync</code> 命令将同步过的本地 Wiki 目录更新到 Keybase，好处就是可以基于 <code>rsync</code> 的同步机制自动只更新差异的文件，这里需要先启动 Keybase 应用程序哦：</p>
<pre class="brush: bash; title: ; notranslate">
~# rsync -ruv --exclude=notes.sqlite --exclude=.sync ~/Documents/wiki/ /run/user/$(id -u)/keybase/kbfs/public/nocwat/wiki/
</pre>
<p><code>rsync</code> 的 <code>--exclude</code> 参数可以忽略不需要同步的文件，这里我指定 <code>notes.sqlite</code> 忽略 Markdown 编辑器使用的数据库文件，并指定 <code>.sync</code> 忽略 Resilio Sync 使用的内部同步目录。</p>
<p>Linux 系统上 rsync 的目标目录是：<code>/run/user/$(id -u)/keybase/kbfs</code>，与当前本地用户的用户 ID 有关；Windows 系统如果 Keybase 客户端启用了在资源管理器中显示 KBFS 盘符，rsync 的目标目录可能就是这样：<code>K:\</code>（实际的 KBFS 盘符可能并不是 K 盘）。</p>
<h2 id="summary">总结</h2>
<p>我的 MDwiki 知识库在 Keybase 上的备用网站 <a href="https://wiki.vp8.win/">wiki.vp8.win</a> 目前已经正常运行一段时间了，而且 Keybase 的静态文件托管服务在国内访问也很正常，并没有出现被墙之类的问题。</p>
<p>由于 kbpbot 除了 Keybase 公共、私有和团队 KBFS 目录，还支持使用 Git 仓库，我们也可以将类似 Hexo 这种静态博客通过 Keybase Git 来管理，本地修改构建之后提交到仓库更新，这样小伙伴们通过 Keybase 来托管静态博客也还是比较省事的。</p>
<p>虽然 Keybase 被 Zoom 收购之后发展势头不太好，更新节奏也变慢了，但还是希望 kbpbot 的静态文件托管服务能继续维持下去，毕竟 Keybase 提供的各项加密网络服务还是非常出色的，祝大家玩的开心哦。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/kbpbot-keybase/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Madoko Local本地使用的问题</title>
		<link>https://zohead.com/archives/madoko-local/</link>
		<comments>https://zohead.com/archives/madoko-local/#comments</comments>
		<pubDate>Mon, 27 Mar 2023 16:33:21 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[工具]]></category>
		<category><![CDATA[LaTeX]]></category>
		<category><![CDATA[Madoko]]></category>
		<category><![CDATA[Madoko Local]]></category>
		<category><![CDATA[Markdown]]></category>
		<category><![CDATA[编辑器]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1827</guid>
		<description><![CDATA[关于 Madoko Madoko 是微软研究院之前推出的一款在线 Markdown 编辑器，只不过更加偏向学术使用，主要亮点在于 Madoko 可以和 LaTeX 结合，支持 LaTeX 一些语法和功能。 LaTeX 是一种基于 TEX 的排版系统，功能非常强大，不少写学术论文的研究者们应该都用过，但其比较底层，学习曲线和难度也挺大。Madoko 可以将 LaTeX 与简单的 Markdown 语法相结合，大部分的文档格式和结构可以使用现有的 Markdown 语法，用户也可以使用 LaTeX 的语法和命令进行扩展，可以很轻松地生成 LaTeX 排版效果的文档，默认也支持输出 HTML 和 P [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="about-madoko">关于 Madoko</h2>
<p><a href="https://github.com/koka-lang/madoko" target="_blank">Madoko</a> 是微软研究院之前推出的一款在线 Markdown 编辑器，只不过更加偏向学术使用，主要亮点在于 Madoko 可以和 LaTeX 结合，支持 LaTeX 一些语法和功能。</p>
<p>LaTeX 是一种基于 TEX 的排版系统，功能非常强大，不少写学术论文的研究者们应该都用过，但其比较底层，学习曲线和难度也挺大。Madoko 可以将 LaTeX 与简单的 Markdown 语法相结合，大部分的文档格式和结构可以使用现有的 Markdown 语法，用户也可以使用 LaTeX 的语法和命令进行扩展，可以很轻松地生成 LaTeX 排版效果的文档，默认也支持输出 HTML 和 PDF 文档。</p>
<p>Madoko 官方的在线编辑器 Madoko Editor 网站是：</p>
<p><a href="https://www.madoko.net/" target="_blank">https://www.madoko.net/</a></p>
<p>最近 Madoko Editor 网站访问有点问题，不过我之前也已经把 Madoko Editor 网站备份过了，能够自行部署运行，编辑器的界面和默认的示例文档如下：</p>
<p><img src="https://images.weserv.nl/?url=http://res.cloudinary.com/digwht2y0/image/upload/v1737372294/madoko-editor.png" alt="Madoko Editor"></p>
<p>可以看到 Madoko 文档的扩展名是 <code>mdk</code>，而且 mdk 文档基本都是使用现有的 Markdown 语法加上一点 LaTeX 语法，即使不使用 Madoko Editor，也可以用其它 Markdown 编辑器打开进行编辑。</p>
<h2 id="madoko-local">Madoko Local</h2>
<p>mdk 文档在 Madoko Editor 中编辑修改之后，支持保存到 Dropbox、GitHub、OneDrive 以及 Local Disk 本地：</p>
<p><img src="https://images.weserv.nl/?url=http://res.cloudinary.com/digwht2y0/image/upload/v1737442733/makodo-editor-save.png" alt="Madoko Local Disk 保存"></p>
<p>由于众所周知的原因，前面三个存储目标国内用起来都不太稳定，而且之前我使用 Madoko 的时候就发现用这些云存储进行文档编辑似乎也容易出问题，可能就用微软自己的 OneDrive 做存储稳定一些。</p>
<p>另外考虑到数据放在自己手里才是最安全的，因此我一般都是用 Local Disk 本地保存方式。</p>
<p>Madoko 的本地磁盘访问则需要 <a href="https://github.com/koka-lang/madoko/tree/master/support/madoko-local" target="_blank">Madoko Local</a> 这个程序的配合，另外如果想要本地生成 PDF 也需要使用 Madoko Local。</p>
<p>Madoko Local 是用 Node.js 编写的，本机安装完 Node.js 环境后，运行命令完成安装之后就可以使用 <code>madoko-local</code> 命令了：</p>
<pre class="brush: bash; title: ; notranslate">
~# npm install -g madoko-local
</pre>
<p><code>madoko-local</code> 命令使用起来也非常简单：</p>
<pre class="brush: bash; title: ; notranslate">
~# madoko-local -r -l .
listening on           : http://localhost:8080
connecting securely to : https://www.madoko.net
serving files under    : /home/zzm/Documents

---------------------------------------------------------------
access server at       : http://localhost:8080#secret=XXX
---------------------------------------------------------------
</pre>
<p>最后一个参数就是 Madoko 能够访问的本地目录，<code>.</code> 就表示只导出当前的目录。</p>
<p>Madoko Local 启动之后就会输出绑定的端口（默认为 <code>8080</code>）、导出的本地目录路径，最重要的是包含密码的访问地址，浏览器通过这个访问地址就可以打开 Madoko Editor 界面。</p>
<p>Madoko Local 程序的其它参数可以通过 <code>madoko-local -h</code> 命令来查询，另外也可以使用 <code>config.json</code> 配置文件来配置端口等参数，这里我把 Madoko Local 的监听端口改为了 <code>18080</code>，也可以使用 <code>mountdir</code> 来配置导出的本地目录：</p>
<pre class="brush: bash; title: ; notranslate">
~$ cat &gt; ~/.madoko/config.json
{
  &quot;port&quot;:18080,
  &quot;secret&quot;:&quot;XXX&quot;,
  &quot;origin&quot;:&quot;http://XXX&quot;,
  &quot;mountdir&quot;:&quot;/home/zzm/Documents&quot;
}
</pre>
<p>配置文件所在的 <code>.madoko</code> 目录默认位于当前用户的主目录下，这个主目录的路径也可以通过 <code>madoko-local</code> 命令的 <code>--homedir</code> 参数进行自定义。</p>
<p>如果对应目录下存在有效的 <code>config.json</code> 配置文件，后续就可以直接 <code>madoko-local</code> 命令启动程序而不需要加任何参数了。</p>
<h2 id="madoko-local-issue">Madoko Local 问题</h2>
<p>不过我在使用 Madoko Local 时也发现一些问题：</p>
<ol>
<li>通过 <code>madoko-local</code> 命令来指定端口、密码等各种参数可能存在问题；</li>
<li>如果 <code>madoko-local</code> 程序默认绑定了本地 IPv6 的 <code>::1</code> 地址（也就是 <code>localhost</code> 地址），会报 <code>only serving localhost</code> 错误。</li>
</ol>
<p>为此我稍微修改了一下 Madoko Local Node.js 程序源码，生成了 Madoko Local 最新 0.9.4 版本的 patch 文件，有需要的朋友可以从下面任选一个下载：</p>
<ol>
<li><a href="https://pastebin.com/raw/jcSFMVsH" target="_blank">Pastebin 分享链接</a></li>
<li><a href="https://github.com/zohead/madoko/commit/6fd522fca1f7b25bd51a1150ac988bb24d591636.patch" target="_blank">GitHub 提交</a></li>
</ol>
<p>下载了 patch 文件之后，就可以进入 Madoko Local 的安装目录运行命令进行合并了（假设下载保存的文件名是 <code>madoko-local-npm-v0.9.4.patch</code>）：</p>
<pre class="brush: bash; title: ; notranslate">
~# cd /usr/lib/node_modules/
~# patch -p2 madoko-local-npm-v0.9.4.patch
</pre>
<p>上面命令使用的是 Linux 系统下 <code>npm</code> 默认的全局安装路径 <code>/usr/lib/node_modules</code>，其他系统可以使用 <code>npm list -g | head -n 1</code> 命令来确认。</p>
<p>为了方便，我也直接 fork 了 Madoko 的源仓库，还把自己做的其它的一些修改也提交了：</p>
<p><a href="https://github.com/zohead/madoko" target="_blank">https://github.com/zohead/madoko</a></p>
<p>Madoko Local 程序的代码在 <code>support/madoko-local</code> 子目录下供大家参考。</p>
<p>最后祝跨过三年疫情的朋友们都能玩得开心 ^_^。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/madoko-local/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux实现惠尔顿上网认证客户端</title>
		<link>https://zohead.com/archives/wholeton-linux-client/</link>
		<comments>https://zohead.com/archives/wholeton-linux-client/#comments</comments>
		<pubDate>Sun, 15 May 2022 03:34:10 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[WebSocket]]></category>
		<category><![CDATA[惠尔顿]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1745</guid>
		<description><![CDATA[最近公司由于某些特殊原因，需要上一套惠尔顿的上网行为管理系统，局域网内的所有设备在连接互联网时都需要通过惠尔顿上网认证客户端进行登录，或者需要管理员在上网行为管理中设置免监控。 惠尔顿官方只提供了 Windows 上的客户端进行用户登录，也可以访问网页版进行临时登录，效果如下： 不过 Linux 服务器设备就没法登录了，为此我写了个 Python 程序模拟惠尔顿的网页版客户端实现 Linux / macOS 等系统下的上网认证功能。 Python 程序 我编写的 wholeton-auth.py Python 程序可以通过 Pastebin 分享链接 下载，写的很简单粗糙，这里贴出来说明一下： [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>最近公司由于某些特殊原因，需要上一套惠尔顿的上网行为管理系统，局域网内的所有设备在连接互联网时都需要通过惠尔顿上网认证客户端进行登录，或者需要管理员在上网行为管理中设置免监控。</p>
<p>惠尔顿官方只提供了 Windows 上的客户端进行用户登录，也可以访问网页版进行临时登录，效果如下：</p>
<p><img src="https://images.weserv.nl/?url=http://res.cloudinary.com/digwht2y0/image/upload/v1737442965/wholeton-login.jpg" alt="惠尔顿上网认证系统网页版"></p>
<p>不过 Linux 服务器设备就没法登录了，为此我写了个 Python 程序模拟惠尔顿的网页版客户端实现 Linux / macOS 等系统下的上网认证功能。</p>
<h2 id="python-wholeton-client">Python 程序</h2>
<p>我编写的 <code>wholeton-auth.py</code> Python 程序可以通过 <a href="https://pastebin.com/raw/5XhfVS2S">Pastebin 分享链接</a> 下载，写的很简单粗糙，这里贴出来说明一下：</p>
<pre class="brush: python; highlight: [20,21,22,23,24,25]; title: ; notranslate">
#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import socket
from datetime import datetime
from uuid import getnode
import urllib
try:
    import urllib2
except Exception:
    from urllib import request as urllib2
try:
    import Cookie as cookies
except Exception:
    from http import cookies
import websocket
import json

wholeton_host = '192.168.1.254'
wholeton_user = 'test'
wholeton_pass = '123456'
wholeton_ip = ''
wholeton_mac = ''
update_secs = 28800

def url_encode(obj):
    try:
        return urllib.urlencode(obj)
    except Exception:
        return urllib.parse.urlencode(obj)

def get_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(('10.255.255.255', 1))
        IP = s.getsockname()[0]
    except Exception:
        IP = '127.0.0.1'
    finally:
        s.close()
    return IP

def get_mac():
    return ':'.join((&quot;%012x&quot; % getnode())[i:i+2] for i in range(0, 12, 2))

if not wholeton_ip:
    wholeton_ip = get_ip()

if not wholeton_mac:
    wholeton_mac = get_mac()

uri_keys = { 'id' : 0, 'url' : 'mail.126.com', 'user' : wholeton_ip, 'mac' : wholeton_mac }
uri_data = url_encode(uri_keys).replace('%3A', ':')

auth_data = url_encode({ 'param[UserName]' : wholeton_user, 'param[UserPswd]' : wholeton_pass, 'uri' : uri_data, 'force' : 0 })
# convert for python 3
if sys.version_info[0] == 3:
    auth_data = auth_data.encode('ascii')

ws = None
loop = True

try:
    while loop:
        resp = urllib2.urlopen('http://' + wholeton_host + '/user-login-auth?' + uri_data, timeout = 5, data = auth_data)

        # get session cookie
        cookie = cookies.SimpleCookie()
        cookie.load(resp.info()['Set-Cookie'])

        resp_data = resp.read()
        if resp_data:
            print('Login response:')
            print(resp_data)

        ws = websocket.WebSocket()
        ws.connect('ws://' + wholeton_host + '/go-ws/user-auth', cookie = 'fms_session=' + cookie.get('fms_session').value, origin = 'http://' + wholeton_host)

        dt_start = datetime.now()
        while ws:
            try:
                ws_data = ws.recv()
            except KeyboardInterrupt:
                loop = False
                break
            except:
                break

            if ws_data:
                dt_now = datetime.now()
                if (dt_now - dt_start).seconds &gt;= update_secs:
                    break

                print(dt_now)
                print(ws_data)

                ws_obj = json.loads(ws_data)
                if ws_obj and ws_obj[&quot;type&quot;] == &quot;logged-out&quot;:
                    break

        if ws:
            ws.close()
        ws = None
except KeyboardInterrupt:
    pass

if ws:
    ws.close()
</pre>
<p>为了支持比较老的服务器系统，此 Python 程序同时支持 Python 2 和 Python 3，在 Python 2.6 和 Python 3.6 版本上做了测试。为了减少依赖，本程序基本都是用的 Python 自带的模块，并针对 Python 2 和 Python 3 进行了区分处理。</p>
<p>程序首先通过惠尔顿网页版的 HTTP 登录接口进行登录，登录成功之后获取会话信息，然后建立 WebSocket 连接，并保持 WebSocket 连接状态，如果关闭了连接上网认证功能就会自动失效。</p>
<p>由于本程序需要建立 WebSocket 连接，也不需要任何 WebSocket 服务器的功能，所以没有用各种功能齐全的 WebSocket 库，只额外使用了 <a href="https://pypi.org/project/websocket-client/">websocket-client</a> 这个非常简单的 WebSocket 客户端库。</p>
<p>另外最新 1.3.2 版本的 websocket-client 库只支持 Python 3.7 及以上版本，为了能给 Python 2.6 版本使用，本程序使用的是 websocket-client 老一点的 <a href="https://pypi.org/project/websocket-client/0.59.0/">0.59.0</a> 版本，websocket-client 0.59.0 版本支持 Python 2.6 以上或 Python 3.4 以上版本，而且只依赖 <a href="https://pypi.org/project/six/">six</a> 这个 Python 兼容库，同时 six 库也使用 <a href="https://pypi.org/project/six/1.13.0/">1.13.0</a> 版本以兼容 Python 2.6 版本。</p>
<p>可以使用 pip 命令先安装 six 1.13.0 版本和 websocket-client 0.59.0 版本：</p>
<pre class="brush: bash; title: ; notranslate">
~$ pip install six==1.13.0
~$ pip install websocket-client==0.59.0
</pre>
<h2 id="wholton-using">上网认证使用</h2>
<p>上面依赖的库安装好之后，首先需要修改 <code>wholeton-auth.py</code> Python 程序中的配置（上面已经高亮显示了）：</p>
<pre class="brush: plain; title: ; notranslate">
wholeton_host = '192.168.1.254'
wholeton_user = 'test'
wholeton_pass = '123456'
wholeton_ip = ''
wholeton_mac = ''
update_secs = 28800
</pre>
<p>简单说明如下：</p>
<ul>
<li><code>wholeton_host</code> 指定惠尔顿上网行为管理系统 IP 地址；</li>
<li><code>wholeton_user</code> 和 <code>wholeton_pass</code> 指定上网行为管理的用户名和密码；</li>
<li><code>wholeton_ip</code> 为本机 IP 地址，默认为空，程序会自动获取，也可以自行修改覆盖；</li>
<li><code>wholeton_mac</code> 为本机上网网卡（也就是连接上网行为管理系统的网卡）的 MAC 地址，默认为空，程序也会自动获取（这里为了图省事使用的 <code>uuid</code> 库的 <code>getnode</code> 函数获取网卡 MAC 地址），也可以自行修改覆盖；</li>
<li><code>update_secs</code> 指定登录多长时间之后就强制断开连接进行重新登录，单位为秒，默认为 <code>28800</code> 秒也就是 8 个小时。</li>
</ul>
<p>因为我这边使用本程序测试发现惠尔顿的上网认证系统即使在保持 WebSocket 连接打开的情况下，大概 9 ~ 10 个小时之后也会无法上网，所以大家可以根据实际情况测试确认之后，修改程序里的 <code>update_secs</code> 重新登录时间。</p>
<p>修改完配置之后，使用起来就很简单了，根据 Python 版本在终端里运行 <code>python2 wholeton-auth.py</code> 或者 <code>python3 wholeton-auth.py</code> 就可以了，程序运行之后会在标准输出中显示上网认证服务器返回的响应数据。</p>
<p>最后本程序还是需要一直保持运行的，大家可以根据自己的需要通过 screen 或者服务等方式来实现后台运行哦。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/wholeton-linux-client/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>酷走行车记录仪root和GPS时间同步</title>
		<link>https://zohead.com/archives/kuzo-root-gps-time-sync/</link>
		<comments>https://zohead.com/archives/kuzo-root-gps-time-sync/#comments</comments>
		<pubDate>Sat, 26 Feb 2022 13:30:33 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[GPS]]></category>
		<category><![CDATA[root]]></category>
		<category><![CDATA[SuperSU]]></category>
		<category><![CDATA[时间同步]]></category>
		<category><![CDATA[行车记录仪]]></category>
		<category><![CDATA[酷走]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1734</guid>
		<description><![CDATA[首先很惭愧我已经有一年多没有更新博客内容了，之前写过一篇 酷走 Android 行车记录仪研究 的文章，行车记录仪的 ADB 调试已经可以用起来了，不过还存在网络不可用时系统时间不正确的问题，刚好再研究一下行车记录仪的 root 以及通过 GPS 同步记录仪的系统时间。 root 系统 其实酷走这款 Android 行车记录仪系统已经自带了“假” root 权限，只是没有带 su 管理的工具。虽然 SuperSU 很久没更新了，我还是准备安装试试，首先从 SuperSU 官网下载最新 2.82 版本的刷机 zip 包： http://supersuroot.org/downloads/Supe [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>首先很惭愧我已经有一年多没有更新博客内容了，之前写过一篇 <a href="https://zohead.com/archives/kuzo-android-internal/" target="_blank">酷走 Android 行车记录仪研究</a> 的文章，行车记录仪的 ADB 调试已经可以用起来了，不过还存在网络不可用时系统时间不正确的问题，刚好再研究一下行车记录仪的 root 以及通过 GPS 同步记录仪的系统时间。</p>
<h2 id="root-system">root 系统</h2>
<p>其实酷走这款 Android 行车记录仪系统已经自带了“假” root 权限，只是没有带 su 管理的工具。虽然 SuperSU 很久没更新了，我还是准备安装试试，首先从 SuperSU 官网下载最新 2.82 版本的刷机 zip 包：</p>
<pre><code>http://supersuroot.org/downloads/SuperSU-v2.82-201705271822.zip
</code></pre>
<p>解压缩之后，首先使用 adb 命令将系统重新挂载为可读写状态：</p>
<pre class="brush: bash; title: ; notranslate">
~$ adb remount
</pre>
<p>参考之前折腾刷机的经验，先拷贝刷机 zip 包中的文件：</p>
<pre class="brush: bash; title: ; notranslate">
~$ adb push common/Superuser.apk /sdcard/
~$ adb push common/install-recovery.sh /sdcard/
~$ adb push x86/su /sdcard/
~$ adb push x86/supolicy /sdcard/
~$ adb push x86/libsupol.so /sdcard/
</pre>
<p>这个时候可以使用 <code>adb shell</code> 命令登录到行车记录仪，再将文件移动到系统目录：</p>
<pre class="brush: bash; title: ; notranslate">
root@Sf3gr_mrd6_p2_720# mkdir /system/app/SuperSU
root@Sf3gr_mrd6_p2_720# mv /sdcard/Superuser.apk /system/app/SuperSU/
root@Sf3gr_mrd6_p2_720# mv /sdcard/install-recovery.sh /system/bin/
root@Sf3gr_mrd6_p2_720# cp /sdcard/su /system/xbin/
root@Sf3gr_mrd6_p2_720# mkdir /system/bin/.ext
root@Sf3gr_mrd6_p2_720# cp /sdcard/su /system/bin/.ext/.su
root@Sf3gr_mrd6_p2_720# mv /sdcard/su /system/xbin/daemonsu
root@Sf3gr_mrd6_p2_720# mv /sdcard/supolicy /system/xbin/
root@Sf3gr_mrd6_p2_720# mv /sdcard/libsupol.so /system/lib/
</pre>
<p>还需要设置文件权限：</p>
<pre class="brush: bash; title: ; notranslate">
root@Sf3gr_mrd6_p2_720# chmod 0644 /system/app/SuperSU/Superuser.apk
root@Sf3gr_mrd6_p2_720# chmod 0755 /system/bin/install-recovery.sh /system/xbin/su /system/bin/.ext/.su /system/xbin/daemonsu /system/xbin/supolicy /system/lib/libsupol.so
root@Sf3gr_mrd6_p2_720# chcon u:object_r:system_file:s0 /system/app/SuperSU/Superuser.apk /system/bin/install-recovery.sh /system/xbin/su /system/bin/.ext/.su /system/xbin/daemonsu /system/xbin/supolicy /system/lib/libsupol.so
</pre>
<p>不过最后运行 SuperSu 安装命令的时候却出错了：</p>
<pre class="brush: bash; title: ; notranslate">
root@Sf3gr_mrd6_p2_720# /system/xbin/su --install
error: only position independent executables (PIE) are supported.
</pre>
<p>看来是想当然了，我拷贝的是非 PIE 格式的 su 程序导致运行不了。那就重新拷贝正确的 su 程序（需要使用 <code>su.pie</code> 地址无关的可执行程序版本），这次就 adb 命令一步到位了：</p>
<pre class="brush: bash; title: ; notranslate">
~$ adb push common/Superuser.apk /system/app/SuperSU/Superuser.apk
~$ adb push common/install-recovery.sh /system/bin/install-recovery.sh
~$ adb push x86/su.pie /system/xbin/su
~$ adb push x86/su.pie /system/bin/.ext/.su
~$ adb push x86/su.pie /system/xbin/daemonsu
~$ adb push x86/supolicy /system/xbin/supolicy
~$ adb push x86/libsupol.so /system/lib/libsupol.so
</pre>
<p>设置文件权限之后再重新执行 <code>/system/xbin/su --install</code> 命令安装就没有问题了，测试确认 SuperSU App 也可以正常运行了。</p>
<h2 id="gps-time-sync">GPS 时间同步</h2>
<p>为了能修正行车记录仪的系统时间，我在行车记录仪上安装了 <a href="https://play.google.com/store/apps/details?id=com.pautinanet.smarttimesync" target="_blank">Smart Time Sync</a> 这款 App 进行时间同步。Smart Time Sync 支持通过 GPS、NTP  或 HTTP 同步系统时间，不过行车记录仪上都没有装 SIM 卡更没有网络，只能通过 GPS 进行时间同步了。</p>
<p>如果 Android 系统没有 root，Smart Time Sync 只能进行手工同步，因此为了实际使用过程中能实现后台自动时间同步，上面的系统 root 操作还是必须的。</p>
<p>Smart Time Sync 运行效果如下：</p>
<p><img src="https://images.weserv.nl/?url=http://res.cloudinary.com/digwht2y0/image/upload/v1737372134/kuzo-smart-time-sync.png" alt="Smart Time Sync"></p>
<p>App 使用起来也很简单，首先在 Source 源选择 <code>GPS</code>，然后点 <code>Sync Time</code> 按钮就可以手工同步系统时间了，当然前提是设备必须放在有 GPS 信号的地方。</p>
<p>点击后面的设置图标就可以配置后台自动进行时间同步了，现在我的行车记录仪即使掉电了保存的录像文件时间也是正确的了。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/kuzo-root-gps-time-sync/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>ARM64 Linux下ARM容器使用yum的问题</title>
		<link>https://zohead.com/archives/arm64-arm-container-yum/</link>
		<comments>https://zohead.com/archives/arm64-arm-container-yum/#comments</comments>
		<pubDate>Thu, 29 Oct 2020 16:52:31 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[aarch64]]></category>
		<category><![CDATA[ARM]]></category>
		<category><![CDATA[ARM64]]></category>
		<category><![CDATA[armhf]]></category>
		<category><![CDATA[RPM]]></category>
		<category><![CDATA[YUM]]></category>
		<category><![CDATA[容器]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1689</guid>
		<description><![CDATA[ARM 容器问题 前段时间需要验证某客户的 32 位 ARM Linux 程序，由于我们只有现成的 ARM64 设备和系统，而 ARM64 兼容 32 位 armel 和 armhf，因此想到直接在 ARM64（aarch64）Linux 系统中运行 32 位 ARM（armhf）容器来进行测试。 客户程序使用的是 CentOS 7 32 位 ARM 开发环境，这里我使用 LXC 官方镜像来创建一个完整的 CentOS 7 容器系统： 32 位 ARM 容器可以正常启动，但使用 yum 安装任何软件包会报错： yum 修改 上面 yum 命令的报错提示找不到 repo 为 base/7/arm [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="arm-container-issue">ARM 容器问题</h2>
<p>前段时间需要验证某客户的 32 位 ARM Linux 程序，由于我们只有现成的 ARM64 设备和系统，而 ARM64 兼容 32 位 armel 和 armhf，因此想到直接在 ARM64（aarch64）Linux 系统中运行 32 位 ARM（armhf）容器来进行测试。</p>
<p>客户程序使用的是 CentOS 7 32 位 ARM 开发环境，这里我使用 LXC 官方镜像来创建一个完整的 CentOS 7 容器系统：</p>
<pre class="brush: bash; title: ; notranslate">
~ # lxc-create -n arm -t download --dir=/var/lib/lxc/arm/rootfs -- --server mirrors.tuna.tsinghua.edu.cn/lxc-images -d centos -r 7 -a armhf
</pre>
<p>32 位 ARM 容器可以正常启动，但使用 yum 安装任何软件包会报错：</p>
<pre class="brush: bash; title: ; notranslate">
bash-4.2# yum install openssh-server
Loaded plugins: fastestmirror
Determining fastest mirrors


 One of the configured repositories failed (Unknown),
 and yum doesn't have enough cached data to continue. At this point the only
 safe thing yum can do is fail. There are a few ways to work &quot;fix&quot; this:

     1. Contact the upstream for the repository and get them to fix the problem.

     2. Reconfigure the baseurl/etc. for the repository, to point to a working
        upstream. This is most often useful if you are using a newer
        distribution release than is supported by the repository (and the
        packages for the previous distribution release still work).

     3. Run the command with the repository temporarily disabled
            yum --disablerepo=&lt;repoid&gt; ...

     4. Disable the repository permanently, so yum won't use it by default. Yum
        will then just ignore the repository until you permanently enable it
        again or use --enablerepo for temporary usage:

            yum-config-manager --disable &lt;repoid&gt;
        or
            subscription-manager repos --disable=&lt;repoid&gt;

     5. Configure the failing repository to be skipped, if it is unavailable.
        Note that yum will try to contact the repo. when it runs most commands,
        so will have to try and fail each time (and thus. yum will be be much
        slower). If it is a very temporary problem though, this is often a nice
        compromise:

            yum-config-manager --save --setopt=&lt;repoid&gt;.skip_if_unavailable=true

Cannot find a valid baseurl for repo: base/7/armv8l
</pre>
<h2 id="yum-patch">yum 修改</h2>
<p>上面 yum 命令的报错提示找不到 repo 为 <code>base/7/armv8l</code> 的地址，我们可以在容器中使用 <code>uname</code> 命令确认当前的系统架构为 <code>armv8l</code>（ARM64 内核运行 32 位 ARM 程序）：</p>
<pre class="brush: bash; title: ; notranslate">
bash-4.2# uname -m
armv8l
</pre>
<p>而在主机端则可以看到真正的系统架构为 <code>aarch64</code>：</p>
<pre class="brush: bash; title: ; notranslate">
~ # uname -m
aarch64
</pre>
<p>正是由于 yum 不能正确识别 <code>armv8l</code>这个特殊的系统架构，导致命令执行失败。</p>
<p>我们可以修改 <code>/usr/lib/python2.7/site-packages/rpmUtils/arch.py</code> 文件，使 yum 可以正确处理 <code>armv8l</code> 架构，以下为 patch 修改内容：</p>
<pre class="brush: diff; title: ; notranslate">
diff -rNp a/arch.py b/arch.py
*** a/arch.py   Thu Oct 29 23:46:21 2020
--- b/arch.py   Thu Oct 29 23:48:55 2020
*************** arches = {
*** 73,78 ****
--- 73,79 ----
      &quot;armv5tel&quot;: &quot;noarch&quot;,
  
      #arm hardware floating point
+     &quot;armv8l&quot;: &quot;armv7hl&quot;,
      &quot;armv7hnl&quot;: &quot;armv7hl&quot;,
      &quot;armv7hl&quot;: &quot;noarch&quot;,
  
*************** def getBaseArch(myarch=None):
*** 442,447 ****
--- 443,450 ----
          return &quot;ppc&quot;
      elif myarch.startswith(&quot;arm64&quot;):
          return &quot;arm64&quot;
+     elif myarch == &quot;armv8l&quot;:
+         return &quot;armhfp&quot;
      elif myarch.startswith(&quot;armv7h&quot;):
          return &quot;armhfp&quot;
      elif myarch.startswith(&quot;arm&quot;):
</pre>
<p>此时再运行 yum 就可以正确找到软件包了：</p>
<pre class="brush: bash; title: ; notranslate">
bash-4.2# yum install openssh-server
Loaded plugins: fastestmirror
Determining fastest mirrors
 * base: mirrors.bfsu.edu.cn
 * extras: mirrors.bfsu.edu.cn
 * updates: mirrors.bfsu.edu.cn
base                                                                                                                                                                                              | 3.6 kB  00:00:00
extras                                                                                                                                                                                            | 2.9 kB  00:00:00
updates                                                                                                                                                                                           | 2.9 kB  00:00:00
(1/4): base/7/armhfp/group_gz                                                                                                                                                                     | 153 kB  00:00:00
(2/4): extras/7/armhfp/primary_db                                                                                                                                                                 | 170 kB  00:00:00
(3/4): updates/7/armhfp/primary_db                                                                                                                                                                | 562 kB  00:00:00
(4/4): base/7/armhfp/primary_db                                                                                                                                                                   | 4.1 MB  00:00:00
Resolving Dependencies
--&gt; Running transaction check
---&gt; Package openssh-server.armv7hl 0:7.4p1-21.el7 will be installed
--&gt; Processing Dependency: libwrap.so.0 for package: openssh-server-7.4p1-21.el7.armv7hl
--&gt; Running transaction check
---&gt; Package tcp_wrappers-libs.armv7hl 0:7.6-77.el7 will be installed
--&gt; Finished Dependency Resolution

Dependencies Resolved

=========================================================================================================================================================================================================================
 Package                                                    Arch                                             Version                                                 Repository                                     Size
=========================================================================================================================================================================================================================
Installing:
 openssh-server                                             armv7hl                                          7.4p1-21.el7                                            base                                          446 k
Installing for dependencies:
 tcp_wrappers-libs                                          armv7hl                                          7.6-77.el7                                              base                                           64 k

Transaction Summary
=========================================================================================================================================================================================================================
Install  1 Package (+1 Dependent package)

Total download size: 510 k
Installed size: 1.0 M
Is this ok [y/d/N]: y
Downloading packages:
(1/2): tcp_wrappers-libs-7.6-77.el7.armv7hl.rpm                                                                                                                                                   |  64 kB  00:00:00
(2/2): openssh-server-7.4p1-21.el7.armv7hl.rpm                                                                                                                                                    | 446 kB  00:00:00
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Total                                                                                                                                                                                    1.7 MB/s | 510 kB  00:00:00
Running transaction check
Running transaction test


Transaction check error:
  package tcp_wrappers-libs-7.6-77.el7.armv7hl is intended for a different architecture
  package openssh-server-7.4p1-21.el7.armv7hl is intended for a different architecture
</pre>
<p>不过这次轮到 rpm 报错了，提示 RPM 包的架构不相符。</p>
<h2 id="rpm-patch">rpm 修改</h2>
<p>我们先试着手工安装 yum 下载下来但安装失败的 RPM 包，可以看到还是相同的报错：</p>
<pre class="brush: bash; title: ; notranslate">
bash-4.2# rpm -ivh /var/cache/yum/armhfp/7/base/packages/tcp_wrappers-libs-7.6-77.el7.armv7hl.rpm
Preparing...                          ################################# [100%]
        package tcp_wrappers-libs-7.6-77.el7.armv7hl is intended for a different architecture
</pre>
<p>这也是由于 rpm 命令不能正确处理 <code>armv8l</code> 架构，其实对于这种 RPM 包的架构不相符的情况，我们可以为 rpm 命令增加 <code>--ignorearch</code> 参数强制安装：</p>
<pre class="brush: bash; title: ; notranslate">
bash-4.2# rpm -ivh --ignorearch /var/cache/yum/armhfp/7/base/packages/tcp_wrappers-libs-7.6-77.el7.armv7hl.rpm
Preparing...                          ################################# [100%]
Updating / installing...
   1:tcp_wrappers-libs-7.6-77.el7     ################################# [100%]
</pre>
<p>当然我们的最终目的是要让 yum 能够自动安装软件包，因此还需要让 rpm 命令也能正确识别 <code>armv8l</code> 架构。</p>
<p>为此我们需要修改 <code>/usr/lib/rpm/rpmrc</code> 文件，将 <code>armv8l</code> 视为 <code>armv7hnl</code>：</p>
<pre class="brush: diff; title: ; notranslate">
diff -rNp a/rpmrc b/rpmrc 
*** a/rpmrc     Fri Oct 30 00:15:17 2020
--- b/rpmrc     Fri Oct 30 00:16:32 2020
*************** arch_compat: armv5tel: armv4tl
*** 398,403 ****
--- 398,404 ----
  arch_compat: armv4tl: armv4l
  arch_compat: armv4l: armv3l
  arch_compat: armv3l: noarch
+ arch_compat: armv8l: armv7hnl
  arch_compat: armv7hnl: armv7hl
  arch_compat: armv7hl: noarch
</pre>
<p>此时我们再使用 yum 命令就可以正确下载和安装软件包了：</p>
<pre class="brush: bash; title: ; notranslate">
bash-4.2# yum install openssh-server
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
 * base: mirrors.bfsu.edu.cn
 * extras: mirrors.bfsu.edu.cn
 * updates: mirrors.bfsu.edu.cn
Resolving Dependencies
--&gt; Running transaction check
---&gt; Package openssh-server.armv7hl 0:7.4p1-21.el7 will be installed
--&gt; Finished Dependency Resolution

Dependencies Resolved

=========================================================================================================================================================================================================================
 Package                                                  Arch                                              Version                                                 Repository                                      Size
=========================================================================================================================================================================================================================
Installing:
 openssh-server                                           armv7hl                                           7.4p1-21.el7                                            base                                           446 k

Transaction Summary
=========================================================================================================================================================================================================================
Install  1 Package

Total size: 446 k
Installed size: 921 k
Is this ok [y/d/N]: y
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
Warning: RPMDB altered outside of yum.
  Installing : openssh-server-7.4p1-21.el7.armv7hl                                                                                                                                                                   1/1
  Verifying  : openssh-server-7.4p1-21.el7.armv7hl                                                                                                                                                                   1/1

Installed:
  openssh-server.armv7hl 0:7.4p1-21.el7

Complete!
</pre>
<p>后续安装其它软件包也都没有问题了，例如安装基础开发包：</p>
<pre class="brush: bash; title: ; notranslate">
bash-4.2# yum -y install gcc make openssl-devel glibc-static openssl-static
</pre>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/arm64-arm-container-yum/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Keybase加密网络服务初步体验</title>
		<link>https://zohead.com/archives/keybase/</link>
		<comments>https://zohead.com/archives/keybase/#comments</comments>
		<pubDate>Sun, 27 Oct 2019 04:00:52 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[网络技术]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[gpg]]></category>
		<category><![CDATA[KBFS]]></category>
		<category><![CDATA[Keybase]]></category>
		<category><![CDATA[PGP]]></category>
		<category><![CDATA[Stellar]]></category>
		<category><![CDATA[XLM]]></category>
		<category><![CDATA[加密]]></category>
		<category><![CDATA[加密货币]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1602</guid>
		<description><![CDATA[关于 Keybase 体验之前需要先了解 Keybase 到底是啥，维基百科上对 Keybase 的定义是基于 PGP 技术的社交网络平台，可以将用户的身份映射到公钥，反之亦然。最常见的用法就是 Keybase 做为公共的 PGP key server。Keybase 可以对用户的 Twitter、GitHub、Reddit 等社交网络账户提供身份验证功能。 做为开源项目，Keybase 提供的服务已经远不止这些了，除了在社交网络身份验证之外增加了域名、网站管理员以及比特币和 Zcash 加密货币地址的验证功能，目前还提供了不少扩展功能： 端对端加密聊天通讯； 类似 Slack 的加密团队聊天 [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="about-keybase">关于 Keybase</h2>
<p>体验之前需要先了解 <a href="https://keybase.io/" target="_blank">Keybase</a> 到底是啥，维基百科上对 Keybase 的定义是基于 PGP 技术的社交网络平台，可以将用户的身份映射到公钥，反之亦然。最常见的用法就是 Keybase 做为公共的 PGP key server。Keybase 可以对用户的 Twitter、GitHub、Reddit 等社交网络账户提供身份验证功能。</p>
<p>做为开源项目，Keybase 提供的服务已经远不止这些了，除了在社交网络身份验证之外增加了域名、网站管理员以及比特币和 Zcash 加密货币地址的验证功能，目前还提供了不少扩展功能：</p>
<ul>
<li>端对端加密聊天通讯；</li>
<li>类似 Slack 的加密团队聊天和协作；</li>
<li>KBFS 加密文件存储服务，支持公共文件和私有文件，公共文件支持静态文件托管服务；</li>
<li>集成 Stellar（XLM）钱包（恒星币）功能，支持创建和导入 Stellar 账户，可以通过 Keybase 发送 XLM 给其它用户；</li>
<li>加密 Git 代码仓库托管服务，用户可以创建自己的私有仓库。</li>
</ul>
<h2 id="pgp-encrypt">PGP 加密</h2>
<p>还是从最基本的 PGP 加密说起，在 Keybase 官网注册账户并安装客户端之后，用户可以把自己的 PGP 公钥上传到 Keybase。Linux 下我一般使用系统自带的 gpg（GnuPG）来管理密钥，gpg 的知识网上有很多这里就不做详细介绍了，Keybase 做为公共 PGP key server 还是比较好用的。</p>
<p>例如，我的 Keybase 用户名是 <strong><a href="https://keybase.io/nocwat" target="_blank">nocwat</a></strong>，欢迎大家添加好友，我已经把自己的 PGP 公钥上传到 Keybase 了，如果你需要在 Linux 系统里导入我的公钥就很简单：</p>
<pre class="brush: bash; title: ; notranslate">
~$ curl https://keybase.io/nocwat/pgp_keys.asc | gpg --import
</pre>
<p>如果你已经安装了 Keybase 客户端，也可以这样导入：</p>
<pre class="brush: bash; title: ; notranslate">
~$ keybase pgp pull nocwat
</pre>
<p>导入公钥之后，就可以用 gpg 再做加密、解密、签名、验证之类的操作了。</p>
<p>当然 Keybase 客户端也是直接支持 PGP 的这些操作了，例如：</p>
<pre class="brush: bash; title: ; notranslate">
~$ keybase encrypt -i input -o output user
</pre>
<blockquote>
<p><strong>注意</strong></p>
<p>Keybase 可能觉得用户直接使用 PGP 的门槛太高太复杂，又自己搞了一套新的认证机制，可以参考这里：</p>
<p><a href="https://keybase.io/blog/keybase-new-key-model" target="_blank">Keybase’s New Key Model</a></p>
<p>新的认证机制使用 Keybase 设备密钥（device key）和 paper key（非常重要，可以更方便地认证 Keybase 设备），至于新的密钥系统则是基于 <a href="http://nacl.cr.yp.to/" target="_blank">NaCl</a> 新开发出来的 <a href="https://saltpack.org/" target="_blank">SaltPack</a>，具体可以看看 <a href="https://keybase.io/docs/crypto/overview" target="_blank">Keybase Crypto Documents</a>。</p>
</blockquote>
<p>对于 Keybase 这套新的认证机制，我不太好评价其优劣点。Keybase 用户的设备列表是所有用户都可见的，例如我的 <a href="https://keybase.io/nocwat/devices" target="_blank">Keybase devices</a>，这里包含了所有已认证的设备和 paper key。</p>
<p>在 <a href="https://keybase.io/nocwat/graph" target="_blank">Keybase graph</a> 里甚至还能看到 PGP key、device key、paper key 及其它已认证项目的对应关系图。从 graph 图里就能看到目前我还是最信赖自己的 PGP 密钥的。</p>
<blockquote>
<p><strong>提示</strong></p>
<p>Keybase 也提供 PGP 私钥托管保存功能，Keybase 声称 PGP 私钥必须通过用户的 passphrase 才能解密，解密是通过浏览器客户端本地完成，服务器并不会保存 passphrase。这个托管功能并不是强制的，上传与否要看你是否足够信任 Keybase 了。</p>
</blockquote>
<p>另外 Keybase 网页版也有一个 <strong><a href="https://keybase.io/encrypt" target="_blank">PGP Encrypt</a></strong> 页面提供 PGP 加解密等功能：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371721/keybase-pgp.png" alt="Keybase PGP Encrypt"></p>
<p>PGP 私钥托管之后确实会方便不少，如果你的 PGP 私钥跟我一样没有上传到 Keybase 上托管，那这个页面只能使用 Encrypt 加密发送消息和 Verify 功能，Decrypt 和 Sign 功能必须通过 PGP 或 Keybase 客户端来完成。</p>
<h2 id="encrypt-im">加密聊天通讯</h2>
<p>Keybase 端对端的加密聊天功能需要安装客户端才能使用，与其它聊天软件不同，即使其他人只有 Twitter、GitHub 等社交网络账户还没有注册 Keybase，你也可以通过 Keybase 发送消息给他们（直接指定社交网络账户名，而不需要知道 Keybase 账户名），待他们通过 Keybase 认证这些账户之后就能收到之前的消息。</p>
<p>你甚至可以直接通过 Keybase 发送消息给 PGP 公钥所有者，如果对方的 Keybase 账户添加验证了此 PGP 公钥就能收到消息。</p>
<p>Keybase 聊天的消息目前是基于 device key 和 paper key 进行加密的，而不是 PGP 密钥，Keybase 给出的解释是无需单独管理 PGP 密钥更有利于非技术人员使用。</p>
<p>Keybase 命令行 <code>keybase chat</code> 也可以进行简单的聊天，而且聊天功能还支持 JSON API 接口，可以很方便地实现聊天机器人之类的功能。</p>
<p>另外 Keybase 团队聊天同样也是端对端加密的，具体可以看看 <a href="https://keybase.io/docs/teams/crypto" target="_blank">Teams Crypto</a> 的介绍。由于 Keybase 代码是开源的，因此总体看起来 Keybase 似乎是一个比 Telegram 安全性更高的即时通讯软件。</p>
<h2 id="kbfs-storage">KBFS 文件存储</h2>
<p>KBFS（Keybase 文件系统）的介绍可以参考 <a href="https://keybase.io/docs/kbfs" target="_blank">Introducing the Keybase filesystem</a>，用户存入的所有文件都会自动签名加密。与聊天功能类似，所有 KBFS 数据都是基于 device key 和 paper key 进行加密的，而不是 PGP 密钥，因此 paper key 特别需要注意备份不能丢失。</p>
<p>KBFS 需要安装 Keybase 客户端才可以使用，Linux 下是基于 FUSE 实现的，Windows 则是基于常见的 Dokan 用户态文件系统了：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371495/keybase-dokan.jpg" alt="Keybase Windows Dokan Library"></p>
<p>Keybase 客户端的 Files 里可以直接看到 private、public 和 team 三个顶层目录，分别对应用户私有、用户公共和团队文件：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371494/keybase-files.jpg" alt="KBFS 文件系统"></p>
<p>我的私有文件在 Linux 上的访问路径是类似这样：<code>/keybase/private/nocwat</code>，Windows 上则是：<code>k:\private\nocwat</code>，公共目录则是：<code>/keybase/public/nocwat</code>。</p>
<p>你也可以创建一个只有几个特定用户能访问的私有目录，类似这样：</p>
<pre class="brush: plain; title: ; notranslate">
/keybase/private/nocwat,xxx,yyy
</pre>
<p>对于公共目录，Keybase 还提供了 <a href="https://keybase.pub/" target="_blank">Keybase.pub</a> 网站方便其它用户访问使用，例如我的公共目录地址就是：<a href="https://keybase.pub/nocwat/" target="_blank"><code>https://keybase.pub/nocwat/</code></a>。</p>
<p>Keybase.pub 还提供了静态文件托管网站功能，例如你可以直接访问下面的地址查看我放到公共目录的 Wiki 知识库：<a href="https://nocwat.keybase.pub/wiki/" target="_blank"><code>https://nocwat.keybase.pub/wiki/</code></a>。其实这个地址就相当于访问我的 <code>/keybase/public/nocwat/wiki</code> 公共目录。</p>
<p>目前 Keybase 给每个用户提供了 250 GB 的 KBFS 存储空间，目前 KBFS 用户量还不是很多可能存在不稳定的问题，最好注意备份数据。</p>
<p>KBFS 文件系统可以直接看到空间使用情况：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371722/keybase-kbfs.png" alt="KBFS 空间使用"></p>
<h2 id="stellar-wallet">Stellar 钱包</h2>
<p>Stellar 是基于 Ripple 修改而来的支付网络系统，用户使用 Lumen（XLM）作为基础货币，可以通过 XLM 转账任意一种货币（可以是各国法定货币或者其它加密货币），其交易确认速度也非常快，有关恒星币的详细内容还请自行了解哦。</p>
<p>Stellar Lumens 加密货币（恒星币）的钱包功能同样必须安装客户端才能使用，目前所有 Keybase 用户都可以使用：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371721/keybase-wallet.jpg" alt="Keybase Stellar 钱包"></p>
<p>Keybase 将聊天功能和钱包功能进行整合，你可以直接通过 Keybase 用户名而不是很长的 Stellar 钱包地址进行转账操作，当然也可以通过 Keybase 客户端转账给其它未使用 Keybase 的 Stellar 钱包地址。</p>
<p>目前 Keybase 正在搞 Stellar 空投活动，一共会发送 20 亿个 Lumens，只要 Keybase 账户的条件合格就可以每个月领到 Lumens，详情请参考 <a href="https://keybase.io/airdrop" target="_blank">The Big Stellar Space Drop</a> 页面的介绍。</p>
<h2 id="git-repo">Git 代码仓库</h2>
<p>不得不说 Keybase 新推出的 Git 私有仓库托管服务才是我使用的原因，你可以通过 Keybase 客户端建立自己的私有仓库，也可以为团队建立代码仓库。Keybase 为每个用户和团队提供了 100 GB 的仓库存储空间，实际已经足够用了。Git 代码仓库同样是端对端加密的，只有自己或团队的密钥才能解密。</p>
<p>Keybase 的仓库托管服务是基于 Git 的 remote helpers 功能来实现的，因此仓库地址和常见的 GitHub 不太一样。例如下面是我建立的一个名为 <code>ssicd</code> 的私有代码仓库：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371498/keybase-git.jpg" alt="Keybase Git 代码仓库"></p>
<p>使用 Keybase Git 仓库的机器上必须安装 Keybase 客户端（通过 <code>git-remote-keybase</code> 程序来实现），我就可以通过命令行来 clone 仓库：</p>
<pre class="brush: bash; title: ; notranslate">
~$ git clone keybase://private/nocwat/ssicd.git
</pre>
<p>接下来的使用方法就和其它 Git 仓库没什么区别了，只是 push 等操作时需要使 Keybase 客户端保持运行状态。</p>
<p>另外还有一个小窍门就是 Keybase Git 代码仓库中的文件是可以直接通过 KBFS 进行访问的，这样无需 clone 整个仓库就能下载其中的某个文件，例如上面我的私有仓库目录路径就是：</p>
<pre class="brush: plain; title: ; notranslate">
/keybase/private/nocwat/.kbfs_autogit/ssicd
</pre>
<p>路径中的 <code>.kbfs_autogit</code> 是表示 Git 仓库的特殊目录名，不过需要注意直接列举 <code>/keybase/private/{user}</code> 并不能看到这个目录，需要在目录路径中特别指定。</p>
<p>你甚至可以通过 KBFS 直接访问仓库的某个分支，路径形式如下：</p>
<pre class="brush: plain; title: ; notranslate">
/keybase/private/nocwat/.kbfs_autogit/ssicd/.kbfs_autogit_branch_develop
</pre>
<p>路径中的 <code>.kbfs_autogit_branch_</code> 后面跟 Git 分支名称即可。</p>
<p>你如果想直接访问具体某个 commit 的数据，也可以指定 KBFS 路径：</p>
<pre class="brush: plain; title: ; notranslate">
/keybase/private/nocwat/.kbfs_autogit/ssicd/.kbfs_autogit_commit_XXXXXX
</pre>
<p>路径中的 <code>.kbfs_autogit_commit_</code> 后面跟 commit 的完整 hash 值。</p>
<h2 id="summary">总结</h2>
<p>Keybase 做为一个开源项目，基于 PGP 技术做了很多身份验证之外的扩展和改进，增加了很多加密相关的服务，例如端对端加密的聊天、协作、存储、钱包、Git 仓库等功能确实降低了用户使用门槛。我这段时间用下来还是比较方便的，不过国内某些网络下 Keybase 可能访问速度很慢或者干脆被墙了。</p>
<p>不过 Keybase 设计了新的认证机制而不是直接使用成熟的 PGP 可能会引起一些争议，另外可能由于集成了太多功能导致 Keybase 客户端稍显臃肿，现在 Windows 客户端安装包体积就已经有 180 多 MB 了。其实我倒希望 Keybase 能发布一个体积更小的纯命令行版本，或者能可选安装图形程序（我可能也只需要图形界面的聊天功能）。</p>
<p>目前 Keybase 我使用最多的场景就是 PGP key server、KBFS 文件存储和私有 Git 代码仓库功能了，由于基本没有添加好友，端对端加密聊天功能基本没有用到。</p>
<p>最后我写这篇文章时 Stellar（XLM）价格为 $0.06（0.000006 BTC）还非常低廉，哈哈，欢迎大家通过 Keybase 用户名 <strong><a href="https://keybase.io/nocwat/sigchain#2e5759ea8c98e03c36c49b04282dbef7728a75d39c2b10a31ffe118761a098c022" target="_blank">nocwat</a></strong> 或者 <strong><a href="https://steexp.com/account/GBJZINB72KUDGZQSGJP6BQCQHBNQWXBAS6VQ2H4VTAZRPK5MRCP6DSR3" target="_blank">Stellar 钱包地址</a></strong> 给我打币。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/keybase/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>酷走Android行车记录仪研究</title>
		<link>https://zohead.com/archives/kuzo-android-internal/</link>
		<comments>https://zohead.com/archives/kuzo-android-internal/#comments</comments>
		<pubDate>Wed, 03 Jul 2019 17:04:56 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[ADB]]></category>
		<category><![CDATA[Atom]]></category>
		<category><![CDATA[baksmali]]></category>
		<category><![CDATA[oat2dex]]></category>
		<category><![CDATA[Vysor]]></category>
		<category><![CDATA[行车记录仪]]></category>
		<category><![CDATA[酷走]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1586</guid>
		<description><![CDATA[还是之前用的酷走 Android 行车记录仪，最近放在车上使用倒挺顺畅，不过也有一些明显的问题（例如 SIM 卡未联网时系统时间不正确等），因此想着最好能找到调试这款 Android 行车记录仪的途径。 ADB 调试 最开始想到这款行车记录仪既然是用的 Android 系统，应该是支持 ADB 调试功能的，将行车记录仪的 USB 接口直接用数据线接到我的笔记本电脑上，却没有找到 ADB 设备。以为记录仪内部是不是有一个专用的调试串口之类的，为此找到硬件同事把记录仪拆解了一番： 不过并没有找到除 USB 之外其它的调试接口，这时无意将记录仪通过 USB 接到另一台主机上，却认到 ADB 设备了。 [&#8230;]]]></description>
				<content:encoded><![CDATA[<p>还是之前用的<a href="https://zohead.com/archives/kuzo-recorder/">酷走 Android 行车记录仪</a>，最近放在车上使用倒挺顺畅，不过也有一些明显的问题（例如 SIM 卡未联网时系统时间不正确等），因此想着最好能找到调试这款 Android 行车记录仪的途径。</p>
<h2 id="adb-debug">ADB 调试</h2>
<p>最开始想到这款行车记录仪既然是用的 Android 系统，应该是支持 ADB 调试功能的，将行车记录仪的 USB 接口直接用数据线接到我的笔记本电脑上，却没有找到 ADB 设备。以为记录仪内部是不是有一个专用的调试串口之类的，为此找到硬件同事把记录仪拆解了一番：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737372135/kuzo-teardown.jpg" alt="酷走行车记录仪拆解" title="酷走行车记录仪拆解"></p>
<p>不过并没有找到除 USB 之外其它的调试接口，这时无意将记录仪通过 USB 接到另一台主机上，却认到 ADB 设备了。无奈地认识到自己犯蠢没有用别的机器验证是否和 USB 接口有关系，毕竟把记录仪拆解了上面的样子也基本不可能装回去咯 -_-#。初步估计我的老笔记本 USB 接口供电有问题，记录仪通过 USB 插到 PC 机之后没有独立供电也只能从 PC 机 USB 接口取电。</p>
<p>比较好的消息是我日常使用的 Chromebook 接记录仪也能认到 ADB 设备，这样就能直接通过 <a href="https://chrome.google.com/webstore/detail/vysor/gidgenkbbabolejbgbpnhbimgjbffefm" target="_blank">Vysor</a> 这个 Chrome 应用来访问行车记录仪了，<a href="http://vysor.io/" target="_blank">Vysor</a> 官网上也有 Windows、Mac、Linux 系统下的桌面版本。另外为了方便通过 ADB 访问行车记录仪的 shell 以及上传下载文件之类的，我还在 Chromebook 的 <a href="https://github.com/dnschneid/crouton" target="_blank">crouton</a> 环境中安装了 adb 支持。</p>
<p>启动 Vysor Chrome 应用，授权允许访问 ADB 设备之后，应该就能看到记录仪设备了：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737442964/vysor-devices.png" alt="行车记录仪 ADB 设备" title="行车记录仪 ADB 设备"></p>
<p>上图中的 <code>Full AOSP on Sofia</code> 就是行车记录仪设备，点击 Vysor 上的 <code>View</code> 按钮会自动通过 ADB 在设备上安装 Vysor 受控端 App，稍等片刻就可以看到记录仪的 Android 系统主界面咯：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371940/kuzo-android-ui.png" alt="行车记录仪 Android 主界面" title="行车记录仪 Android 主界面"></p>
<blockquote>
<p><strong>注意</strong></p>
<p>由于通过 USB 数据线将行车记录仪连接到电脑时功率一般达不到要求的 5V / 2A，记录仪自带的 250 mAh 电池续航时间也有限，因此 ADB 调试操作前最好先使用标准 5V / 2A 充电器将记录仪充满电。</p>
</blockquote>
<h2 id="system-hardware">系统和硬件信息</h2>
<p>解锁进入系统之后，可以先看看我这台行车记录仪的系统版本信息，看起来比较明显用的是 Android 5.1.1 AOSP 版本，内核版本为 3.14.0，版本号为 2.1.5：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737371891/kuzo-android-about.png" alt="行车记录仪 Android 版本" title="行车记录仪 Android 版本"></p>
<p>值得一提的是另外一台同样型号的酷走行车记录仪，通过 Vysor Chrome 应用能找到 ADB 设备，但是自动安装 Vysor App 之后无法正确显示 Android 系统主界面，这样只能通过 ADB Shell 来获取系统版本信息了：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ adb shell getprop | grep 'ro\.build\.'
[ro.build.characteristics]: [tablet]
[ro.build.date.utc]: [1473825617]
[ro.build.date]: [Wed Sep 14 12:00:17 CST 2016]
[ro.build.description]: [Sf3gr_mrd6_p2_720-userdebug 5.1.1 LMY47V eng.softteam.20160914.115752 release-keys]
[ro.build.display.id]: [2.2.3]
[ro.build.fingerprint]: [Intel/Sf3gr_mrd6_p2_720/5.1.1/LMY47V/softteam09141159:userdebug/release-keys]
[ro.build.flavor]: [Sf3gr_mrd6_p2_720-userdebug]
[ro.build.host]: [pdd-build]
[ro.build.id]: [LMY47V]
[ro.build.product]: [Sf3gr_mrd6_p2_720]
[ro.build.tags]: [release-keys]
[ro.build.type]: [userdebug]
[ro.build.user]: [softteam]
[ro.build.version.all_codenames]: [REL]
[ro.build.version.base_os]: []
[ro.build.version.codename]: [REL]
[ro.build.version.incremental]: [eng.softteam.20160914.115752]
[ro.build.version.release]: [5.1.1]
[ro.build.version.sdk]: [22]
[ro.build.version.security_patch]: [2015-11-01]
</pre>
<p>可以看到这台记录仪的版本号是新一点的 2.2.3，进入 Shell 查看内核版本：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ $ uname -a
Linux localhost 3.14.0 #1 SMP PREEMPT Wed Sep 14 12:14:50 CST 2016 i686 GNU/Linux
</pre>
<p>记录仪使用的是 Intel SOFIA Atom x3 超低功耗 x86 处理器，下面的 <code>/proc/cpuinfo</code> 输出只列出了 4 个核中第一个核的信息，从核数和缓存大小来看具体处理器型号估计是最大频率 1.1 GHz 的 <a href="https://ark.intel.com/content/www/us/en/ark/products/87461/intel-atom-x3-c3230rk-processor-1m-cache-up-to-1-10-ghz.html" target="_blank">x3-C3230RK</a>。</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ $ cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 93
model name      : Genuine Intel(R) CPU         @ 728
stepping        : 1
microcode       : 0x102
cpu MHz         : 416.000
cache size      : 512 KB
physical id     : 0
siblings        : 4
core id         : 1
cpu cores       : 4
apicid          : 2
initial apicid  : 2
fdiv_bug        : no
f00f_bug        : no
coma_bug        : no
fpu             : yes
fpu_exception   : yes
cpuid level     : 11
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat clflush mmx fxsr sse sse2 ss ht nx rdtscp lm constant_tsc arch_perfmon xtopology pni pclmulqdq monitor ssse3 cx16 xtpr pdcm sse4_1 sse4_2 movbe popcnt aes lahf_lm 3dnowprefetch tsc_adjust smep erms
bogomips        : 892.92
clflush size    : 64
cache_alignment : 64
address sizes   : 32 bits physical, 48 bits virtual
power management:
</pre>
<p>x3-C3230RK 处理器集成了 A-GOLD 620 射频收发器，支持 3G、Wi-Fi（802.11 B/G/N）、蓝牙 4.0、GPS 以及 FM 功能，这几项功能记录仪基本都用上了。GPU 则是 600 MHz 的 Mali-450 MP4。处理器最大支持 2 GB 内存，不过这款行车记录仪上只带了 1 GB 内存：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ $ cat /proc/meminfo
MemTotal:         948616 kB
MemFree:           26592 kB
MemAvailable:     498968 kB
Buffers:            3696 kB
Cached:           498580 kB
</pre>
<p>我们也可以使用 <code>dumpsys</code> 命令查询记录仪电池信息：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ $ dumpsys battery
Current Battery Service state:
  AC powered: false
  USB powered: true
  Wireless powered: false
  status: 2
  health: 2
  present: true
  level: 0
  scale: 100
  voltage: 3289
  temperature: 240
  technology: Li-ion
</pre>
<h2 id="system-app">系统 App</h2>
<p>上面的 Android 主界面里可以看到 Tong-server 和 VideoRecorderService 这两个服务在运行，分别对应记录仪的整体管理功能和录像功能，使用老版本系统的记录仪上可以打开 VideoServer 录像的系统 App 界面：</p>
<p><img src="http://res.cloudinary.com/digwht2y0/image/upload/v1737372135/kuzo-video-server.png" alt="行车记录仪录像 App" title="行车记录仪录像 App"></p>
<p>对于 Vysor 不能打开图形界面的新系统可以用命令列举系统 App 列表（下面只列出了关键的几个）：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ $ pm list packages -f
package:/system/app/KuzoInstaller/KuzoInstaller.apk=com.hpyl.kuzo.installer
package:/system/app/ArcCamera/ArcCamera.apk=com.arcsoft.camera2
package:/system/app/KuzoLauncher/KuzoLauncher.apk=com.hpyl.kuzolauncher
package:/system/app/TongTermManager/TongTermManager.apk=com.kh.tong.termmanager
package:/system/app/VideoServer/VideoServer.apk=com.kh.videoserver
</pre>
<p>然后使用 <code>dumpsys</code> 命令查看相应 App 的信息：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ $ dumpsys package com.kh.tong.termmanager
  Package [com.kh.tong.termmanager] (24d3573f):
    userId=10016 gids=[3003, 1028, 1015, 1023, 3002, 3001]
    pkg=Package{c4b880c com.kh.tong.termmanager}
    codePath=/system/app/TongTermManager
    resourcePath=/system/app/TongTermManager
    legacyNativeLibraryDir=/system/app/TongTermManager/lib
    primaryCpuAbi=armeabi-v7a
    secondaryCpuAbi=null
    versionCode=1000027 targetSdk=23
    versionName=1.0.27
shell@Sf3gr_mrd6_p2_720:/ $ dumpsys package com.kh.videoserver
  Package [com.kh.videoserver] (31228436):
    userId=10018 gids=[3003, 1028, 1015, 1023]
    pkg=Package{27beef37 com.kh.videoserver}
    codePath=/system/app/VideoServer
    resourcePath=/system/app/VideoServer
    legacyNativeLibraryDir=/system/app/VideoServer/lib
    primaryCpuAbi=null
    secondaryCpuAbi=null
    versionCode=1000027 targetSdk=22
    versionName=1.0.27
</pre>
<p>可以看到新版本系统下 <code>Tong-server</code> 和 <code>VideoServer</code> App 都是 1.0.27 版本，如果把系统 App 的 apk 文件拷贝出来了也能用 <code>aapt</code> 命令查看：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ aapt dump badging TongTermManager.apk | grep &quot;VersionName&quot;
package: name='com.kh.tong.termmanager' versionCode='1000027' versionName='1.0.27' platformBuildVersionName='5.1.1-1819727'
(xenial)zzm@localhost:~$ aapt dump badging VideoServer.apk | grep &quot;VersionName&quot;
package: name='com.kh.videoserver' versionCode='1000027' versionName='1.0.27' platformBuildVersionName='5.1.1-1819727'
</pre>
<p>老版本系统下这两个 App 也是低一些的 1.0.23 版本：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ $ dumpsys package com.kh.tong.termmanager
  Package [com.kh.tong.termmanager] (2d37c5ca):
    userId=10017 gids=[3003, 1028, 1015, 1023, 3002, 3001]
    pkg=Package{33ef33b com.kh.tong.termmanager}
    codePath=/system/app/TongTermManager
    resourcePath=/system/app/TongTermManager
    legacyNativeLibraryDir=/system/app/TongTermManager/lib
    primaryCpuAbi=x86
    secondaryCpuAbi=null
    versionCode=1000023 targetSdk=22
    versionName=1.0.23
</pre>
<h2 id="mod-system">修改系统</h2>
<p>记录仪 Android AOSP 系统自带了 root 权限，如果需要修改系统，可以先关闭系统分区保护：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ adb disable-verity
Verity disabled on /system
Now reboot your device for settings to take effect
</pre>
<p>运行 <code>adb reboot</code> 命令重启记录仪之后，就可以直接 <code>su</code> 切换为 root 用户，重新挂载修改系统分区了：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ # mount -o remount,rw /dev/block/platform/soc0/e0000000.noc/by-name/ImcPartID068 /system
</pre>
<h2 id="update-system-app">尝试升级系统 App</h2>
<p>经过测试我发现 2.1.5 老版本系统相比新的 2.2.3 版本存在以下问题：</p>
<ul>
<li>音乐播放的暂停状态无法保存，记录仪重新开机时始终恢复播放上次的歌曲，这个时候只能使用蓝牙按键或者手机酷走 App 暂停播放；</li>
<li>记录仪录的视频画面中没有当前车速和时间 OSD 显示。</li>
</ul>
<p>因此我想着将 2.1.5 版本系统里的 <code>Tong-server</code> 和 <code>VideoServer</code> App 升级为 2.2.3 里自带的新版本，是不是就能解决这些问题。</p>
<p>不过由于这两个系统 App 的 apk 文件是不完整的，apk 中不包含 <code>classes.dex</code> 并且有单独的 odex 文件，这样的 apk 文件也无法直接安装，最好能先将 odex 反编译出来。</p>
<p>我先试了常用的 <a href="https://github.com/JesusFreke/smali" target="_blank">smali/baksmali</a> 来反编译 <code>VideoServer</code> 的 odex，注意 <code>baksmali</code> 更新之后命令的使用方式和网上很多文章不太一样了：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ java -jar baksmali-2.2.7.jar x VideoServer/x86/VideoServer.odex
Exception in thread &quot;main&quot; org.jf.dexlib2.DexFileFactory$UnsupportedOatVersionException: Unsupported oat version: 48
        at org.jf.dexlib2.DexFileFactory.loadDexFile(DexFileFactory.java:120)
        at org.jf.baksmali.DexInputCommand.loadDexFile(DexInputCommand.java:149)
        at org.jf.baksmali.DisassembleCommand.run(DisassembleCommand.java:162)
        at org.jf.baksmali.Main.main(Main.java:102)
</pre>
<p>额，原来 <code>baksmali</code> 并不支持比较老的 Android 5.1 版本。</p>
<p>看起来我只能使出 <a href="https://github.com/testwhat/SmaliEx" target="_blank">oat2dex</a> 工具来反编译试试了，<code>oat2dex</code> 使用上比 <code>baksmali</code> 复杂一些：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~/Downloads/tmp$ java -jar oat2dex.jar
Easy oat2dex 0.90
Usage:
 java -jar oat2dex.jar [options] &lt;action&gt;
[options]
 Api level: -a &lt;integer&gt;
 Output folder: -o &lt;folder path&gt;
 Print detail : -v
&lt;action&gt;
 Get dex of boot(.oat) : boot &lt;boot.oat/boot-folder&gt;
 Get dex (de-optimize) : &lt;oat/odex file&gt; &lt;boot-class-folder&gt;
                         &lt;vdex file&gt;
 Get raw odex          : odex &lt;oat/odex/vdex file&gt;
 Get raw odex smali    : smali &lt;oat/odex/vdex file&gt;
 Deodex framework (exp): devfw [empty or path of /system/framework/]
</pre>
<p>首先需要将新系统上的 <code>boot.oat</code> 文件拷贝出来：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ adb pull /data/dalvik-cache/x86/system@framework@boot.oat .
2063 KB/s (57823168 bytes in 27.362s)
</pre>
<p>使用 <code>oat2dex</code> 将 <code>boot.oat</code> 反优化为 dex 文件，默认会输出到当前目录下的 <code>boot.oat-dex</code> 子目录：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~/Downloads/tmp$ java -jar oat2dex.jar boot boot.oat
06-28 22:07:36:757 Art version=48 (boot.oat)
06-28 22:07:36:890 De-optimizing /system/framework/core-libart.jar
06-28 22:07:48:650 Output to /home/zzm/boot.oat-dex/core-libart.dex
06-28 22:07:48:651 De-optimizing /system/framework/conscrypt.jar
06-28 22:07:50:095 Output to /home/zzm/boot.oat-dex/conscrypt.dex
06-28 22:07:50:096 De-optimizing /system/framework/okhttp.jar
06-28 22:07:50:750 Output to /home/zzm/boot.oat-dex/okhttp.dex
06-28 22:07:50:751 De-optimizing /system/framework/core-junit.jar
06-28 22:07:50:801 Output to /home/zzm/boot.oat-dex/core-junit.dex
06-28 22:07:50:802 De-optimizing /system/framework/bouncycastle.jar
06-28 22:07:53:226 Output to /home/zzm/boot.oat-dex/bouncycastle.dex
06-28 22:07:53:228 De-optimizing /system/framework/ext.jar
06-28 22:07:55:777 Output to /home/zzm/boot.oat-dex/ext.dex
06-28 22:07:55:777 De-optimizing /system/framework/framework.jar
06-28 22:08:13:350 Output to /home/zzm/boot.oat-dex/framework.dex
06-28 22:08:13:351 De-optimizing /system/framework/framework.jar:classes2.dex
06-28 22:08:15:468 Analysis error in class=Landroid/widget/NumberPicker; method=&lt;init&gt;
Method: Landroid/widget/NumberPicker;-&gt;&lt;init&gt;(Landroid/content/Context;Landroid/util/AttributeSet;II)V
Near line: 604 (address 75)
Instructions: 
 [36] const/4 regA=15
 [37] move-object/from16 regA=0 regB=19
 [38] iput-boolean-quick regA=15 regB=0 &lt;-----
 [39] const/16 regA=15
 [40] const/16 regA=16

06-28 22:08:15:470 org.jf.dexlib2.analysis.AnalysisException: Could not resolve the field in class (Reference,Landroid/content/Context;) at offset 778 in &lt;init&gt;
        at org.jf.dexlib2.analysis.MethodAnalyzer.analyzeIputIgetQuick(MethodAnalyzer.java:2422)
        at org.jf.dexlib2.analysis.MethodAnalyzer.analyzeInstruction(MethodAnalyzer.java:1269)
        at org.jf.dexlib2.analysis.MethodAnalyzer.analyze(MethodAnalyzer.java:259)
        at org.jf.dexlib2.analysis.MethodAnalyzer.&lt;init&gt;(MethodAnalyzer.java:190)
        at org.rh.smaliex.deopt.OdexRewriter$OdexRewriterModule$1$1.getInstructions(OdexRewriter.java:232)
        ...(Skip 24 traces)
        at org.rh.smaliex.OatUtil.convertToDex(OatUtil.java:271)
        at org.rh.smaliex.OatUtil.convertDexFromBootOat(OatUtil.java:192)
        at org.rh.smaliex.OatUtil.bootOat2Dex(OatUtil.java:75)
        at org.rh.smaliex.Main.mainImpl(Main.java:106)
        at org.rh.smaliex.Main.main(Main.java:43)

06-28 22:08:15:471 Failed to re-construct dex java.lang.ClassCastException: org.jf.dexlib2.analysis.UnresolvedOdexInstruction (in module: Unnamed Module) cannot be cast to org.jf.dexlib2.iface.instruction.formats.Instruction22cs (in module: Unnamed Module)
06-28 22:08:15:471 convertToDex: skip /system/framework/framework.jar:classes2.dex
06-28 22:08:15:472 De-optimizing /system/framework/telephony-common.jar
06-28 22:08:18:110 Output to /home/zzm/boot.oat-dex/telephony-common.dex
06-28 22:08:18:111 De-optimizing /system/framework/voip-common.jar
06-28 22:08:18:313 Output to /home/zzm/boot.oat-dex/voip-common.dex
06-28 22:08:18:314 De-optimizing /system/framework/ims-common.jar
06-28 22:08:18:503 Output to /home/zzm/boot.oat-dex/ims-common.dex
06-28 22:08:18:519 De-optimizing /system/framework/mms-common.jar
06-28 22:08:18:579 Output to /home/zzm/boot.oat-dex/mms-common.dex
06-28 22:08:18:599 De-optimizing /system/framework/android.policy.jar
06-28 22:08:19:007 Output to /home/zzm/boot.oat-dex/android.policy.dex
06-28 22:08:19:008 De-optimizing /system/framework/apache-xml.jar
06-28 22:08:20:770 Output to /home/zzm/boot.oat-dex/apache-xml.dex
</pre>
<p>处理 <code>boot.oat</code> 文件有些报错我们先忽略，接着我们可以用 <code>oat2dex</code> 反优化系统 App 的 odex 文件，这里用到了刚才 <code>boot.oat</code> 反优化出来的 dex 文件，类似的会输出到当前目录的子目录：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ java -jar oat2dex.jar VideoServer.odex boot.oat-dex/
06-28 22:13:09:212 Art version=48 (VideoServer.odex)
06-28 22:13:09:250 De-optimizing /system/app/VideoServer/VideoServer.apk
06-28 22:13:27:979 Output to /home/zzm/VideoServer.odex-dex/VideoServer.dex
</pre>
<p>反编译得到的 dex 文件，并重新编译成 <code>classes.dex</code>：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ java -jar baksmali-2.2.7.jar disassemble VideoServer.odex-dex/VideoServer.dex -o video
(xenial)zzm@localhost:~$ java -jar smali-2.2.7.jar assemble video -o classes.dex
</pre>
<p>然后将 <code>classes.dex</code> 打包添加到原来的 apk 文件中：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ aapt add VideoServer.apk classes.dex
</pre>
<p>安装新的 apk 文件存在报错：</p>
<pre class="brush: plain; title: ; notranslate">
W/PackageManager(  584): Package com.kh.videoserver signatures do not match the previously installed version; ignoring!
</pre>
<p>原来是没有签名，这里我使用 <a href="https://github.com/patrickfav/uber-apk-signer" target="_blank">Uber Apk Signer</a> 工具对 apk 进行重新签名：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ java -jar uber-apk-signer-1.0.0.jar -a VideoServer.apk
</pre>
<p>初步测试新编译出来的 <code>VideoServer</code> apk 可以正常安装启动，不过 <code>oat2dex</code> 处理 <code>Tong-server</code> 新版本 App 的时候就有问题了：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ java -jar oat2dex.jar TongTermManager.odex boot.oat-dex/
06-29 23:02:32:138 Art version=48 (TongTermManager.odex)
06-29 23:02:32:203 De-optimizing /system/app/TongTermManager/TongTermManager.apk
06-29 23:02:58:217 Output to /home/zzm/TongTermManager.odex-dex/TongTermManager.dex
06-29 23:02:58:217 De-optimizing /system/app/TongTermManager/TongTermManager.apk:classes2.dex
06-29 23:03:04:420 Analysis error in class=Lorg/jsoup/parser/Tag; method=&lt;clinit&gt;
Method: Lorg/jsoup/parser/Tag;-&gt;&lt;clinit&gt;()V
Near line: 295 (address 1223)
Instructions: 
 [624] check-cast regA=0
 [625] invoke-static regC=0
 [626] iput-boolean-quick regA=7 regB=0 &lt;-----
 [627] add-int/lit8 regA=3 regB=3
 [628] goto

06-29 23:03:04:433 org.jf.dexlib2.analysis.AnalysisException: Could not resolve the field in class (Reference,Lorg/jsoup/parser/Tag;) at offset 15 in &lt;clinit&gt;
        at org.jf.dexlib2.analysis.MethodAnalyzer.analyzeIputIgetQuick(MethodAnalyzer.java:2422)
        at org.jf.dexlib2.analysis.MethodAnalyzer.analyzeInstruction(MethodAnalyzer.java:1269)
        at org.jf.dexlib2.analysis.MethodAnalyzer.analyze(MethodAnalyzer.java:259)
        at org.jf.dexlib2.analysis.MethodAnalyzer.&lt;init&gt;(MethodAnalyzer.java:190)
        at org.rh.smaliex.deopt.OdexRewriter$OdexRewriterModule$1$1.getInstructions(OdexRewriter.java:232)
        ...(Skip 24 traces)
        at org.rh.smaliex.OatUtil.convertToDex(OatUtil.java:271)
        at org.rh.smaliex.OatUtil.convertDexFromBootOat(OatUtil.java:192)
        at org.rh.smaliex.OatUtil.oat2dex(OatUtil.java:81)
        at org.rh.smaliex.Main.mainImpl(Main.java:127)
        at org.rh.smaliex.Main.main(Main.java:43)

06-29 23:03:04:437 Failed to re-construct dex java.lang.ClassCastException: org.jf.dexlib2.analysis.UnresolvedOdexInstruction (in module: Unnamed Module) cannot be cast to org.jf.dexlib2.iface.instruction.formats.Instruction22cs (in module: Unnamed Module)
06-29 23:03:04:437 convertToDex: skip /system/app/TongTermManager/TongTermManager.apk:classes2.dex
</pre>
<p>这个问题目前还没有找到好的解决方法，由于担心整个系统升级也会碰到 Vysor 主界面无法显示的问题，而且酷走记录仪的生产厂家也不提供支持没有单独的升级包，老版本系统的管理和录像 App 也就继续使用了。</p>
<h2 id="mod-app-config">修改系统 App 配置</h2>
<h3 id="tong-server">Tong-server</h3>
<p>前面说的 2.1.5 版本系统音乐播放的暂停状态问题，可以看看 <code>Tong-server</code> 系统 App 的配置文件 <code>/data/data/com.kh.tong.termmanager/shared_prefs/KUZOConfig.xml</code>，例如我的记录仪配置：</p>
<pre class="brush: xml; title: ; notranslate">
&lt;?xml version='1.0' encoding='utf-8' standalone='yes' ?&gt;
&lt;map&gt;
    &lt;string name=&quot;LOCAL_SAVE_MUSIC_INFO_KEY2F06A8D7CA314388B58DC46719702844&quot;&gt;{&amp;quot;des&amp;quot;:null,&amp;quot;id&amp;quot;:0,&amp;quot;md5&amp;quot;:&amp;quot;bdb368fe-b568-4157-9b17-818104295f57&amp;quot;,&amp;quot;seekProgress&amp;quot;:5418,&amp;quot;title&amp;quot;:null,&amp;quot;url&amp;quot;:&amp;quot;/storage/sdcard1/Kuzo/Music/2F06A8D7CA314388B58DC46719702844/15/15/bdb368fe-b568-4157-9b17-818104295f57.mp3&amp;quot;}&lt;/string&gt;
    &lt;string name=&quot;USER_TERM_MUSIC_LIST_KEY&quot;&gt;[{&amp;quot;fileSize&amp;quot;:4008624,&amp;quot;id&amp;quot;:0,&amp;quot;length&amp;quot;:0,&amp;quot;md5&amp;quot;:&amp;quot;bdb368fe-b568-4157-9b17-818104295f57&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;?? ?? - ?????&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/storage/sdcard1/Kuzo/Music/2F06A8D7CA314388B58DC46719702844/0/0/bdb368fe-b568-4157-9b17-818104295f57.mp3&amp;quot;,&amp;quot;singer&amp;quot;:null,&amp;quot;userUuid&amp;quot;:&amp;quot;2F06A8D7CA314388B58DC46719702844&amp;quot;,&amp;quot;uuid&amp;quot;:&amp;quot;bdb368fe-b568-4157-9b17-818104295f57&amp;quot;},{&amp;quot;fileSize&amp;quot;:6749379,&amp;quot;id&amp;quot;:0,&amp;quot;length&amp;quot;:0,&amp;quot;md5&amp;quot;:&amp;quot;b49b55b0-ae7c-47d5-9cb4-6cf03249efdc&amp;quot;,&amp;quot;name&amp;quot;:&amp;quot;????? - ???&amp;quot;,&amp;quot;path&amp;quot;:&amp;quot;/storage/sdcard1/Kuzo/Music/2F06A8D7CA314388B58DC46719702844/1/0/b49b55b0-ae7c-47d5-9cb4-6cf03249efdc.mp3&amp;quot;,&amp;quot;singer&amp;quot;:null,&amp;quot;userUuid&amp;quot;:&amp;quot;2F06A8D7CA314388B58DC46719702844&amp;quot;,&amp;quot;uuid&amp;quot;:&amp;quot;b49b55b0-ae7c-47d5-9cb4-6cf03249efdc&amp;quot;}]&lt;/string&gt;
    &lt;string name=&quot;userUUID&quot;&gt;2F06A8D7CA314388B58DC46719702844&lt;/string&gt;
    &lt;string name=&quot;LOCAL_SAVE_RADIO_INFO_KEY2F06A8D7CA314388B58DC46719702844&quot;&gt;{&amp;quot;coverUrlLarge&amp;quot;:null,&amp;quot;coverUrlSmall&amp;quot;:null,&amp;quot;id&amp;quot;:-1,&amp;quot;isCollected&amp;quot;:false,&amp;quot;kind&amp;quot;:null,&amp;quot;programName&amp;quot;:null,&amp;quot;radioDesc&amp;quot;:null,&amp;quot;radioName&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;radioPlayCount&amp;quot;:null,&amp;quot;rate24AacUrl&amp;quot;:&amp;quot;http://live.xmcdn.com/live/536/24.m3u8&amp;quot;,&amp;quot;rate24TsUrl&amp;quot;:&amp;quot;http://live.xmcdn.com/live/536/24.m3u8&amp;quot;,&amp;quot;rate64AacUrl&amp;quot;:&amp;quot;http://live.xmcdn.com/live/536/24.m3u8&amp;quot;,&amp;quot;rate64TsUrl&amp;quot;:&amp;quot;http://live.xmcdn.com/live/536/24.m3u8&amp;quot;,&amp;quot;scheduleID&amp;quot;:null,&amp;quot;supportBitRates&amp;quot;:null,&amp;quot;updatedAt&amp;quot;:null}&lt;/string&gt;
    &lt;boolean name=&quot;LOCAL_SAVE_MUSIC_IS_PAUSE_KEY&quot; value=&quot;false&quot; /&gt;
    &lt;string name=&quot;USER_TERM_TONG_SESSION_LIST_KEY&quot;&gt;[]&lt;/string&gt;
    &lt;int name=&quot;com.kh.tong.term.statemachine.TermMenuSM.LAST_SAVED_MENU_KEY&quot; value=&quot;0&quot; /&gt;
    &lt;int name=&quot;com.kh.tong.term.statemachine.TermAudioVolumeSM.PrefKeyVolumn&quot; value=&quot;7&quot; /&gt;
    &lt;boolean name=&quot;LOCAL_SAVE_RADIO_IS_PAUSE_KEY&quot; value=&quot;false&quot; /&gt;
    &lt;string name=&quot;Password&quot;&gt;8888&lt;/string&gt;
&lt;/map&gt;
</pre>
<p>主要配置项如下：</p>
<ul>
<li><code>userUUID</code> 应该是酷走 App 的用户 ID；</li>
<li><code>USER_TERM_MUSIC_LIST_KEY</code> 是通过酷走 App 收藏上传的歌曲列表；</li>
<li><code>LOCAL_SAVE_MUSIC_INFO_KEYxxx</code> 是当前用户的歌曲播放信息；</li>
<li><code>LOCAL_SAVE_RADIO_INFO_KEYxxx</code> 是通过酷走 App 收藏添加的在线电台列表；</li>
<li><code>LOCAL_SAVE_MUSIC_IS_PAUSE_KEY</code> 表示保存的音乐播放暂停状态；</li>
<li><code>LOCAL_SAVE_RADIO_IS_PAUSE_KEY</code> 表示保存的电台播放暂停状态；</li>
<li><code>com.kh.tong.term.statemachine.TermMenuSM.LAST_SAVED_MENU_KEY</code> 表示保存的菜单键状态（0：音乐，1：点播，2：电台）；</li>
<li><code>com.kh.tong.term.statemachine.TermAudioVolumeSM.PrefKeyVolumn</code> 为系统音量值；</li>
<li><code>Password</code> 为记录仪的管理密码。</li>
</ul>
<p>对于老版本系统音乐播放的暂停状态得不到保存的问题，可以修改系统脚本强制设为暂停状态，需要播放音乐时通过蓝牙按键启动，我是在 <code>/system/bin/load_iwlwifi.sh</code> 文件后附加操作命令的：</p>
<pre class="brush: bash; title: ; notranslate">
shell@Sf3gr_mrd6_p2_720:/ # cat &gt;&gt; /system/bin/load_iwlwifi.sh
KUZO_CFG=&quot;/data/data/com.kh.tong.termmanager/shared_prefs/KUZOConfig.xml&quot;

busybox sed -i '/LOCAL_SAVE_MUSIC_IS_PAUSE_KEY/s/value=&quot;.*&quot;/value=&quot;true&quot;/' $KUZO_CFG
busybox sed -i '/LOCAL_SAVE_RADIO_IS_PAUSE_KEY/s/value=&quot;.*&quot;/value=&quot;true&quot;/' $KUZO_CFG
</pre>
<h3 id="videoserver">VideoServer</h3>
<p>录像 App 的配置文件 <code>/data/data/com.kh.videoserver/shared_prefs/com.kh.videoserver_preferences.xml</code> 比较简单：</p>
<pre class="brush: xml; title: ; notranslate">
&lt;?xml version='1.0' encoding='utf-8' standalone='yes' ?&gt;
&lt;map&gt;
    &lt;string name=&quot;rtsp_port&quot;&gt;8886&lt;/string&gt;
&lt;/map&gt;
</pre>
<p>可以通过 <code>rtsp_port</code> 参数修改视频服务器的 RTSP 端口号。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/kuzo-android-internal/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>龙芯MIPS64 QEMU ioctl的问题</title>
		<link>https://zohead.com/archives/loongson-qemu-ioctl/</link>
		<comments>https://zohead.com/archives/loongson-qemu-ioctl/#comments</comments>
		<pubDate>Wed, 26 Dec 2018 16:59:20 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[ioctl]]></category>
		<category><![CDATA[MIPS64]]></category>
		<category><![CDATA[QEMU]]></category>
		<category><![CDATA[龙芯]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1561</guid>
		<description><![CDATA[龙芯 QEMU 问题 最近搞了一块使用国产龙芯 3A3000 处理器的开源开发板，顺便捣鼓移植我们的服务程序，安装龙芯开源社区的 Loongnix 系统之后测试下来 3A3000 处理器的性能还是基本能用的。龙芯处理器自带的内存控制器也能到 DDR3-1600 了，只是目前还在使用 HT3.0 总线确实拖累系统性能。 我们部署的一些服务目前还依赖几个 x86 系统的闭源程序，无法直接在龙芯 MIPS64 系统下运行，就想到使用 QEMU 的 User-mode emulation 来实现了。 Loongnix 系统中的 qemu-i386 运行比较简单的 x86 程序一般都没有问题，不过我发现 [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="loongson-qemu-issue">龙芯 QEMU 问题</h2>
<p>最近搞了一块使用国产<a href="http://www.loongson.cn/product/cpu/3/3A3000.html" target="_blank">龙芯 3A3000</a> 处理器的开源开发板，顺便捣鼓移植我们的服务程序，安装龙芯开源社区的 <a href="http://www.loongnix.org/" target="_blank">Loongnix</a> 系统之后测试下来 3A3000 处理器的性能还是基本能用的。龙芯处理器自带的内存控制器也能到 DDR3-1600 了，只是目前还在使用 HT3.0 总线确实拖累系统性能。</p>
<p>我们部署的一些服务目前还依赖几个 x86 系统的闭源程序，无法直接在龙芯 MIPS64 系统下运行，就想到使用 QEMU 的 User-mode emulation 来实现了。</p>
<p>Loongnix 系统中的 qemu-i386 运行比较简单的 x86 程序一般都没有问题，不过我发现即使用打了 ioctl patch 的 QEMU 运行其中一个使用了私有 ioctl 调用的 x86 程序仍然会出错，通过开启 qemu-i386 的 strace 可以看到错误信息：</p>
<pre class="brush: bash; title: ; notranslate">
zzm@loongnix:~$ qemu-i386 -strace /lib/i386-linux-gnu/ld-linux.so.2 --library-path /lib/i386-linux-gnu /usr/sbin/test-agent

2459 ioctl(5,-1055634175,-42368,0,0,-42056) = -1 errno=25 (Inappropriate ioctl for device)
2459 ioctl(5,-1055634175,-41488,0,0,-41176) = -1 errno=25 (Inappropriate ioctl for device)
2459 ioctl(5,-1055634175,-44192,0,0,-43880) = -1 errno=25 (Inappropriate ioctl for device)
</pre>
<p>上面的 <code>/usr/sbin/test-agent</code> 就是要模拟运行的 x86 二进制程序，<code>/lib/i386-linux-gnu</code> 是我在龙芯 MIPS64 系统上增加的 x86 程序依赖基础库目录，其中最重要的就是 <code>libc.so.6</code> 和 <code>ld-linux.so.2</code> 库了。</p>
<p>从 strace 数据应该可以发现该 ioctl 已经发到设备，但是 kernel 返回 <code>Inappropriate ioctl</code> 错误，而 <code>-1055634175</code>  则表示 x86 二进制程序发送的是 compat ioctl，此时我才想起来确认下 qemu-i386 程序是否正确：</p>
<pre class="brush: bash; title: ; notranslate">
zzm@loongnix:~$ file `which qemu-i386`
/usr/bin/qemu-i386: ELF 64-bit LSB executable, MIPS, MIPS64 rel2 version 1 (SYSV), dynamically linked (uses shared libs), BuildID[sha1]=39cac8103f052b4ffa161f615159c888024ef4e3, for GNU/Linux 2.6.32, stripped
</pre>
<p>龙芯 Loongnix MIPS64 系统目前兼容几种 ABI：</p>
<ul>
<li>O32：MIPS 32 位处理器使用；</li>
<li>N64：只适用于 MIPS 64 位处理器，使用 64 位指针和 long integers；</li>
<li>N32：只适用于 MIPS 64 位处理器，在保留 MIPS N64 ABI 的几乎所有特性的情况下，使用 32 位指针和 long integers。</li>
</ul>
<p>这个 qemu-i386 使用的是 MIPS64 N64 ABI，而 qemu-i386 实际运行的 x86 二进制程序在龙芯 64 位 kernel 下会发送 compat ioctl，这样最终 qemu-i386 程序调用 compat ioctl 时就显然会有问题，报 <code>Inappropriate ioctl</code> 也就不奇怪了。</p>
<p>解决方法也就很简单了，我只要重新编译出使用 N32 或者 O32 ABI 的 qemu-i386 程序应该就能解决，为了性能考虑可以尽量使用 N32 ABI。</p>
<h2 id="n32-abi-qemu">编译 N32 ABI QEMU</h2>
<p>首先需要在 Loongnix MIPS64 系统下安装编译 QEMU 需要的 N32 版本的开发包，这里我只安装了 QEMU 用户模式需要的开发包：</p>
<pre class="brush: bash; title: ; notranslate">
zzm@loongnix:~$ yum install glibc-devel.mipsn32el glib2-devel.mipsn32el zlib-devel.mipsn32el libffi.mipsn32el pixman-devel.mipsn32el
</pre>
<blockquote>
<p><strong>提示</strong></p>
<p>Loongnix 系统的 N64 相关库文件一般放在 <code>/usr/lib64</code> 目录下，N32 相关库文件在 <code>/usr/lib32</code> 目录下，而 O32 相关库文件则在 <code>/usr/lib</code> 目录下。</p>
</blockquote>
<p>然后进入 QEMU 源代码目录重新 configure，增加特定编译参数指定生成 N32 位的程序：</p>
<pre class="brush: bash; title: ; notranslate">
zzm@loongnix:qemu$ PKG_CONFIG_LIBDIR=/usr/lib32/pkgconfig ./configure --target-list=i386-linux-user --disable-kvm --disable-xen --enable-pie --extra-cflags=-mabi=n32
</pre>
<p>简单说明一下：</p>
<ul>
<li><code>PKG_CONFIG_LIBDIR</code> 环境变量指定使用什么版本的 pkgconfig 查找库及开发包，这里使用 <code>/usr/lib32/pkgconfig</code> N32 版本；</li>
<li><code>--target-list</code> 指定 QEMU 编译目标，这里我们只需要 qemu-i386，因此把 KVM、Xen 等功能也禁用了；</li>
<li><code>--extra-cflags</code> 指定额外编译参数为 <code>-mabi=n32</code>，这样编译和链接时都是 N32 版本了。</li>
</ul>
<p>重新编译安装之后可以看到新的 qemu-i386 使用的就是 N32 ABI 了：</p>
<pre class="brush: bash; title: ; notranslate">
zzm@loongnix:~$ file `which qemu-i386`
/usr/local/bin/qemu-i386: ELF 32-bit LSB executable, MIPS, N32 MIPS64 rel2 version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=12e0025c42a9a63b8045c31159950f9b185f8745, stripped
</pre>
<p>最后测试使用新的 qemu-i386 运行依赖 ioctl 的 x86 二进制程序，现在就没有直接报错的问题了。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/loongson-qemu-ioctl/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>初探酷走Android行车记录仪</title>
		<link>https://zohead.com/archives/kuzo-recorder/</link>
		<comments>https://zohead.com/archives/kuzo-recorder/#comments</comments>
		<pubDate>Tue, 30 Oct 2018 15:44:55 +0000</pubDate>
		<dc:creator><![CDATA[Uranus Zhou]]></dc:creator>
				<category><![CDATA[Android]]></category>
		<category><![CDATA[技术]]></category>
		<category><![CDATA[RTSP]]></category>
		<category><![CDATA[行车记录仪]]></category>
		<category><![CDATA[酷走]]></category>

		<guid isPermaLink="false">https://zohead.com/?p=1554</guid>
		<description><![CDATA[酷走记录仪 博客有段时间没有更新了，提前说好这不是一般的行车记录仪评测文章，我现在开的 SUV 上并没有装记录仪，只是同事刚好送了一个厂商已倒闭的记录仪，看到这款采用 Intel Atom 处理器的行车记录仪有点兴趣，准备初步研究下。 酷走行车记录仪由深圳汉普云联科技生产，具体型号为 KZV201，网上关于此行车记录仪的评测文章还是有一些的，之前 京东众筹 上的链接应该还在，想了解的朋友们可以看看。 这里我就不做具体介绍了，外观图也就不上了。初步了解此记录仪使用 Intel SOFIA Atom x3 处理器，因此集成了 3G 上网功能（带 SIM 卡插槽），支持 2.4 GHz Wi-Fi  [&#8230;]]]></description>
				<content:encoded><![CDATA[<h2 id="kuzo-recorder">酷走记录仪</h2>
<p>博客有段时间没有更新了，提前说好这不是一般的行车记录仪评测文章，我现在开的 SUV 上并没有装记录仪，只是同事刚好送了一个厂商已倒闭的记录仪，看到这款采用 Intel Atom 处理器的行车记录仪有点兴趣，准备初步研究下。</p>
<p>酷走行车记录仪由深圳汉普云联科技生产，具体型号为 KZV201，网上关于此行车记录仪的评测文章还是有一些的，之前 <a href="http://z.jd.com/project/details/42623.html" target="_blank">京东众筹</a> 上的链接应该还在，想了解的朋友们可以看看。</p>
<p>这里我就不做具体介绍了，外观图也就不上了。初步了解此记录仪使用 Intel SOFIA Atom x3 处理器，因此集成了 3G 上网功能（带 SIM 卡插槽），支持 2.4 GHz Wi-Fi 网络、蓝牙 4.0、GPS 及 FM 发射功能。酷走记录仪虽然号称支持 1080p 全高清视频录制，然而实际测试录像效果也是比较一般，另外采用 Android 5.0 系统也挺少见。</p>
<p>由于机身没有任何物理按键，所有功能都要通过手机 App 连接行车记录仪 Wi-Fi 热点来完成，这也是相当坑的地方：由于厂家已经不提供支持更新了，目前手机 App 里不少功能缺失，最基本的 SIM 卡（专用物联网卡）数据流量充值功能都不能使用；手机 App 连修改记录仪系统时间的功能都没有提供（可能厂家考虑的是自动通过 SIM 卡数据流量进行时间同步），导致目前记录仪的时间都不正确。</p>
<h2 id="web-api">Web 接口使用</h2>
<p>为了摆脱随时可能会完全崩掉的手机 App，我就需要知道基本的管理接口和视频调阅接口。</p>
<p>记录仪自带的 Wi-Fi 热点使用固定的 <code>192.168.43.1</code> IP 地址（别指望能修改了），手机或者电脑连接酷走的 Wi-Fi 热点之后就可以 ping 通记录仪地址了，下面我贴出来的例子都是在 Chromebook 上测试的，首先我用 Linux 自带的 <code>nc</code> 命令来扫一下记录仪开放的端口：</p>
<pre class="brush: bash; title: ; notranslate">
(xenial)zzm@localhost:~$ nc -znv 192.168.43.1 20-20000 2&gt;&amp;1 | grep 'succeeded'
Connection to 192.168.43.1 53 port [tcp/*] succeeded!
Connection to 192.168.43.1 5556 port [tcp/*] succeeded!
Connection to 192.168.43.1 8080 port [tcp/*] succeeded!
Connection to 192.168.43.1 8886 port [tcp/*] succeeded!
</pre>
<p>显然 <code>53</code> 是 Wi-Fi 热点自带的 DNS 服务器端口，看起来 <code>8080</code> 就是 Web 接口的端口了，我在安装了酷走 App 的手机上运行 tcpdump 程序进行抓包就可以分析 8080 端口的请求了。</p>
<h3 id="用户登录">用户登录</h3>
<p>首先是用户登录请求：</p>
<pre class="brush: bash; title: ; notranslate">
chronos@localhost ~/Downloads $ curl -v &quot;http://192.168.43.1:8080/term?act=user_login&amp;user=\{%22email%22:null,%22emailVerifyCode%22:null,%22emailVerifyCodeValidtime%22:null,%22emailVerifyStatus%22:0,%22iconUuid%22:null,%22id%22:0,%22lastLoginTime%22:null,%22name%22:null,%22nickname%22:null,%22password%22:null,%22phone%22:null,%22phoneLoginCode%22:null,%22phoneLoginCodeValidtime%22:null,%22realName%22:null,%22regTime%22:null,%22uuid%22:%222F06A8D7CA314388B58DC46719702844%22\}&amp;password=8888&amp;termtype=3&amp;force=false&quot;
*   Trying 192.168.43.1...
* TCP_NODELAY set
* Connected to 192.168.43.1 (192.168.43.1) port 8080 (#0)
&gt; GET /term?act=user_login&amp;user={%22email%22:null,%22emailVerifyCode%22:null,%22emailVerifyCodeValidtime%22:null,%22emailVerifyStatus%22:0,%22iconUuid%22:null,%22id%22:0,%22lastLoginTime%22:null,%22name%22:null,%22nickname%22:null,%22password%22:null,%22phone%22:null,%22phoneLoginCode%22:null,%22phoneLoginCodeValidtime%22:null,%22realName%22:null,%22regTime%22:null,%22uuid%22:%222F06A8D7CA314388B58DC46719702844%22}&amp;password=8888&amp;termtype=3&amp;force=false HTTP/1.1
&gt; Host: 192.168.43.1:8080
&gt; User-Agent: curl/7.60.0
&gt; Accept: */*
&gt; 
&lt; HTTP/1.1 200 OK
&lt; Set-Cookie: JSESSIONID=1lqydpb54hnunwlfbfj8eqkzi;Path=/
&lt; Set-Cookie: TONG_TOKEN_ID=a2664fc77f2d4c2abf90bea58e3bb6bf;Path=/;Expires=Tue, 29-Feb-2000 00:26:30 GMT
&lt; Expires: Thu, 01 Jan 1970 00:00:00 GMT
&lt; Content-Type: text/plain;charset=UTF-8
&lt; Access-Control-Allow-Methods: GET, POST
&lt; Access-Control-Allow-Credentials: true
&lt; Transfer-Encoding: chunked
&lt; Server: Jetty(i-jetty 1.0.27)
&lt; 
* Connection #0 to host 192.168.43.1 left intact
{&quot;data&quot;:&quot;a2664fc77f2d4c2abf90bea58e3bb6bf&quot;,&quot;elapsedTime&quot;:94,&quot;message&quot;:&quot;鉴权通过!&quot;,&quot;responseCode&quot;:100}
</pre>
<blockquote>
<p><strong>注意</strong></p>
<p>上面 curl 命令中的括号做了转义防止 curl 请求错误，实际请求地址中并没有反斜杠，另外请求地址中的 <code>%22</code> 就是双引号，如果在浏览器中访问也可以直接把 <code>%22</code> 换成双引号。</p>
</blockquote>
<p>可以看到记录仪 Web 服务器是基于 Jetty 写的，GET 请求地址中的 <code>user</code> 和 <code>password</code> 参数不能缺少，<code>password</code> 就是记录仪的管理密码（默认：8888），<code>user</code> 参数是个 JSON 对象，基本所有参数都可以使用 <code>null</code>，除了 <code>uuid</code> 参数表示登录用户（可以随机生成一串 UUID），重复发送登录请求将会报错。</p>
<p>最值得关注的是返回 Cookie 数据中的 <code>JSESSIONID=1lqydpb54hnunwlfbfj8eqkzi</code> 值，后面所有 Web 请求都会用到这个会话 ID。</p>
<h3 id="device-info">查看设备信息</h3>
<p>基础设备信息也可以使用 curl 附带 <code>JSESSIONID</code> Cookie 值进行查询，下面其它的请求就不详细列出来了：</p>
<pre class="brush: bash; title: ; notranslate">
curl -v -b &quot;JSESSIONID=1lqydpb54hnunwlfbfj8eqkzi&quot; &quot;http://192.168.43.1:8080/term?act=device_info&amp;appID=null&quot;
</pre>
<p>设备信息输出如下：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=device_info&amp;appID=null HTTP/1.1
Host: 192.168.43.1:8080
Cookie: JSESSIONID=73jb4a4t1urf1un1krkwn6drr

HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Credentials: true
Transfer-Encoding: chunked
Server: Jetty(i-jetty 1.0.27)

{&quot;data&quot;:{&quot;hotName&quot;:null,&quot;hotPassword&quot;:null,&quot;hwVerion&quot;:&quot;4&quot;,&quot;imei&quot;:&quot;XXXXXXX&quot;,&quot;osVersion&quot;:&quot;2.2.3&quot;,&quot;password&quot;:null,&quot;productCode&quot;:null,&quot;randomCode&quot;:null,&quot;sn&quot;:&quot;KXXXXXX&quot;,&quot;swVersion&quot;:&quot;1.0.27&quot;,&quot;videoPassword&quot;:null},&quot;elapsedTime&quot;:9,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>查看记录仪配置请求：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_termconfiginfo HTTP/1.1

{&quot;data&quot;:{&quot;hotName&quot;:&quot;KUZO_KXXXXXX&quot;,&quot;hotPassword&quot;:&quot;password&quot;,&quot;hwVerion&quot;:&quot;4&quot;,&quot;imei&quot;:&quot;XXXXXXX&quot;,&quot;osVersion&quot;:&quot;2.2.3&quot;,&quot;password&quot;:&quot;8888&quot;,&quot;productCode&quot;:null,&quot;randomCode&quot;:null,&quot;sn&quot;:&quot;KXXXXXX&quot;,&quot;swVersion&quot;:&quot;1.0.27&quot;,&quot;videoPassword&quot;:&quot;&quot;},&quot;elapsedTime&quot;:14,&quot;message&quot;:&quot;Success!&quot;,&quot;responseCode&quot;:100}
</pre>
<p>查看 SIM 卡 ICCID（返回值在 <code>data</code> 中）：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_sim_iccid HTTP/1.1

{&quot;data&quot;:&quot;XXXXXXX&quot;,&quot;elapsedTime&quot;:14,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>查看 FM 发射状态（这个 POST 请求没有附带任何参数）：</p>
<pre class="brush: plain; title: ; notranslate">
POST /term?act=query_fm_open_status HTTP/1.1
Content-Length: 0

{&quot;data&quot;:false,&quot;elapsedTime&quot;:35,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>查看 GPS 状态：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_gps_status HTTP/1.1

{&quot;data&quot;:{&quot;position&quot;:{&quot;accuracy&quot;:14.33,&quot;altitude&quot;:15.030368,&quot;angle&quot;:337.0591,&quot;crs&quot;:1,&quot;gpsTime&quot;:&quot;20181023180046&quot;,&quot;latitude&quot;:31.907135402870722,&quot;longitude&quot;:118.77761111400555,&quot;speed&quot;:0.0},&quot;positionFix&quot;:true,&quot;positionType&quot;:1,&quot;satellitesInUse&quot;:5,&quot;satellitesInView&quot;:6},&quot;elapsedTime&quot;:3,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>可以看到这里能直接查询到 GPS 的当前经纬度以及精确度，还能看到卫星的使用情况，比较讽刺的是这边 GPS 时间都得到了，记录仪系统在网络不可用的情况下却没考虑使用 GPS 时间。</p>
<p>查询存储状态（装了 TF 卡之后就能看到存储卡容量状态了）：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_storage_info HTTP/1.1

{&quot;data&quot;:{&quot;TFExist&quot;:true,&quot;TFTotalCapacity&quot;:63847890944,&quot;TFUsedCapacity&quot;:6595575808,&quot;internalTotalCapacity&quot;:3918114816,&quot;internalUsedCapacity&quot;:2259316736},&quot;elapsedTime&quot;:3,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>查询 3G 移动网络状态：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_3g_status HTTP/1.1

{&quot;data&quot;:{&quot;dialUpSuccess&quot;:false,&quot;intensity&quot;:0,&quot;mnc&quot;:&quot;01&quot;,&quot;type&quot;:&quot;3G&quot;},&quot;elapsedTime&quot;:19,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<h3 id="music-api">音乐管理</h3>
<p>此记录仪支持通过手机 App 上传音乐到记录仪的存储卡，然后通过蓝牙进行播放控制，列举音乐接口：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=list_music HTTP/1.1

{&quot;data&quot;:[{&quot;fileSize&quot;:4008624,&quot;id&quot;:0,&quot;length&quot;:0,&quot;md5&quot;:&quot;e8b1a4e5-0a4d-4c1f-b6bf-44052507fc37&quot;,&quot;name&quot;:null,&quot;path&quot;:&quot;/mnt/media_rw/sdcard1/Kuzo/Music/2F06A8D7CA314388B58DC46719702844/2/13/e8b1a4e5-0a4d-4c1f-b6bf-44052507fc37.mp3&quot;,&quot;singer&quot;:null,&quot;userUuid&quot;:&quot;2F06A8D7CA314388B58DC46719702844&quot;,&quot;uuid&quot;:&quot;e8b1a4e5-0a4d-4c1f-b6bf-44052507fc37&quot;}],&quot;elapsedTime&quot;:7,&quot;message&quot;:&quot;Success!&quot;,&quot;responseCode&quot;:100}
</pre>
<p>查询最大音量（最大级别为 15）：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_max_volume HTTP/1.1

{&quot;data&quot;:15,&quot;elapsedTime&quot;:5,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>查看当前音量：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_volume HTTP/1.1

{&quot;data&quot;:10,&quot;elapsedTime&quot;:9,&quot;message&quot;:&quot;15 15&quot;,&quot;responseCode&quot;:100}
</pre>
<h3 id="video-api">视频管理</h3>
<p>首先按小时段列举视频，请忽略不正确的系统时间：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=list_video_hour HTTP/1.1

{&quot;data&quot;:[&quot;20000115230000&quot;,&quot;20000116000000&quot;,&quot;20000116010000&quot;,&quot;20000116020000&quot;,&quot;20000116030000&quot;],&quot;elapsedTime&quot;:171,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>指定开始和结束的小时时间段列举视频：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=list_poi&amp;start_time=20000116030000&amp;end_time=20000116040000 HTTP/1.1

{&quot;data&quot;:[],&quot;elapsedTime&quot;:9,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>列举某个小时的所有视频，可以看到存储卡里视频的完整路径：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=list_video_db&amp;hour_time=20000116030000 HTTP/1.1

{&quot;data&quot;:[{&quot;createTime&quot;:&quot;20000116030127&quot;,&quot;id&quot;:42,&quot;length&quot;:120009,&quot;lockType&quot;:0,&quot;name&quot;:&quot;2000-01-16_03-01-27.mp4&quot;,&quot;path&quot;:&quot;/mnt/media_rw/sdcard1/Kuzo/Video/2000/1/16/3/2000-01-16_03-01-27.mp4&quot;,&quot;sn&quot;:null,&quot;uuid&quot;:&quot;a6035028ded14482936c1580aba8e298&quot;,&quot;videoType&quot;:0},{&quot;createTime&quot;:&quot;20000116030327&quot;,&quot;id&quot;:43,&quot;length&quot;:119940,&quot;lockType&quot;:0,&quot;name&quot;:&quot;2000-01-16_03-03-27.mp4&quot;,&quot;path&quot;:&quot;/mnt/media_rw/sdcard1/Kuzo/Video/2000/1/16/3/2000-01-16_03-03-27.mp4&quot;,&quot;sn&quot;:null,&quot;uuid&quot;:&quot;291a4796c1274dfea99a978c84ca33d0&quot;,&quot;videoType&quot;:0}],&quot;elapsedTime&quot;:986,&quot;message&quot;:null,&quot;responseCode&quot;:100}
</pre>
<p>获取某个视频文件的封面截图，此请求直接返回 jpg 图像数据：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=snapshot&amp;name=2000-01-16_03-21-26.mp4&amp;time=0 HTTP/1.1

HTTP/1.1 200 OK
Content-Type: image/jpeg
Accept-Ranges: bytes
Content-Length: 42351
Content-Disposition: attachment;filename=2000-01-16_03-19-26_0.jpg
Server: Jetty(i-jetty 1.0.27)
</pre>
<p>指定开始和结束时间段获取历史轨迹，由于记录仪的 GPS 是一直开启的，还好轨迹还算比较准确，这里轨迹数据比较长我就不详细贴出来了：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=query_history_track&amp;start_time=20181013180104&amp;end_time=20181013180304&amp;len=121 HTTP/1.1
</pre>
<p>最后是最关键的下载录像接口，通过 <code>name</code> 参数指定视频文件名即可：</p>
<pre class="brush: plain; title: ; notranslate">
GET /term?act=get_video&amp;name=2000-01-16_03-19-26.mp4 HTTP/1.1
User-Agent: Lavf/57.56.100
Accept: */*
Range: bytes=0-
Connection: close
Host: 192.168.43.1:8080
Icy-MetaData: 1

HTTP/1.1 206 Partial Content
Content-Type: application/octet-stream
Accept-Ranges: bytes
Content-Length: 61722040
Content-Disposition: attachment;filename=2000-01-16_03-19-26.mp4
Content-Range: bytes 3104-61722039/61722040
Connection: close
Server: Jetty(i-jetty 1.0.27)
</pre>
<p>有点奇葩的就是这个下载录像的请求却并没有验证用户会话 ID，因此你也可以通过任何一款播放器指定地址直接远程回放录像：</p>
<pre>http://192.168.43.1:8080/term?act=get_video&#038;name=2000-01-16_03-19-26.mp4</pre>
<h2 id="live-video-api">实时视频接口</h2>
<p>通过 tcpdump 抓包就会发现记录仪的实时视频预览是通过上面扫描出来的 8886 端口，记录仪提供 RTSP 形式的实时预览接口：</p>
<pre class="brush: plain; title: ; notranslate">
rtsp://192.168.43.1:8886?videoapi=mc2&amp;transport=tcp
</pre>
<p>需要注意的是记录仪自带的 RTSP 服务器似乎只支持 TCP 流传输，如果使用 VLC 播放器，则需要将 Live555 流传输选项改为：RTP over RTSP (TCP) 才能正常播放。</p>
<p>厂家可能出于预览流畅性考虑 RTSP 并没有直接提供 1080p 的视频，然而我通过 VLC 播放器进行 RTSP 实时预览还是存在两至三秒的延时，在 Chromebook 下切换使用 <a href="https://chrome.google.com/webstore/detail/vxg-media-player/hncknjnnbahamgpjoafdebabmoamcnni" target="_blank">VXG Media Player</a> 插件进行实时预览则不存在延时问题。</p>
<h2 id="summarize">总结</h2>
<p>目前该行车记录仪使用下来还存在几个主要问题：</p>
<ul>
<li>SIM 卡无法联网的情况下系统时间没办法修改；</li>
<li>手机 App 提供的记录仪通过扫码连接外部 Wi-Fi 或者手机热点的功能无法工作，如果可以的话系统时间同步应该就不是问题；</li>
<li>记录仪硬件上有降噪麦克风，而且支持通过蓝牙的语音对讲功能（只用于目前手机 App 里基本不可用的结伴出行组队），但是视频录像却不支持音频录制，同样 RTSP 实时视频中也没有带音频流。</li>
</ul>
<p>这些问题只能后面有空的话再抽时间看看能否进入记录仪的 Android 系统进行修改了，祝大家玩的开心。</p>
]]></content:encoded>
			<wfw:commentRss>https://zohead.com/archives/kuzo-recorder/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
	</channel>
</rss>
