<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Wuming</title><description>The personal site of Aaron: a blog running since 2005 (life, technology, poetry, Raspberry Pi and Arduino tinkering), projects, links and a guestbook.</description><link>https://wuming.si/</link><language>en</language><item><title>一台平板，一只 dongle，没有电脑也没有 root（七·完）</title><link>https://wuming.si/blog/4008/</link><guid isPermaLink="true">https://wuming.si/blog/4008/</guid><pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;系列的最后一篇，条件最苛刻：一台 Pixel Tablet，唯一的 USB-C 口插着那只 4G dongle。没有电脑可以帮忙，没有 root，插了 dongle 就连充电线都插不上。&lt;/p&gt;
&lt;p&gt;目标是让平板&lt;strong&gt;自己&lt;/strong&gt;变成一部电话：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;打电话、接电话，声音走平板的扬声器和麦克风；&lt;/li&gt;
&lt;li&gt;收发短信，中文和 emoji 都要对；&lt;/li&gt;
&lt;li&gt;通话记录、未接来电通知、通讯录匹配；&lt;/li&gt;
&lt;li&gt;在&amp;quot;平板用 SIM 上网&amp;quot;和&amp;quot;SIM 只管电话短信&amp;quot;两种模式之间切换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不依赖前面那台树莓派网关。App 直接驱动模组。&lt;/p&gt;
&lt;h2&gt;约束把设计定死了&lt;/h2&gt;
&lt;p&gt;这个项目几乎没有多少自由发挥的余地，每一个设计决定都是被某个约束逼出来的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;约束&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;平板上没有 adb 程序，也没有 root&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;App 自己实现 adb 协议&lt;/strong&gt;，通过 Android 的 USB Host API 直接和模组里的调试服务对话&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模组的串口在 Android 上没有驱动&lt;/td&gt;
&lt;td&gt;AT 命令走 adb 的 shell 流，和 Mac 版一样&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;来电和新短信的通知发到 USB AT 口，收不到&lt;/td&gt;
&lt;td&gt;通话状态和新短信&lt;strong&gt;靠轮询&lt;/strong&gt;，不靠等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;语音路由的一切都在内存盘里&lt;/td&gt;
&lt;td&gt;每通电话前重搭，已搭好时很便宜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;驱动是 GPL 衍生物&lt;/td&gt;
&lt;td&gt;不打包，用户自己导入一次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Android 一个 App 只能有一路活跃录音&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;把模组声卡从 Android 手里抢过来，用原生代码自己搬数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIM 卡只能存几十条短信，满了就拒收&lt;/td&gt;
&lt;td&gt;把 SIM 当收件箱，定期清空到平板上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;平板没有听筒，只能外放&lt;/td&gt;
&lt;td&gt;麦克风用通信模式，让系统的回声消除生效&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;倒数第三条是这一篇的主线，后面会详细讲。&lt;/p&gt;
&lt;h2&gt;装起来&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平板只有一个 USB 口，插了 dongle 就不能插电脑，所以调试只能走无线：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;adb mdns services          # 找到平板的配对服务
adb pair &amp;lt;ip&amp;gt;:&amp;lt;port&amp;gt;       # 用平板上显示的配对码
adb connect &amp;lt;ip&amp;gt;:&amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;坑：系统自带的老版 adb 配不上对。&lt;/strong&gt; 要用 Android SDK platform-tools 里的新版。这个错误的表现是配对超时，不会告诉你是版本问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;语音驱动&lt;/strong&gt;：在 App 里选&amp;quot;从文件夹导入&amp;quot;，或者先&lt;strong&gt;运行一次 App&lt;/strong&gt;，再推送到它的私有目录。没有驱动也能接通电话，只是没有声音。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一个必须改的系统设置&lt;/strong&gt;：开发者选项里停用 USB 音频路由，然后把 dongle 拔插一次。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;adb shell settings put secure usb_audio_automatic_routing_disabled 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要改，后面那个坑会解释。&lt;/p&gt;
&lt;h2&gt;自己实现一个 adb 客户端&lt;/h2&gt;
&lt;p&gt;听起来吓人，实际上几百行。adb 的线协议很简单：一个握手包，可选的密钥认证，然后是打开流、写数据、确认、关闭这四种包。一条 USB 连接多路复用所有的流。&lt;/p&gt;
&lt;p&gt;只用老式帧格式（4 KiB 负载、字节和校验），这个年代的调试服务一定接受。手上这只 dongle 从不要求认证。&lt;/p&gt;
&lt;p&gt;自己实现还带来一个意外的性能好处：每条 AT 命令只是已打开连接上的一次往返，而 Mac 版每条命令都要启动一个 &lt;code&gt;adb shell&lt;/code&gt; 进程。差距很明显。&lt;/p&gt;
&lt;p&gt;AT 层的结构和 Mac 版一样：一个长期的读流，每条命令一个短的写流。写的时候用八进制转义，是为了让任何字节（引号、控制字符）对模组的 shell 都没有特殊含义。启动前先杀掉模组里残留的读进程——第五篇那个&amp;quot;幽灵读者&amp;quot;的坑，在这里被提前预防了。&lt;/p&gt;
&lt;p&gt;老版调试服务不报告退出码（第四篇踩过），所以脚本自己打印标记，程序检查标记而不是返回值。&lt;/p&gt;
&lt;p&gt;轮询的节奏：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;频率&lt;/th&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;通话中 1 秒，空闲 2 秒&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AT+CLCC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;通话状态，也是发现来电的唯一方式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空闲每 20 秒&lt;/td&gt;
&lt;td&gt;列出并删除短信&lt;/td&gt;
&lt;td&gt;清空 SIM 收件箱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空闲约 14 秒&lt;/td&gt;
&lt;td&gt;一组状态查询&lt;/td&gt;
&lt;td&gt;状态栏和网络页&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;有个固件怪癖：通话列表里会多出一个幻影数据条目，所以只认&amp;quot;类型是语音且有号码&amp;quot;的条目（来电振铃是例外，号码可能是隐藏的）。连续两次轮询都看不到才算挂断，拨号后给 8 秒宽限。&lt;/p&gt;
&lt;h2&gt;最大的那个坑：每通电话都有一边没声音&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：路由正常，电话接通，但&amp;quot;对方&amp;quot;的电平表几乎不动，&amp;quot;我&amp;quot;的电平表完全不动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;取证&lt;/strong&gt;：拨一个有固定语音提示的服务号码，逐秒记录两路录音的音量。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;打开的录音&lt;/th&gt;
&lt;th&gt;模组下行音量&lt;/th&gt;
&lt;th&gt;平板麦克风&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;模组 + 平板麦克风&lt;/td&gt;
&lt;td&gt;0.001–0.002&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只开模组&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.28–0.39&lt;/strong&gt;，峰值 0.96&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只开平板麦克风&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.0038（环境噪声）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;平板麦克风 + 模组&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;精确的 0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;换一组录音源类型&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.003–0.005&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这张表读起来很清楚：单独开任何一路都正常，同时开两路就有一路是死的。而且&lt;strong&gt;换录音源类型只是换了另一边输&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Android 报告每一路录音都&amp;quot;没有被静音&amp;quot;，路由配置也全对。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;strong&gt;Android 只给一个 App 一路活跃录音。&lt;/strong&gt; 模组的 USB 音频输入和平板麦克风同时录，其中一路必然拿到静音。这不是 bug，是平台的既定行为。&lt;/p&gt;
&lt;p&gt;试过的一条死路：桥接程序有个模式可以把音频通过 adb 流传出来，绕开录音系统。但在这个固件上打不开设备，报 &lt;code&gt;errno = -22&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决办法只能是釜底抽薪：把模组的声卡从 Android 手里拿走。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;模组一连上，就强制认领它的 USB 音频控制接口和两个流接口。这会把系统的 USB 音频驱动踢掉——从 Android 的角度看，这块声卡消失了。然后通话开始时自己设置采样率，自己提交等时传输请求，自己搬数据。&lt;/p&gt;
&lt;p&gt;具体参数：8000 Hz 单声道 16 位，高速总线上每毫秒一个 16 字节的包；每个方向 4 个请求在途，每个请求 8 个包（8 毫秒）。两端时钟不同，所以环形缓冲积压超过 200 毫秒就砍回 60 毫秒。关闭时必须先丢弃并回收所有请求再释放资源——&lt;strong&gt;请求在被回收之前属于内核，不是你的&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这样 Android 就只看到一路录音：平板麦克风。结果是下行音量 0.40 到 0.51，每个方向精确的 16000 字节每秒，零传输错误。&lt;/p&gt;
&lt;h2&gt;Android 把所有声音都送进了 dongle&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Android 把 dongle 识别成一副 USB 耳机，还设成了通信设备。音频日志里，通话输出在半秒之内从扬声器切到了&amp;quot;USB 耳机&amp;quot;。别的 App 放的音乐也跑进去了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：模组的音频功能看起来就是一副标准 USB 耳机，而 Android 在通话和媒体上都优先选耳机。这个行为本身完全合理，只是这副&amp;quot;耳机&amp;quot;是给电话对面的人听的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;要三步一起做：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;桥接把内置扬声器同时钉为通信设备和音轨的首选设备；&lt;/li&gt;
&lt;li&gt;认领 USB 接口之后，系统里不再有这块声卡；&lt;/li&gt;
&lt;li&gt;但 Android 的音频框架&lt;strong&gt;不会注意到驱动被卸载了&lt;/strong&gt;，它会继续往一个已经不存在的耳机送声音，直到设备重新枚举。所以要打开&amp;quot;停用 USB 音频路由&amp;quot;并让 dongle 重新枚举一次。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第 3 条就是前面那个系统设置的由来。这类&amp;quot;框架缓存了一个已经消失的东西&amp;quot;的问题，在整个系列里出现了不止一次。&lt;/p&gt;
&lt;h2&gt;其余的坑&lt;/h2&gt;
&lt;h3&gt;拨号把自己的语音路由取消了&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：电话接通了，屏幕上却显示一句奇怪的话——某个操作&amp;quot;因为协程作用域离开了组合&amp;quot;而失败，日志里写着&amp;quot;没有语音路由就开始拨号&amp;quot;，没有声音。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：每个按钮都在界面自己的协程作用域里启动工作。而拨号会切换到通话界面，拨号盘随之离开组合，它的作用域被取消——正在搭建的语音路由跟着被取消了一半。更糟的是，路由代码把&amp;quot;被取消&amp;quot;当成普通失败报告了，所以错误信息完全没指向真正的原因。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：所有模组操作都在一个随进程存活的作用域里运行；取消异常要原样抛出，不能当成失败吞掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这是 Android 和 Compose 上非常容易犯的错&lt;/strong&gt;：界面的生命周期和工作的生命周期是两回事，绑错了就会在用户操作的那一刻自毁。&lt;/p&gt;
&lt;h3&gt;驱动推上去了，App 却说找不到&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：那个目录是在 App 首次运行之前用命令行创建的，属主是 shell 用户，App 读不了自己目录下的文件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：先运行一次 App 让它自己建目录，再推送；或者干脆用 App 内的导入功能。&lt;/p&gt;
&lt;h3&gt;命令的回复被当成了主动通知&lt;/h3&gt;
&lt;p&gt;移植时发现的一个老 bug：Mac 版把所有&amp;quot;长得像通知&amp;quot;的行一律交给通知处理，即使它们正是当前在途命令的回复。所以在 Mac 上查询 SIM 卡状态总是看起来是空的。&lt;/p&gt;
&lt;p&gt;平板版的做法是：如果一行的前缀正好对应当前在途的命令，那它就是回复，留在回复里。&lt;/p&gt;
&lt;p&gt;（顺带一提，Mac 版的这个 bug 还在。一个项目做到第七篇，总会有这种&amp;quot;知道但还没修&amp;quot;的东西。）&lt;/p&gt;
&lt;p&gt;另外，模组对象还有第二把锁，用于多命令序列——短信流程中间会切换字符集，如果这时插进来一条轮询，它会把 UCS2 的十六进制当成普通文本读。&lt;/p&gt;
&lt;h3&gt;联网模式下平板只有 IPv6&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：刚切到 ECM 模式，平板拿到了运营商的 IPv6，但没有 IPv4，连不上只支持 IPv4 的服务。过一会儿又自己好了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：网卡一出现，内核就自行配置了 IPv6；而 IPv4 要靠 DHCP，模组只在自己的数据连接建立之后才应答 DHCP。问早了，Android 就一直停在只有 IPv6 的状态，直到它自己重试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：模组侧无能为力。程序检测到这个状态时，在网络页提示用户拔插一次。最近一次测试，点下按钮后 31 秒 IPv4 到位。&lt;/p&gt;
&lt;h3&gt;两个从网关继承过来的短信坑&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;来电和新短信通知发往 USB AT 口 → 用轮询兜底，能改配置就把通知端口改成&amp;quot;全部&amp;quot;。&lt;/li&gt;
&lt;li&gt;SIM 存满会拒收新短信 → 每次清扫&lt;strong&gt;先&lt;/strong&gt;存进平板本地，&lt;strong&gt;然后&lt;/strong&gt;才从 SIM 删除。顺序反了就会丢短信。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;一个号码两个会话&lt;/h3&gt;
&lt;p&gt;发出去用 &lt;code&gt;138…&lt;/code&gt;，回来变成 &lt;code&gt;+86138…&lt;/code&gt;，于是开了第二个会话。统一折叠国家码前缀，对收信、去信、通话记录、存储条目全都生效。通讯录匹配用的是同一套规则。&lt;/p&gt;
&lt;h3&gt;平板占着模组时，电脑没法调试模组&lt;/h3&gt;
&lt;p&gt;dongle 插在平板上，它的调试接口归 App 独占。这让调试变得很尴尬。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在 App 里加一个受权限保护的调试接收器（只有 shell 和系统能调用），通过 App 自己的连接转发命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;B=&amp;quot;adb shell am broadcast -n &amp;lt;包名&amp;gt;/.DebugReceiver -a &amp;lt;包名&amp;gt;.DEBUG&amp;quot;
$B --es shell &amp;#39;cat /proc/asound/cards&amp;#39;     # 在模组里执行命令
$B --es at &amp;#39;AT+CLCC&amp;#39;                        # 发一条 AT
$B --es dial 10086   /   $B --es hangup 1   # 走真实的拨号/挂断路径
$B --es route 1                             # 搭语音路由
$B --es usbdesc 1                           # 看接口和端点描述
adb logcat -s VoHiveDebug AudioBridge UsbAudio VoiceRoute
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;音频桥每秒记录两个方向的音量、峰值和字节计数。&lt;strong&gt;上面所有音频问题，都是靠这几个数字诊断出来的&lt;/strong&gt;，没有一个是靠读代码猜出来的。&lt;/p&gt;
&lt;p&gt;给一个自己都够不着的设备留一条调试通道，前期看着像浪费时间，实际上是这个项目里回报最高的一次投入。&lt;/p&gt;
&lt;h2&gt;其他功能&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;两种模式，不是四种&lt;/strong&gt;：模组支持四种联网模式，但平板上只有两个有意义——ECM（Android 当 USB 以太网用）叫&amp;quot;联网模式&amp;quot;，QMI（Android 驱动不了）叫&amp;quot;电话短信模式&amp;quot;。切换要重启模组，App 预期断开，重新枚举后自动重连，大约 30 秒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态归平板所有&lt;/strong&gt;：短信和通话记录存在 App 的私有目录里，由进程级的控制器持有。所以切换模式——这会重启模组并断开重连——不会丢任何东西。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通讯录&lt;/strong&gt;：读系统通讯录，在通话记录、会话、来电界面和通知里显示名字；处理 &lt;code&gt;tel:&lt;/code&gt; 链接，在系统通讯录里点号码就能用它拨出；未知号码可以一键交给系统通讯录保存。它自己从不写通讯录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;还没做完的&lt;/h2&gt;
&lt;p&gt;写下来是因为&amp;quot;还没做完&amp;quot;和&amp;quot;做完了&amp;quot;之间的距离，通常比看起来远：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;真人说话时的上行&lt;/td&gt;
&lt;td&gt;采集在跑，字节数正确，但安静房间里降噪后音量接近 0，还没让对端确认听到人声&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;真实来电&lt;/td&gt;
&lt;td&gt;检测、铃声、全屏界面、接听都实现了，还没用真实来电验证过&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长短信&lt;/td&gt;
&lt;td&gt;分段会显示成多个气泡，拼接需要改用 PDU 模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;回声消除质量&lt;/td&gt;
&lt;td&gt;外放加系统回声消除，还没在真实对话里评估&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;系统电话集成&lt;/td&gt;
&lt;td&gt;没接 Android 的电话框架，用自己的通知和全屏界面响铃&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;七篇之后&lt;/h2&gt;
&lt;p&gt;同一只 dongle，四个平台，四种完全不同的驱动方式：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台&lt;/th&gt;
&lt;th&gt;控制通道&lt;/th&gt;
&lt;th&gt;通话音频&lt;/th&gt;
&lt;th&gt;最大的坑&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;树莓派&lt;/td&gt;
&lt;td&gt;串口 AT（udev 固定名）&lt;/td&gt;
&lt;td&gt;ALSA 读 USB 声卡&lt;/td&gt;
&lt;td&gt;每通电话重建路由；Wi-Fi 通话协议栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mac&lt;/td&gt;
&lt;td&gt;adb 进模组&lt;/td&gt;
&lt;td&gt;CoreAudio 聚合设备&lt;/td&gt;
&lt;td&gt;时钟域污染、必须签名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone&lt;/td&gt;
&lt;td&gt;通过网关的 HTTPS&lt;/td&gt;
&lt;td&gt;WebSocket PCM&lt;/td&gt;
&lt;td&gt;CallKit 生命周期&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安卓平板&lt;/td&gt;
&lt;td&gt;App 自己实现 adb&lt;/td&gt;
&lt;td&gt;原生等时 USB 传输&lt;/td&gt;
&lt;td&gt;一个 App 只能一路录音&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;回头看，反复出现的教训只有五条：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一、&amp;quot;已就绪&amp;quot;必须带外确认。&lt;/strong&gt; 空条件判断出来的 ready，四个标志全是 true 却打不出电话的状态接口，设备节点存在但读出来全是零的声卡——状态值是代码自己写的，事实要去别处查。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二、一个共享的底层被多个消费者读，就会各丢一半。&lt;/strong&gt; 语音丢一半、事件丢一半、两个读进程瓜分 AT 回复，是同一个 bug 的三张脸。发现它的办法是在数据通路的两个层各埋一个计数器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三、不要从&amp;quot;沉默&amp;quot;推断对方的策略。&lt;/strong&gt; 曾经写下&amp;quot;这是运营商签约问题&amp;quot;，实际上是自己的注册消息少了一个头。把消息打出来，逐条对规范。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;四、不要从一段听不懂的录音推断任何事&lt;/strong&gt;，先确认号码拨对了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;五、迁移时，没有测试覆盖的补丁就是会丢的补丁。&lt;/strong&gt; 同一个来电进语音信箱的问题，修了两次。&lt;/p&gt;
&lt;p&gt;一只二手价不到一百块的配件，里面是一台完整的 LTE 模组。这大概是这个系列最值得说的一件事：很多看起来封闭的硬件，只是&lt;strong&gt;没有人去问它是什么&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;全系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;这个系列是个人学习和技术研究记录。模组的语音驱动是 GPL 衍生物，不随项目分发，需要自行准备。请遵守所在地法律法规和运营商服务条款。&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>VMware 偷走了我的 IPv4：一次纯靠 IPv6 完成的远程抢救</title><link>https://wuming.si/blog/4001/</link><guid isPermaLink="true">https://wuming.si/blog/4001/</guid><pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;那天我远程启动了 Mac Studio 上的 VMware Fusion，然后这台机器就&amp;quot;消失&amp;quot;了。&lt;/p&gt;
&lt;p&gt;ping 不通，VNC 连不上，SSH 也没有任何回应。而我人在外地，走过去按电源键这个选项并不存在。&lt;/p&gt;
&lt;p&gt;最后的结局是：&lt;strong&gt;机器一秒都没有宕过&lt;/strong&gt;，只是它的 IPv4 协议栈被 VMware 悄悄地掐断了。而 IPv6 完好无损——那就是那扇没上锁的后门。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;先说结论&lt;/h2&gt;
&lt;p&gt;VMware Fusion 的某个 &lt;code&gt;vmnet&lt;/code&gt; 虚拟网卡抢占了 &lt;code&gt;192.168.50.0/24&lt;/code&gt; 这个网段，而这&lt;strong&gt;正是局域网真实使用的网段&lt;/strong&gt;。于是 macOS 在做路由查找时，把所有发往 &lt;code&gt;192.168.50.*&lt;/code&gt; 的回包——包括发给自己网关的回包——全都丢进了那块虚拟网卡，而不是 &lt;code&gt;en0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;结果就是：&lt;strong&gt;包进得来，出不去&lt;/strong&gt;。从外面看，这台机器像是彻底死了。&lt;/p&gt;
&lt;p&gt;而 IPv6 使用带 scope 的路由（&lt;code&gt;%接口&lt;/code&gt;），一个 IPv4 网段冲突根本碰不到它，VMware 的 vmnet 也不会去动 IPv6。所以这台机器的 IPv6 链路本地地址自始至终是通的。&lt;/p&gt;
&lt;p&gt;修复方式：通过同网段的一台跳板机用 IPv6 SSH 进去，执行 &lt;code&gt;vmnet-cli --stop&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;排查过程&lt;/h2&gt;
&lt;h3&gt;一、网络路径是好的，只有这台 Mac 是黑的&lt;/h3&gt;
&lt;p&gt;从我远程的机器（位于 &lt;code&gt;10.10.10.0/24&lt;/code&gt;）测试：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目标&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;192.168.50.254&lt;/code&gt;（网关）&lt;/td&gt;
&lt;td&gt;ping 约 8ms，&lt;strong&gt;健康&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;192.168.50.48&lt;/code&gt;（Mac Studio）&lt;/td&gt;
&lt;td&gt;100% 丢包，22 / 5900 / 445 / 548 等所有端口全部静默&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;网关通、Mac 不通，说明到这个网段的路由和链路都没问题，故障被精确地圈定在这台 Mac 自己身上。&lt;/p&gt;
&lt;h3&gt;二、&lt;code&gt;known_hosts&lt;/code&gt; 帮了大忙&lt;/h3&gt;
&lt;p&gt;翻了一下本地的 &lt;code&gt;known_hosts&lt;/code&gt;：&lt;code&gt;192.168.50.48&lt;/code&gt; 有 ed25519、rsa、ecdsa 三条记录，说明这台机器上 SSH 是开着的、而且我以前成功连上过。&lt;/p&gt;
&lt;p&gt;更有用的是：&lt;strong&gt;同网段还有好几台机器也在 &lt;code&gt;known_hosts&lt;/code&gt; 里&lt;/strong&gt;（&lt;code&gt;.20 .21 .30 .45 .88 .137&lt;/code&gt;）。这些都是现成的跳板机。&lt;/p&gt;
&lt;p&gt;这是一个很容易被忽略的细节——&lt;code&gt;known_hosts&lt;/code&gt; 本质上是一份&amp;quot;我曾经能连上的机器清单&amp;quot;，救急的时候它就是地图。&lt;/p&gt;
&lt;h3&gt;三、跳到同一个二层网段上去看&lt;/h3&gt;
&lt;p&gt;选了 &lt;code&gt;192.168.50.137&lt;/code&gt;（一台 RHEL8，网卡 &lt;code&gt;ens192&lt;/code&gt;，地址 &lt;code&gt;192.168.50.137/24&lt;/code&gt;）。从它上面看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ARP 查询 .48      -&amp;gt; ac:de:48:11:22:33   (Apple 的 OUI，不是 VMware 的)
arping .48        -&amp;gt; 约 0.9ms 有回应       (二层活着)
ping .48 (IPv4)   -&amp;gt; 100% 丢包             (IPv4 回包路由坏了)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三行信息量很大：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;arping 有回应&lt;/strong&gt; → 机器在二层是活的，网卡在工作，系统没死机。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MAC 是 Apple 的 OUI&lt;/strong&gt; → 排除了另一个高度可疑的原因：&amp;quot;某台桥接模式的虚拟机配了同样的静态 IP &lt;code&gt;.48&lt;/code&gt;，把地址抢走了&amp;quot;。如果是那样，ARP 回来的应该是 VMware 的 OUI（&lt;code&gt;00:0c:29&lt;/code&gt; / &lt;code&gt;00:50:56&lt;/code&gt; 之类）。地址还在 Mac 自己手里，它只是回不了话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv4 ping 不通&lt;/strong&gt; → 问题就在 IPv4 的出向路由上。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;四、IPv6 那边门是敞开的&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Mac 的链路本地地址：fe80::1234:5678:9abc:def0
ping6               -&amp;gt; 0.47ms
SSH (tcp/22)        -&amp;gt; SSH-2.0-OpenSSH_10.2
屏幕共享 (tcp/5900) -&amp;gt; RFB 003.889
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里就完全确认了：&lt;strong&gt;机器健健康康，只有 IPv4 的路由被污染了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;根因&lt;/h2&gt;
&lt;p&gt;VMware Fusion 会给它的 &lt;code&gt;vmnet1&lt;/code&gt;（host-only）和 &lt;code&gt;vmnet8&lt;/code&gt;（NAT）半随机地分配 &lt;code&gt;192.168.x.0/24&lt;/code&gt; 网段。这次它挑中（或者被配置成）了 &lt;code&gt;192.168.50.0/24&lt;/code&gt;——和真实局域网撞了个正着。&lt;/p&gt;
&lt;p&gt;于是这台 Mac 上有两块网卡同时宣称拥有同一个前缀，路由查找开始把 &lt;code&gt;192.168.50.*&lt;/code&gt; 解析到 vmnet 而不是 &lt;code&gt;en0&lt;/code&gt;。IPv4 全面静默，但系统其他一切正常。&lt;/p&gt;
&lt;p&gt;这是个特别阴险的故障模式：它不会报错，不会写日志告警，只在你启动虚拟机的那一刻，安静地把这台机器从网上抹掉。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;救援&lt;/h2&gt;
&lt;h3&gt;入口：经跳板机，用 IPv6 链路本地地址 SSH&lt;/h3&gt;
&lt;p&gt;把 &lt;code&gt;USER&lt;/code&gt; 换成 Mac 上的真实账号名：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -J root@192.168.50.137 &amp;#39;USER@fe80::1234:5678:9abc:def0%ens192&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个关键点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%ens192&lt;/code&gt; 是&lt;strong&gt;跳板机&lt;/strong&gt;在该网段上的接口名。链路本地地址必须带 scope，因为 &lt;code&gt;fe80::/10&lt;/code&gt; 在每块网卡上都有效，不指定接口内核不知道该往哪发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-J&lt;/code&gt; 表示通过 &lt;code&gt;192.168.50.137&lt;/code&gt; 做 ProxyJump。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;进去之后：确认冲突，然后清掉&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 这里会看到某个 vmnet 接口也在认领 192.168.50.x：
netstat -rn -f inet | grep 192.168.50

# 停掉 VMware 的所有虚拟网络，IPv4 几秒内就会回来：
sudo /Applications/VMware\ Fusion.app/Contents/Library/vmnet-cli --stop

# 再确认一次，现在应该只有 en0 认领这个网段：
netstat -rn -f inet | grep 192.168.50
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不想把所有虚拟机都干掉，只想停掉那台闯祸的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;vmrun list
vmrun stop &amp;quot;/path/to/vm.vmx&amp;quot; soft
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备选方案：不用 SSH，直接用 VNC&lt;/h3&gt;
&lt;p&gt;屏幕共享在 IPv6 上是通的，可以把它通过跳板机隧道出来，然后用 VNC 客户端连 &lt;code&gt;localhost:5900&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh -L 5900:[fe80::1234:5678:9abc:def0%ens192]:5900 root@192.168.50.137
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;永久解决：把 VMware 挪出局域网网段&lt;/h2&gt;
&lt;p&gt;治标之后要治本，让它以后再也撞不上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo vi &amp;quot;/Library/Preferences/VMware Fusion/networking&amp;quot;
#   把  VNET_8_HOSTONLY_SUBNET 192.168.50.0  改成  172.16.108.0
#   （如果 VNET_1_ 也落在 192.168.50.0 上，一并改掉）
sudo /Applications/VMware\ Fusion.app/Contents/Library/vmnet-cli --configure
sudo /Applications/VMware\ Fusion.app/Contents/Library/vmnet-cli --start
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;下次怎么才能不这么狼狈&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 给这台 Mac 装 Tailscale。&lt;/strong&gt;
它跑在 &lt;code&gt;utun&lt;/code&gt; 接口上，天然免疫 IPv4 前缀冲突。恰恰是这种故障模式下，它还是通的——连跳板机都不需要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 把 VMware 的 vmnet 网段固定在你的局域网永远不会用的私有段&lt;/strong&gt;，比如 &lt;code&gt;172.16.x.0/24&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 记住 IPv6 这条逃生通道。&lt;/strong&gt; 这是这次最大的收获：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 在同网段任意一台机器上，枚举所有邻居：
ping6 ff02::1%&amp;lt;接口名&amp;gt;

# 即使目标主机的 IPv4 完全死透，带 scope 的 SSH 依然能进去：
ssh user@fe80::xxxx:xxxx:xxxx:xxxx%&amp;lt;接口名&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很多人把 IPv6 当成&amp;quot;以后才用得上的东西&amp;quot;顺手关掉了。这次它是唯一那条把机器救回来的路。&lt;strong&gt;双栈不是冗余，是保险。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;速查表&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Mac Studio IPv4          192.168.50.48
Mac Studio MAC           ac:de:48:11:22:33   (Apple OUI)
Mac Studio IPv6 链路本地  fe80::1234:5678:9abc:def0
网关                     192.168.50.254
跳板机                   192.168.50.137  (RHEL8, ens192)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Technology</category></item><item><title>让 iPhone 拨打一张不在 iPhone 里的 SIM 卡（六）</title><link>https://wuming.si/blog/4007/</link><guid isPermaLink="true">https://wuming.si/blog/4007/</guid><pubDate>Wed, 16 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt; ｜ 下一篇：&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前面几篇的成果是一台树莓派网关：上面插着几张 SIM 卡，能收发短信、能打电话、还能通过 Wi-Fi 连上运营商核心网。&lt;/p&gt;
&lt;p&gt;但它是一台放在角落里的机器。真正想要的是：&lt;strong&gt;手机上多出几条线路&lt;/strong&gt;，用起来和手机自己的号码一样——来电有全屏来电界面，锁屏能接，短信是熟悉的气泡会话，拨号时能用系统键盘输 IVR 的分机号。&lt;/p&gt;
&lt;p&gt;iPhone 没有 USB Host 能力，插不了也驱动不了模组。所以这个 App 是网关的客户端。界面四个标签：电话、短信、录音、设置，导航栏里一个线路选择器。&lt;/p&gt;
&lt;h2&gt;不用 WebRTC，也不做 SIP 客户端&lt;/h2&gt;
&lt;p&gt;第一反应可能是在手机上实现一个 SIP 客户端，或者上 WebRTC。两条路都没必要——网关已经把难的部分做完了，手机只要会说 HTTPS 就够了。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;手机需要&lt;/th&gt;
&lt;th&gt;网关提供&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;线路列表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /api/lines&lt;/code&gt;：编号、名称、号码、信号、能否拨号、进行中的通话。全部来自缓存，回到前台随便刷，不花模组一条 AT 命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;所有线路的事件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /api/events&lt;/code&gt;：一条 SSE 流，通话状态变化和新短信，每条带线路标识，外加心跳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;通话音频&lt;/td&gt;
&lt;td&gt;一条双向 WebSocket，二进制帧，8 kHz 单声道 16 位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;录音&lt;/td&gt;
&lt;td&gt;列表加支持断点的下载&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后台来电&lt;/td&gt;
&lt;td&gt;VoIP 推送&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这些接口全部写进了一份 OpenAPI 描述（72 个路径）。其中语音 WebSocket 的帧格式，之前只有那个处理函数自己知道——&lt;strong&gt;一个接口的格式如果只存在于代码里，它就还不算一个接口。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通话用 &lt;strong&gt;CallKit&lt;/strong&gt;，而不是 App 内部自己画的界面。CallKit 是 iOS 给第三方通话应用的系统框架，用它的好处很实在：来电界面能出现在锁屏上，通话在离开 App 之后继续，按键音用系统键盘（这样自动语音菜单才好用）。&lt;/p&gt;
&lt;p&gt;音频这边要在 8 kHz 和手机硬件的 48 kHz 之间转换，并保持一个 &lt;strong&gt;60 毫秒的抖动缓冲&lt;/strong&gt;。为什么需要缓冲：音频走的是 TCP，到达是一阵一阵的，而扬声器消耗是匀速的，没有缓冲每次突发之间都会留下一个空洞。但缓冲又必须浅，因为这里加的每一毫秒延迟都会被直接听成通话延迟。&lt;/p&gt;
&lt;h2&gt;装起来&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;cd ios/VoHivePhone
xcodegen generate           # 工程由配置生成，不手改工程文件
open VoHivePhone.xcodeproj  # 选开发团队，连真机运行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首次打开在设置里填网关地址。输入框接受人类会输入的任何形式——&lt;code&gt;192.168.1.20:7575&lt;/code&gt;、主机名、完整 URL——其余部分自动补全。&lt;/p&gt;
&lt;p&gt;网关地址是整个 App 的&amp;quot;根&amp;quot;：改掉它意味着重新登录、丢掉旧网关的缓存、重连事件流。这类&amp;quot;改一个设置要连带做五件事&amp;quot;的地方，最好一次写对，否则会留下各种看不见的残留状态。&lt;/p&gt;
&lt;h2&gt;网关先要补两个洞&lt;/h2&gt;
&lt;p&gt;为了支持一台常连着的手机，网关这边先补了两样东西。&lt;/p&gt;
&lt;h3&gt;服务刚启动时，来电没人知道&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：模组的来电回调属于一个控制对象，而这个对象只在有客户端主动查询通话状态时才被创建。刚启动的服务上根本没有回调，来电在有人来问之前不会被注意到。&lt;/p&gt;
&lt;p&gt;这在网页后台时代不是问题——人打开页面就会开始轮询。但一台一直开着事件流的手机，不能依赖&amp;quot;有人来问&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：启动时就为每个设备安装一个事件监视器，随设备增减自动调整。&lt;/p&gt;
&lt;h3&gt;两个消费者，各看到一半事件&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：事件是一个共享 channel，而话机桥已经在读它了。两个读者各拿到大约一半的状态变化。&lt;/p&gt;
&lt;p&gt;这和第三篇&amp;quot;语音丢一半&amp;quot;是&lt;strong&gt;完全一样的陷阱&lt;/strong&gt;：Go 的 channel 一次只把一个值送给一个接收者。同一个 bug 在这个项目里出现了三次，形式各不相同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：改成订阅模式，每个消费者一个独立 channel。投递是有损的——发送发生在模组的通知路径上，绝不能被一个慢读者阻塞。&lt;/p&gt;
&lt;p&gt;验证：从一张国内卡给网关上的另一条线路发短信，经 Wi-Fi 通话链路到达，从事件流里出来：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&amp;quot;type&amp;quot;:&amp;quot;sms.received&amp;quot;,&amp;quot;line_id&amp;quot;:&amp;quot;us&amp;quot;,&amp;quot;sms&amp;quot;:{&amp;quot;sender&amp;quot;:&amp;quot;+86138xxxxxxxx&amp;quot;, ...}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;App 这边的坑&lt;/h2&gt;
&lt;h3&gt;短信会话打开是空的&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;URLComponents&lt;/code&gt; 在查询参数里保留字面量的 &lt;code&gt;+&lt;/code&gt;，而服务端按表单规则把 &lt;code&gt;+&lt;/code&gt; 解码成空格。于是 &lt;code&gt;+86138…&lt;/code&gt; 变成了 &lt;code&gt; 86138…&lt;/code&gt;，前面多一个空格，什么都匹配不上。&lt;/p&gt;
&lt;p&gt;用线上网关验证了一遍：字面量形式返回空数组，百分号编码形式返回消息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：所有要放进查询参数的电话号码，统一在一处编码，而不是在每个调用点各写一遍。这类&amp;quot;每个调用点自己处理&amp;quot;的约定，迟早会有人漏掉一个。&lt;/p&gt;
&lt;h3&gt;第二通电话被拒绝，错误码 7&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：错误码 7 是&amp;quot;通话组数量达到上限&amp;quot;。CallKit 的 Provider 只允许一个通话组，而一个&lt;strong&gt;没接通的拨号从未被报告为结束&lt;/strong&gt;，永远占着那个组。&lt;/p&gt;
&lt;p&gt;本地状态清了，CallKit 没被告知——两边只清一边，系统就会一直相信有一通根本不存在的电话正在进行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：每一条失败路径都要向 CallKit 报告通话结束；发起新通话前先清掉残留；既没接通也没被拒绝的拨号设一个 45 秒的截止时间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只清一边&lt;/strong&gt;是这类系统集成里最常见的错误形状：状态存在两个地方，代码只更新了自己那份。&lt;/p&gt;
&lt;h3&gt;每通电话都会没声音&lt;/h3&gt;
&lt;p&gt;这个 bug 是读代码时按时序推出来的，不是在真机上撞到的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：CallKit 在拨号动作完成后&lt;strong&gt;立刻&lt;/strong&gt;交出音频会话，而这发生在音频引擎被创建&lt;strong&gt;之前&lt;/strong&gt;。于是&amp;quot;音频会话已激活&amp;quot;的回调里，对着一个 &lt;code&gt;nil&lt;/code&gt; 调用了启动方法，什么也没发生，之后也没有任何人再启动它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：把&amp;quot;音频会话激活&amp;quot;和&amp;quot;引擎已创建&amp;quot;当成两个顺序不定的事件，两者都去调用同一个&amp;quot;条件满足就启动&amp;quot;的函数，后到的那个生效。引擎自身的保护让重复调用无害。&lt;/p&gt;
&lt;p&gt;这类竞态值得强调一句：&lt;code&gt;nil&lt;/code&gt; 上的可选调用不会崩溃，也不会报错，它只是什么都不做。Swift 里 &lt;code&gt;try audio?.start()&lt;/code&gt; 这行代码，在 &lt;code&gt;audio&lt;/code&gt; 为 nil 时是一个完全合法、完全静默的空操作。&lt;/p&gt;
&lt;h3&gt;App 在前台时，来电不响&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：事件流明明告诉了 App&amp;quot;有电话在响&amp;quot;，App 什么都没做——报告来电的代码只写在推送回调里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：前台收到的来电也报告给 CallKit。两个来源可能对同一通电话都触发，所以已经报告过的要跳过，否则屏幕上会出现两个来电界面。网关说通话结束时也要告诉 CallKit，否则来电界面会一直挂在那儿，而后面什么都不会发生。&lt;/p&gt;
&lt;h3&gt;第一次 TLS 握手就崩溃&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：局域网网关用自签证书，需要在网络会话的代理方法里放行。而那个代理方法跑在网络库自己的队列上，代码从这里去访问主线程隔离的数据，触发了底层的队列断言，进程在第一次握手时直接挂掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：只对&lt;strong&gt;配置的那个主机&lt;/strong&gt;放行证书，不是所有主机；持有例外的代理对象自己保存一份主机名快照，完全不去碰主线程的状态。&lt;/p&gt;
&lt;h3&gt;VoIP 推送的两条硬规则&lt;/h3&gt;
&lt;p&gt;这两条不遵守，后果都不是报错，而是被系统惩罚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;报告新来电&lt;strong&gt;必须在推送回调里同步调用&lt;/strong&gt;。否则 iOS 直接杀掉进程，并且会减少后续推送的投递。&lt;/li&gt;
&lt;li&gt;音频引擎必须在 CallKit 的&amp;quot;会话已激活&amp;quot;回调里启动，而不是拨号时。否则它会绑定到错误的音频路由，第一通电话没声音。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;网关侧的推送&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用证书认证的 APNs，HTTP/2，没有引入新依赖。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个很安静的坑&lt;/strong&gt;：Go 里一旦设置了 &lt;code&gt;TLSClientConfig&lt;/code&gt;，自动 HTTP/2 就被关掉了，而 APNs 拒绝 HTTP/1.1。必须显式打开 &lt;code&gt;ForceAttemptHTTP2&lt;/code&gt;。这个错误不会告诉你原因。&lt;/li&gt;
&lt;li&gt;沙盒还是生产环境由客户端上报，不靠猜。&lt;/li&gt;
&lt;li&gt;APNs 报告永久失效的令牌直接删除，不要在以后每通电话都重试一遍。&lt;/li&gt;
&lt;li&gt;推送和事件流里的&amp;quot;振铃&amp;quot;由&lt;strong&gt;同一个状态转换&lt;/strong&gt;触发，保证前台的手机和睡着的手机不会被告知不同的事。&lt;/li&gt;
&lt;li&gt;有一个状态接口，推送失败时能看见，而不是静默。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推送需要自己的 Apple VoIP 证书，配置好之前保持关闭，App 只在前台连着事件流时响铃。&lt;/p&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;单元测试直接解码真实的网关 JSON 数据。任何一侧改了字段名，都会在测试里失败，而不是在界面上显示一片空白。&lt;/p&gt;
&lt;p&gt;这是客户端加服务端项目里最省事的一种测试：不需要 mock，不需要跑服务，就是把真实响应存下来喂给解码器。&lt;/p&gt;
&lt;h2&gt;这一篇的 bug 都长一个样&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;根源&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;会话为空&lt;/td&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt; 在查询参数里被解码成空格&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;错误码 7&lt;/td&gt;
&lt;td&gt;CallKit 和本地状态只清了一边&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;没声音&lt;/td&gt;
&lt;td&gt;音频会话激活和引擎创建的竞态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;前台不响&lt;/td&gt;
&lt;td&gt;只有推送路径报告了来电&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS 崩溃&lt;/td&gt;
&lt;td&gt;在网络库的队列上访问主线程状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事件丢一半&lt;/td&gt;
&lt;td&gt;共享 channel，多个消费者&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;六个里有四个是&lt;strong&gt;同一件事在两个地方各有一份状态，而代码只维护了一份&lt;/strong&gt;。写客户端和系统框架打交道时，这个模式值得随时提防。&lt;/p&gt;
&lt;p&gt;最后一篇：一台安卓平板，唯一的 USB-C 口插着 dongle，没有电脑、没有 root，怎么把它变成一部电话。&lt;/p&gt;
&lt;h2&gt;全系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;这个系列是个人学习和技术研究记录。请遵守所在地法律法规和运营商服务条款。&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>同一只 dongle 插在 Mac 上：没有驱动的平台怎么办（五）</title><link>https://wuming.si/blog/4006/</link><guid isPermaLink="true">https://wuming.si/blog/4006/</guid><pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt; ｜ 下一篇：&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前四篇都在树莓派上。Linux 对这类设备很友好：串口有驱动，网卡有驱动，声卡有 ALSA，热插拔有 udev。&lt;/p&gt;
&lt;p&gt;把同一只 dongle 插在 Mac 上，这些全都没有。&lt;/p&gt;
&lt;p&gt;第一篇的结论在这里变成了具体的麻烦：模组的 AT 接口、网络接口、诊断接口都是 USB 厂商自定义类，macOS 不给它们绑任何驱动，&lt;code&gt;/dev/cu.*&lt;/code&gt; 里连影子都看不到。&lt;/p&gt;
&lt;p&gt;唯一够得着的通道是 ADB。安装很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;brew install android-platform-tools
adb devices -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mac 上有三种用法，由浅入深：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用法&lt;/th&gt;
&lt;th&gt;适合谁&lt;/th&gt;
&lt;th&gt;需要什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;A. 只上网&lt;/td&gt;
&lt;td&gt;出门给 Mac 用 4G&lt;/td&gt;
&lt;td&gt;切到 ECM 模式，什么软件都不装&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B. 跑一份服务端&lt;/td&gt;
&lt;td&gt;想要和树莓派一样的网页后台、eSIM 管理&lt;/td&gt;
&lt;td&gt;编译一个 macOS 版本&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C. 原生 App&lt;/td&gt;
&lt;td&gt;在 Mac 上直接打电话、发短信&lt;/td&gt;
&lt;td&gt;Xcode 构建，外加语音驱动&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;A. 只上网&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AT+QCFG=&amp;quot;usbnet&amp;quot;,1
AT+CFUN=1,1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模组重启后，&amp;quot;系统设置 → 网络&amp;quot;里会多出一块有线网卡。模组自己做 DHCP 和 NAT，Mac 拿到一个 &lt;code&gt;192.168.225.x&lt;/code&gt; 的地址。不用拨号，插上就有网。切回去用 &lt;code&gt;usbnet,0&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;它会悄悄接管 Mac 的 IPv6&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Wi-Fi 看起来连得好好的，但浏览器的流量其实在走 SIM 卡。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：ECM 网卡会带来一个运营商的 IPv6 前缀。如果 Mac 当前的 Wi-Fi 没有 IPv6（很多家庭和办公网络都没有），这个前缀就成了&lt;strong&gt;整台机器唯一的 IPv6 默认路由&lt;/strong&gt;。所有支持 IPv6 的网站——也就是今天大部分主流网站——全都从 SIM 卡走。&lt;/p&gt;
&lt;p&gt;屏幕上没有任何提示。对一张漫游卡来说，这就是账单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：原生 App 的默认策略是&amp;quot;不给 Mac 用 SIM 的数据&amp;quot;——连上模组时如果它处于 ECM 模式，就切到 QMI。因为 macOS 没有 QMI 驱动，也就不会出现任何网卡，自然无路可走。这个设置存在模组里，所以重启只会发生一次，而且从不在通话中发生。&lt;/p&gt;
&lt;p&gt;在网络页手动切换会同时更新这个策略，下次连接不会被改回去。还加了一层防护，避免&amp;quot;切换没生效 → 重连 → 再切换&amp;quot;的死循环。&lt;/p&gt;
&lt;h2&gt;B. 服务端的 macOS 版本&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;make build-mac
./dist/vohive_&amp;lt;ver&amp;gt;_darwin_arm64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这棵代码树原本只为 Linux 写过，有四个子系统直接调用了 Linux 内核接口。移植的原则是：每一处都在最窄的接缝上拆开，Linux 那一侧的代码保持不变。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;子系统&lt;/th&gt;
&lt;th&gt;macOS 上怎么办&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;把出站连接绑到指定网卡&lt;/td&gt;
&lt;td&gt;Linux 有专门的套接字选项，BSD 系有另一个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;串口参数设置&lt;/td&gt;
&lt;td&gt;增加一条 BSD 路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wi-Fi 通话的加密数据面&lt;/td&gt;
&lt;td&gt;留空，所以 &lt;strong&gt;macOS 上没有 Wi-Fi 通话&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;热插拔监听&lt;/td&gt;
&lt;td&gt;留空，需要手动重新扫描&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;设备占用检测&lt;/td&gt;
&lt;td&gt;返回&amp;quot;未知&amp;quot;，走保守分支&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;还有一件事是移植前没想到的：&lt;strong&gt;设备发现方式也得换。&lt;/strong&gt; Linux 靠读 &lt;code&gt;/sys/bus/usb/devices&lt;/code&gt; 和找 &lt;code&gt;ttyUSB*&lt;/code&gt;，这两样在 macOS 上都不存在，结果就是后台的设备列表永远是空的。&lt;/p&gt;
&lt;p&gt;解决办法是加一种&amp;quot;adb 传输&amp;quot;：系统路径不存在时，用 &lt;code&gt;adb devices -l&lt;/code&gt; 来发现设备；端口名以 &lt;code&gt;adb:&lt;/code&gt; 开头时，AT 管理器就打开一个由 adb 支撑的虚拟串口。上层——eSIM、短信、设备管理——完全不用改。&lt;/p&gt;
&lt;p&gt;验证下来：能识别 IMEI，能看到 LTE 注册和 −65 dBm 的信号，eSIM 接口返回卡信息和已启用的配置文件。&lt;/p&gt;
&lt;h3&gt;几个 macOS 特有的坑&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;压缩过的二进制启动不了。&lt;/strong&gt; UPX 压缩过的 Mach-O 过不了代码签名检查，系统直接拒绝运行。macOS 版本不做压缩。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;双击启动找不到配置文件：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;初始化配置管理器失败: open config/config.yaml: no such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 Finder 启动或者用绝对路径启动时，工作目录是主目录或者根目录，不是程序所在的目录。现在找不到配置时会改用二进制所在目录。&lt;/p&gt;
&lt;h3&gt;adb 传输的三个细节&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;不能用序列号选设备。&lt;/strong&gt; 第一篇说过，序列号是 256 个 &lt;code&gt;0xAA&lt;/code&gt;，&lt;code&gt;adb -s&lt;/code&gt; 用不了。只能用 transport id，但它每次重新枚举都会变（拔插、切换联网模式之后都会变）。所以持久身份用 USB 路径，每次使用时再解析出当前的 transport id。不这么做的话，一切换模式就满屏的&amp;quot;没有这个 transport id 的设备&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;读和写必须分开。&lt;/strong&gt; 一个长期运行的读进程负责收，写用一次性调用。不能用交互式 shell——它会分配伪终端并改写字节。也不能开第二个读进程——两个读者会把回复各分走一半，看起来就像命令没有回应。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;探测超时要放宽。&lt;/strong&gt; 调用方原本给了约 1.2 秒，这是按串口速度估的。但 adb 每写一次都是一次进程启动，读进程附着也要时间。超时的后果是连锁的：探测超时 → IMEI 视为未验证 → 设备被丢弃 → 后台什么都没有。现在 adb 端口的超时下限是 6 秒。&lt;/p&gt;
&lt;p&gt;（这里有个测试拦住了一个偷懒的修法——&amp;quot;超时就沿用发现阶段拿到的 IMEI&amp;quot;。拦得对：只有经过验证的 AT 口给出的 IMEI 才可信。）&lt;/p&gt;
&lt;h3&gt;每次重启服务，模组里就多一个幽灵&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：服务重启几次之后，设备发现失败，报告&amp;quot;没有匹配的硬件&amp;quot;，怎么都起不来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：杀掉本地的 adb 客户端，&lt;strong&gt;不会&lt;/strong&gt;杀掉模组里那个读进程。adbd 会把那个 shell 保留下来。于是下一个读者只能拿到一半的回复，看起来就像模组不响应了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：打开端口时先清理残留的读者。这里有两个反直觉的细节：&lt;/p&gt;
&lt;p&gt;清理脚本的命令行里包含了它要查找的路径，所以它会匹配到自己——必须靠一个自己的标记跳过自己，否则第一件事就是把正在执行清理的那个 shell 杀掉。&lt;/p&gt;
&lt;p&gt;更重要的是，&lt;strong&gt;只在本进程第一次打开这个节点时清理&lt;/strong&gt;。一开始写成了每次打开都清理，结果重新扫描设备时的探测过程，会杀掉&lt;strong&gt;健康模组的&lt;/strong&gt;读者——正是这个修复本来要防止的那种故障，被这个修复亲手制造了出来。&lt;/p&gt;
&lt;h3&gt;网页通话在 Mac 上失败&lt;/h3&gt;
&lt;p&gt;报错是&amp;quot;蜂窝音频设备不可用&amp;quot;。原因是蜂窝语音会话有两处写死了 Linux：在 &lt;code&gt;/proc/asound&lt;/code&gt; 里找 USB 声卡，用 ALSA 的命令行工具搬运数据。&lt;/p&gt;
&lt;p&gt;现在平台相关的部分分成了不同文件，macOS 这边去找模组的两个 CoreAudio 设备，用 sox 转成 8 kHz 单声道。&lt;/p&gt;
&lt;p&gt;还有一个顺序问题：&lt;strong&gt;模组的 USB 音频功能在语音路由建好之前根本不存在。&lt;/strong&gt; 所以主机侧的音频设备必须在路由起来之后再去解析，否则模组重启后的第一通电话必然失败。&lt;/p&gt;
&lt;h2&gt;C. 原生 App&lt;/h2&gt;
&lt;p&gt;不需要网关，App 直接通过 ADB 驱动模组：打电话、发短信、切换联网模式、多模组、选耳机、静音。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;cd mac/VoHiveMac
xcodegen generate
xcodebuild -scheme VoHiveMac -allowProvisioningUpdates build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;语音驱动放在应用支持目录下，App 每次通话前自动搭路由。&lt;/p&gt;
&lt;p&gt;AT 层有两个设计决定：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;读必须是连续的。&lt;/strong&gt; 来电和新短信的通知是不请自来的，靠轮询会错过来电。一个长期的读进程喂给解析器，写用单独的短调用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一时刻只允许一条命令在途。&lt;/strong&gt; AT 协议没有请求 ID，回复和请求靠顺序对应。用一个 actor 来保证这一点。那些夹在别的命令回复中间的主动通知，交给通话状态逻辑处理。&lt;/p&gt;
&lt;p&gt;短信用文本模式，超出 GSM 字符集的内容（所有中文）切到 UCS2 编码。&lt;strong&gt;解码这里有个坑&lt;/strong&gt;：手机号本身也是合法的十六进制字符串。所以 UCS2 的检测条件必须是&amp;quot;长度是 4 的倍数&lt;strong&gt;并且&lt;/strong&gt;含有字母&amp;quot;，否则 &lt;code&gt;13800138000&lt;/code&gt; 会被显示成一串汉字。这条有测试。&lt;/p&gt;
&lt;h3&gt;连上了，却看不到运营商、号码和信号&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：超时逻辑写反了一个对象。它失败掉的是&lt;strong&gt;当前在途&lt;/strong&gt;的那个请求，而不是给它起定时器的那个请求。&lt;/p&gt;
&lt;p&gt;具体是这样：第一个状态查询早就成功返回了，但它的 5 秒定时器之后才触发，杀掉了当时正在进行的另一个请求；那个请求的定时器又杀掉了下一个……六个快速查询，只有第一个活了下来。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：每个请求带上自己的身份，定时器只能失败自己。回归测试精确重放了这个序列。&lt;/p&gt;
&lt;h3&gt;接通了，但完全没有声音&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;证据&lt;/strong&gt;：模组那一侧全对——声卡已加载、音频使能、两个内部通道都在运行。但 USB 方向的两个流是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pcm5p PREPARED    上行，没人喂数据
pcm6c PREPARED    下行，没人读数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;quot;已准备好&amp;quot;但没有数据流动。问题在主机侧。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：macOS 上一个 &lt;code&gt;AVAudioEngine&lt;/code&gt; 的输入和输出&lt;strong&gt;必须是同一个设备&lt;/strong&gt;。而这里给每个引擎配了两个不同的设备（Mac 麦克风进、模组出；模组进、Mac 扬声器出）。它不报错，只是静默地不工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：每个方向创建一个私有的&lt;strong&gt;聚合设备&lt;/strong&gt;（Aggregate Device），把需要的输入和输出配成一对，引擎跑在聚合设备上。私有的意思是它不出现在系统声音设置里，随进程消亡。&lt;/p&gt;
&lt;p&gt;另一个静默失败：设置当前设备的那个属性，输入输出都是全局作用域的元素 0。对输入写元素 1 不会报错，只是悄悄地留在默认设备上。&lt;/p&gt;
&lt;h3&gt;对方听不到我，而且没有任何报错&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：没有麦克风权限时，采集照常运行，只是输出全是静音。表现是单通，而不是一个权限错误。&lt;/p&gt;
&lt;p&gt;系统的隐私日志说明了一切——App 的标识是&amp;quot;无效代码的标识&amp;quot;。因为 App 没有签名，就没有稳定的身份，麦克风授权既无法有意义地申请，也无法保存。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：用开发者团队签名（&lt;strong&gt;不要用关闭代码签名的方式构建&lt;/strong&gt;）；启动时就申请权限，而不是等到通话接通；在设置页和通话页显示权限状态。开发签名大约一周过期，过期后重新构建即可。&lt;/p&gt;
&lt;h3&gt;用过 App 之后，Mac 自己的扬声器坏了&lt;/h3&gt;
&lt;p&gt;这是整篇里最离谱的一个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：其他 App 的声音开始卡顿、失真，一直持续到重启电脑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;证据&lt;/strong&gt;：系统音频服务记录了 671 条 IO 错误，点名是这个 App，带着&amp;quot;安全违规&amp;quot;和&amp;quot;过载&amp;quot;标记，设备是内置扬声器，而且同时存在 48000 Hz 和 8000 Hz 两种采样率的客户端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：上行的聚合设备把模组的 8 kHz 输出设成了主设备，也就是&lt;strong&gt;时钟源&lt;/strong&gt;，又把 Mac 的麦克风加了进来。而 Mac 的扬声器、麦克风、编解码器同属&lt;strong&gt;一个时钟域、一个硬件引擎&lt;/strong&gt;。于是整台机器的内置音频引擎，被拉到了一颗外部的 8 kHz 晶振上。&lt;/p&gt;
&lt;p&gt;而且 App 退出时没有销毁聚合设备，这个状态就一直保持着。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：两个聚合设备都用 &lt;strong&gt;Mac 自己的设备做时钟源&lt;/strong&gt;，其余子设备开启漂移补偿——这才是调和两颗不同晶振的正确方式。退出时同步销毁聚合设备，启动时清扫崩溃遗留下来的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已经中招的话&lt;/strong&gt;，不用重启：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo killall coreaudiod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查证据用 &lt;code&gt;log show --start &amp;lt;时间&amp;gt; --predicate &amp;#39;process == &amp;quot;coreaudiod&amp;quot;&amp;#39;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;调试器停止 App 之后，下一次运行看不到通话&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：读进程是 App 的子进程，而 macOS 上子进程会比父进程活得久。调试器的&amp;quot;停止&amp;quot;是强制终止，清理代码没有机会运行，那个读进程被系统收养，继续读着。&lt;/p&gt;
&lt;p&gt;下一次运行又开一个读者，两个读者瓜分回复：查询通话状态的回复落到了上一次运行留下的管道里，于是通话永远到不了&amp;quot;已接通&amp;quot;，音频桥也就永远不启动。&lt;/p&gt;
&lt;p&gt;曾经找到过一个凌晨 4 点 24 分启动、5 点 46 分还在读的进程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：在 actor 之外记录每个读者的进程号（终止回调里不能等待异步操作），退出时杀掉，启动时清扫。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同理，App 连着模组的时候，永远不要在终端里再跑一个 AT 脚本。&lt;/strong&gt; 两边都在读同一个设备，会互相偷字节。&lt;/p&gt;
&lt;h3&gt;静音不能停流&lt;/h3&gt;
&lt;p&gt;静音时上行引擎要继续运行，只是喂静音数据。停掉引擎会让模组的上行流中断，而有些网络会把上行停滞当作掉话处理。&lt;/p&gt;
&lt;p&gt;通话界面明确显示&amp;quot;已静音&amp;quot;也是必要的——&lt;strong&gt;一个被忘掉的静音，看起来和刚修好的单通 bug 一模一样。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;其他&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多模组&lt;/strong&gt;：每只模组有独立的 AT 口、独立的语音路由（驱动加载在各自模组里）、独立的短信历史。任何一只来电都会接管窗口。模组按 USB 路径记住名字，只要插在同一个口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;耳机选择&lt;/strong&gt;：按 UID 记忆，因为 CoreAudio 的设备 ID 每次开机或拔插都会变。模组自己的两个音频接口不出现在列表里——选中它们会把通话路由回通话本身。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双向电平表&lt;/strong&gt;：只有下行电平表时，&amp;quot;麦克风没进通话&amp;quot;和&amp;quot;模组没把声音传出去&amp;quot;看起来完全一样。多一个表，少一半猜测。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;一个附带的实验：电话机器人&lt;/h3&gt;
&lt;p&gt;App 里还有一个实验性的页面：拨号，然后听、想、答。语音识别用 SenseVoice，合成用 Kokoro，大模型是局域网里 Ollama 上跑的 qwen3:30b-a3b（首个 token 0.2 到 0.4 秒）。通话内容不离开局域网。&lt;/p&gt;
&lt;p&gt;有个意外的好处：因为模组把远端和本端分成了两个独立设备，机器人听到的声音里天然没有自己的声音。打断（说话时让它闭嘴）只是一个能量门限，完全不涉及回声消除。&lt;/p&gt;
&lt;p&gt;这部分自己的坑够单独写一篇——音频引擎在 8 kHz 设备上的各种静默失败、语音活动检测吃掉第一个字、逐块重采样引入的不连续。这里不展开。&lt;/p&gt;
&lt;h2&gt;平台差异一览&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Linux 上有的&lt;/th&gt;
&lt;th&gt;Mac 上的替代&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;串口&lt;/td&gt;
&lt;td&gt;adb 进模组，走内部设备节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sysfs / udev&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adb devices -l&lt;/code&gt; 加 USB 路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ALSA&lt;/td&gt;
&lt;td&gt;CoreAudio 聚合设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wi-Fi 通话数据面&lt;/td&gt;
&lt;td&gt;没有，留在树莓派上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不需要签名&lt;/td&gt;
&lt;td&gt;必须签名，否则拿不到麦克风&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;下一篇回到树莓派网关，给它做一个 iPhone 客户端——让网关上插着的每一张 SIM 卡，都变成手机上的一条线路。&lt;/p&gt;
&lt;h2&gt;全系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;这个系列是个人学习和技术研究记录。模组的语音驱动是 GPL 衍生物，不随项目分发，需要自行准备。&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>声音从哪来，到哪去：USB 话机、浏览器通话和一块 ESP32（四）</title><link>https://wuming.si/blog/4005/</link><guid isPermaLink="true">https://wuming.si/blog/4005/</guid><pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt; ｜ 下一篇：&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上一篇把 Wi-Fi 通话的信令和语音流跑通了。但&amp;quot;跑通&amp;quot;的意思只是：树莓派内存里有一串 8 kHz 的采样数据在流动。人还是听不到。&lt;/p&gt;
&lt;p&gt;这一篇解决的全是&amp;quot;声音到底走哪条线&amp;quot;的问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;模组自己打的蜂窝电话，怎么才能有声音；&lt;/li&gt;
&lt;li&gt;一只真正的 USB 电话机，怎么接进来——能摘机、能按键拨号、能振铃；&lt;/li&gt;
&lt;li&gt;浏览器怎么直接当电话用；&lt;/li&gt;
&lt;li&gt;通话录音；&lt;/li&gt;
&lt;li&gt;一块内存小得可怜的 ESP32 开发板，怎么当一部 Wi-Fi 分机。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一个音频总线，一种格式&lt;/h2&gt;
&lt;p&gt;系统里所有音频只有一种格式：&lt;strong&gt;8 kHz、单声道、16 位、每 20 毫秒一帧，正好 320 字节&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这不是随便挑的。它正好是模组那块 USB 声卡的原生格式，也正好是 G.711 电话编码的采样率。所有东西都统一到这个格式之后，任何端点接进来都不需要编解码器、不需要重采样、不需要协商——把字节搬过去就行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   Wi-Fi 通话的语音 ─┐                   ┌─ USB 电话机
   模组的蜂窝通话 ───┼──&amp;gt;  音频总线  &amp;lt;───┼─ 浏览器
                     │  8k/单声道/16位   ├─ 手机 App
                     │                   ├─ ESP32 分机
                     └───────────────────└─ 录音
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一条贯穿全局的规则：&lt;strong&gt;AT 命令口只允许一个协程持有&lt;/strong&gt;，所有功能排队使用。这样&amp;quot;两个地方同时发 AT 命令导致回复错位&amp;quot;这种问题，在结构上就不可能发生。第一篇讲过这类错位有多难查，干脆从设计上堵死。&lt;/p&gt;
&lt;h2&gt;让模组自己的电话有声音&lt;/h2&gt;
&lt;p&gt;第一篇说过，&lt;code&gt;ATD&lt;/code&gt; 只管信令。要有声音，得在模组内部搭一条路由出来：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;做什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;打开模组的 ADB 和音频功能（某些版本出厂就是开的）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;把语音驱动推进模组并加载。内核 3.18.44&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;跑一遍音频校准&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;启动桥接程序，把 DSP 里的音频接到 USB 声卡上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;主机侧读写这块声卡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;确认通话真的接通之后&lt;/strong&gt;再开始搬音频&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;第 3 步最容易漏，而漏掉的表现就是纯粹的静音——所有东西看起来都正常，就是没声音。校准程序要以守护进程的方式跑着，发一条设置命令，然后等日志里出现校准完成的标志。&lt;/p&gt;
&lt;p&gt;第 6 步的坑是：这个固件通常不发那条通用的&amp;quot;已接通&amp;quot;通知，得靠轮询 &lt;code&gt;AT+CLCC&lt;/code&gt; 判断。&lt;/p&gt;
&lt;p&gt;整个流程被写成了一个脚本，VoHive 在&lt;strong&gt;每一次&lt;/strong&gt;拨出和接听之前都运行它：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo /opt/vohive-call/qdc507-route status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常状态要同时看到三样东西：模组内部的声卡存在、路由会话是活的、两个数据通道都处于运行状态。&lt;/p&gt;
&lt;p&gt;驱动本身是 GPL 衍生物，再分发有提供源码的义务，所以项目不打包它，只从指定目录加载。&lt;/p&gt;
&lt;h3&gt;第一通电话有声音，之后全是静音&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数据&lt;/strong&gt;：第一通电话 7410 帧里有 7412 帧非静音（几乎全程有声音），之后每一通只有 0 到 2 帧，大约 20 秒后被网络挂断。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：桥接程序搭的路由是&lt;strong&gt;单次通话的状态&lt;/strong&gt;，通话一结束就失效了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：每次通话前重启桥接程序。脚本做成自愈式的——模组重启过（内存盘被清空、声卡消失）就重新加载驱动，校准标记不在就重新校准，然后无论如何都重启一遍桥接程序。完整恢复大约 13 秒，正常路径 1.4 秒。模组重启后实测 1000 帧里有 997 帧非静音。&lt;/p&gt;
&lt;h3&gt;两个 adb 陷阱，各花了几个小时&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;adb shell&lt;/code&gt; 不传递远端的退出码。&lt;/strong&gt; &lt;code&gt;adb shell test -f xxx&lt;/code&gt; 对一个不存在的文件也会&amp;quot;成功&amp;quot;返回。于是&amp;quot;检查驱动在不在，不在就推送&amp;quot;这个步骤，被静默跳过了。&lt;/p&gt;
&lt;p&gt;绕过办法是在远端 &lt;code&gt;echo&lt;/code&gt; 一个标记，然后检查输出里有没有它——不要相信退出码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一次性 &lt;code&gt;adb shell&lt;/code&gt; 里放到后台的进程，会在 shell 退出时被回收。&lt;/strong&gt; 校准守护进程和桥接程序都被杀掉了，而杀掉之后，之后每次往校准管道里写数据都会&lt;strong&gt;永久阻塞&lt;/strong&gt;。表现是整个流程卡死，没有任何错误。&lt;/p&gt;
&lt;p&gt;解决办法是它们必须运行在一个从主机侧一直保持打开的 adb 连接下面。&lt;/p&gt;
&lt;h3&gt;有声卡不等于有声音&lt;/h3&gt;
&lt;p&gt;模组重启会清掉内核模块，但树莓派上可能还挂着之前枚举出来的那块声卡。设备节点在，&lt;code&gt;ls&lt;/code&gt; 看得见，读出来全是零。&lt;/p&gt;
&lt;p&gt;这正是每次通话前都要跑一遍状态检查、按需重建的原因。&lt;strong&gt;设备存在、进程在跑、状态是 true，这三件事都不能证明数据真的在流动。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;桥接建好了，但没有东西驱动它&lt;/h3&gt;
&lt;p&gt;音频桥被注册成了总线的一个端点，但没有任何东西去&amp;quot;泵&amp;quot;它，于是一帧都没流动过。&lt;/p&gt;
&lt;p&gt;解决办法是让音频桥本身成为总线的帧泵，由采集流来打拍子。同时记录帧数和非静音帧数——&lt;strong&gt;让&amp;quot;静音的路由&amp;quot;在日志里能被看见&lt;/strong&gt;，而不是等人去抱怨听不见。&lt;/p&gt;
&lt;h2&gt;接一只真正的电话机&lt;/h2&gt;
&lt;p&gt;用的是一只带拨号键盘和叉簧的 USB 电话机（Koncept KU1110，也有 AICO TalkPro U-100、wizarDial WZU-02 这些牌子，同一个东西）。&lt;/p&gt;
&lt;p&gt;它不是标准 USB 音频设备。按键、叉簧、音频全是私有协议，只能用 libusb 抓包逆向一遍。&lt;/p&gt;
&lt;p&gt;现在两个方向都在真机上验证过了：来电时它会响，摘机就能通话；摘机能听到拨号音，按键有按键音，号码能拨出去。还支持速拨——配置一到六位的短码，话机和网页拨号盘都会先展开再拨。（&lt;code&gt;#&lt;/code&gt; 不能用作速拨码，因为它在话机上是&amp;quot;拨号结束&amp;quot;键。）&lt;/p&gt;
&lt;p&gt;因为要用 libusb，这部分需要在树莓派上本机编译，不能交叉编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo apt install libusb-1.0-0-dev
CGO_ENABLED=1 go build -tags handset ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;音频断断续续，还录到了全零的采样&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：传输层用了一把普通互斥锁，而状态轮询每 200 毫秒就要抢一次。播放和采集被活活饿死。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：所有操作共享一把读写锁。一个改动同时修好了断续和零采样两个症状——它们本来就是一个病。&lt;/p&gt;
&lt;h3&gt;播放延迟越来越大&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：按定时器打拍子，每次调用还预填充一点，积压越滚越多。打一通长电话，到后面对方的声音会延迟好几秒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：改成按队列深度打拍子——队列里的数据少了才补，多了就等。切换声音（比如从拨号音切到通话）时清空队列。&lt;/p&gt;
&lt;h3&gt;通话中莫名其妙挂断&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：叉簧（听筒压着的那个开关）是机械的，会抖动。一次&amp;quot;挂机&amp;quot;读数不代表真的挂机了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：去抖，连续多次读到同一个状态才算数。&lt;/p&gt;
&lt;h3&gt;初始化失败时，先怀疑话机自己&lt;/h3&gt;
&lt;p&gt;有两条顺序规则很关键：某条初始化命令每个通道只能发一次，而且两次之间必须先提交一次播放；状态轮询不能和初始化重叠，否则启动命令会超时。&lt;/p&gt;
&lt;p&gt;但更值得说的是：&lt;strong&gt;这只话机的固件很容易卡死，卡死之后不管用什么驱动都以完全相同的方式失败。&lt;/strong&gt; 有好几轮排查是在完全正确的代码里找一个想象中的 bug。&lt;/p&gt;
&lt;p&gt;现在的做法是：初始化失败时，先跑一个最小的参考程序验证硬件本身。恢复用切换 USB 授权状态的办法，不需要拔插。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不要用 &lt;code&gt;usbreset&lt;/code&gt;，也不要给树莓派 4 的 USB 口断电&lt;/strong&gt;——上一篇说过，它的 USB 口是联动的，断一个等于断全部，会把模组一起带走。&lt;/p&gt;
&lt;h3&gt;来电转到话机，摘机后没有声音&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;数据&lt;/strong&gt;：蜂窝那一侧总线收到了 750 帧里 699 帧非静音，话机听筒里一片寂静。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：这是两个总线桥接的转接场景。话机被注册成了总线的一个端点，但&lt;strong&gt;没有任何东西泵这个总线&lt;/strong&gt;——和前面音频桥那个坑一模一样的形状。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：话机这条线路用自己的 20 毫秒节拍直接驱动。另外，话机故意不作为总线成员，否则它的麦克风会混进自己的听筒里，说话时自己听到自己。&lt;/p&gt;
&lt;h3&gt;正在振铃的电话被当成&amp;quot;忙&amp;quot;挂掉了&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：来电号码通知会重复上报振铃状态，第二次被当成了一通新来电，会话层回了个&amp;quot;忙&amp;quot;，顺手把真正在响的那一通挂了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：忽略振铃状态的重复上报。&lt;/p&gt;
&lt;h2&gt;浏览器直接打电话&lt;/h2&gt;
&lt;p&gt;后台的通话页面可以直接拨号、接听，浏览器通过 WebSocket 和音频总线交换原始 PCM：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/api/devices/{id}/call/voice/{session}/ws     二进制帧，8 kHz 单声道 16 位
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;浏览器没声音&lt;/strong&gt;的两个原因，都和权限有关：浏览器录音要求&lt;strong&gt;安全上下文&lt;/strong&gt;，WebSocket 被证书问题拒绝就没有语音——所以必须把自签证书加进系统信任，而不是在警告页点&amp;quot;继续访问&amp;quot;（第二篇讲过）。麦克风权限被拒绝时同样是静默失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;网页拨出的电话只有信令没有声音&lt;/strong&gt;：拨号的处理函数用了一个空总线，只建立了信令会话。解决是拨号时把本地音频设备挂到这通电话的总线上，直到通话结束。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;蜂窝来电在网页上接听后没声音&lt;/strong&gt;：媒体在通话真正接通之前就开始搭建了。顺序改成先接听，再建立媒体。&lt;/p&gt;
&lt;p&gt;这三个坑放在一起看很有意思：它们都是&amp;quot;信令成功了，所以以为整件事成功了&amp;quot;。信令和媒体是两条独立的路，这是第一篇就说过的事，但它会以各种新形式反复出现。&lt;/p&gt;
&lt;h2&gt;录音&lt;/h2&gt;
&lt;p&gt;录音是接在音频总线上的一个旁路，而不是从 WebSocket 里录。这个设计决定带来一个好处：用 USB 话机接的电话——此时没有任何客户端在串流——照样能录下来。&lt;/p&gt;
&lt;p&gt;几个细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;录成立体声&lt;/strong&gt;：远端在左声道，近端在右声道。单声道混音之后事后无法分离，而录音的意义通常恰恰是分清谁说了什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对齐&lt;/strong&gt;：两个方向由不同的调用送来，正常情况下每 20 毫秒各一帧交替。某一侧停了超过 1 秒，就用静音补齐另一侧，而不是干等着，然后爆发式地写进来一堆对不齐的帧。&lt;/li&gt;
&lt;li&gt;写盘放在独立协程上，慢速存储不会拖慢音频时钟。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认关闭&lt;/strong&gt;。录音涉及通话双方，不同地区的法规不一样。保留策略按天数和总大小两个维度，都设为 0 表示不限制。&lt;/li&gt;
&lt;li&gt;没有音频的通话不留记录。下载支持断点续传，可以直接拖进度条而不用下载整个文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证的方式：一通 42.34 秒的电话，左声道远端音量 0.142，右声道静音——因为这一侧当时没接任何设备。数字对得上，说明声道没有搞反。&lt;/p&gt;
&lt;h2&gt;一块 ESP32 当分机&lt;/h2&gt;
&lt;p&gt;这是一块叫 FoloToy AI Passport 的小板子：ESP32-C3，8 MB 存储，&lt;strong&gt;没有 PSRAM&lt;/strong&gt;，带一块 240×320 的屏和三个按键。它没有任何蜂窝功能，目标是让它借用网关的线路，当一部 Wi-Fi 话机。&lt;/p&gt;
&lt;h3&gt;TLS 放不下&lt;/h3&gt;
&lt;p&gt;握手期间 mbedTLS 峰值需要 35 到 45 KB 的内部内存，而这块板子实测最大连续空闲块只有约 13 KB。加密握手和一通进行中的电话，挤不进同一颗芯片。&lt;/p&gt;
&lt;p&gt;试过调小 TLS 的缓冲区参数，没用——服务端是 Go，而 Go 的 TLS 服务端不协商那个能减小分片的扩展。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：网关开两扇&amp;quot;便宜的门&amp;quot;，默认都关着：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;server:
  lan_plain_http: true      # 只对本机和私有网段放行明文
  plain_http_cidrs: []      # 额外放行的网段，每条非私网地址启动时都会告警
  voice_udp_port: 7576      # 语音 UDP 共享端口
  voice_udp_public_port: 0  # 经过端口映射且对外端口不同时填
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;每个会话一个临时端口，没法做端口映射&lt;/h3&gt;
&lt;p&gt;设备在另一个网络里够不着网关，只能靠端口映射。但&lt;strong&gt;没有人能映射一个服务端在通话开始后才随机选出来的端口&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;解决办法是所有会话共用一个固定端口，靠接入密钥区分：设备先把密钥发到这个端口，服务端记住它的地址并回一个确认。不在已知地址列表里的音频直接丢弃，密钥不认识就干脆不回复——对扫描器保持沉默，对真实客户端最多多一次重试。&lt;/p&gt;
&lt;p&gt;用 UDP 而不是 TCP 是有意的：迟到的语音帧没有重传的价值，而重传一帧会拖慢它后面的所有帧。&lt;/p&gt;
&lt;h3&gt;管理员令牌不该出现在一块开发板里&lt;/h3&gt;
&lt;p&gt;走明文链路时，谁抓到一个会话令牌，就等于拿到了整个 API 三十天的权限，包括切换 eSIM 和设备管理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：增加一个签发&lt;strong&gt;受限令牌&lt;/strong&gt;的接口——只能通话、收发短信、订阅事件，不能再签发新令牌。&lt;/p&gt;
&lt;p&gt;需要说清楚的是：这只是&lt;strong&gt;缩小权限范围，并不提供保密性&lt;/strong&gt;。明文就是明文。真正的解决是让设备加入 VPN，比如在 ESP32 上跑 WireGuard——它用的 ChaCha20 比 TLS 握手便宜得多，内存够用。&lt;/p&gt;
&lt;h3&gt;没有回声消除&lt;/h3&gt;
&lt;p&gt;这块芯片做不了回声消除（相关的方案基本都要求更强的型号加 PSRAM），板子也没有耳机口，只能外放。&lt;/p&gt;
&lt;p&gt;固件的做法是：远端说话时关掉麦克风（声控切换，300 毫秒保持），再加一个按住说话的按键作为确定性的后备。这能减轻回声，但消除不了。有些限制就是硬件的限制，承认它比假装解决了要好。&lt;/p&gt;
&lt;h2&gt;现在这台树莓派能干什么&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;端点&lt;/th&gt;
&lt;th&gt;接入方式&lt;/th&gt;
&lt;th&gt;最大的坑&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;模组蜂窝通话&lt;/td&gt;
&lt;td&gt;USB 声卡&lt;/td&gt;
&lt;td&gt;校准、每通电话重建路由、adb 的两个陷阱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USB 电话机&lt;/td&gt;
&lt;td&gt;libusb 私有协议&lt;/td&gt;
&lt;td&gt;锁饥饿、队列积压、叉簧抖动、固件卡死&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浏览器&lt;/td&gt;
&lt;td&gt;WebSocket PCM&lt;/td&gt;
&lt;td&gt;证书信任、空总线&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;录音&lt;/td&gt;
&lt;td&gt;总线旁路&lt;/td&gt;
&lt;td&gt;双声道对齐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESP32&lt;/td&gt;
&lt;td&gt;明文 HTTP + UDP&lt;/td&gt;
&lt;td&gt;内存、端口映射、令牌权限&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;树莓派这一侧到此就完整了。下一篇换个平台：同一只 dongle 插在 Mac 上，没有串口、没有 ALSA，从头再来一遍。&lt;/p&gt;
&lt;h2&gt;全系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;这个系列是个人学习和技术研究记录。模组的语音驱动是 GPL 衍生物，不随项目分发，需要自行准备。录音功能请遵守所在地关于通话录音的法律规定。&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>没有信号也能打电话：在树莓派上从零实现 VoWiFi（三）</title><link>https://wuming.si/blog/4004/</link><guid isPermaLink="true">https://wuming.si/blog/4004/</guid><pubDate>Wed, 26 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt; ｜ 下一篇：&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;手机设置里有个开关叫&amp;quot;Wi-Fi 通话&amp;quot;，打开之后，在地下室、电梯、信号盲区，只要有 Wi-Fi 就照样能打电话收短信。这个功能的正式名字是 VoWiFi。&lt;/p&gt;
&lt;p&gt;它的原理不是把电话转成微信语音。手机做的事情是：通过任意一条宽带网络，加密连回运营商的核心网，然后用 SIM 卡证明&amp;quot;我是这个号码&amp;quot;，之后所有通话和短信都和走蜂窝时一模一样——对方看到的还是你的手机号，来电还是打到你的号码上。&lt;/p&gt;
&lt;p&gt;这一篇要在树莓派上把这套东西实现出来。SIM 卡插在 4G 模组里，但&lt;strong&gt;只用它做身份认证&lt;/strong&gt;，数据全部走树莓派的有线网。蜂窝射频可以一直停在飞行模式。&lt;/p&gt;
&lt;p&gt;对一张常年漫游的卡来说，这意味着：不产生任何漫游费，在家里稳定收短信、打电话、接电话。&lt;/p&gt;
&lt;p&gt;这是整个系列里坑最多的一篇。一路踩下来的十几个坑，几乎每一个都对应着一份规范里的一句话。&lt;/p&gt;
&lt;h2&gt;它是怎么运作的&lt;/h2&gt;
&lt;p&gt;整件事分三层，从下往上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; ┌───────────────────────── 第三层：业务（SIP）─────────────────────────┐
 │  REGISTER 注册 → 401 挑战 → 200 成功     MESSAGE 短信    INVITE 通话  │
 └────────────────────────── 受保护通道 ───────────────────────────────┘
      第二层：IPsec 加密 + 用户态网络协议栈
 ┌────────────────────────── 第一层：SWu 隧道 ──────────────────────────┐
 │  IKEv2 握手 + 用 SIM 卡做 EAP-AKA 认证 → 加密隧道 → 拿到一个内层地址   │
 └──────────── 公网 UDP 500/4500 连到运营商的 ePDG 网关 ─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第一层，建隧道。&lt;/strong&gt; 运营商在公网上开着一台叫 ePDG 的网关，专门接收从任意网络过来的手机。和它建立一条 IPsec 加密隧道，用的是 IKEv2 协议，而身份认证不用密码，用 SIM 卡——这套叫 EAP-AKA。SIM 卡里有一个运营商也知道的密钥，双方各自算一遍，对上了就认证通过。隧道建好后，会拿到一个内层地址，从运营商的角度看，这台设备就&amp;quot;在它的网络里&amp;quot;了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二层，加密。&lt;/strong&gt; 隧道内部再套一层 IPsec 保护，专门保护上面的信令。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三层，业务。&lt;/strong&gt; 在受保护的通道里跑 SIP 协议。SIP 是互联网电话的通用信令协议，注册用 &lt;code&gt;REGISTER&lt;/code&gt;，短信用 &lt;code&gt;MESSAGE&lt;/code&gt;（里面装的是 3GPP 的短信数据），通话用 &lt;code&gt;INVITE&lt;/code&gt;，语音流用 RTP。&lt;/p&gt;
&lt;p&gt;运营商这一整套核心网叫 IMS。手机日常打电话（VoLTE）走的也是它，只不过接入方式是蜂窝而不是 Wi-Fi。&lt;/p&gt;
&lt;h2&gt;选库：顺带上了一堂许可证课&lt;/h2&gt;
&lt;p&gt;自己从零写 IKEv2 不现实，先找现成的库：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;库&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;一个公开的 VoWiFi 库&lt;/td&gt;
&lt;td&gt;握手第一步的密钥交换算法写死成 X25519，而运营商的网关要的是传统的 MODP。对面直接回&amp;quot;没有可接受的提议&amp;quot;。而且它是 AGPL-3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一个公开的 SWu 隧道库&lt;/td&gt;
&lt;td&gt;打三个补丁之后能建起真隧道，但它上面那层走不通&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一个完整的 UE 侧实现&lt;/td&gt;
&lt;td&gt;隧道和 IMS 都能跑通真实运营商，许可证是 PolyForm Noncommercial&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;最后用了第三个，项目许可证也跟着改成 PolyForm Noncommercial，同时&lt;strong&gt;主动把那个 AGPL 的库移除了&lt;/strong&gt;——AGPL 第 7 条不允许在分发时附加&amp;quot;仅限非商业使用&amp;quot;这种限制，两者不能合在同一个分发作品里。这一点做开源集成的时候值得留心：许可证不是装饰，它会决定你能不能把两个库放进同一个仓库。&lt;/p&gt;
&lt;h3&gt;一类很隐蔽的 bug：库提供了自己用不了的算法&lt;/h3&gt;
&lt;p&gt;那个 SWu 库建隧道失败的三个原因，本质上是同一个问题：&lt;strong&gt;协商时报了自己实现不了的算法，对面一选中就死。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;协议握手通常是&amp;quot;我支持 A、B、C，你挑一个&amp;quot;。如果 A 只是列在清单上但代码里没实现，而对面偏偏挑了 A，后面每一步都会以看起来毫不相干的方式失败。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;位置&lt;/th&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;修法&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;握手的密钥交换组&lt;/td&gt;
&lt;td&gt;清单里有 MODP-3072 和 1024，实现只有 group 14。对面选了 3072，回了&amp;quot;密钥载荷无效&amp;quot;，而库不处理这个重试，报的错是&amp;quot;响应中缺少强制性载荷&amp;quot;&lt;/td&gt;
&lt;td&gt;只提议 group 14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;握手的加密算法&lt;/td&gt;
&lt;td&gt;提议了 AES-GCM，但密钥派生有误，解密时报 &lt;code&gt;cipher: message authentication failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只提议 AES-CBC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据隧道的加密&lt;/td&gt;
&lt;td&gt;首选 AES-GCM，结果发出去正常，收进来的每个包认证都失败&lt;/td&gt;
&lt;td&gt;只提议 AES-CBC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;最终协商出来的组合是 AES-CBC-256 加 SHA2-256 加 MODP-2048，握手耗时 1.5 到 1.9 秒。&lt;/p&gt;
&lt;p&gt;排查这类问题有个通用办法：&lt;strong&gt;先把提议清单砍到只剩一个你确信实现正确的算法&lt;/strong&gt;，能通就说明问题在协商，不通再往别处找。&lt;/p&gt;
&lt;h2&gt;&amp;quot;就绪&amp;quot;这个词要小心&lt;/h2&gt;
&lt;p&gt;后台里有四个状态：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;vowifi_enabled = true      # 只表示配置里开了这个功能
tunnel_ready   = true      # 隧道建好了
ims_ready      = true      # 注册成功了
sms_ready      = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;只有后三个同时为 true，才真的能收短信、接电话。&lt;/strong&gt; 服务重启之后，隧道要 10 到 130 秒才能走到这一步。在这之前测试失败，什么都说明不了——这一点浪费过不少时间，因为人总是重启完就立刻去试。&lt;/p&gt;
&lt;h3&gt;坑：&lt;code&gt;Start&lt;/code&gt; 返回成功，其实什么都没做&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：日志明明白白写着已就绪，但去看系统：没有隧道网卡，加密状态表是空的，连端口都没监听。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：库里有两行这样的代码——&amp;quot;没有传注册器，就当注册成功&amp;quot;，&amp;quot;短信状态直接写死为真&amp;quot;。这在逻辑上叫&lt;strong&gt;空真&lt;/strong&gt;：条件为空所以判断通过。另外，数据面模式没配置的话，它会直接跳过建隧道这一步，然后返回成功。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：必须显式配置数据面模式并传入注册器；判断&amp;quot;已注册&amp;quot;必须同时看隧道和注册两个标志。更重要的一条是：&lt;strong&gt;任何&amp;quot;已就绪&amp;quot;都要用带外的方式确认&lt;/strong&gt;——去看网卡在不在、加密状态表有没有条目、端口有没有监听。这条经验在后面几篇里会反复救命。&lt;/p&gt;
&lt;h3&gt;坑：库自带的&amp;quot;自动恢复&amp;quot;会重启射频&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：一个对着假接口跑的单元测试跑了 14 秒，命令日志里出现了四轮射频关闭再打开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：库在读取 SIM 卡身份失败时，默认的恢复策略是给射频断电再上电。在真机上这会挂断正在进行的通话，让模组重新附着网络——对一张漫游卡来说，正是最该避免的动作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：实现一个恢复钩子，直接返回错误，让库退回到从配置推导身份。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;顺带一提&lt;/strong&gt;：一个单元测试跑了 14 秒本身就是信号。测试通过不代表没问题，测试变慢往往意味着代码在偷偷做真实的、昂贵的事情。&lt;/p&gt;
&lt;h2&gt;注册阶段的坑&lt;/h2&gt;
&lt;p&gt;注册就是向运营商说&amp;quot;我在这里，有电话请打到这个地址&amp;quot;。这一步不对，来电就永远不会来。&lt;/p&gt;
&lt;h3&gt;MNC 是两位还是三位&lt;/h3&gt;
&lt;p&gt;IMSI 是 SIM 卡的身份号码，前面几位是国家码和运营商码。运营商码在北美是三位，在很多其他地方是两位。库只要 IMSI 够长就按三位切，切错了身份就构造错了。解决办法是用 &lt;code&gt;AT+CRSM&lt;/code&gt; 读卡里的一个文件，拿到真实位数，不要猜。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;400 Bad Request&lt;/code&gt; 和 &lt;code&gt;421 Extension Required&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;两条协议细节，各让人查了半天：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一次注册请求必须带一个&lt;strong&gt;空的认证头&lt;/strong&gt;（里面写上身份，密码留空）。这是 RFC 3310 的要求，服务器靠它知道该发什么挑战。库只在收到挑战之后才填这个头，于是第一条请求就被拒了。&lt;/li&gt;
&lt;li&gt;服务器回 &lt;code&gt;421&lt;/code&gt; 并回显 &lt;code&gt;Require: sec-agree&lt;/code&gt; 时，意思是&amp;quot;你还得跟代理也说一声&amp;quot;。因为对面是个代理服务器，除了 &lt;code&gt;Require&lt;/code&gt; 还需要 &lt;code&gt;Proxy-Require&lt;/code&gt;（RFC 3329）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这类问题的共同点是：规范里写了，代码里没写，而错误码只告诉你&amp;quot;不行&amp;quot;，不告诉你缺什么。&lt;/p&gt;
&lt;h3&gt;内核加密是条死路&lt;/h3&gt;
&lt;p&gt;Linux 内核自己能做 IPsec，看起来正是现成的工具。实际上走不通：空加密算法被渲染成空密钥，内核直接拒绝；勉强装上之后，第二条注册请求石沉大海。&lt;/p&gt;
&lt;p&gt;后来读那个能跑通的实现才明白，&lt;strong&gt;真正对接得上运营商的做法根本不用内核&lt;/strong&gt;：IPsec 在用户态自己做，上面跑一个用户态的 TCP/IP 协议栈，再把它当成普通网络连接交给 SIP 库。&lt;/p&gt;
&lt;p&gt;花在&amp;quot;为什么内核没转发第二条请求&amp;quot;上的时间，全部浪费在了错误的架构上。这种时候值得停下来问一句：&lt;strong&gt;别人是怎么做的？&lt;/strong&gt; 而不是继续在自己选的路上加力。&lt;/p&gt;
&lt;h3&gt;认证适配器的两个集成陷阱&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;运行时对&lt;strong&gt;适配器类型本身&lt;/strong&gt;做接口断言，靠嵌入字段实现不算数，报的错是&amp;quot;配置要求 ISIM 但提供方不支持&amp;quot;。&lt;/li&gt;
&lt;li&gt;认证要用卡里的 ISIM 应用，而不是默认的 USIM。这两个应用都在同一张卡里，选错了算出来的东西就是错的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自己实现的这一侧：在逻辑通道上打开 ISIM，读出身份标识，跑认证命令，成功时卡会返回一个标签 &lt;code&gt;0xDB&lt;/code&gt; 带着结果和密钥，同步失败时返回 &lt;code&gt;0xDC&lt;/code&gt; 带着一个重同步参数。&lt;/p&gt;
&lt;h3&gt;每次都停在同一个词上&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;IMS REGISTER failed: status=0 result=auth_phase_reached
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：这是一条串起来的错误链，很典型。&lt;/p&gt;
&lt;p&gt;卡返回了 &lt;code&gt;0xDC&lt;/code&gt;，要求序列号重新同步。这在协议里是完全正常的一步——卡和网络对不上账，重来一次就好。但代码把它当成&amp;quot;正常事件&amp;quot;返回了 &lt;strong&gt;nil 错误&lt;/strong&gt;。而上层判断&amp;quot;是否需要重同步&amp;quot;的方式是检查错误类型，nil 当然匹配不上，于是被当成&amp;quot;认证成功但没拿到密钥&amp;quot;，接着用空结果去算摘要，最后死在密钥检查上。&lt;/p&gt;
&lt;p&gt;更糟的是两件事：这条错误恰好匹配了库里&amp;quot;脱敏错误信息&amp;quot;的规则，真实原因被改写成了一个毫无意义的词；而且&lt;strong&gt;有一个单元测试锁死了这个 bug&lt;/strong&gt;，它断言这里应该返回 nil。测试不是真理，它只是把某一时刻的理解固定下来了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：同步失败时返回专门的错误类型和参数。之后流程就顺了：请求重同步 → 发送 → 第二轮挑战 → 认证完成 → 装载加密 → 200 成功。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;调试经验&lt;/strong&gt;：这个库故意对注册失败做了脱敏。在认证命令外面自己包了一层日志（只记录结果类型和数据长度，绝不记录密钥材料），一次就找到了问题。注册失败的时候，先看这一层。&lt;/p&gt;
&lt;p&gt;成功的完整链路长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;initial_response 401 → auth_challenge → auth_success aka_complete
→ ipsec_install installed → protected_send → complete 200 ok
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;短信：只收到一部分，重启后越来越糟&lt;/h2&gt;
&lt;p&gt;注册成功之后，短信就走 SIP 的 &lt;code&gt;MESSAGE&lt;/code&gt; 消息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：注册时要带一个设备实例标识，代码每次启动都随机生成一个。注册服务器按这个标识区分设备，所以每次重启不是&lt;strong&gt;替换&lt;/strong&gt;旧的注册，而是&lt;strong&gt;新增&lt;/strong&gt;一条。同一个号码下攒了 10 条注册记录，网络投递短信时挑中的，多半是某个早就死掉的进程留下的地址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：按 RFC 7255 的规定，用设备的 IMEI 生成一个稳定标识。拿不到 IMEI 的话（射频停在飞行模式时 IMEI 就是空的，这里恰好就是这种情况），按 IMSI 生成一个 UUID 存进数据库，以后一直用它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;清理旧的注册也有讲究&lt;/strong&gt;：旧记录只能等注册服务器自己过期，最长约一小时，这期间&lt;strong&gt;不要重启服务&lt;/strong&gt;（每次重启内层地址都会变，又多一条）。有些运营商拒绝用通配符一次性注销。想看真实的注册列表，要用订阅的方式去查——注册成功响应里回显的过期时间是&amp;quot;被授予的时长&amp;quot;，不是&amp;quot;剩余时长&amp;quot;，看着像还有一小时，其实早就快过期了。&lt;/p&gt;
&lt;h2&gt;通话：库里根本没有语音&lt;/h2&gt;
&lt;p&gt;读代码才发现，那个库做了短信和 USSD，&lt;strong&gt;语音是一个 52 行的空壳&lt;/strong&gt;，调用拨号会返回&amp;quot;尚未实现&amp;quot;。&lt;/p&gt;
&lt;p&gt;于是语音这部分是自己写的，大约 2700 行，带测试：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模块&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SDP&lt;/td&gt;
&lt;td&gt;媒体能力协商，兼容运营商应答里的各种附加属性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTP&lt;/td&gt;
&lt;td&gt;语音包的封装和解析。时间戳按&lt;strong&gt;采样数&lt;/strong&gt;递增（每包 160），不是字节数也不是毫秒&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;媒体&lt;/td&gt;
&lt;td&gt;RTP 和内部音频总线之间的搬运，自己每 20 毫秒打一次拍子&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIP 消息&lt;/td&gt;
&lt;td&gt;构造和解析，多值头要保序，长度字段永远重算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;对话&lt;/td&gt;
&lt;td&gt;完整的呼叫状态机：临时响应、早期媒体、确认、取消、挂断&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;一个好消息：测试的运营商接受 G.711 编码，不需要 AMR。这意味着整个项目可以保持纯 Go，不需要引入 C 依赖，一条命令就能交叉编译到树莓派。&lt;/p&gt;
&lt;h3&gt;拨号发出去，什么回复都没有&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：外呼一直停在&amp;quot;拨号中&amp;quot;，连一个错误码都没有。而同一条通道上的注册请求一切正常。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;INVITE sip:+1877xxxxxxx SIP/2.0
To: &amp;lt;sip:+1877xxxxxxx&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：这个地址是非法的。SIP 地址必须有主机部分（就像邮箱必须有 &lt;code&gt;@&lt;/code&gt; 后面那一截），没有主机的地址代理服务器直接丢弃，&lt;strong&gt;不回任何东西&lt;/strong&gt;。规范要求用 &lt;code&gt;tel:&lt;/code&gt; 格式，或者带上归属域的完整 SIP 地址。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：写成 &lt;code&gt;sip:&amp;lt;号码&amp;gt;@&amp;lt;归属域&amp;gt;;user=phone&lt;/code&gt;，收件人头也一样。改完之后立刻收到 &lt;code&gt;100 Trying&lt;/code&gt;，九秒后接通。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里有个通用的排查方法&lt;/strong&gt;：日志里只有&amp;quot;发出&amp;quot;没有&amp;quot;收到&amp;quot;，说明请求被网络默默丢掉了。这种情况优先怀疑&lt;strong&gt;消息本身格式错误&lt;/strong&gt;，而不是路由、签约或者注册状态。网络设备对不合法的消息通常是沉默，不是报错。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;603 Decline&lt;/code&gt;，理由是&amp;quot;用户未知&amp;quot;&lt;/h3&gt;
&lt;p&gt;看起来像签约问题。但同一张卡在手机上用 Wi-Fi 通话完全正常，所以不是。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：卡里存了多个公开身份，代码直接取了第一个，而这张卡的第一个是从 IMSI 推导出来的内部身份。计费系统按这个身份查不到用户。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：优先选归属域内、用户部分不是 IMSI 的那一个——也就是手机号码那个。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;408 Request Timeout&lt;/code&gt;，其实对方已经接受了&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SIP/2.0 183 Session Progress
Require: 100rel
RSeq: 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;183&lt;/code&gt; 是一条临时响应，而 &lt;code&gt;Require: 100rel&lt;/code&gt; 表示这是一条&lt;strong&gt;可靠临时响应&lt;/strong&gt;，按 RFC 3262 必须用 &lt;code&gt;PRACK&lt;/code&gt; 确认收到。代码在请求里声明了支持这个特性，却从来不发确认。网络重传几次之后放弃，整个请求超时。&lt;/p&gt;
&lt;p&gt;日志里没有任何一处提到&amp;quot;缺少确认&amp;quot;。只有对着规范一条条读，才会注意到自己声明了一个没有实现的能力——&lt;strong&gt;和前面那个&amp;quot;提议了实现不了的算法&amp;quot;是同一类错误&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：收到带序号的临时响应就发确认，重传的不重复发。结果：请求 → 临时响应 → 确认 → 接通，四秒。&lt;/p&gt;
&lt;h3&gt;几个容易写错的协议细节&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;上游库的请求函数在收到第一个响应时就结束事务。这对短信是对的，对通话是错的：&lt;code&gt;100 Trying&lt;/code&gt; 会吃掉事务，后面真正的 &lt;code&gt;200 OK&lt;/code&gt; 被当成不匹配丢掉。通话需要一个能跨越临时响应的版本。&lt;/li&gt;
&lt;li&gt;成功响应的确认是独立事务，要用&lt;strong&gt;新的分支标识&lt;/strong&gt;；失败响应的确认沿用原来的；取消请求则必须重复原请求的分支标识和序号。&lt;/li&gt;
&lt;li&gt;通话请求也要带接入网信息头。这一条在下一节会变得非常重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;拨出去听到一段录音&lt;/h3&gt;
&lt;p&gt;呼叫立刻被一个网络设备接听并播放了一段录音，没有振铃过程。当时怀疑是紧急呼叫地址没登记，还去研究了相关模块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最后发现是测试号码拨错了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;写下来是因为这个错误很典型：在用一个已知可用的号码复测之前，不要从一段听不懂的录音里推导任何结论。&lt;/p&gt;
&lt;h3&gt;通话声音丢一半&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：连续三次测试，音频以 20 毫秒为单位丢掉大约一半。听起来是断断续续的机器音。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：隧道库的&amp;quot;取内层数据包&amp;quot;函数，给所有调用者返回的是&lt;strong&gt;同一个 channel&lt;/strong&gt;。通话时有两个协议栈同时在读它：信令栈和媒体栈。Go 的 channel 一次只把一个值送给一个接收者，于是每个包随机落到其中一个——落到信令栈那一半，因为那边没有对应的接收端口，被静默丢弃了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：改成订阅模式，每个消费者拿一个独立的带缓冲 channel 和一份数据拷贝，非阻塞地扇出。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;收到的包&lt;/th&gt;
&lt;th&gt;补偿的空洞&lt;/th&gt;
&lt;th&gt;最大单帧间隙&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;修复前&lt;/td&gt;
&lt;td&gt;521&lt;/td&gt;
&lt;td&gt;531&lt;/td&gt;
&lt;td&gt;395&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修复后&lt;/td&gt;
&lt;td&gt;1052&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;被推翻的努力&lt;/strong&gt;：在此之前试过加抖动缓冲、解耦播放、改成 40 毫秒一帧——全都打在错误的层上，一个都没有改善这三个数字。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;破案的方法&lt;/strong&gt;：把隧道层收到的包数和媒体层收到的包数放在一起对比。包到了隧道却没到媒体，丢失的位置立刻就定位了。&lt;strong&gt;同一条数据通路上，在两个不同的层各埋一个计数器，往往比任何猜测都快。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;来电：一度以为是运营商的问题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：短信收得到，外呼打得出，&lt;strong&gt;来电全部进语音信箱&lt;/strong&gt;。协议追踪里看不到任何呼叫请求。&lt;/p&gt;
&lt;p&gt;当时排除了一圈本地原因，写下了这样一句结论：&amp;quot;这是运营商侧的设备签约或策略问题，不是我们的协议栈。&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这句话是错的。&lt;/strong&gt; 两个本地缺陷叠在一起造成了它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一个&lt;/strong&gt;：注册请求里没有带接入网信息头。规范（GSMA IR.51）要求 Wi-Fi 接入的终端必须带这个头，里面写明自己接在哪个 Wi-Fi 上。网络在有来电时要靠它做一个叫&amp;quot;终结接入域选择&amp;quot;的判断——这个号码现在应该往蜂窝送还是往 Wi-Fi 送。没有这个头，网络不认为 Wi-Fi 这条路可用。&lt;/p&gt;
&lt;p&gt;而外呼不走这个判断，短信有独立的终结逻辑——&lt;strong&gt;这正好解释了&amp;quot;外呼行、短信行、来电不行&amp;quot;这个奇怪的组合&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二个&lt;/strong&gt;：受保护连接上只保存了一条回复通路，第一个响应发出后就清掉了。对短信没问题（一问一答），但通话要在同一条连接上依次发出临时响应、振铃、接通，最后那条接通写不出去，报&amp;quot;没有源通路&amp;quot;。网络等不到接通，就取消呼叫转去了语音信箱。改成按呼叫标识和序号记录，临时响应保留，最终响应才释放。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：当网络只扣下&lt;strong&gt;一种&lt;/strong&gt;业务，而同一个注册地址上的其他业务都正常时，应该把自己的注册消息和规范逐项对比，而不是去猜签约。缺失的那个头就在早就抓到的注册消息里，从头到尾都在屏幕上。&lt;/p&gt;
&lt;p&gt;还有一句：&lt;strong&gt;&amp;quot;iPhone 能用&amp;quot;只证明这条线路有签约，不证明自己的实现合规。&lt;/strong&gt; 这两件事经常被混为一谈。&lt;/p&gt;
&lt;h3&gt;同一个问题修了两次&lt;/h3&gt;
&lt;p&gt;后来把整个东西迁移到新的后端底座上时，那个&amp;quot;稳定设备标识&amp;quot;的修复只存在于旧代码里，没人带过来——来电又一次进了语音信箱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;没有测试覆盖的补丁，就是会在迁移中丢掉的补丁。&lt;/strong&gt; 现在的验证方式很简单：重启前后各拨一次，确认注册消息里的设备标识没变。&lt;/p&gt;
&lt;h2&gt;还没解决的一个问题&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;隧道重建之后外呼失败，只有重启服务才能恢复。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SWu outbound inner packet rejected {&amp;quot;error&amp;quot;: &amp;quot;netstack dataplane closed&amp;quot;, &amp;quot;packet_len&amp;quot;: 220}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;旧隧道那一代的网络栈没有被关闭，而语音路径还钉在它上面。更麻烦的是，状态接口里隧道、注册、就绪全都是 true——注册确实已经在新隧道上重建好了——唯一的线索是语音那一项是空的。&lt;/p&gt;
&lt;p&gt;思路是让语音客户端重新绑定当前的隧道会话，或者在会话关闭时一并拆掉旧的网络栈。&lt;/p&gt;
&lt;p&gt;它和&amp;quot;声音丢一半&amp;quot;其实是同一家族的问题：&lt;strong&gt;一个共享的底层资源，被多个生命周期不同的上层同时持有。&lt;/strong&gt; 这个模式在后面几篇里还会出现两次。&lt;/p&gt;
&lt;h2&gt;这一篇踩的坑，按协议层排&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;协议层&lt;/th&gt;
&lt;th&gt;踩的坑&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;隧道握手&lt;/td&gt;
&lt;td&gt;提议了自己不支持的算法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIM 认证&lt;/td&gt;
&lt;td&gt;序列号重同步被当成了成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;加密&lt;/td&gt;
&lt;td&gt;内核方案走不通，要在用户态做&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;注册&lt;/td&gt;
&lt;td&gt;空认证头、代理要求、接入网信息头、稳定的设备标识&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;呼叫&lt;/td&gt;
&lt;td&gt;地址缺主机、身份选错、没发确认、回复通路只存一条&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;媒体&lt;/td&gt;
&lt;td&gt;共享 channel 被两个协议栈抢包&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;信令通了，声音正在树莓派里以 8 kHz PCM 的形式流动。下一篇解决一个更朴素的问题：这些声音怎么才能真的从一只听筒里出来。&lt;/p&gt;
&lt;h2&gt;全系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;这个系列是个人学习和技术研究记录。请遵守所在地法律法规和运营商服务条款。&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>把 SIM 卡变成一台服务器：树莓派上的短信与 eSIM 网关（二）</title><link>https://wuming.si/blog/4003/</link><guid isPermaLink="true">https://wuming.si/blog/4003/</guid><pubDate>Wed, 19 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;上一篇：&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt; ｜ 下一篇：&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上一篇搞清楚了那只 4G dongle 是什么：一台跑着 Linux 的 LTE 模组，能打电话、发短信、管 eSIM。这一篇让它真正开始干活。&lt;/p&gt;
&lt;p&gt;目标是一台常年开机的树莓派，上面插一到两只模组，提供这些东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个网页后台，能看模组状态、信号、SIM 卡信息；&lt;/li&gt;
&lt;li&gt;短信收发和会话记录，重要短信自动转发到 Telegram、邮件或者其他地方；&lt;/li&gt;
&lt;li&gt;eSIM 配置文件的下载、启用、停用、删除；&lt;/li&gt;
&lt;li&gt;把 4G 网络以代理的形式共享给局域网。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个很实际的用途：一张不常用但不能停的号码——银行验证码、国外的号码、备用卡——不必占着手机卡槽，扔在这台树莓派上，短信自动推到手机上，需要时还能打电话。&lt;/p&gt;
&lt;p&gt;通话和 Wi-Fi 通话是更大的话题，放在第三、四篇。&lt;/p&gt;
&lt;h2&gt;为什么不自己从头写&lt;/h2&gt;
&lt;p&gt;一开始确实自己写了一个 Go 守护进程。写着写着发现，在 Linux 上大部分脏活系统已经替你干了：串口驱动、网卡驱动、防火墙转发，都是现成的。剩下要写的是设备管理、短信解码、eSIM 流程、通知推送、网页后台——而这些有个叫 VoHive 的项目已经做得相当完整了，后端 Go，前端 Vue，eSIM 基于 euicc-go。&lt;/p&gt;
&lt;p&gt;于是换了思路：用 VoHive 的后端作为底座，把自己在真机上验证过的东西（通话、Wi-Fi 通话的一堆修复、USB 话机支持）一个个移植上去。&lt;/p&gt;
&lt;p&gt;这个决定本身没问题，但它埋了一个雷，值得先说：&lt;strong&gt;迁移时最容易丢的，恰恰是那几行没有测试覆盖的修复。&lt;/strong&gt; 后来 Wi-Fi 通话的来电又一次全部进了语音信箱，外呼又一次没反应，查到最后都是同一个原因——旧代码里的单行修复没被带过来。第三篇会细说这两次重复劳动。&lt;/p&gt;
&lt;h2&gt;硬件&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;配置&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;主机&lt;/td&gt;
&lt;td&gt;Raspberry Pi 4 Model B，Debian 13（arm64）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模组&lt;/td&gt;
&lt;td&gt;QDC507 两只，挂在同一个 USB Hub 上（一张国外卡，一张国内卡）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;供电&lt;/td&gt;
&lt;td&gt;必须足&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;供电那一条不是客套话。Pi 4 的几个 USB 口是联动的：&lt;strong&gt;断掉一个口，会把所有口一起断掉&lt;/strong&gt;。后面讲到 USB 话机死机时会再提到这件事——很多&amp;quot;复位一下设备&amp;quot;的常规操作，在 Pi 4 上会顺手把模组也复位掉。&lt;/p&gt;
&lt;h2&gt;编译和部署&lt;/h2&gt;
&lt;p&gt;纯 Go，不需要 cgo，可以在 Mac 或者 Linux 开发机上交叉编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;make build-arm64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;第一个坑：没装 UPX 的话，&lt;code&gt;make&lt;/code&gt; 直接报错。&lt;/strong&gt; UPX 是个压缩可执行文件体积的工具，Makefile 把它当成了必需品。不想装的话，把 go build 那几行单独拿出来跑就行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;npm ci --prefix web &amp;amp;&amp;amp; npm run build --prefix web
rm -rf internal/web/dist &amp;amp;&amp;amp; cp -R web/dist internal/web/dist
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -tags &amp;quot;with_utls nomsgpack&amp;quot; \
  -ldflags &amp;quot;-s -w&amp;quot; -o dist/vohive_linux_arm64 ./cmd/vohive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺带一提，&lt;code&gt;go build ./cmd/vohive&lt;/code&gt; 不带 &lt;code&gt;-o&lt;/code&gt; 会在仓库根目录留下一个 87 MB 的二进制文件，一不小心就提交上去了。&lt;/p&gt;
&lt;p&gt;部署目录长这样，配置、数据、日志都相对于二进制所在的位置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;/opt/vohive-call/
├── vohive                 # 二进制
├── config/config.yaml
├── data/                  # 数据库、证书、录音
├── logs/app.log
├── qdc507-route           # 语音路由脚本（第四篇）
└── qdc507/                # 模组语音驱动（自备，第四篇）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最小配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;server:
  port: 7575
  debug: false          # 排查 eSIM 问题时改成 true，要重启才生效
web:
  username: admin
  password: 请改掉
devices:
  - id: us
    name: 主卡
    modem_imei: &amp;quot;86xxxxxxxxxxxxx&amp;quot;
    device_backend: at
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配一个 systemd 服务让它开机自启：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# /etc/systemd/system/vohive-call.service
[Unit]
Description=VoHive gateway
After=network-online.target

[Service]
WorkingDirectory=/opt/vohive-call
ExecStart=/opt/vohive-call/vohive
Restart=on-failure

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo systemctl daemon-reload
sudo systemctl enable --now vohive-call
tail -f /opt/vohive-call/logs/app.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以后更新就是覆盖二进制再重启服务。&lt;/p&gt;
&lt;h2&gt;udev 规则：现在不做，以后一定出事&lt;/h2&gt;
&lt;p&gt;udev 是 Linux 管理设备节点的机制，可以给设备起固定名字、设权限、打标记。这里有两件事必须做。&lt;/p&gt;
&lt;h3&gt;让 ModemManager 别碰这只模组&lt;/h3&gt;
&lt;p&gt;ModemManager 是桌面 Linux 上管理蜂窝设备的系统服务。它的工作方式是往串口里写 AT 命令探测设备能力。问题在于，它的命令和你的命令会交错，回复也就错位了——你发 &lt;code&gt;AT+CSQ&lt;/code&gt; 问信号，收到的是它那条命令的回复。&lt;/p&gt;
&lt;p&gt;这种故障看起来和固件 bug 一模一样，能让人查很久。&lt;/p&gt;
&lt;p&gt;不要直接停掉 ModemManager（机器上别的设备可能还要用），标记让它忽略就好：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-udev&quot;&gt;SUBSYSTEM==&amp;quot;usb&amp;quot;, ATTRS{idVendor}==&amp;quot;2c7c&amp;quot;, ATTRS{idProduct}==&amp;quot;0125&amp;quot;, ENV{ID_MM_DEVICE_IGNORE}=&amp;quot;1&amp;quot;
SUBSYSTEM==&amp;quot;tty&amp;quot;, ATTRS{idVendor}==&amp;quot;2c7c&amp;quot;, ATTRS{idProduct}==&amp;quot;0125&amp;quot;, ENV{ID_MM_DEVICE_IGNORE}=&amp;quot;1&amp;quot;
SUBSYSTEM==&amp;quot;net&amp;quot;, ATTRS{idVendor}==&amp;quot;2c7c&amp;quot;, ATTRS{idProduct}==&amp;quot;0125&amp;quot;, ENV{ID_MM_DEVICE_IGNORE}=&amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;给每只模组一个固定的名字&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：两条线路同时掉线，一掉就是好几天，日志里一直在刷 &lt;code&gt;ATE0: command timed out&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：两只模组的 &lt;code&gt;ttyUSB&lt;/code&gt; 编号整段互换了。第一只从 &lt;code&gt;ttyUSB2&lt;/code&gt; 变成了 &lt;code&gt;ttyUSB3&lt;/code&gt;，第二只从 &lt;code&gt;ttyUSB6&lt;/code&gt; 变成了 &lt;code&gt;ttyUSB5&lt;/code&gt;。程序还对着老编号发命令，而那个位置现在是另一只模组的诊断口——诊断口当然不会回应 AT 命令。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么不能靠常规办法&lt;/strong&gt;：两只模组的厂商 ID、产品 ID、序列号完全一样（序列号还是上一篇说的那 256 个 &lt;code&gt;0xAA&lt;/code&gt;）。唯一能区分它们的只有插在哪个物理端口上。&lt;/p&gt;
&lt;p&gt;网上到处能搜到的这种写法&lt;strong&gt;根本不会生效&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-udev&quot;&gt;SUBSYSTEM==&amp;quot;tty&amp;quot;, ATTRS{bInterfaceNumber}==&amp;quot;02&amp;quot;, ATTRS{idVendor}==&amp;quot;2c7c&amp;quot;, ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是 udev 要求一条规则里所有 &lt;code&gt;ATTRS&lt;/code&gt; 匹配的是&lt;strong&gt;同一个父设备&lt;/strong&gt;，而 &lt;code&gt;bInterfaceNumber&lt;/code&gt; 和 &lt;code&gt;idVendor&lt;/code&gt; 在设备树的不同层级上，永远凑不到一起。这是个很常见的误解，抄来的规则不报错，只是静静地不匹配。&lt;/p&gt;
&lt;p&gt;正确做法是直接匹配接口的设备名，格式是 &lt;code&gt;Hub端口:配置.接口&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-udev&quot;&gt;SUBSYSTEM==&amp;quot;tty&amp;quot;, KERNELS==&amp;quot;1-1.1:1.2&amp;quot;, SYMLINK+=&amp;quot;modem-a-at&amp;quot;
SUBSYSTEM==&amp;quot;tty&amp;quot;, KERNELS==&amp;quot;1-1.2:1.2&amp;quot;, SYMLINK+=&amp;quot;modem-b-at&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;sudo udevadm control --reload &amp;amp;&amp;amp; sudo udevadm trigger
ls -l /dev/modem-a-at /dev/modem-b-at
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后所有地方都用这两个符号链接。&lt;strong&gt;永远不要在配置里写 &lt;code&gt;/dev/ttyUSB2&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;网页后台和证书&lt;/h2&gt;
&lt;p&gt;后台地址是 &lt;code&gt;https://&amp;lt;树莓派的IP&amp;gt;:7575&lt;/code&gt;，默认用自签名证书。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：浏览器弹出安全警告，点了&amp;quot;继续访问&amp;quot;，页面能打开，但部分功能仍然不工作，服务端日志不停地出现 &lt;code&gt;tls: unknown certificate&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;原因是那个&amp;quot;继续访问&amp;quot;只对当前页面生效，页面里后续建立的 WebSocket 连接（网页通话要用）仍然会被拒绝。&lt;/p&gt;
&lt;p&gt;真正的解决办法是把证书加进系统信任，然后&lt;strong&gt;完全退出浏览器&lt;/strong&gt;再打开：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;# macOS
security add-trusted-cert -d -r trustRoot \
  -k &amp;quot;$HOME/Library/Keychains/login.keychain-db&amp;quot; server.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条后面还会再出现一次——第四篇的网页通话、第六篇的 iPhone App，都栽在同一件事上。&lt;/p&gt;
&lt;h2&gt;短信&lt;/h2&gt;
&lt;p&gt;短信的编解码（PDU 格式、中文的 UCS2 编码）VoHive 已经处理好了，收到之后入库、按会话展示、按规则推送。&lt;/p&gt;
&lt;p&gt;真正遇到的问题都不在编解码，而在&lt;strong&gt;短信根本没到程序手里&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;SIM 卡存满，短信被静默拒收两周&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：从某天起一条短信都收不到。没有报错，没有告警，程序日志一切正常。&lt;/p&gt;
&lt;p&gt;一查：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AT+CPMS?
+CPMS: &amp;quot;ME&amp;quot;,23,23,&amp;quot;ME&amp;quot;,23,23,&amp;quot;ME&amp;quot;,23,23
AT+CNMI?
+CNMI: 2,1,0,0,0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一行的意思是：存储容量 23 条，已用 23 条，满了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：&lt;code&gt;+CNMI&lt;/code&gt; 的第二个参数是 1，意思是&amp;quot;新短信先存进存储，再上报一个通知&amp;quot;。存储满了，投递在短信层就被拒绝，网络重试一阵子之后放弃。&lt;/p&gt;
&lt;p&gt;而程序只在收到通知时才去读短信、读完删除。那些程序没运行时到的、换卡之前留下的短信，永远没人读，也就永远没人删。那 23 条全是上一张卡收到的营销短信。&lt;/p&gt;
&lt;p&gt;这个坑的杀伤力在于：&lt;strong&gt;一张卡留下的积压，会让之后插进来的每一张卡都收不到短信。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：加一个定期清扫——&lt;code&gt;AT+CPMS?&lt;/code&gt; 看占用，&lt;code&gt;AT+CMGL=4&lt;/code&gt; 把存储里的全部读出来，每条都走和实时短信相同的解码与回调，然后 &lt;code&gt;AT+CMGD&lt;/code&gt; 逐条删掉。存储为空时只花一条命令，所以可以放心频繁跑。&lt;/p&gt;
&lt;h3&gt;清扫看起来有效，其实是巧合&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：清扫挂在&amp;quot;SIM 卡就绪&amp;quot;事件上，新短信确实能进来了。但仔细一看，新短信通知的路径一次都没有真正触发过。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：就是上一篇那个坑——URC 端口是 &lt;code&gt;usbat&lt;/code&gt;，通过 ADB 读 &lt;code&gt;/dev/smd7&lt;/code&gt; 的程序收不到通知。日志里那些看起来像通知的 &lt;code&gt;+CPIN&lt;/code&gt;、&lt;code&gt;+CSQ&lt;/code&gt;，其实是轮询命令的回复被误认成了主动上报，恰好触发了清扫。&lt;/p&gt;
&lt;p&gt;一个 bug 掩护了另一个 bug，整件事看起来是工作的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：收件箱改成独立的 20 秒定时轮询，不再依赖任何事件；同时在初始化时检查 URC 端口，不是 &lt;code&gt;&amp;quot;all&amp;quot;&lt;/code&gt; 就改成 &lt;code&gt;&amp;quot;all&amp;quot;&lt;/code&gt;（先读再改，避免每次启动都写一次掉电保存的参数）。能收到通知的模组立刻收到短信，收不到的最多晚 20 秒。&lt;/p&gt;
&lt;p&gt;验证的时候看到一条运营商已经重试了几个小时的验证码，终于落进了数据库。&lt;/p&gt;
&lt;h3&gt;同一个号码开了两个会话&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;138xxxxxxxx&lt;/code&gt; 和 &lt;code&gt;+86138xxxxxxxx&lt;/code&gt; 被当成两个不同的人。解决办法是统一折叠国家码前缀，短号码（运营商服务号）和国外号码保持原样。&lt;/p&gt;
&lt;h2&gt;eSIM&lt;/h2&gt;
&lt;p&gt;eSIM 不是一张特殊的卡，而是一张&lt;strong&gt;能装配置文件的卡&lt;/strong&gt;。运营商给你的那个二维码或激活码，本质上是让卡去某台服务器下载一份配置。这个流程叫 SGP.22，是 GSMA 的标准。&lt;/p&gt;
&lt;p&gt;在这里，整个流程走的是 AT 命令的逻辑通道，不依赖任何专用驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AT+CCHO=&amp;quot;A0000005591010FFFFFFFF8900000100&amp;quot;   -&amp;gt; +CCHO: 1          # 打开卡里的管理程序
AT+CGLA=1,22,&amp;quot;81E2910006BF3E035C015A&amp;quot;        -&amp;gt; +CGLA: 4,&amp;quot;6115&amp;quot;   # 问 EID（卡的唯一编号）
AT+CGLA=1,10,&amp;quot;81C0000015&amp;quot;                    -&amp;gt; BF3E125A10&amp;lt;EID&amp;gt;9000
AT+CCHC=1                                    -&amp;gt; OK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后台里能看到卡的厂商、固件、证书、已装的配置文件，可以用激活码下载新的，也可以启用、停用、重命名、删除。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;AT+CGLA&lt;/code&gt; 的三个怪癖&lt;/h3&gt;
&lt;p&gt;这三条哪一条不对都是直接报错，而错误信息不会告诉你原因：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;怪癖&lt;/th&gt;
&lt;th&gt;表现&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;十六进制参数&lt;strong&gt;必须加引号&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不加引号直接 &lt;code&gt;ERROR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;长度写的是&lt;strong&gt;十六进制字符数&lt;/strong&gt;，不是字节数&lt;/td&gt;
&lt;td&gt;5 个字节的数据，长度要写 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;逻辑通道 1 上取回数据要用 &lt;code&gt;0x81&lt;/code&gt; 开头&lt;/td&gt;
&lt;td&gt;按 ISO 标准写 &lt;code&gt;0x01&lt;/code&gt; 会返回 &lt;code&gt;6F00&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;另外，这类命令先回一个 &lt;code&gt;61XX&lt;/code&gt;，表示&amp;quot;有 XX 字节数据等着取&amp;quot;，要再发一条取数据的命令才拿得到。&lt;/p&gt;
&lt;p&gt;普通 SIM 卡（不是 eUICC）没有这个管理程序，&lt;code&gt;AT+CCHO&lt;/code&gt; 会直接 &lt;code&gt;ERROR&lt;/code&gt;。这是正常的，不是坏了。&lt;/p&gt;
&lt;h3&gt;每次下载都死在同一句话上&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;下载 eSIM profile 失败 ... err=&amp;quot;tag encoding with less than one byte\nEOF&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这句话翻译过来只是&amp;quot;解析器拿到了 0 个字节&amp;quot;，完全没说为什么是 0 个字节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;怎么查的&lt;/strong&gt;：把 &lt;code&gt;server.debug&lt;/code&gt; 打开重启，库会记录每一条和卡之间的往返，以及和运营商服务器的完整请求响应。又额外在每次卡命令往返上加了一行日志，只记命令头、长度和状态字。&lt;/p&gt;
&lt;p&gt;关键在于：&lt;strong&gt;只有分块级别的状态字能看出是在哪一块放弃的&lt;/strong&gt;，上层只能看到拼好的整条响应，而那条响应是空的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;81E21100 9000   81E21101 9000   ...   81E21106 9000     &amp;lt;- 一直是&amp;quot;继续&amp;quot;，没有&amp;quot;结束&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：这一次的数据恰好是 840 字节，库按 120 字节一块切开，正好 7 块。而它判断&amp;quot;哪一块是最后一块&amp;quot;用的是 &lt;code&gt;长度 / 120&lt;/code&gt;，840 除以 120 等于 7，但块的下标只到 6。于是每一块都被标成&amp;quot;后面还有&amp;quot;，卡就一直回&amp;quot;好的，继续&amp;quot;，等着永远不会来的下一块。上层读到一条空响应。&lt;/p&gt;
&lt;p&gt;一个正好卡在整除边界上的差一错误。数据长度不是 120 的整数倍时，一切正常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：上游已经修了（&lt;code&gt;blockCount = 1 + (len-1)/mss&lt;/code&gt;），升级依赖即可。修完之后第 7 块终于发出了&amp;quot;结束&amp;quot;标志，流程第一次真正走到运营商服务器。&lt;/p&gt;
&lt;h3&gt;修好之后，服务器说&amp;quot;不&amp;quot;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;8.1.1 / 3.8  EID does not match the expected value, or there is no pending RSP Session for this eUICC.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次是一个真实的拒绝：那个激活码已经在一台手机上用掉了。&lt;/p&gt;
&lt;p&gt;激活码通常是一次性的，有些还绑定了卡的 EID。测试时要准备一个没用过的码，或者换一张卡。&lt;/p&gt;
&lt;p&gt;值得高兴的是，&lt;strong&gt;一个真实的业务拒绝，比一个解析错误有价值得多&lt;/strong&gt;——它说明前面几十步全对了。&lt;/p&gt;
&lt;h2&gt;把 4G 共享给局域网&lt;/h2&gt;
&lt;p&gt;每个模组可以起一个代理实例，出站流量严格绑定到该模组的网卡上，不会串。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;共享之前，先确认这张卡是不是在漫游。&lt;/strong&gt; 测试用的一张卡注册状态就是漫游（&lt;code&gt;AT+CEREG?&lt;/code&gt; 返回 &lt;code&gt;0,5&lt;/code&gt;，5 表示已注册且漫游）。这种状态下，一条建立数据连接的命令，或者一次 DHCP，都可能产生很贵的流量费。&lt;/p&gt;
&lt;p&gt;这张卡后来只用来做 Wi-Fi 通话（下一篇），蜂窝射频甚至可以一直停在飞行模式。&lt;/p&gt;
&lt;h2&gt;几个小坑&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;现象&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;th&gt;解决&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;不在安装目录下启动就报 &lt;code&gt;open config/config.yaml: no such file or directory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;配置路径相对于工作目录&lt;/td&gt;
&lt;td&gt;工作目录下找不到时，改用二进制所在目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unable to open database file: out of memory (14)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data/&lt;/code&gt; 目录不存在。这个错误信息完全在误导人&lt;/td&gt;
&lt;td&gt;打开数据库前先建目录&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调试时 AT 口被占用&lt;/td&gt;
&lt;td&gt;服务正在运行并独占了它&lt;/td&gt;
&lt;td&gt;调试开始时停一次服务，调完再起，不要每一步都重启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一个读进程被遗留在模组里，之后的程序&amp;quot;收不到回复&amp;quot;&lt;/td&gt;
&lt;td&gt;杀掉本地的 adb 客户端&lt;strong&gt;不会&lt;/strong&gt;杀掉模组里的读进程&lt;/td&gt;
&lt;td&gt;第一次打开设备节点时清理残留（第五篇详述）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;现在有什么了&lt;/h2&gt;
&lt;p&gt;一台插着电、常年开机的树莓派，管着两张 SIM 卡，收短信、发短信、下载 eSIM、共享 4G。短信推到手机上，整个过程不占任何一个手机卡槽。&lt;/p&gt;
&lt;p&gt;下一篇是整个系列里最硬的一篇：不用蜂窝信号，让 SIM 卡通过 Wi-Fi 连上运营商的核心网，照样打电话收短信。&lt;/p&gt;
&lt;h2&gt;全系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;这个系列是个人学习和技术研究记录。请遵守所在地法律法规和运营商服务条款。&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Raspberry Pi</category><category>Technology</category></item><item><title>一只大疆 4G dongle，拆开发现里面跑着 Linux（一）</title><link>https://wuming.si/blog/4002/</link><guid isPermaLink="true">https://wuming.si/blog/4002/</guid><pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;下一篇：&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大疆有一个给无人机和遥控器用的 4G 配件，一只 U 盘大小的东西，插上 SIM 卡就能让设备联网。二手市场上很便宜。买它的人大多拿它当上网卡用。&lt;/p&gt;
&lt;p&gt;把它插到电脑上，第一印象也确实就是个上网卡：macOS 什么反应都没有，Linux 冒出一排叫 &lt;code&gt;ttyUSB0&lt;/code&gt; 到 &lt;code&gt;ttyUSB3&lt;/code&gt; 的设备。看上去乏善可陈。&lt;/p&gt;
&lt;p&gt;但它里面其实是一台完整的 LTE 模组——跑着 Linux，有 root 权限的调试接口，能打电话、收发短信、读写 eSIM。接下来这个系列会把它接到树莓派、Mac、iPhone 和安卓平板上，让它真的当一部电话用。所有后面的东西，都建立在这一篇搞清楚的几件事上。&lt;/p&gt;
&lt;p&gt;先解释两个词，后面会反复出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;模组（module）&lt;/strong&gt;：一块完整的蜂窝通信芯片组，自己有 CPU、有系统、有射频。手机里的&amp;quot;基带&amp;quot;就是这种东西，只是被焊死在主板上。独立卖的模组通常通过 USB 或串口接到主机上，由主机发命令指挥它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AT 命令&lt;/strong&gt;：指挥模组的语言，一种非常古老的文本协议，从上世纪的拨号猫一直用到今天。你发一行 &lt;code&gt;ATI&lt;/code&gt; 过去，它回一段自我介绍。所有蜂窝模组都说这套话。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;它到底是什么&lt;/h2&gt;
&lt;p&gt;先问它自己是谁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;ATI
Baiwang
QDC507
Revision: QDC507GLEFM21
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查下来是这样一台东西：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;项目&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;USB 标识（大疆版）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2CA3:4006&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USB 标识（通用版）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2C7C:0125&lt;/code&gt;（和移远 EC25 系列同一个 ID）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;基带芯片&lt;/td&gt;
&lt;td&gt;高通 MDM9607，Cat.4 LTE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;模组内部系统&lt;/td&gt;
&lt;td&gt;Linux，内核 3.18.44，调试服务以 root 运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AT 命令风格&lt;/td&gt;
&lt;td&gt;移远（Quectel）系，&lt;code&gt;AT+QCFG&lt;/code&gt;、&lt;code&gt;AT+QNETDEVCTL&lt;/code&gt;、&lt;code&gt;AT+QGPS&lt;/code&gt; 这一套&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这个发现很有用：网上关于移远 EC20/EC25 的资料基本都能套用——AT 命令、USB 配置、驱动都一样。大疆版和市面上的通用版在我们要用的功能上没有区别，后面统一叫它 QDC507。&lt;/p&gt;
&lt;h2&gt;插上去之后，究竟露出了什么&lt;/h2&gt;
&lt;p&gt;一个 USB 设备可以同时提供好几个&amp;quot;接口&amp;quot;，就像一根线里塞了好几条独立通道。模组露出哪几条，由一个叫 &lt;code&gt;usbcfg&lt;/code&gt; 的配置决定：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AT+QCFG=&amp;quot;usbcfg&amp;quot;
+QCFG: &amp;quot;usbcfg&amp;quot;,0x2C7C,0x125,1,1,1,1,1,1,1
                             │ │ │ │ │ │ └─ USB 音频
                             │ │ │ │ │ └─── ADB
                             │ │ │ │ └───── 网卡
                             │ │ │ └─────── Modem
                             │ │ └───────── AT
                             │ └─────────── NMEA（GPS）
                             └───────────── DIAG（诊断）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后两位是整个系列的钥匙。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ADB 开着&lt;/strong&gt;，意味着可以像调试安卓手机那样 &lt;code&gt;adb shell&lt;/code&gt; 进到模组里面的 Linux，而且直接就是 root。ADB 是安卓的调试桥，高通的模组普遍带着它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;音频开着&lt;/strong&gt;，意味着模组会额外伪装成一块 USB 声卡。通话的声音将来就从这块声卡进出。&lt;/p&gt;
&lt;p&gt;手上这只大疆版出厂就是两个都开。另一只通用版出厂是两个都关，需要先解锁：&lt;code&gt;AT+QADBKEY?&lt;/code&gt; 会给一个 8 位挑战码，用 MD5-crypt 算出应答，再用 &lt;code&gt;AT+QADBKEY=&amp;quot;…&amp;quot;&lt;/code&gt; 提交，然后才能改 &lt;code&gt;usbcfg&lt;/code&gt;。改完发 &lt;code&gt;AT+CFUN=1,1&lt;/code&gt; 让模组重启，USB 会重新枚举一次，不用拔插。&lt;/p&gt;
&lt;p&gt;还有一个细节，它决定了后面每个平台的命运：前五个接口都是 &lt;strong&gt;USB 厂商自定义类&lt;/strong&gt;。USB 有一套标准分类（键盘是一类，U 盘是一类，声卡是一类），操作系统看到标准类就自动装驱动。厂商自定义类等于告诉系统&amp;quot;这是我自己的东西，你别管&amp;quot;。结果是：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;平台&lt;/th&gt;
&lt;th&gt;能看到什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;&lt;code&gt;option&lt;/code&gt; 驱动认领这些接口，出现 &lt;code&gt;ttyUSB0~3&lt;/code&gt;，网卡另有驱动认领&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;没有任何驱动认领，所以 &lt;code&gt;/dev/cu.*&lt;/code&gt; 里根本不会出现这只模组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;同样没有驱动，App 只能用 USB Host API 自己去碰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;也就是说，在 Mac 和安卓上，那条&amp;quot;发 AT 命令&amp;quot;的常规通道从一开始就不存在。&lt;/p&gt;
&lt;h2&gt;绕过去：模组内部有一条通道&lt;/h2&gt;
&lt;p&gt;既然外面没有串口，那就从里面走。&lt;code&gt;adb shell&lt;/code&gt; 进去看看：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;adb shell
# ls /dev/smd*
/dev/smd7 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/dev/smd7&lt;/code&gt; 是模组内部直通基带 AT 解释器的一个设备节点。只要 ADB 能用，任何平台都可以这样发 AT 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;adb shell &amp;quot;cat /dev/smd7 &amp;gt; /tmp/at.out 2&amp;gt;/dev/null &amp;amp; CP=\$!; sleep 0.3; \
  printf &amp;#39;ATI\r&amp;#39; &amp;gt; /dev/smd7; sleep 2; kill \$CP 2&amp;gt;/dev/null; cat /tmp/at.out&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看着别扭，但它管用，而且它成了后来 Mac App 和安卓 App 共同的控制通道。一个没有驱动的平台，只要能说 ADB，就能完整指挥这只模组。&lt;/p&gt;
&lt;h2&gt;四种联网方式&lt;/h2&gt;
&lt;p&gt;模组怎么把网络交给主机，也是可配置的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AT+QCFG=&amp;quot;usbnet&amp;quot;,N     改完要 AT+CFUN=1,1 重启生效
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;Linux&lt;/th&gt;
&lt;th&gt;macOS&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;QMI&lt;/td&gt;
&lt;td&gt;有驱动，但要自己拨号&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ECM&lt;/td&gt;
&lt;td&gt;有驱动&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;直接出现一块网卡&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;直接当 USB 以太网&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;MBIM&lt;/td&gt;
&lt;td&gt;有驱动&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;部分支持&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;RNDIS&lt;/td&gt;
&lt;td&gt;有驱动&lt;/td&gt;
&lt;td&gt;没有&lt;/td&gt;
&lt;td&gt;部分支持&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;ECM 是这里面最省事的一个：模组自己做 NAT 和 DHCP，主机拿到一个 &lt;code&gt;192.168.225.x&lt;/code&gt; 的地址，模组自己是 &lt;code&gt;.1&lt;/code&gt;。插上就有网，什么都不用配。中国移动的卡还会直接下发原生 IPv6。&lt;/p&gt;
&lt;p&gt;这个&amp;quot;省事&amp;quot;后面会变成一个麻烦，第五篇会讲到——它省事到你根本不知道自己的流量已经走了 SIM 卡。&lt;/p&gt;
&lt;h2&gt;拨通了，但没有声音&lt;/h2&gt;
&lt;p&gt;这是第一个真正让人困惑的地方。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ATD10086;&lt;/code&gt; 能拨出去，&lt;code&gt;AT+CLCC&lt;/code&gt; 能查到通话处于接通状态，对面也确实接了——但两边都听不到任何声音。&lt;/p&gt;
&lt;p&gt;原因是&lt;strong&gt;信令和声音是两套完全独立的东西&lt;/strong&gt;。AT 命令负责信令：拨号、振铃、接通、挂断。声音走的是另一条路，而这条路默认是断的。&lt;/p&gt;
&lt;p&gt;移远系列模组传统上有个 &lt;code&gt;AT+QPCMV&lt;/code&gt; 命令，可以把通话音频从 USB 串口里吐出来。这个固件上它是个空壳：&lt;code&gt;AT+QPCMV=?&lt;/code&gt; 说支持，真去执行 &lt;code&gt;AT+QPCMV=1,0&lt;/code&gt; 回 &lt;code&gt;ERROR&lt;/code&gt;，而用 &lt;code&gt;AT+CLAC&lt;/code&gt; 列出的 246 条可用命令里根本没有它。&lt;/p&gt;
&lt;p&gt;真正的路径在模组内部：加载语音驱动，跑一遍音频校准，再启动一个桥接程序，把高通 DSP 里的通话音频接到那块 USB 声卡上。主机侧看到的，就是一块 &lt;strong&gt;8 kHz 单声道 16 bit&lt;/strong&gt; 的声卡——电话音质的标准规格。&lt;/p&gt;
&lt;p&gt;这条路细节很多，留到第四篇。这里只要记住一件事：&lt;strong&gt;&lt;code&gt;ATD&lt;/code&gt; 只负责信令，声音要另外搭，而且每通电话之前都得检查一遍。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;还有几个能力&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;eSIM&lt;/strong&gt;：插入 eUICC 卡（可写入的 eSIM 卡）时，可以用 &lt;code&gt;AT+CCHO&lt;/code&gt;、&lt;code&gt;AT+CGLA&lt;/code&gt;、&lt;code&gt;AT+CCHC&lt;/code&gt; 直接和卡里的管理程序对话，完整跑一遍 eSIM 下载流程。第二篇讲。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISIM&lt;/strong&gt;：同一套命令能读取 ISIM 应用、跑 IMS 鉴权，这是 Wi-Fi 通话的基础。第三篇讲。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPS&lt;/strong&gt;：支持，默认关。但这个功能有毒——&lt;code&gt;AT+QGPS=1&lt;/code&gt; 之后，定位数据会&lt;strong&gt;直接灌进 &lt;code&gt;/dev/smd7&lt;/code&gt;&lt;/strong&gt;，把 AT 命令的回复全部淹掉，而且这个固件不支持把定位输出改到别的端口。开着 GPS 期间，通话和短信全都会乱。想用 GPS 就别想用别的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;刚上手就会撞到的五个坑&lt;/h2&gt;
&lt;p&gt;这些不是什么高深问题，但每一个都足够让人卡上大半天。&lt;/p&gt;
&lt;h3&gt;USB 序列号是 256 个 0xAA&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;：Mac 上 &lt;code&gt;adb devices&lt;/code&gt; 明明能看到设备，但程序里发出去的所有 adb 命令都像石沉大海，没有任何输出。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;原因&lt;/strong&gt;：这个固件的 USB 序列号字段是 256 个字节的 &lt;code&gt;0xAA&lt;/code&gt;，不是合法的 UTF-8 文本。Swift 的严格解码直接返回 &lt;code&gt;nil&lt;/code&gt;，之后所有命令都带着一个空序列号，静默失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;怎么办&lt;/strong&gt;：解码要宽松。更重要的是，&lt;strong&gt;不要用序列号区分模组&lt;/strong&gt;，因为每一只都一模一样。用 &lt;code&gt;adb -t &amp;lt;transport_id&amp;gt;&lt;/code&gt; 寻址，用 USB 物理路径（比如 &lt;code&gt;usb:1-1.2&lt;/code&gt;）作为持久身份——transport id 每次重新枚举都会变，USB 路径不会。&lt;/p&gt;
&lt;h3&gt;两只一样的模组分不清&lt;/h3&gt;
&lt;p&gt;同样的原因：Linux 上两只 &lt;code&gt;2c7c:0125&lt;/code&gt; 也无法靠厂商 ID、产品 ID 或序列号区分。一次重新枚举之后，两只模组的 &lt;code&gt;ttyUSB&lt;/code&gt; 编号整段互换，所有写死的路径同时失效。真正的解法是按物理端口写规则，第二篇给出具体写法。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;adb forward&lt;/code&gt; 看起来成功，其实不通&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;adb forward tcp:PORT dev:/dev/smd7&lt;/code&gt; 能建立，返回也正常，但读写都没有任何数据。不要在它上面搭任何东西。&lt;/p&gt;
&lt;h3&gt;来电提示根本没发到你在读的地方&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AT+QURCCFG=&amp;quot;urcport&amp;quot;
+QURCCFG: &amp;quot;urcport&amp;quot;,&amp;quot;usbat&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;模组主动上报的事件（&lt;code&gt;RING&lt;/code&gt; 来电、&lt;code&gt;+CLIP&lt;/code&gt; 来电号码、&lt;code&gt;+CMTI&lt;/code&gt; 新短信）叫 URC，而这个配置说：URC 只发到 USB 的 AT 接口。&lt;/p&gt;
&lt;p&gt;于是，通过 ADB 读 &lt;code&gt;/dev/smd7&lt;/code&gt; 的程序&lt;strong&gt;永远收不到来电通知&lt;/strong&gt;。你的代码没错，事件发到了另一扇门。&lt;/p&gt;
&lt;p&gt;两个办法一起用：把 URC 端口改成 &lt;code&gt;&amp;quot;all&amp;quot;&lt;/code&gt;（它是掉电保存的参数，先读再改，别每次启动都写）；同时把轮询 &lt;code&gt;AT+CLCC&lt;/code&gt; 和定期检查短信收件箱当作真正的事实来源，URC 只当&amp;quot;提醒你快去看一眼&amp;quot;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;AT+CFUN=1,1&lt;/code&gt; 会清空一切&lt;/h3&gt;
&lt;p&gt;这条命令重启模组。模组的 &lt;code&gt;/tmp&lt;/code&gt; 是内存盘，重启后加载进去的语音驱动、校准标记、桥接进程全部消失，USB 也重新枚举一次。&lt;/p&gt;
&lt;p&gt;结论是：任何&amp;quot;装一次就好了&amp;quot;的设计在这里都会失败。所有状态都必须做成每次检查、按需重建。这一条贯穿整个系列。&lt;/p&gt;
&lt;h2&gt;这一篇的结论&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;直觉&lt;/th&gt;
&lt;th&gt;实际&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;一个上网卡&lt;/td&gt;
&lt;td&gt;一台跑着 Linux、带 root 调试口的 LTE 模组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;没有串口就没法控制&lt;/td&gt;
&lt;td&gt;&lt;code&gt;adb shell&lt;/code&gt; 进去，走 &lt;code&gt;/dev/smd7&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;拨通了就该有声音&lt;/td&gt;
&lt;td&gt;声音要在模组内部另外搭一条路由&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;来电会有提示&lt;/td&gt;
&lt;td&gt;提示发到了你没在读的那个端口&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;下一篇把它插到树莓派上，让它开始干活：收发短信、管理 eSIM、给局域网共享 4G。&lt;/p&gt;
&lt;h2&gt;全系列&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4002&quot;&gt;它到底是什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4003&quot;&gt;树莓派上的短信与 eSIM 网关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4004&quot;&gt;从零实现 VoWiFi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4005&quot;&gt;USB 话机、浏览器通话与 ESP32&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4006&quot;&gt;插在 Mac 上&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4007&quot;&gt;iPhone 客户端&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wuming.si/blog/4008&quot;&gt;安卓平板直接驱动&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;这个系列是个人学习和技术研究记录。模组的语音驱动是 GPL 衍生物，不随项目分发，需要自行准备。请遵守所在地法律法规和运营商服务条款。&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>Finally, I am back</title><link>https://wuming.si/blog/4000/</link><guid isPermaLink="true">https://wuming.si/blog/4000/</guid><pubDate>Sat, 08 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;服务器挂了，数据全丢了。写了二十年的博客，一夜之间没了。&lt;/p&gt;
&lt;p&gt;还好有 archive.org。这个博客从 2005 年到 2025 年的 316 篇文章、147 张图片，都是从 Wayback Machine 的快照里一点点恢复回来的。旧的链接（&lt;code&gt;/archives/文章ID&lt;/code&gt;）依然有效。&lt;/p&gt;
&lt;p&gt;现在它是一个纯静态站点——没有数据库，没有后台，只有 Markdown 文件和 git。这次，它不会再丢了。&lt;/p&gt;
&lt;p&gt;Finally, I am back.&lt;/p&gt;
</content:encoded><category>Life</category></item><item><title>Preserving Original Client IP Addresses with OpenWrt Port Forwarding: A Fail2Ban Savior</title><link>https://wuming.si/blog/3497/</link><guid isPermaLink="true">https://wuming.si/blog/3497/</guid><pubDate>Wed, 04 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you want to read the Chinese version, move down to the Chinese Part, 本文包含中文版本，请移到后面的中文版本。&lt;/p&gt;
&lt;p&gt;I run a self-hosted environment at home, including an Immich photo server and an SSH server, both happily residing on my local network (LAN). To access these services remotely, I configured port forwarding on my OpenWrt router. Initially, everything seemed to function flawlessly – I could access my services from anywhere.&lt;/p&gt;
&lt;p&gt;However, a peculiar issue soon surfaced: my SSH server began refusing connections. A dive into the auth.log and Nginx access logs revealed something unexpected. All incoming internet traffic, whether to SSH or Immich, appeared to originate from 192.168.x.x – the LAN IP address of my OpenWrt router itself! This became a critical problem because persistent SSH scanning attempts from the internet were, understandably, being flagged by Fail2Ban. The consequence? Fail2Ban banned the “offending” IP, which was my router, effectively severing access to  &lt;em&gt;all&lt;/em&gt; my forwarded services for everyone, including myself.&lt;/p&gt;
&lt;p&gt;My troubleshooting journey involved extensive Googling and even consulting AI assistants, but these efforts proved fruitless. One AI suggestion was to install Nginx on the OpenWrt router itself to act as a reverse proxy. While a reverse proxy  &lt;em&gt;can&lt;/em&gt; preserve client IPs (using headers like X-Forwarded-For), installing a full-fledged Nginx instance on a resource-constrained router solely for this felt like overkill and not the most direct solution.&lt;/p&gt;
&lt;p&gt;The breakthrough came from an unexpected source: an iStoreOS issue tracker. (For the uninitiated, iStoreOS is a popular OpenWrt fork). While the issue thread didn’t contain a step-by-step solution, the original poster mentioned resolving a similar problem by tweaking NAT rules to prevent source address translation. Given the shared lineage between OpenWrt and iStoreOS, I suspected this approach might be applicable.&lt;/p&gt;
&lt;p&gt;Armed with this clue, I navigated to my OpenWrt LuCI interface: &lt;strong&gt;Network - &amp;gt; Firewall -&amp;gt; NAT Rules&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here, I added a new rule with the following key parameters:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://web.archive.org/web/2//images/2025/06/Screenshot-2025-06-04-at-6.40.19%E2%80%AFPM.png&quot;&gt;&lt;img src=&quot;https://web.archive.org/web/2//images/2025/06/Screenshot-2025-06-04-at-6.40.19%E2%80%AFPM-1024x582.png&quot; alt=&quot;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Action&lt;/strong&gt; : ACCEPT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Crucially&lt;/strong&gt; : I enabled an option typically labeled something like “&lt;strong&gt;Disable Source NAT&lt;/strong&gt; ” or “&lt;strong&gt;Disable Address Rewrite&lt;/strong&gt; ” (the exact wording might vary slightly depending on your OpenWrt version/theme, but the intent is to prevent the router from rewriting the source IP address of the incoming packets).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;(Self-correction: More accurately, the typical OpenWrt port forward is a DNAT rule. The issue is that an overly broad SNAT/Masquerade rule might be applying to this forwarded traffic. The key is to ensure that for these specific port-forwarded connections, SNAT is not performed, allowing the original source IP to pass through to the internal server. The “Disable Source NAT” option is often available directly within the port forward (DNAT) rule definition itself, or a specific “NO SNAT” rule needs to be crafted).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The result? &lt;strong&gt;Success!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Immediately after applying and saving this NAT rule, my server logs began showing the  &lt;em&gt;actual&lt;/em&gt; public IP addresses of external visitors. Most importantly, Fail2Ban could now accurately identify and ban malicious external IPs without inadvertently blacklisting my entire home network via the router’s IP.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why this works (The Geeky Bit):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Standard port forwarding involves Destination NAT (DNAT), where the router changes the destination IP of an incoming packet (from its public WAN IP to your internal server’s LAN IP). However, many default OpenWrt setups also apply Source NAT (SNAT) or Masquerading for traffic passing through it, especially from LAN to WAN. In some configurations, this SNAT can also incorrectly get applied to WAN-to-LAN forwarded traffic, replacing the original client’s IP with the router’s LAN IP before the packet reaches your internal server.&lt;/p&gt;
&lt;p&gt;The “Disable Source NAT” or “Disable Address Rewrite” option for the relevant traffic flow instructs OpenWrt’s netfilter firewall to  &lt;em&gt;not&lt;/em&gt; perform this source IP alteration for packets destined for your forwarded ports. This allows your internal services to see the true originating IP, essential for accurate logging, geolocation, and security tools like Fail2Ban.&lt;/p&gt;
&lt;p&gt;If you’re facing a similar issue where your internal services behind an OpenWrt router only see the router’s IP for external connections, diving into your NAT rules and looking for an option to prevent source address translation on your port forwards might just be the elegant solution you need.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Following is the Chinese part（中文版本）&lt;/p&gt;
&lt;p&gt;OpenWrt 端口转发：保留原始客户端 IP，解决 Fail2Ban 困境&lt;/p&gt;
&lt;p&gt;我在家中搭建了一套自托管服务环境，包括一个 Immich 照片服务器和一个 SSH 服务器，它们都安稳地运行在我的本地网络（LAN）中。为了能够远程访问这些服务，我在我的 OpenWrt 路由器上配置了端口转发。起初，一切似乎都完美无缺——我可以从任何地方访问我的服务。&lt;/p&gt;
&lt;p&gt;然而，一个奇怪的问题很快浮出水面：我的 SSH 服务器开始拒绝连接。深入查看 auth.log 和 Nginx 的访问日志后，我发现了一些出乎意料的情况。所有来自互联网的访问流量，无论是到 SSH还是 Immich，都显示其源 IP 地址为 192.168.x.x——这正是我 OpenWrt 路由器的局域网 IP 地址！这是一个严重的问题，因为来自互联网的持续性 SSH 扫描尝试，理所当然地被 Fail2Ban 标记了。结果呢？Fail2Ban 封禁了这个“违规”的 IP，也就是我的路由器，从而有效地切断了所有人（包括我自己）对我所有转发服务的访问。&lt;/p&gt;
&lt;p&gt;我的故障排除过程包括了大量的谷歌搜索，甚至咨询了 AI 助手，但这些努力都未能奏效。一个 AI 给出的建议是在 OpenWrt 路由器上安装 Nginx 作为反向代理。虽然反向代理 &lt;em&gt;确实&lt;/em&gt; 可以通过 X-Forwarded-For 之类的头部信息来保留客户端 IP，但在资源受限的路由器上仅仅为此目的安装一个完整的 Nginx 实例，感觉有些小题大做，也不是最直接的解决方案。&lt;/p&gt;
&lt;p&gt;转机来自一个意想不到的地方：一个 iStoreOS 的问题追踪器。（对于不熟悉的人来说，iStoreOS 是一个流行的 OpenWrt 分支）。虽然那个问题帖子里没有包含手把手的解决方案，但原帖主提到他通过调整 NAT 规则，阻止了源地址转换，从而解决了类似问题。考虑到 OpenWrt 和 iStoreOS 之间的同源关系，我猜测这种方法可能也适用。&lt;/p&gt;
&lt;p&gt;带着这条线索，我进入了我的 OpenWrt LuCI 管理界面：&lt;strong&gt;网络 (Network) - &amp;gt; 防火墙 (Firewall) -&amp;gt; NAT 规则 (NAT Rules)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在这里，我添加了一条新的规则，关键参数如下：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://web.archive.org/web/2//images/2025/06/Screenshot-2025-06-04-at-6.40.19%E2%80%AFPM.png&quot;&gt;&lt;img src=&quot;https://web.archive.org/web/2//images/2025/06/Screenshot-2025-06-04-at-6.40.19%E2%80%AFPM-1024x582.png&quot; alt=&quot;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动作 (Action)&lt;/strong&gt; : ACCEPT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;至关重要的一点&lt;/strong&gt; ：我启用了通常标记为“&lt;strong&gt;禁用源 NAT (Disable Source NAT)&lt;/strong&gt; ”或“&lt;strong&gt;禁用地址重写 (Disable Address Rewrite)&lt;/strong&gt; ”之类的选项（具体措辞可能因您的 OpenWrt 版本/主题而略有不同，但其目的是阻止路由器重写传入数据包的源 IP 地址）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;(自我修正：更准确地说，典型的 OpenWrt 端口转发是一条 DNAT 规则。问题在于，一个过于宽泛的 SNAT/Masquerade 规则可能也应用到了这些转发流量上。关键在于确保对于这些特定的端口转发连接，不执行 SNAT，从而允许原始源 IP 传递到内部服务器。“禁用源 NAT”选项通常可以直接在端口转发（DNAT）规则定义中找到，或者需要创建一个特定的“NO SNAT”规则。)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;结果呢？&lt;strong&gt;成功了！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在应用并保存这条 NAT 规则后，我的服务器日志立即开始显示外部访问者 &lt;em&gt;真实&lt;/em&gt; 的公网 IP 地址。最重要的是，Fail2Ban 现在可以准确识别并封禁恶意的外部 IP，而不会通过路由器的 IP 无意中将我的整个家庭网络列入黑名单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为何有效（技术解读）：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;标准的端口转发涉及目标网络地址转换 (DNAT)，路由器会将传入数据包的目标 IP（从其公网 WAN IP）更改为您内部服务器的 LAN IP。然而，许多默认的 OpenWrt 设置也会对通过它的流量（尤其是从 LAN 到 WAN 的流量）应用源网络地址转换 (SNAT) 或 IP 伪装 (Masquerading)。在某些配置下，这种 SNAT 也可能错误地应用于 WAN 到 LAN 的转发流量，在数据包到达您的内部服务器之前，将原始客户端的 IP 替换为路由器的 LAN IP。&lt;/p&gt;
&lt;p&gt;针对相关流量的“禁用源 NAT”或“禁用地址重写”选项，会指示 OpenWrt 的 netfilter 防火墙 &lt;em&gt;不要&lt;/em&gt; 对发往您转发端口的数据包执行这种源 IP 地址变更。这使得您的内部服务能够看到真实的原始 IP，这对于准确的日志记录、地理位置定位以及像 Fail2Ban 这样的安全工具至关重要。&lt;/p&gt;
&lt;p&gt;如果您也遇到类似的问题，即 OpenWrt 路由器后的内部服务对于外部连接只看得到路由器的 IP，那么深入研究您的 NAT 规则，寻找一个选项来阻止对端口转发流量进行源地址转换，或许正是您需要的那个优雅的解决方案。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;#OpenWrt Fail2Ban bans router IP&lt;/p&gt;
&lt;p&gt;#OpenWrt server sees router IP&lt;/p&gt;
&lt;p&gt;#OpenWrt logs show router IP&lt;/p&gt;
&lt;p&gt;#Fix OpenWrt port forward IP&lt;/p&gt;
&lt;p&gt;#SSH server behind OpenWrt shows router IP&lt;/p&gt;
&lt;p&gt;#Nginx behind OpenWrt shows router IP&lt;/p&gt;
&lt;p&gt;#OpenWrt hide client IP&lt;/p&gt;
</content:encoded></item><item><title>play mp3 on ubuntu cli – sound card not detecting</title><link>https://wuming.si/blog/3494/</link><guid isPermaLink="true">https://wuming.si/blog/3494/</guid><pubDate>Tue, 08 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When I was trying play mp3 files on ubuntu with cli tools, like mp123, there was no sound.&lt;/p&gt;
&lt;p&gt;And I tried when I logged into gnome, there was sound.&lt;/p&gt;
&lt;p&gt;I googled, there were lots of ways, but none works for me.&lt;/p&gt;
&lt;p&gt;Then I got this from this page: &lt;a href=&quot;https://askubuntu.com/questions/471433/sound-card-not-detecting&quot;&gt;https://askubuntu.com/questions/471433/sound-card-not-detecting&lt;/a&gt;, the reason is current user has no right or not in the audio group.&lt;/p&gt;
&lt;p&gt;sudo apt-get update sudo apt-get –reinstall install pavucontrol linux-sound-base alsa-base alsa-utils libasound2 sudo usermod -aG $(grep -e ‘^pulse:’ -e ‘^audio:’ -e \ ‘^pulse-access:’ -e ‘^pulse-rt:’ -e ‘^video:’ /etc/group \ | awk -F: ‘{print $1}’ \ | tr ‘\n’ ‘,’ \ | sed ‘s:,$::g’) $(whoami)&lt;/p&gt;
&lt;p&gt;Da~da, it works. Hope it good for you.&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>AI on Mac Studio – 1:  Running fuyu-8B on mac studio</title><link>https://wuming.si/blog/3473/</link><guid isPermaLink="true">https://wuming.si/blog/3473/</guid><pubDate>Wed, 01 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is the first article about running an AI model on Mac Studio, and I will continue to migrate the environment from CUDA / Nvidia GPU to Mac MPS.&lt;/p&gt;
&lt;p&gt;Why did I choose Mac Studio?&lt;/p&gt;
&lt;p&gt;I chose Mac Studio because it is less expensive. It has 192GB of memory that can be used as a GPU. This means that it is possible to migrate the program from Nvidia GPU and save some money for personal use.&lt;/p&gt;
&lt;p&gt;What is Fuyu-8B?&lt;/p&gt;
&lt;p&gt;We are releasing Fuyu-8B, a small version of the multimodal model that powers our product. The model is available on HuggingFace. We think Fuyu-8B is exciting because:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It has a much simpler architecture and training procedure than other multimodal models, making it easier to understand, scale, and deploy.&lt;/li&gt;
&lt;li&gt;It is designed from the ground up for digital agents, so it can support arbitrary image resolutions, answer questions about graphs and diagrams, answer UI-based questions, and perform fine-grained localization on screen images.&lt;/li&gt;
&lt;li&gt;It is fast – we can get responses for large images in less than 100 milliseconds.&lt;/li&gt;
&lt;li&gt;Despite being optimized for our use case, it performs well at standard image understanding benchmarks such as visual question-answering and natural-image-captioning.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ok, let’s do it now.&lt;/p&gt;
&lt;p&gt;Prepare the environment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You need Python 3.6+ and virtualenv installed. Conda or venv also work.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;cssCopy code&lt;/p&gt;
&lt;p&gt;&lt;code&gt;virtualenv -p python3 py3&lt;/code&gt;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;Download the HuggingFace transformers and clone the transformer from GitHub.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;bashCopy code&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git clone https://github.com/huggingface/transformers.git cd transformers pip install .&lt;/code&gt;&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;Download the model from &lt;a href=&quot;https://huggingface.co/adept/fuyu-8b&quot;&gt;https://huggingface.co/adept/fuyu-8b&lt;/a&gt;. You can use clone or download it manually, depending on your network speed.&lt;/li&gt;
&lt;li&gt;Install PyTorch from this page &lt;a href=&quot;https://pytorch.org/get-started/locally/&quot;&gt;https://pytorch.org/get-started/locally/&lt;/a&gt; depending on your environment. Since I am using Mac M2 Studio, I should install this:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Copy code&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pip3 install torch torchvision torchaudio&lt;/code&gt;&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;You are almost done here; now we can start the samples.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Sample 1: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from transformers import FuyuProcessor, FuyuForCausalLM
from PIL import Image
# load model and processor
model_id = &amp;quot;.&amp;quot;
processor = FuyuProcessor.from_pretrained(model_id)
model = FuyuForCausalLM.from_pretrained(model_id, device_map=&amp;quot;mps&amp;quot;, torch_dtype=torch.float16)
# prepare inputs for the model
text_prompt = &amp;quot;Generate a coco-style caption.\n&amp;quot;
image_path = &amp;quot;bus.png&amp;quot;  # https://huggingface.co/adept-hf-collab/fuyu-8b/blob/main/bus.png
image = Image.open(image_path)
inputs = processor(text=text_prompt, images=image, return_tensors=&amp;quot;pt&amp;quot;)
for k, v in inputs.items():
    inputs[k] = v.to(&amp;quot;mps&amp;quot;)
# autoregressively generate text
generation_output = model.generate(**inputs, max_new_tokens=7)
generation_text = processor.batch_decode(generation_output[:, -7:], skip_special_tokens=True)
print(generation_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sample 2:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from transformers import FuyuProcessor, FuyuForCausalLM
from PIL import Image
import torch
def list_files_in_directory(path, extensions=[&amp;quot;.png&amp;quot;, &amp;quot;.jpeg&amp;quot;, &amp;quot;.jpg&amp;quot;, &amp;quot;.JPG&amp;quot;, &amp;quot;.PNG&amp;quot;, &amp;quot;.JPEG&amp;quot;]):
    files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and any(f.endswith(ext) for ext in extensions)]
    return files
def main():
    # load model and processor
    model_id = &amp;quot;.&amp;quot; #adept/fuyu-8b&amp;quot;
    processor = FuyuProcessor.from_pretrained(model_id)
    model = FuyuForCausalLM.from_pretrained(model_id, device_map=&amp;quot;mps&amp;quot;, torch_dtype=torch.float16) # To solve OOM, float16 enables operation with only 24GB of VRAM. Alternatively float16 can be replaced with bfloat16 with differences in loading time and inference time.
    # Load last image path or ask user
    try:
        with open(&amp;quot;last_path.txt&amp;quot;, &amp;quot;r&amp;quot;) as f:
            last_path = f.read().strip()
        user_input = input(f&amp;quot;Do you want to use the last path &amp;#39;{last_path}&amp;#39;? (yes/no, default yes): &amp;quot;)
        if not user_input or user_input.lower() != &amp;#39;no&amp;#39;:
            last_path = last_path
        else:
            raise ValueError(&amp;quot;User chose to input a new path.&amp;quot;)
    except:
        last_path = input(&amp;quot;Please provide the image directory path: &amp;quot;)
        with open(&amp;quot;last_path.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:
            f.write(last_path)
    while True:
        # List the first 10 images in the directory
        images = list_files_in_directory(last_path)[:10]
        for idx, image in enumerate(images, start=1):
            print(f&amp;quot;{idx}. {image}&amp;quot;)
        # Allow the user to select an image
        image_choice = input(f&amp;quot;Choose an image (1-{len(images)}) or enter its name: &amp;quot;)
        try:
            idx = int(image_choice)
            image_path = os.path.join(last_path, images[idx-1])
        except ValueError:
            image_path = os.path.join(last_path, image_choice)
        try:
            image = Image.open(image_path)
        except:
            print(&amp;quot;Cannot open the image. Please check the path and try again.&amp;quot;)
            continue
        questions = [
            &amp;quot;Generate a coco-style caption.&amp;quot;,
            &amp;quot;What color is the object?&amp;quot;,
            &amp;quot;Describe the scene.&amp;quot;,
            &amp;quot;Describe the facial expression of the character.&amp;quot;,
            &amp;quot;Tell me about the story from the image.&amp;quot;,
            &amp;quot;Enter your own question&amp;quot;
        ]
        # Asking the user to select a question from list, or select to input one
        for idx, q in enumerate(questions, start=1):
            print(f&amp;quot;{idx}. {q}&amp;quot;)
        q_choice = int(input(&amp;quot;Choose a question or enter your own: &amp;quot;))
        if q_choice &amp;lt;= 5:
            text_prompt = questions[q_choice-1] + &amp;#39;\n&amp;#39;
        else:
            text_prompt = input(&amp;quot;Please enter your question: &amp;quot;) + &amp;#39;\n&amp;#39;
        while True: # To enable the user to ask further question about an image
            inputs = processor(text=text_prompt, images=image, return_tensors=&amp;quot;pt&amp;quot;)
            for k, v in inputs.items():
                inputs[k] = v.to(&amp;quot;mps&amp;quot;)
            # To eliminate attention_mask warning
            inputs[&amp;quot;attention_mask&amp;quot;] = torch.ones(inputs[&amp;quot;input_ids&amp;quot;].shape, device=&amp;quot;mps&amp;quot;)
            generation_output = model.generate(**inputs, max_new_tokens=50, pad_token_id=model.config.eos_token_id)
            generation_text = processor.batch_decode(generation_output[:, -50:], skip_special_tokens=True)
            print(&amp;quot;Answer:&amp;quot;, generation_text[0])
            text_prompt = input(&amp;quot;Ask another question about the same image or type &amp;#39;/exit&amp;#39; to exit: &amp;quot;) + &amp;#39;\n&amp;#39;
            if text_prompt.strip() == &amp;#39;/exit&amp;#39;:
                break
#if name == &amp;quot;main&amp;quot;:
main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://web.archive.org/web/2//images/2023/11/download-27.jpeg&quot;&gt;&lt;img src=&quot;https://web.archive.org/web/2//images/2023/11/download-27.jpeg&quot; alt=&quot;&quot;&gt;&lt;/a&gt; &lt;a href=&quot;https://web.archive.org/web/2//images/2023/11/Screenshot-2023-11-01-at-6.46.12%E2%80%AFPM.png&quot;&gt;&lt;img src=&quot;https://web.archive.org/web/2//images/2023/11/Screenshot-2023-11-01-at-6.46.12%E2%80%AFPM-1024x507.png&quot; alt=&quot;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;yes, It is Chinese. But not the Chinese fuyu-7b knows. It is not “食” (eating) , but “我不想洗碗”( i don’t want to wash the dishes). Fuyu-7b is lying. lol.&lt;/p&gt;
</content:encoded><category>AI</category><category>Technology</category></item><item><title>redsocks升级记录</title><link>https://wuming.si/blog/3470/</link><guid isPermaLink="true">https://wuming.si/blog/3470/</guid><pubDate>Wed, 18 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一直在用redsocks做流量转发，用着还是挺不错的，但是偶尔还是会hang住，导致网络卡住。老早就有升级的想法，一直没有动手。&lt;/p&gt;
&lt;p&gt;什么是redsocks？&lt;/p&gt;
&lt;p&gt;This tool allows you to redirect any TCP connection to SOCKS or HTTPS proxy using your firewall, so redirection may be system-wide or network-wide.&lt;/p&gt;
&lt;p&gt;When is redsocks useful?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you want to route part of TCP traffic via OpenSSH &lt;code&gt;DynamicForward&lt;/code&gt; Socks5 port using firewall policies. That was original redsocks development goal;&lt;/li&gt;
&lt;li&gt;you use DVB ISP and this ISP provides internet connectivity with some special daemon that may be also called “Internet accelerator” and the accelerator acts as a proxy and has no “transparent proxy” feature and you need it. Globax was an example of alike accelerator, but Globax 5 has transparent proxy feature. That was the second redsocks` development goal;&lt;/li&gt;
&lt;li&gt;you have to pass traffic through proxy due to corporate network limitation. That was never a goal for redsocks, but users have reported success with some proxy configurations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上是官方介绍。功能确实很强大，毕竟是俄国大佬的作品，继承了俄国产品强大而又粗糙的刻板印象。翻了他的repo，发现最后更新是2019年，而且还是改typo，其他的issue没有回复，大概大佬也不打算维护了吧。找了好久替代，发现了它的一个fork，似乎改了不少东西，就拖下来试试看吧。运行了一晚上，稳定性似乎还不错。&lt;/p&gt;
&lt;p&gt;github地址 &lt;a href=&quot;https://github.com/semigodking/redsocks&quot;&gt;https://github.com/semigodking/redsocks&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;该repo依赖openssl和libevent2，libevent2在树莓派的repo中没有了，所以只能现编译。&lt;/p&gt;
&lt;p&gt;现在把安装过程记录一下（仅限Raspberry Pi）：&lt;/p&gt;
&lt;p&gt;#安装openssl开发包&lt;/p&gt;
&lt;p&gt;sudo apt install libssl-dev &lt;/p&gt;
&lt;p&gt;#获取代码&lt;/p&gt;
&lt;p&gt;wget &lt;a href=&quot;https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz&quot;&gt;https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;tar xvfz libevent-2.1.12-stable.tar.gz&lt;/p&gt;
&lt;p&gt;cd libevent-2.1.12-stable&lt;/p&gt;
&lt;p&gt;编译、安装&lt;/p&gt;
&lt;p&gt;make&lt;br&gt;sudo make install&lt;/p&gt;
&lt;p&gt;#克隆redsocks2的代码&lt;/p&gt;
&lt;p&gt;git clone &lt;a href=&quot;https://github.com/semigodking/redsocks&quot;&gt;https://github.com/semigodking/redsocks&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;cd redsocks&lt;/p&gt;
&lt;p&gt;make DISABLE_SHADOWSOCKS=1 ENABLE_STATIC=1&lt;/p&gt;
&lt;p&gt;编译完，修改之前redsocks的配置文件、替换redsocks文件，重启服务即可。&lt;/p&gt;
&lt;p&gt;目前暂时稳定。&lt;/p&gt;
</content:encoded><category>Raspberry Pi</category><category>Technology</category></item><item><title>訓練diffusion模型</title><link>https://wuming.si/blog/3467/</link><guid isPermaLink="true">https://wuming.si/blog/3467/</guid><pubDate>Fri, 25 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;訓練擴散模型&lt;/h1&gt;
&lt;p&gt;無條件圖像生成是擴散模型的一種流行應用，它生成的圖像與用於訓練的數據集中的圖像相似。通常，最好的結果是通過在特定數據集上微調預訓練模型來獲得的。您可以在 Hub 上找到許多這樣的檢查點，但如果您找不到您喜歡的檢查點，您可以隨時訓練自己的檢查點！&lt;/p&gt;
&lt;p&gt;本教程將教您如何在 Smithsonian Butterflies 數據集的子集上從頭開始訓練 UNet2DModel，以生成您自己的 🦋 蝴蝶 🦋。&lt;/p&gt;
&lt;p&gt;💡 本培訓教程基於 🧨 擴散器筆記本進行培訓。有關擴散模型的更多詳細信息和背景（例如它們的工作原理），請查看筆記本！&lt;/p&gt;
&lt;p&gt;在開始之前，請確保您安裝了🤗數據集來加載和預處理圖像數據集，並安裝了🤗加速，以簡化在任意數量的 GPU 上的訓練。以下命令還將安裝 TensorBoard 以可視化訓練指標（您還可以使用權重和偏差來跟踪您的訓練）。&lt;/p&gt;
&lt;p&gt;我們鼓勵您與社區分享您的模型，為此，您需要登錄您的 Hugging Face 帳戶（如果您還沒有帳戶，請在此處創建一個！）。您可以從筆記本登錄並在出現提示時輸入您的令牌：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from huggingface_hub import notebook_login &amp;gt;&amp;gt;&amp;gt; notebook_login()&lt;/p&gt;
&lt;p&gt;或者從終端登錄：&lt;/p&gt;
&lt;p&gt;Copiedhuggingface-cli login&lt;/p&gt;
&lt;p&gt;由於模型檢查點非常大，因此安裝 Git-LFS 來對這些大文件進行版本控制：&lt;/p&gt;
&lt;p&gt;Copied!sudo apt -qq install git-lfs !git config –global credential.helper store&lt;/p&gt;
&lt;h2&gt;訓練配置&lt;/h2&gt;
&lt;p&gt;為了方便起見，創建一個包含訓練超參數的 &lt;code&gt;TrainingConfig&lt;/code&gt; 類（隨意調整它們）：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from dataclasses import dataclass &amp;gt;&amp;gt;&amp;gt; @dataclass … class TrainingConfig: … image_size = 128 &lt;em&gt;# the generated image resolution&lt;/em&gt; … train_batch_size = 16 … eval_batch_size = 16 &lt;em&gt;# how many images to sample during evaluation&lt;/em&gt; … num_epochs = 50 … gradient_accumulation_steps = 1 … learning_rate = 1e-4 … lr_warmup_steps = 500 … save_image_epochs = 10 … save_model_epochs = 30 … mixed_precision = “fp16” &lt;em&gt;# &lt;code&gt;no&lt;/code&gt; for float32, &lt;code&gt;fp16&lt;/code&gt; for automatic mixed precision&lt;/em&gt; … output_dir = “ddpm-butterflies-128” &lt;em&gt;# the model name locally and on the HF Hub&lt;/em&gt; … push_to_hub = True &lt;em&gt;# whether to upload the saved model to the HF Hub&lt;/em&gt; … hub_private_repo = False … overwrite_output_dir = True &lt;em&gt;# overwrite the old model when re-running the notebook&lt;/em&gt; … seed = 0 &amp;gt;&amp;gt;&amp;gt; config = TrainingConfig()&lt;/p&gt;
&lt;h2&gt;加載數據集&lt;/h2&gt;
&lt;p&gt;您可以使用 🤗 數據集庫輕鬆加載史密森尼蝴蝶數據集：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from datasets import load_dataset &amp;gt;&amp;gt;&amp;gt; config.dataset_name = “huggan/smithsonian_butterflies_subset” &amp;gt;&amp;gt;&amp;gt; dataset = load_dataset(config.dataset_name, split=”train”)&lt;/p&gt;
&lt;p&gt;💡 您可以從 HugGan 社區活動中找到其他數據集，也可以通過創建本地 &lt;code&gt;ImageFolder&lt;/code&gt; 來使用自己的數據集。如果數據集來自 HugGan 社區活動，則將 &lt;code&gt;config.dataset_name&lt;/code&gt; 設置為數據集的存儲庫 ID；如果您使用自己的圖像，則將 &lt;code&gt;imagefolder&lt;/code&gt; 設置為。&lt;/p&gt;
&lt;p&gt;🤗 Datasets 使用 Image 功能自動解碼圖像數據並將其加載為我們可以可視化的 &lt;code&gt;PIL.Image&lt;/code&gt; ：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; import matplotlib.pyplot as plt &amp;gt;&amp;gt;&amp;gt; fig, axs = plt.subplots(1, 4, figsize=(16, 4)) &amp;gt;&amp;gt;&amp;gt; for i, image in enumerate(dataset[:4][“image”]): … axs[i].imshow(image) … axs[i].set_axis_off() &amp;gt;&amp;gt;&amp;gt; fig.show()&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/butterflies_ds.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不過，這些圖像的尺寸各不相同，因此您需要先對它們進行預處理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Resize&lt;/code&gt; 將圖像大小更改為 &lt;code&gt;config.image_size&lt;/code&gt; 中定義的大小。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RandomHorizontalFlip&lt;/code&gt; 通過隨機鏡像圖像來擴充數據集。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Normalize&lt;/code&gt; 對於將像素值重新縮放到 [-1, 1] 範圍非常重要，這是模型所期望的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from torchvision import transforms &amp;gt;&amp;gt;&amp;gt; preprocess = transforms.Compose( … [ … transforms.Resize((config.image_size, config.image_size)), … transforms.RandomHorizontalFlip(), … transforms.ToTensor(), … transforms.Normalize([0.5], [0.5]), … ] … )&lt;/p&gt;
&lt;p&gt;使用 🤗 數據集的 set_transform 方法在訓練期間動態應用 &lt;code&gt;preprocess&lt;/code&gt; 函數：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; def transform(examples): … images = [preprocess(image.convert(“RGB”)) for image in examples[“image”]] … return {“images”: images} &amp;gt;&amp;gt;&amp;gt; dataset.set_transform(transform)&lt;/p&gt;
&lt;p&gt;請隨意再次可視化圖像以確認它們已調整大小。現在您已準備好將數據集包裝在 DataLoader 中進行訓練！&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; import torch &amp;gt;&amp;gt;&amp;gt; train_dataloader = torch.utils.data.DataLoader(dataset, batch_size=config.train_batch_size, shuffle=True)&lt;/p&gt;
&lt;h2&gt;創建 UNet2D 模型&lt;/h2&gt;
&lt;p&gt;🧨 Diffusers 中的預訓練模型可以使用您想要的參數輕鬆地從其模型類創建。例如，要創建 UNet2DModel：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from diffusers import UNet2DModel &amp;gt;&amp;gt;&amp;gt; model = UNet2DModel( … sample_size=config.image_size, &lt;em&gt;# the target image resolution&lt;/em&gt; … in_channels=3, &lt;em&gt;# the number of input channels, 3 for RGB images&lt;/em&gt; … out_channels=3, &lt;em&gt;# the number of output channels&lt;/em&gt; … layers_per_block=2, &lt;em&gt;# how many ResNet layers to use per UNet block&lt;/em&gt; … block_out_channels=(128, 128, 256, 256, 512, 512), &lt;em&gt;# the number of output channels for each UNet block&lt;/em&gt; … down_block_types=( … “DownBlock2D”, &lt;em&gt;# a regular ResNet downsampling block&lt;/em&gt; … “DownBlock2D”, … “DownBlock2D”, … “DownBlock2D”, … “AttnDownBlock2D”, &lt;em&gt;# a ResNet downsampling block with spatial self-attention&lt;/em&gt; … “DownBlock2D”, … ), … up_block_types=( … “UpBlock2D”, &lt;em&gt;# a regular ResNet upsampling block&lt;/em&gt; … “AttnUpBlock2D”, &lt;em&gt;# a ResNet upsampling block with spatial self-attention&lt;/em&gt; … “UpBlock2D”, … “UpBlock2D”, … “UpBlock2D”, … “UpBlock2D”, … ), … )&lt;/p&gt;
&lt;p&gt;快速檢查樣本圖像形狀與模型輸出形狀是否匹配通常是個好主意：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; sample_image = dataset[0][“images”].unsqueeze(0) &amp;gt;&amp;gt;&amp;gt; print(“Input shape:”, sample_image.shape) Input shape: torch.Size([1, 3, 128, 128]) &amp;gt;&amp;gt;&amp;gt; print(“Output shape:”, model(sample_image, timestep=0).sample.shape) Output shape: torch.Size([1, 3, 128, 128])&lt;/p&gt;
&lt;p&gt;偉大的！接下來，您需要一個調度程序來向圖像添加一些噪聲。&lt;/p&gt;
&lt;h2&gt;創建調度程序&lt;/h2&gt;
&lt;p&gt;根據您使用模型進行訓練還是推理，調度程序的行為會有所不同。在推理過程中，調度器根據噪聲生成圖像。在訓練期間，調度程序從擴散過程中的特定點獲取模型輸出（或樣本），並根據噪聲調度和更新規則將噪聲應用於圖像。&lt;/p&gt;
&lt;p&gt;讓我們看一下 DDPMScheduler，並使用 &lt;code&gt;add_noise&lt;/code&gt; 方法向之前的 &lt;code&gt;sample_image&lt;/code&gt; 添加一些隨機噪聲：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; import torch &amp;gt;&amp;gt;&amp;gt; from PIL import Image &amp;gt;&amp;gt;&amp;gt; from diffusers import DDPMScheduler &amp;gt;&amp;gt;&amp;gt; noise_scheduler = DDPMScheduler(num_train_timesteps=1000) &amp;gt;&amp;gt;&amp;gt; noise = torch.randn(sample_image.shape) &amp;gt;&amp;gt;&amp;gt; timesteps = torch.LongTensor([50]) &amp;gt;&amp;gt;&amp;gt; noisy_image = noise_scheduler.add_noise(sample_image, noise, timesteps) &amp;gt;&amp;gt;&amp;gt; Image.fromarray(((noisy_image.permute(0, 2, 3, 1) + 1.0) * 127.5).type(torch.uint8).numpy()[0])&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/noisy_butterfly.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;模型的訓練目標是預測添加到圖像中的噪聲。這一步的損失可以通過下式計算：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; import torch.nn.functional as F &amp;gt;&amp;gt;&amp;gt; noise_pred = model(noisy_image, timesteps).sample &amp;gt;&amp;gt;&amp;gt; loss = F.mse_loss(noise_pred, noise)&lt;/p&gt;
&lt;h2&gt;訓練模型&lt;/h2&gt;
&lt;p&gt;到目前為止，您已經掌握了開始訓練模型的大部分內容，剩下的就是將所有內容組合在一起。&lt;/p&gt;
&lt;p&gt;首先，您需要一個優化器和一個學習率調度器：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from diffusers.optimization import get_cosine_schedule_with_warmup &amp;gt;&amp;gt;&amp;gt; optimizer = torch.optim.AdamW(model.parameters(), lr=config.learning_rate) &amp;gt;&amp;gt;&amp;gt; lr_scheduler = get_cosine_schedule_with_warmup( … optimizer=optimizer, … num_warmup_steps=config.lr_warmup_steps, … num_training_steps=(len(train_dataloader) * config.num_epochs), … )&lt;/p&gt;
&lt;p&gt;然後，您需要一種評估模型的方法。為了進行評估，您可以使用 DDPMPipeline 生成一批樣本圖像並將其保存為網格：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from diffusers import DDPMPipeline &amp;gt;&amp;gt;&amp;gt; from diffusers.utils import make_image_grid &amp;gt;&amp;gt;&amp;gt; import math &amp;gt;&amp;gt;&amp;gt; import os &amp;gt;&amp;gt;&amp;gt; def evaluate(config, epoch, pipeline): … &lt;em&gt;# Sample some images from random noise (this is the backward diffusion process).&lt;/em&gt; … &lt;em&gt;# The default pipeline output type is &lt;code&gt;List[PIL.Image]&lt;/code&gt;&lt;/em&gt; … images = pipeline( … batch_size=config.eval_batch_size, … generator=torch.manual_seed(config.seed), … ).images … &lt;em&gt;# Make a grid out of the images&lt;/em&gt; … image_grid = make_image_grid(images, rows=4, cols=4) … &lt;em&gt;# Save the images&lt;/em&gt; … test_dir = os.path.join(config.output_dir, “samples”) … os.makedirs(test_dir, exist_ok=True) … image_grid.save(f”{test_dir}/{epoch:04d}.png”)&lt;/p&gt;
&lt;p&gt;現在，您可以使用 🤗 Accelerate 將所有這些組件包裝在一個訓練循環中，以輕鬆進行 TensorBoard 日誌記錄、梯度累積和混合精度訓練。要將模型上傳到 Hub，請編寫一個函數來獲取存儲庫名稱和信息，然後將其推送到 Hub。&lt;/p&gt;
&lt;p&gt;💡 下面的訓練循環可能看起來令人生畏且漫長，但是當您僅用一行代碼啟動訓練時，這一切都是值得的！如果您迫不及待地想要開始生成圖像，請隨意複製並運行下面的代碼。您可以隨時返回並更仔細地檢查訓練循環，例如當您等待模型完成訓練時。 🤗&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from accelerate import Accelerator &amp;gt;&amp;gt;&amp;gt; from huggingface_hub import HfFolder, Repository, whoami &amp;gt;&amp;gt;&amp;gt; from tqdm.auto import tqdm &amp;gt;&amp;gt;&amp;gt; from pathlib import Path &amp;gt;&amp;gt;&amp;gt; import os &amp;gt;&amp;gt;&amp;gt; def get_full_repo_name(model_id: str, organization: str = None, token: str = None): … if token is None: … token = HfFolder.get_token() … if organization is None: … username = whoami(token)[“name”] … return f”{username}/{model_id}” … else: … return f”{organization}/{model_id}” &amp;gt;&amp;gt;&amp;gt; def train_loop(config, model, noise_scheduler, optimizer, train_dataloader, lr_scheduler): … &lt;em&gt;# Initialize accelerator and tensorboard logging&lt;/em&gt; … accelerator = Accelerator( … mixed_precision=config.mixed_precision, … gradient_accumulation_steps=config.gradient_accumulation_steps, … log_with=”tensorboard”, … project_dir=os.path.join(config.output_dir, “logs”), … ) … if accelerator.is_main_process: … if config.push_to_hub: … repo_name = get_full_repo_name(Path(config.output_dir).name) … repo = Repository(config.output_dir, clone_from=repo_name) … elif config.output_dir is not None: … os.makedirs(config.output_dir, exist_ok=True) … accelerator.init_trackers(“train_example”) … &lt;em&gt;# Prepare everything&lt;/em&gt; … &lt;em&gt;# There is no specific order to remember, you just need to unpack the&lt;/em&gt; … &lt;em&gt;# objects in the same order you gave them to the prepare method.&lt;/em&gt; … model, optimizer, train_dataloader, lr_scheduler = accelerator.prepare( … model, optimizer, train_dataloader, lr_scheduler … ) … global_step = 0 … &lt;em&gt;# Now you train the model&lt;/em&gt; … for epoch in range(config.num_epochs): … progress_bar = tqdm(total=len(train_dataloader), disable=not accelerator.is_local_main_process) … progress_bar.set_description(f”Epoch {epoch}”) … for step, batch in enumerate(train_dataloader): … clean_images = batch[“images”] … &lt;em&gt;# Sample noise to add to the images&lt;/em&gt; … noise = torch.randn(clean_images.shape).to(clean_images.device) … bs = clean_images.shape[0] … &lt;em&gt;# Sample a random timestep for each image&lt;/em&gt; … timesteps = torch.randint( … 0, noise_scheduler.config.num_train_timesteps, (bs,), device=clean_images.device … ).long() … &lt;em&gt;# Add noise to the clean images according to the noise magnitude at each timestep&lt;/em&gt; … &lt;em&gt;# (this is the forward diffusion process)&lt;/em&gt; … noisy_images = noise_scheduler.add_noise(clean_images, noise, timesteps) … with accelerator.accumulate(model): … &lt;em&gt;# Predict the noise residual&lt;/em&gt; … noise_pred = model(noisy_images, timesteps, return_dict=False)[0] … loss = F.mse_loss(noise_pred, noise) … accelerator.backward(loss) … accelerator.clip_grad_norm_(model.parameters(), 1.0) … optimizer.step() … lr_scheduler.step() … optimizer.zero_grad() … progress_bar.update(1) … logs = {“loss”: loss.detach().item(), “lr”: lr_scheduler.get_last_lr()[0], “step”: global_step} … progress_bar.set_postfix(**logs) … accelerator.log(logs, step=global_step) … global_step += 1 … &lt;em&gt;# After each epoch you optionally sample some demo images with evaluate() and save the model&lt;/em&gt; … if accelerator.is_main_process: … pipeline = DDPMPipeline(unet=accelerator.unwrap_model(model), scheduler=noise_scheduler) … if (epoch + 1) % config.save_image_epochs == 0 or epoch == config.num_epochs – 1: … evaluate(config, epoch, pipeline) … if (epoch + 1) % config.save_model_epochs == 0 or epoch == config.num_epochs – 1: … if config.push_to_hub: … repo.push_to_hub(commit_message=f”Epoch {epoch}”, blocking=True) … else: … pipeline.save_pretrained(config.output_dir)&lt;/p&gt;
&lt;p&gt;唷，那是相當多的代碼！但您終於準備好使用 🤗 Accelerate 的 notebook_launcher 函數啟動訓練了。向函數傳遞訓練循環、所有訓練參數和進程數（您可以將此值更改為可用的 GPU 數量）以用於訓練：&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; from accelerate import notebook_launcher &amp;gt;&amp;gt;&amp;gt; args = (config, model, noise_scheduler, optimizer, train_dataloader, lr_scheduler) &amp;gt;&amp;gt;&amp;gt; notebook_launcher(train_loop, args, num_processes=1)&lt;/p&gt;
&lt;p&gt;訓練完成後，查看擴散模型生成的最終 🦋 圖像 🦋！&lt;/p&gt;
&lt;p&gt;Copied&amp;gt;&amp;gt;&amp;gt; import glob &amp;gt;&amp;gt;&amp;gt; sample_images = sorted(glob.glob(f”{config.output_dir}/samples/*.png”)) &amp;gt;&amp;gt;&amp;gt; Image.open(sample_images[-1])&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/butterflies_final.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;下一步&lt;/h2&gt;
&lt;p&gt;無條件圖像生成是可訓練任務的一個示例。您可以訪問🧨 擴散器培訓示例頁面來探索其他任務和培訓技術。以下是您可以學到的一些示例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文本反轉，一種向模型傳授特定視覺概念並將其集成到生成圖像中的算法。&lt;/li&gt;
&lt;li&gt;DreamBooth，一種在給定主題的多個輸入圖像的情況下生成主題的個性化圖像的技術。&lt;/li&gt;
&lt;li&gt;在您自己的數據集上微調穩定擴散模型的指南。&lt;/li&gt;
&lt;li&gt;LoRA 使用指南，這是一種內存高效技術，可以更快地微調大型模型。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>AI</category><category>Technology</category></item><item><title>如何訓練穩定的擴散模型：初學者指南</title><link>https://wuming.si/blog/3465/</link><guid isPermaLink="true">https://wuming.si/blog/3465/</guid><pubDate>Sun, 20 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;掌握訓練穩定擴散模型的基礎知識。通過簡單的分步說明，我們將立即將初學者轉變為自信的模型訓練師。一起來學習吧！&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;近年來，人工智能 (AI) 和機器學習 (ML) 徹底改變了數據分析、預測建模和決策領域。其中一個發展就是穩定擴散模型，它是一種基於歷史數據生成圖像和預測結果的強大工具。&lt;/p&gt;
&lt;p&gt;在這份內容廣泛的指南中，我們將深入研究穩定擴散模型的訓練過程，為您提供掌握這一迷人技術必不可少的步驟、最佳實踐和策略。&lt;/p&gt;
&lt;p&gt;穩定擴散模型簡介&lt;/p&gt;
&lt;p&gt;穩定擴散模型包含一種機器學習算法類型，該算法使用歷史數據來預測特定結果或事件的概率。這些模型採用一種稱為擴散過程的技術，其中包括向輸入圖像添加噪聲，然後隨著時間的推移逐漸減少噪聲以生成最終圖像。此過程產生的圖像比傳統深度學習 (DL) 模型更詳細、更真實。&lt;/p&gt;
&lt;p&gt;穩定擴散模型因其處理複雜和抽象文本描述的能力而特別引人注目，這要歸功於一種稱為穩定訓練的新方法。該技術使模型能夠生成與文本輸入一致的高質量圖像，使其比以前的文本到圖像模型有了顯著改進。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;穩定擴散模型的數據準備&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在訓練穩定擴散模型之前，準備用於訓練模型的數據至關重要。該過程涉及以下步驟：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data collection 數據採集&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;收集與您期望的結果相關的準確且最新的數據。確保數據準確地代表您希望模型解決的問題。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data cleaning 數據清洗&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;消除數據集中的任何異常值、缺失數據或不一致之處，以最大限度地提高模型的準確性。這可能涉及填充缺失值、糾正錯誤或將數據轉換為更可用的格式。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data preprocessing 數據預處理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;應用各種技術來提高模型的準確性和性能。這可能包括歸一化、標準化或降維。&lt;/p&gt;
&lt;p&gt;模型設計和算法選擇&lt;/p&gt;
&lt;p&gt;準備好數據後，下一步就是設計穩定擴散模型。這涉及為模型選擇適當的算法、架構和參數。穩定擴散模型中使用的一些流行算法包括：&lt;/p&gt;
&lt;p&gt;深度卷積神經網絡（DCNN）&lt;/p&gt;
&lt;p&gt;生成對抗網絡（GAN）&lt;/p&gt;
&lt;p&gt;變分自動編碼器（VAE）&lt;/p&gt;
&lt;p&gt;選擇算法和架構時，請考慮問題的複雜性、數據集的大小以及所需的準確度等因素。&lt;/p&gt;
&lt;p&gt;訓練穩定擴散模型&lt;/p&gt;
&lt;p&gt;要訓​​練您自己的穩定擴散模型，您可以使用各種工具和平台，例如 Google Colab、Jupyter Notebooks 或 TensorFlow。這些平台提供了用於運行實驗、管理模型和生成圖像的交互式環境。&lt;/p&gt;
&lt;p&gt;遵循本節中的步驟將使您能夠創建適合您的需求和偏好的擴散模型，從而產生富有洞察力的預測。以下是訓練穩定擴散模型的步驟：&lt;/p&gt;
&lt;p&gt;將準備好的數據集分為訓練集和驗證集。使用訓練集來訓練模型，使用驗證集來評估其性能。&lt;/p&gt;
&lt;p&gt;從各種可用選項中選擇合適的穩定擴散模型。&lt;/p&gt;
&lt;p&gt;使用 PyTorch 或 TensorFlow 等軟件訓練模型。請注意，根據數據集大小和模型複雜性，訓練持續時間可以從幾個小時到幾天不等。&lt;/p&gt;
&lt;p&gt;訓練後，使用驗證集評估模型的性能。&lt;/p&gt;
&lt;p&gt;一旦對模型的性能感到滿意，就可以通過向模型提供隨機噪聲向量來生成圖像。&lt;/p&gt;
&lt;p&gt;在我們詳細指南的幫助下輕鬆安裝穩定擴散。&lt;/p&gt;
&lt;p&gt;模型評估和驗證&lt;/p&gt;
&lt;p&gt;訓練穩定擴散模型後，必須評估其性能並驗證其準確性。這可以通過使用各種指標將模型的預測與實際結果進行比較來完成，例如：&lt;/p&gt;
&lt;p&gt;均方誤差 (MSE)。&lt;/p&gt;
&lt;p&gt;均方根誤差 (RMSE)。&lt;/p&gt;
&lt;p&gt;平均絕對誤差 (MAE)。&lt;/p&gt;
&lt;p&gt;R 平方（決定係數）。&lt;/p&gt;
&lt;p&gt;此外，通過確保模型隨著時間的推移產生一致的結果來評估模型的穩定性也至關重要。&lt;/p&gt;
&lt;h2&gt;穩定擴散模型最佳實踐&lt;/h2&gt;
&lt;p&gt;為了確保穩定擴散模型的成功訓練，請考慮以下最佳實踐：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用準確、最新且具有代表性的數據進行培訓。&lt;/li&gt;
&lt;li&gt;使用不同的數據集測試模型以評估其性能。&lt;/li&gt;
&lt;li&gt;在評估過程中評估模型的準確性和穩定性。&lt;/li&gt;
&lt;li&gt;為模型選擇適當的算法、架構和參數。&lt;/li&gt;
&lt;li&gt;通過應用數據預處理技術來提高模型的準確性。&lt;/li&gt;
&lt;li&gt;並通過持續監控變更或更新來確保其持續性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;擴散模型應用&lt;/h2&gt;
&lt;p&gt;擴散模型在各個行業都有廣泛的應用，包括金融、醫療保健、營銷和遊戲。穩定擴散模型的一些常見用途是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;電子商務：網站可以使用穩定的擴散模型根據文字描述生成產品圖像，從而無需真實照片即可展示產品。&lt;/li&gt;
&lt;li&gt;廣告：廣告公司可以利用穩定的擴散模型為其廣告活動生成獨特且具有視覺吸引力的視覺效果。&lt;/li&gt;
&lt;li&gt;遊戲：遊戲開發者可以採用穩定的擴散模型，根據文本描述生成遊戲資產，例如角色和環境。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;訓練穩定擴散需要多長時間？&lt;/h2&gt;
&lt;p&gt;訓練穩定擴散所需的時間取決於許多因素。因此包括數據集的大小、您使用的硬件以及輸出圖像的質量。&lt;/p&gt;
&lt;p&gt;如果您使用較小的數據集或功能較弱的硬件，則訓練穩定擴散將需要更長的時間。例如，一位用戶報告說，他們花了大約 2 週的時間在單個 V100 GPU 上訓練穩定擴散。&lt;/p&gt;
&lt;p&gt;如果您不確定訓練穩定擴散需要多長時間，您可以先嘗試使用較小的數據集或功能較弱的硬件。您還可以嘗試使用預先訓練的穩定擴散模型，這將為您節省一些時間。&lt;/p&gt;
&lt;p&gt;以下是一些加快穩定擴散訓練的技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用更大的數據集：更大的數據集將為穩定擴散提供更多可供學習的信息，這將加快訓練過程。&lt;/li&gt;
&lt;li&gt;使用更強大的GPU：更強大的GPU將能夠更快地處理數據，這也將加快訓練過程。&lt;/li&gt;
&lt;li&gt;使用預先訓練的模型：預先訓練的模型已經具有一些如何生成圖像的知識，這將加快訓練過程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常見問題 (FAQ)&lt;/p&gt;
&lt;h3&gt;問：如何製作穩定擴散模型？&lt;/h3&gt;
&lt;p&gt;答：製作穩定擴散模型需要大量的技術知識和資源。這不是一件容易或快速就能完成的事情。以下是製作穩定擴散模型的基本步驟：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;收集大量圖像數據集：該數據集應盡可能多樣化，以確保模型可以生成各種圖像。&lt;/li&gt;
&lt;li&gt;預處理數據集：這涉及將圖像轉換為模型可以理解的格式。&lt;/li&gt;
&lt;li&gt;訓練模型：這是最耗時的步驟。該模型需要在數據集上進行長時間的訓練，才能學習如何生成圖像。&lt;/li&gt;
&lt;li&gt;測試模型：模型訓練完成後，需要在新的圖像數據集上進行測試。這將有助於確保模型生成高質量的圖像。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;問：訓練穩定擴散模型需要多長時間？&lt;/h3&gt;
&lt;p&gt;答：訓練穩定擴散模型所需的時間取決於多種因素，包括數據集的大小、模型的複雜性和可用的計算資源。&lt;/p&gt;
&lt;p&gt;一般來說，在大型數據集上訓練穩定擴散模型可能需要幾天甚至幾週的時間。例如，在 ImageNet 數據集上訓練穩定擴散模型在單個 GPU 上可能需要長達 10 天的時間。&lt;/p&gt;
&lt;p&gt;如果您使用較小的數據集或不太複雜的模型，訓練時間會更短。然而，值得注意的是，即使是小模型也可能需要幾個小時來訓練。&lt;/p&gt;
&lt;h3&gt;問：如何通過穩定擴散獲得更好的結果？&lt;/h3&gt;
&lt;p&gt;答：您可以採取一些措施來獲得更好的穩定擴散效果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用更大的數據集：模型需要學習的數據越多，結果就越好。&lt;/li&gt;
&lt;li&gt;使用更複雜的模型：更複雜的模型將能夠學習數據中更複雜的模式，從而獲得更好的結果。&lt;/li&gt;
&lt;li&gt;訓練模型的時間越長：模型訓練的時間越長，結果就越好。&lt;/li&gt;
&lt;li&gt;使用更好的優化器：優化器是一種幫助模型學習的數學算法。使用更好的優化器可以幫助模型更快收斂並獲得更好的結果。&lt;/li&gt;
&lt;li&gt;使用更複雜的損失函數：損失函數是一個數學方程，用於衡量模型的性能。使用更複雜的損失函數可以幫助模型更有效地學習。&lt;/li&gt;
&lt;li&gt;使用正則化技術：正則化是一種有助於防止模型過度擬合數據的技術。當模型學習數據中的噪聲而不是底層模式時，就會發生過度擬合。使用正則化技術有助於提高模型的泛化性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;問：如何訓練穩定擴散權重？&lt;/h3&gt;
&lt;p&gt;用戶可以使用多種方法訓練穩定擴散權重，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;監督學習：在監督學習中，模型在已標記有所需輸出的圖像數據集上進行訓練。例如，該模型可以使用貓和狗的圖像數據集，每個圖像標記為“貓”或“狗”。&lt;/li&gt;
&lt;li&gt;無監督學習：在無監督學習中，模型在沒有任何標籤的圖像數據集上進行訓練。該模型學習在數據中查找模式，而無需明確告知要查找什麼。&lt;/li&gt;
&lt;li&gt;半監督學習：在半監督學習中，模型在已部分標記的圖像數據集上進行訓練。當沒有足夠的標記數據可用時，這可能是提高模型性能的有用方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;訓練穩定擴散權重的最佳方法取決於具體應用。例如，如果你想訓練一個模型來生成貓和狗的圖像，監督學習將是一個不錯的選擇。如果你想訓練一個模型來生成任何物體的圖像，無監督學習將是更好的選擇。&lt;/p&gt;
&lt;h3&gt;問：穩定擴散可以訓練嗎？&lt;/h3&gt;
&lt;p&gt;答：是的，穩定擴散是可以訓練的。它是一種擴散模型，是一類可以訓練生成圖像的生成模型。穩定擴散是一種特殊類型的擴散模型，其設計比其他擴散模型更穩定且更易於訓練。&lt;/p&gt;
&lt;p&gt;穩定擴散模型使用稱為對抗訓練的技術進行訓練。在對抗性訓練中，兩個模型相互訓練。一種模型是生成器，它負責生成圖像。另一個模型是鑑別器，負責區分真實圖像和生成圖像。&lt;/p&gt;
&lt;p&gt;生成器可以生成盡可能真實的圖像。鑑別器可以區分真實圖像和生成圖像。作為生成器和鑑別器，它們在各自的任務上變得更好。最終，生成器變得非常擅長生成逼真的圖像，以至於鑑別器無法再區分它們。&lt;/p&gt;
&lt;h3&gt;問：訓練穩定擴散需要多少張照片？&lt;/h3&gt;
&lt;p&gt;答：訓練穩定擴散所需的照片數量取決於要訓練的模型的大小和復雜性。較大的模型比較小的模型需要更多的照片。&lt;/p&gt;
&lt;p&gt;一般來說，您至少需要幾千張照片來訓練穩定擴散模型。但是，如果使用稱為數據增強的技術，則可以使用更少的照片來訓練模型。數據增強是一種通過從現有圖像創建新圖像來人為地增加數據集大小的技術。&lt;/p&gt;
&lt;h2&gt;結論&lt;/h2&gt;
&lt;p&gt;訓練穩定的擴散模型似乎具有挑戰性。然而，理解這個過程並使用正確的方法可以將其變成結果預測的強大工具。&lt;/p&gt;
&lt;p&gt;本綜合指南的步驟和最佳實踐可以幫助您掌握這些模型的訓練並充分發揮其潛力。&lt;/p&gt;
&lt;p&gt;無論您是業餘愛好者還是專業人士，深入研究穩定擴散模型的令人興奮的世界都可以讓您創造出令人驚嘆的視覺效果並獲得寶貴的見解。&lt;/p&gt;
</content:encoded><category>AI</category><category>Technology</category></item><item><title>今夜は月が綺麗ですね</title><link>https://wuming.si/blog/3442/</link><guid isPermaLink="true">https://wuming.si/blog/3442/</guid><pubDate>Thu, 19 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://web.archive.org/web/2//images/2023/02/v2-b883a9c9323c7ef198c00b6f25f65f7e_1440w.jpeg&quot;&gt;&lt;img src=&quot;https://web.archive.org/web/2//images/2023/02/v2-b883a9c9323c7ef198c00b6f25f65f7e_1440w.jpeg&quot; alt=&quot;&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;月が綺麗な夜に、突然意中の人に「今夜は月が綺麗ですね」と言われたら、どう感じますか。ロマンチックな人や、なんかキザったらしい人だと感じる人も多いでしょう。中には「今夜は月が綺麗ですね」の意味を知っていて、洒落たことを言い返したいと思う人もいるでしょう。&lt;/p&gt;
&lt;p&gt;この「今夜は月が綺麗ですね」にはどんな意味を持っているのか、これを意味することを知れば、日本語の奥ゆかしさに歯がゆさを感じるでしょう。&lt;/p&gt;
&lt;p&gt;夏目漱石说在日语里要表达我爱你，应说成 今夜月色真好。&lt;/p&gt;
&lt;p&gt;那为什么月色又会成为我爱你的表达呢？ 在日语中月读作つき（tsuki），恰好和喜欢的日语好き（すき，suki）读音相近，日本人把谐音哏玩得很溜啊。东方人含蓄蕴藉，隐忍克制，大多不会像西方人那样热烈奔放，直抒胸臆。从他们口中断然不会轻易吐露那三个字，他们只会借赋比兴来表达爱意。他们会说，“思君如满月，夜夜减清辉。”他们会说，“此时相望不相闻，愿逐月华流照君。”他们会说，“愿我如星君如月，夜夜流光相皎洁。”他们会说，“月が绮丽ですね。”&lt;/p&gt;
</content:encoded><category>Life</category><category>Poem &amp; Reading</category></item><item><title>ED-CM4NANO：基于树莓派 CM4 的工业计算机</title><link>https://wuming.si/blog/3411/</link><guid isPermaLink="true">https://wuming.si/blog/3411/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;CM4 NANO 是 EDATEC 为工业应用设计的一款嵌入式计算机，提供与标准树莓派4 相同的功能。其设计兼顾了高性能和低成本的存储方案，能同时支持 eMMC 和 microSD 卡存储。更宽的工作温度、更好的无线性能也使其比标准的树莓派4 更适合用于工业领域。&lt;/p&gt;
&lt;p&gt;ED-CM4NANO 充分利用 CM4 的结构灵活性，解决 CPU、无线模块、PMU 的散热问题。 BT 天线提高了无线通信的可靠性，提供了灵活的扩展能力。&lt;/p&gt;
&lt;p&gt;系统可根据不同应用配置 1GB/2GB/4GB/8GB RAM 和 8GB/16GB/32GB eMMC，2.4/5.8G双频WiFi和蓝牙（可选），支持外接天线。&lt;/p&gt;
&lt;p&gt;对于需要大数据存储容量的应用场景，用户可以使用 eMMC 安装操作系统并存储应用数据，同时用户可以使用 SD 卡存储大用户数据，相比内置 eMMC 内存， SD 卡可以提供更大的容量，其成本更有效。&lt;/p&gt;
&lt;p&gt;ED-CM4NANO 提供报警蜂鸣器，电池供电的实时时钟，支持具有POE功能的千兆网口，兼容官方Raspberry Pi PoE HAT。&lt;/p&gt;
&lt;p&gt;ED-CM4NANO 的 CSI 和 DSI 接口完全兼容 Raspberry Pi4 Model B。CSI 支持 Raspberry Pi 官方 8M 和 12M Pixel HQ 摄像头，DSI 接口支持 Raspberry Pi 官方7寸触摸屏。标准 HDMI 接口可输出 4K 高清视频。&lt;/p&gt;
&lt;p&gt;ED-CM4NANO 提供 2 路 USB3.0 和 1 路 USB2.0 接口，USB2.0 接口可用于更新系统镜像。&lt;/p&gt;
&lt;p&gt;ED-CM4NANO 提供非常强大的扩展能力，支持标准的 Raspberry Pi 40 针扩展接口，用户可以通过板载 USB3.0 Type C 接口机械轻松扩展高速设备。&lt;/p&gt;
&lt;p&gt;系统支持 +12V~18VDC 电源输入，40PIN 插座提供 5V@2A 和 &lt;a href=&quot;mailto:3.3V@1.5A&quot;&gt;3.3V@1.5A&lt;/a&gt; 输出电源，支持大功率扩展模块。&lt;/p&gt;
&lt;p&gt;ED-CM4NANO PCB 的尺寸为 95*58mm，比树莓派4B 略大。出色的散热性能让整个系统能在高温环境下可靠工作。根据我们在实验室的实际测试，ED-CM4NANO 可以在 -25~60°C 的环境温度下可靠工作。&lt;/p&gt;
&lt;p&gt;ED-CM4NANO 机箱底部预留了四个M2.5螺丝孔，方便用户将 ED-CM4NANO 安装在其他设备上。 ED-CM4NANO 还支持 DIN 导轨安装。&lt;/p&gt;
&lt;p&gt;特性&lt;/p&gt;
&lt;p&gt;处理能力&lt;/p&gt;
&lt;p&gt;Broadcom BCM2711 4 核 Cortex A72 1.5GHz (ARM v8) 64 位 CPU&lt;/p&gt;
&lt;p&gt;支持 H.265（HEVC）（最高 4Kp60 解码），H.264（最高 1080p60 解码，1080p30 编码）&lt;/p&gt;
&lt;p&gt;OpenGL ES 3.0 图形支持&lt;/p&gt;
&lt;p&gt;内存 1GB/2GB/4GB/8GB LPDDR4-3200 SDRAM 可选&lt;/p&gt;
&lt;p&gt;8GB/16GB/32GB eMMC 可选&lt;/p&gt;
&lt;p&gt;摄像头和显示器&lt;/p&gt;
&lt;p&gt;1 个 CSI 接口，支持树莓派官方 5M &amp;amp; 8M 像素摄像头模组&lt;/p&gt;
&lt;p&gt;1 个 HDMI 接口&lt;/p&gt;
&lt;p&gt;1 个 DSI 接口，支持树莓派官方显示&lt;/p&gt;
&lt;p&gt;有线接口&lt;/p&gt;
&lt;p&gt;10/100M/1000 以太网，支持 PoE 供电（需要官方 PoE HAT）&lt;/p&gt;
&lt;p&gt;2 个 USB3.0（外置）&lt;/p&gt;
&lt;p&gt;1 个 USB3.0（内置）&lt;/p&gt;
&lt;p&gt;1 个 USB2.0&lt;/p&gt;
&lt;p&gt;无线通信&lt;/p&gt;
&lt;p&gt;2.4G/5.8G WiFi 兼容 IEEE 802.11 b/g/n/ac 标准&lt;/p&gt;
&lt;p&gt;蓝牙 5.0 BLE&lt;/p&gt;
&lt;p&gt;WiFi/BLE 外置天线和 PCB 天线可选&lt;/p&gt;
&lt;p&gt;存储&lt;/p&gt;
&lt;p&gt;microSD 卡座（用于扩展用户数据存储）&lt;/p&gt;
&lt;p&gt;可扩展接口&lt;/p&gt;
&lt;p&gt;Raspberry Pi 标准 40 针连接器&lt;/p&gt;
&lt;p&gt;Raspberry Pi 标准 4 针 POE 连接器&lt;/p&gt;
&lt;p&gt;其他&lt;/p&gt;
&lt;p&gt;带备用电池的 RTC&lt;/p&gt;
&lt;p&gt;红色 LED 电源指示灯&lt;/p&gt;
&lt;p&gt;绿色 LED 状态指示&lt;/p&gt;
&lt;p&gt;系统重置按钮&lt;/p&gt;
&lt;p&gt;蜂鸣器&lt;/p&gt;
&lt;p&gt;电源&lt;/p&gt;
&lt;p&gt;12~18V DC 电压范围输入&lt;/p&gt;
&lt;p&gt;5.5/2.5mm DC Jack 或 Phoenix 端子连接器可选&lt;/p&gt;
&lt;p&gt;产品型号的命名规则如图。&lt;/p&gt;
&lt;p&gt;FAQ&lt;/p&gt;
&lt;p&gt;Q: 我可以从 CM4 NANO 的 SD 卡启动系统吗？&lt;/p&gt;
&lt;p&gt;A: 不能。CM4 NANO 是为工业应用而设计的，为了保证系统的可靠性，CM4 NANO 必须使用带有 eMMC 内存的 CM4。系统只能从 eMMC 启动，SD 卡用于数据存储。&lt;/p&gt;
&lt;p&gt;Q：CM4 NANO 上 40PIN 引脚有没有被占用？&lt;/p&gt;
&lt;p&gt;A：CM4 NANO 的以下功能占用了 40PIN 引脚：&lt;/p&gt;
&lt;p&gt;蜂鸣器占用 GPIO6，如果不需要，可以去掉电阻 R39，将 GPIO6 与蜂鸣器电路断开。&lt;/p&gt;
&lt;p&gt;SD 卡模块占用 GPIO22、23、24、25、26、27。如果您不需要使用它，请不要插入 SD 卡。&lt;/p&gt;
&lt;p&gt;购买&lt;/p&gt;
&lt;p&gt;复制下方链接，在浏览器中打开即可下单：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://link.nxez.com/buy/ed-cm4nano&quot;&gt;http://link.nxez.com/buy/ed-cm4nano&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;介绍视频&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>NAS 私有云和 KODI 软硬集成的套件</title><link>https://wuming.si/blog/3427/</link><guid isPermaLink="true">https://wuming.si/blog/3427/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文作者：硬件老郭&lt;/p&gt;
&lt;p&gt;树莓派4B搭载了博通最新的BCM2711处理器，它采用了4枚性能更为强劲的ARM A72核心。相比上一代树莓派3B+，4B的性能有着2~4倍的提升，处理任务要更为流畅。而在内存上，树莓派4B拥有1GB/2GB/4GB三种规格可选，相比以往的1GB，大大拓展了多任务处理的可能。&lt;/p&gt;
&lt;p&gt;与上一代树莓派的性能对比，树莓派4B不只是频率的提升而已，重大的提升还有，采用了USB3.0接口，并且留够了充足的带宽，不像树莓派3是共享的总线带宽，性能受到制约，何况还是USB2.0。还有就是支持H.265 4K硬件解码。&lt;/p&gt;
&lt;p&gt;提升如此巨大，我们能怎么才能发挥出它的性能呢？&lt;/p&gt;
&lt;p&gt;作为一个“深度”树莓派爱好者，看到树莓派4B性能不再是挤牙膏，而是性能爆炸了，简直是出乎我的意料。要知道，树莓派官方树莓派基金会推出树莓派是为了 慈善目的 的产品，希望将树莓派推广给全世界的青少年电脑爱好者，用于培养计算机程序设计的兴趣和能力，所以价格是非常重要的因素。这也是为什么树莓派不是像手机或者平板那样采用性能强大CPU的原因，但也不排除以后的思路会转变。那么树莓派4B性能大幅提升，价格是不是更贵了？并没有，价格还是和树莓派3发售的价格一致，只不过根据内存大小分为了好几个版本。&lt;/p&gt;
&lt;p&gt;在以前实现不了或者说使用效果体验不好的玩法终于可以实现了。比如：AI图像处理、4K电影播放、NAS网络存储、私有云等等。AI图形处理这里就不是我们关心的重点了，我们接下来主要来谈后面几个。&lt;/p&gt;
&lt;p&gt;Kodi 媒体中心是一个屡获殊荣的自由和开源的跨平台媒体播放器和用于HTPC（Home theater PC，家庭影院PC）的数字媒体娱乐中心软件，可以运行在Windows、Android、iOS、Mac、Linux、Raspberry Pi系统。它是一个优秀的自由和开源的（GPL）媒体中心软件。最初为Xbox而开发，叫XBMC。因为全世界开发者的参与，这个软件已经拥有远远超过以往的功能，这已经不能用‘一个简单播放器’来包含所有的一切了。因为这个原因，XBMC.14后更名为Kodi。&lt;/p&gt;
&lt;p&gt;在树莓派上，我们用LibreELEC_RPi4。它是一个KODI的树莓派定制版本，做了很多优化，去掉了一些不需要的功能。&lt;/p&gt;
&lt;p&gt;这里，经过实际测试，CPU占有率20%左右（4核平均值，以大家习惯的win系统占有率计算方式表达，满载是100%）。&lt;/p&gt;
&lt;p&gt;内存占用在600M左右。可以打开KODI的资源检测查看。&lt;/p&gt;
&lt;p&gt;简单的说，我们可以用KODI 在电视上播放4K电影、电视剧、看电视直播、刷B站视频、听无损音乐，支持安卓苹果手机投屏，手机的照片和视频可以在电视机上播放，与家人分享。&lt;/p&gt;
&lt;p&gt;看到这里，你心动了吗？&lt;/p&gt;
&lt;p&gt;可能有人会说，我有电视机顶盒，也能实现你说的这些功能。弄这些有必要吗？&lt;/p&gt;
&lt;p&gt;是的，没错，但是其实是有很大差别的，虽然有部分是交叉的。听我慢慢道来。。。&lt;/p&gt;
&lt;p&gt;电视机顶盒目前的现状&lt;/p&gt;
&lt;p&gt;电视机顶盒现在配置都比较高了，或者有些是智能电视机，内部其实就是一个电视机顶盒。&lt;/p&gt;
&lt;p&gt;电视机顶盒用的是安卓系统，而我们这个树莓派KODI用的是linux系统。&lt;/p&gt;
&lt;p&gt;但是目前的机顶盒生态其实很不友好。只能安装自带应用市场的app，想通过U盘安装app人为设置了很多障碍。&lt;/p&gt;
&lt;p&gt;机顶盒开始买来的时候，运行都很流畅，但是只要用了2、3年，就发现越来越卡，不管配置有多高，好像都这样，用的还是那些厂家自带的软件。但就是会卡，这好像是安卓系统避免不了的一样，也有人说这是厂家的计划报废。。。&lt;/p&gt;
&lt;p&gt;机顶盒的视频都是会员制了，免费的电影很少，但是很多正在播的电视剧电影等又是独家的，意味着你买的其他视频app的会员可能没有你想看的电视剧、电影。而且就算你买了所有的会员，还有会员专属的广告等着你呢。就算你买了会员，能看的4K电影也是很有限的，少得可怜。在线看4K电影还很考验网速，看的过程中卡顿就很影响观看感受了。&lt;/p&gt;
&lt;p&gt;所以很多时候，机顶盒只是成为了一个手机投屏的工具了，都是通过手机来播放投屏观看的。&lt;/p&gt;
&lt;p&gt;关于看电视的问题，其实现在很少看电视了，但是家里面老一辈的父母是要看电视的，自己也会偶尔看下新闻、体育什么的。但是机顶盒看电视直播是有版权的，不是你想看就能看得，好像安装了电信宽带电信机顶盒的用户可以免费看。而且频道也就是各个地方台，中央台。&lt;/p&gt;
&lt;p&gt;还有，机顶盒它主要是在线播放，目前都是购买了影视的版权的，这也就是说，对于一些资深影迷来说，很多片子没有引进国内是看不到，或者是删减了的。像是很多美剧韩剧等等。&lt;/p&gt;
&lt;p&gt;总的来说，现在的机顶盒很畸形，本来它是可以作为一个大平板，和手机一样的，但是你们看到了，这其中有太多的人为限制。目前来说，机顶盒显得很鸡肋，只需要有一个手机投屏就可以了。&lt;/p&gt;
&lt;p&gt;KODI 系统的优势和特点&lt;/p&gt;
&lt;p&gt;KODI系统虽然使用的Linux内核，但是它是以插件形式安装软件的，并不是linux通用的软件安装方式，它属于高度定制的系统。而由于KODI属于国外开源的软件系统，它的开发都是依据国外的网络使用环境来的，比如：youtube、推特、奈飞等都是正常方式国内用户用不了的。国内用户能使用的我们已经验证并且集成到系统里面了。&lt;/p&gt;
&lt;p&gt;KODI 系统永远也不会卡顿。因为是采用的Linux系统，稳定性是很高的，绝大部分服务器都是使用的Linux系统。而且树莓派4B的性能经过我们实测，运行KODI绰绰有余，是跑不满CPU的。而KODI的插件是很纯净的，没有乱七八糟的后台，广告，获取用户权限搜集用户信息什么的，用再久也不会卡顿。&lt;/p&gt;
&lt;p&gt;KODI同样支持手机投屏，而且我感觉网速快得多，对应的清晰度也高一些，这点还有待验证。&lt;/p&gt;
&lt;p&gt;KODI看电视直播是使用PVR电视直播插件来实现的，支持m3u电视节目单，导入到插件中，重启后就可以看电视了。m3u我们知道是一种流媒体协议，是一种数据广播的形式，点对多传输的。这个电视节目网上搜一下很多的，电视台可达2000多个！不局限于什么地方台中央台，还有国内收费的电视节目，国外各种卫星电视节目，都是免费观看的。&lt;/p&gt;
&lt;p&gt;用KODI可以把存储的电影电视剧导入，然后刮削出海报、剧情介绍等信息，形成自己的影音库。这些影音都是存在本地硬盘的，平时通过我们的树莓派系统里面的Aria2 ,qBittorrent 下载软件下载到硬盘，而机顶盒是属于在线播放的。&lt;/p&gt;
&lt;p&gt;KODI还能结合NAS系统私有云，把手机备份的相片和视频，在电视上播放与家人一同观看。&lt;/p&gt;
&lt;p&gt;KODI还可以安装B站插件，可以观看B站的大量视频，喜欢二次元的不要错过。&lt;/p&gt;
&lt;p&gt;KODI可以使用红外线遥控器，或者鼠标键盘。&lt;/p&gt;
&lt;p&gt;NAS 网络存储私有云&lt;/p&gt;
&lt;p&gt;这里很多同学可能会说了，用树莓派来做NAS性能很拉跨很差劲吧？我买个二手的矿机来做NAS不香吗？价格还便宜不少。&lt;/p&gt;
&lt;p&gt;这里我多说两句，普及下矿机的事。在前些年，出了很多区块链的矿机，因为还可以赚点小钱，十分火爆。像是什么联想、迅雷也出过，蜗牛星际、X家云、X客云等等，后来因为政策，这些产品都踩了红线，一时之间全部退市。这样二手市场流出了大量的矿机。&lt;/p&gt;
&lt;p&gt;因为是二手商品，厂家都关门了，肯定是没有技术支持的没有售后的，只能买来自己折腾。肯定是需要你要有这方面技术能力和时间成本的。&lt;/p&gt;
&lt;p&gt;这些二手矿机年代久远了，由于是矿机，长时间高负荷工作，还能用多久全靠运气，就是花钱碰个运气。&lt;/p&gt;
&lt;p&gt;由于这些都是好多年前的硬件方案了。性能只能说是很勉强。&lt;/p&gt;
&lt;p&gt;由于带有USB3.0，数据传输相对于USB2.0提升了10倍！300M以上的速度我相信对于绝大多数人来说够用的很了。以前用USB2.0来做NAS系统的时代一去不复返了。USB2.0接硬盘做NAS系统传输速度只有10M，十分鸡肋，只能说能用。现在终于不再受USB2.0的速度限制了。盼了好久，终于等到树莓派USB3.0的到来。&lt;/p&gt;
&lt;p&gt;配合上真千兆网口，大大提升了使用体验。&lt;/p&gt;
&lt;p&gt;树莓派的千兆网络没问题, 几乎可以跑满:&lt;/p&gt;
&lt;p&gt;测试用一台工作站与树莓派直连，931M/s 接近1000M. 这意味着跑个NAS, 瓶颈不再是网络了。当然，你要说你家里是万兆宽带，千兆拖了后腿，也没办法。这个也毕竟是少数，远远还没有到普及的程度。树莓派4B的价格平易近人，还能要求什么呢？关键是看能不能满足我们想要的功能。&lt;/p&gt;
&lt;p&gt;整个存储都是放在硬盘上的，而硬盘是通过USB3.0连接到树莓派USB3.0口的。所以USB3.0口对于硬盘的读写速度是非常重要的。&lt;/p&gt;
&lt;p&gt;私有云存储&lt;/p&gt;
&lt;p&gt;采用在树莓派官方32位系统基础上来搭建。应用软件选择owncloud开源私有云系统。在这里，没有选择专门的NAS软件，如：openmediavault 。因为这类系统对于新手小白来说太复杂，而且使用过程中有一些问题。而owncloud使用起来，就像是使用普通的百度网盘一样，容易上手。像是samba共享服务协议，单独安装即可。&lt;/p&gt;
&lt;p&gt;下面我们开始讲解如何快速搭建系统&lt;/p&gt;
&lt;p&gt;分为三步：&lt;/p&gt;
&lt;p&gt;1、准备硬件。先看看自己手边有没有，没有的话我们有套件的。&lt;/p&gt;
&lt;p&gt;2、烧录免费镜像、按要求格式化分区硬盘同时进行。&lt;/p&gt;
&lt;p&gt;3、上电使用。&lt;/p&gt;
&lt;p&gt;这其实是组成一个完整的主机系统。&lt;/p&gt;
&lt;p&gt;硬件准备&lt;/p&gt;
&lt;p&gt;1、已经安装了普通散热片的树莓派4B 2G/4G/8G 主板一块。&lt;/p&gt;
&lt;p&gt;2、16G TF高速存储卡一张。16G够用了，太大的话，备份还原系统会花更多的时间。&lt;/p&gt;
&lt;p&gt;3、USB3.0移动硬盘一个。&lt;/p&gt;
&lt;p&gt;4、树莓派4B电源，至少5V/3A。&lt;/p&gt;
&lt;p&gt;5、hdmi小口转大口转接线。&lt;/p&gt;
&lt;p&gt;6、树莓派4B散热风扇。&lt;/p&gt;
&lt;p&gt;树莓派4B主机套件方案介绍&lt;/p&gt;
&lt;p&gt;这些东西比较零散，接了线之后，也是比较凌乱的。如果你身边有这些东西可以先用起来试试。&lt;/p&gt;
&lt;p&gt;也可以用我们提供的软硬件一体整合解决方案。免费提供已经配置和优化好的镜像文件和相关软件，烧录后即可使用。获取方式见文末。&lt;/p&gt;
&lt;p&gt;有详细的说明文档和视频。这些都是免费给大家使用的。有官方教程有指导，不怕你不会。&lt;/p&gt;
&lt;p&gt;这个方案整合了所有硬件在一个铝合金外壳内，十分的紧凑。内部可以安装2块2.5寸硬盘。软件上把已经优化配置好的系统和软件做成镜像文件，烧录到TF卡上就可以立即使用了。&lt;/p&gt;
&lt;p&gt;最终的一个安装完成效果：&lt;/p&gt;
&lt;p&gt;所有接口走线都在后面板上。&lt;/p&gt;
&lt;p&gt;烧录镜像文件&lt;/p&gt;
&lt;p&gt;TF卡内如果有重要资料的话，请先将TF卡资料备份。待会TF卡将会被格式化，所有数据将会丢失！&lt;/p&gt;
&lt;p&gt;运行格式化软件，插入TF卡，格式化提示不用理会。&lt;/p&gt;
&lt;p&gt;插入TF卡或者硬盘的时候，遇到任何提示需要格式化操作的，都不用理会，格式化会丢失掉所有数据，除非你需要格式化。这是因为TF卡中有linux分区，WIN系统电脑是无法识别的，所以弹出格式化提示。&lt;/p&gt;
&lt;p&gt;我们的系统在WINDOWS系统中会被识别成 RECOVE RY、boot、System 三个盘符。&lt;/p&gt;
&lt;p&gt;选择一个格式化就行。如果你还有其他的USB设备，一定要看清楚盘符，别格式化错了。&lt;/p&gt;
&lt;p&gt;选择Overwrite format，然后点击Format开始格式化。&lt;/p&gt;
&lt;p&gt;选择你要烧录的镜像文件和TF卡的盘符，点击Write开始写入，等待烧录完成。&lt;/p&gt;
&lt;p&gt;3、至少准备一块2.5寸SATA硬盘（机械和固态都可以）。硬盘需要按要求分区和命名，建立文件夹。&lt;/p&gt;
&lt;p&gt;文件路径说明：&lt;/p&gt;
&lt;p&gt;私有云admin账号数据存放位置：NAS1_yun :\owncloud\data\admin\files&lt;/p&gt;
&lt;p&gt;也就是手机里照片视频等备份的位置，其他账号依此类推。&lt;/p&gt;
&lt;p&gt;下载位置：NAS1_xiazai :\xiazai&lt;/p&gt;
&lt;p&gt;Aria2、QB下载软件的下载路径，下载完之后可以移动到其他地方。&lt;/p&gt;
&lt;p&gt;KODI电视剧存放位置：NAS1_xiazai :\电视剧&lt;/p&gt;
&lt;p&gt;KODI电影存放位置：NAS1_xiazai :\电影&lt;/p&gt;
&lt;p&gt;KODI视频存放位置：NAS1_xiazai :\视频&lt;/p&gt;
&lt;p&gt;KODI音乐存放位置：NAS1_xiazai :\音乐&lt;/p&gt;
&lt;p&gt;KODI电视直播m3u文件位置：NAS1_xiazai :\电视直播源&lt;/p&gt;
&lt;p&gt;微力同步同步文件夹：NAS1_xiazai :\verysync&lt;/p&gt;
&lt;p&gt;硬盘分区方法（以一块500G硬盘举例）：&lt;/p&gt;
&lt;p&gt;硬盘采用NTFS格式，逻辑分区。&lt;/p&gt;
&lt;p&gt;不管是多少块硬盘，要求有至少2个分区就可以了。分区大小根据自己需要和硬盘大小来分配。&lt;/p&gt;
&lt;p&gt;一个分区命名为：NAS1_xiazai，大小建议至少200G。这个是用来存放下载文件的，包括电影、电视剧、视频、音乐，是KODI的播放盘。&lt;/p&gt;
&lt;p&gt;另一个分区命名为：NAS1_yun，大小建议至少200G。 这个是owncloud 私有云工作盘符。手机的相片视频自动备份都是在这个盘。&lt;/p&gt;
&lt;p&gt;NAS1_yun分区格式化完成后，需要把官方发布的owncloud文件解压后，放在根目录！&lt;/p&gt;
&lt;p&gt;免费提供已经配置和优化好的镜像文件和相关软件，烧录后即可使用。见文末。&lt;/p&gt;
&lt;p&gt;有详细的说明文档和视频。&lt;/p&gt;
&lt;p&gt;这些都是免费给大家使用的。有官方教程有指导，不怕你不会。&lt;/p&gt;
&lt;p&gt;到这里，树莓派NAS和KODI影音播放系统搭建就已经介绍完成了。大家先利用现有的东西，烧录镜像文件就可以使用了。免去了自己折腾，中间有很多的问题要出处理，不是每个人都能顺利解决的。如果觉得好用的话，可以考虑用我们的软硬件集成方案。整体性会好很多。把树莓派可以当成一个真正的主机来使用，当然你也可以自己安装需要的软件。&lt;/p&gt;
&lt;p&gt;我们提供已经配置、优化好的 应用 、系统整合镜像给大家 免费 使用，仅限个人使用，严禁用于商业用途。&lt;/p&gt;
&lt;p&gt;所需的镜像和相关软件、说明文档打包下载地址：&lt;/p&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://pan.baidu.com/s/1hi-_vfgGtK3psH-gaELSMg&quot;&gt;https://pan.baidu.com/s/1hi-_vfgGtK3psH-gaELSMg&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;提取码：6a9q&lt;/p&gt;
&lt;p&gt;如需文中所用到的组件，可以在这里选购：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://item.taobao.com/item.htm?id=658823159249&quot;&gt;https://item.taobao.com/item.htm?id=658823159249&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;希望大家都能愉快的使用树莓派！&lt;/p&gt;
&lt;p&gt;作者：硬件老郭&lt;/p&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://www.jianshu.com/p/9a03c99644c5&quot;&gt;https://www.jianshu.com/p/9a03c99644c5&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://www.jianshu.com/p/798ed10f812c&quot;&gt;https://www.jianshu.com/p/798ed10f812c&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;链接：&lt;a href=&quot;https://www.jianshu.com/p/95d71b072eae&quot;&gt;https://www.jianshu.com/p/95d71b072eae&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>PALPi 复古游戏主机，基于树莓派 Zero</title><link>https://wuming.si/blog/3419/</link><guid isPermaLink="true">https://wuming.si/blog/3419/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MAKER: Arnov Sharma／译：趣无尽 相逢已是初识（转载请注明出处）&lt;/p&gt;
&lt;p&gt;尽管基于树莓派开发的复古游戏机有不少作品，也很容易买到。我们之前也介绍过「树莓派CM3 掌上游戏机DIY全记录」。&lt;/p&gt;
&lt;p&gt;但是还是有必要推荐一下下面这款，基于树莓派 Zero 的复古游戏机。因为它为你展示了两个版本，其中包含了面包板搭建的版本，更加便于理解游戏机搭建的原理，并大幅降低了自己 DIY 的门槛。所用到的材料也很容易获得，这个项目主要的特性如下：&lt;/p&gt;
&lt;p&gt;1、使用 PAL 制式显示屏。&lt;/p&gt;
&lt;p&gt;2、由 Raspberry Pi Zero 驱动，采用 RecalBox OS 操作系统，该系统还附带一些预装游戏。&lt;/p&gt;
&lt;p&gt;3、由 IP5306 IC 供电，该 IC 是用于移动电源电路的 5V 2A 恒定升压 IC，非常适合为 Raspberry Pi Zero 供电。&lt;/p&gt;
&lt;p&gt;4、项目的前半部分为面包板版本，后半部分为 PCB 版本。&lt;/p&gt;
&lt;p&gt;材料清单&lt;/p&gt;
&lt;p&gt;Raspberry Pi Zero × 1&lt;/p&gt;
&lt;p&gt;16 GB 存储卡（8 GB 也可） × 1&lt;/p&gt;
&lt;p&gt;TV 屏幕 × 1&lt;/p&gt;
&lt;p&gt;HDMI 转 micro HDMI 适配器 × 1&lt;/p&gt;
&lt;p&gt;5V 2A 充电器/电源可稳定输出 2A × 1&lt;/p&gt;
&lt;p&gt;键盘 × 1&lt;/p&gt;
&lt;p&gt;USB 转 microUSB 适配器 × 1&lt;/p&gt;
&lt;p&gt;RecalBox OS 映像文件/ Raspberry Pi Image Flasher × 1&lt;/p&gt;
&lt;p&gt;普通按键 × 1&lt;/p&gt;
&lt;p&gt;定制 PCB 板 × 1&lt;/p&gt;
&lt;p&gt;IP5306 IC × 1&lt;/p&gt;
&lt;p&gt;10uf 0805 电容 × 1&lt;/p&gt;
&lt;p&gt;USB 端口 × 1&lt;/p&gt;
&lt;p&gt;microUSB 端口 × 1&lt;/p&gt;
&lt;p&gt;带有 CON2 连接器线的锂离子电池 × 1&lt;/p&gt;
&lt;p&gt;CON2 连接器 × 1&lt;/p&gt;
&lt;p&gt;10k 0603 电阻 × 1&lt;/p&gt;
&lt;p&gt;2R 0805 电阻 × 1&lt;/p&gt;
&lt;p&gt;垂直按钮 × 1&lt;/p&gt;
&lt;p&gt;一、初步构想&lt;/p&gt;
&lt;p&gt;设备包括 PCB 板和 3D 打印外壳，两者由螺钉连接起来。正面是显示器和按钮，使用 4.3 英寸的显示屏，尺寸大约是 135mm x 140mm，比普通的 Gameboy 屏幕大。反面是 Raspberry Pi Zero 同升压转换电路和锂离子电池。&lt;/p&gt;
&lt;p&gt;二、配置 RecalBox 系统&lt;/p&gt;
&lt;p&gt;下载 Raspberry Pi 映像文件。&lt;/p&gt;
&lt;p&gt;Select the right OS for your device 这里选择 RecalBox&lt;/p&gt;
&lt;p&gt;Select your system 这里选择 Rpi0&lt;/p&gt;
&lt;p&gt;Raspberry Pi imager will do your work of downloading and installing the RecalBox on the memory card.（完成下载与安装）&lt;/p&gt;
&lt;p&gt;安装完成后，将树莓派接上电视屏幕和键盘。&lt;/p&gt;
&lt;p&gt;启动设备，RecalBox 就可以正常工作了。&lt;/p&gt;
&lt;p&gt;三、GPIO 接线原理图&lt;/p&gt;
&lt;p&gt;注：原理图中将按键一端连到 GPIO 口，一端接地。&lt;/p&gt;
&lt;p&gt;原理图在本项目文件库中可以下载：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://make.quwj.com/project/385&quot;&gt;https://make.quwj.com/project/385&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;四、显示器与 GPIO 控制&lt;/p&gt;
&lt;p&gt;显示器种类有很多，既有 HDMI 端口，也有带状电缆端口，在这里从成本考虑选用 PAL 制式的车载显示器(带 PAL 端口)。因为车载显示器工作电压为 12V，而这里需要 5V 或 3.3V 的显示器，所以需要对显示器内部降压电路进行改装，这里移除了 12V 转 3.3V 的降压芯片，并在 Vout 端的电容上增加 VCC 和 GND ，为显示器提供 5V 电压。&lt;/p&gt;
&lt;p&gt;为使 PAL 端口运行显示器，需要在配置文件中编辑一些内容。用键盘在网络设置中输入 SSID 和密码。（将其与 winSCP 连接）&lt;/p&gt;
&lt;p&gt;需要编辑以下内容：&lt;/p&gt;
&lt;p&gt;SDTV 输出的设置&lt;/p&gt;
&lt;p&gt;HDMI 显示器连接到的 Raspberry Pi 上就会输出其视频信号，但在 PAL 端口下，需要设置 SDTV 的输出。&lt;/p&gt;
&lt;p&gt;进入 boot &amp;gt; config.txt 更改默认设置，从 sdtv_mode 中删除 # 并添加 sdtv_aspect = 1，此外，在 HDMI lines 前加 #。（如图，检查 Boot/config.txt 并复制其中的内容）&lt;/p&gt;
&lt;p&gt;GPIO Control&lt;/p&gt;
&lt;p&gt;GPIO 口与按键的设置。&lt;/p&gt;
&lt;p&gt;进入 recalbox &amp;gt; share &amp;gt; system &amp;gt; recalbox.conf&lt;/p&gt;
&lt;p&gt;通过以下两步启用 GPIO 外接按键&lt;/p&gt;
&lt;p&gt;设置 controller.gpio.enabled=1（之前是 0）并将 controller.gpio.args.map=1,2 更改为 controller.gpio.args.map=1（2 是第二个玩家）&lt;/p&gt;
&lt;p&gt;更改完默认设置（如图，检查 D2 GPIO Controlers），然后重启，RecalPi 便可适用于 PAL 端口和自定义按键。&lt;/p&gt;
&lt;p&gt;五、完善面包板版本&lt;/p&gt;
&lt;p&gt;这里用了定制的 GPIO 适配器，接线有所变化，参考图片将按键一端连到 GPIO 口，一端接地，完成后可以通过接 TV 测试按键是否正常工作，没问题后将 TV 更换成车载显示器，车载显示器的 VCC 接到 5V，地线连到一起，然后用 5V 2A 的电源供电，车载显示器显示画面则成功（注意之前的 SDTV 设置）。&lt;/p&gt;
&lt;p&gt;六、设计电路板&lt;/p&gt;
&lt;p&gt;根据面包板搭建的电路绘制 PCB 文件，包括 13 个按键与 Rpi Zero 相连，加 IP5306 IC 驱动的电源电路。其中 IP5306 是一款适用于 3.7 V 锂离子电池的电源管理芯片，可以调节输出电压的大小，这里将其提升到 5V 2A 适用于 Raspberry Pi 和车载显示器。&lt;/p&gt;
&lt;p&gt;设计流程如下：&lt;/p&gt;
&lt;p&gt;首先在 Fusion360 中设计整体结构，然后利用 Fusion360 的 PCB Layout 在 My PCB Cad 软件中对 PCB Outline 进行建模。（这里在经典的 Gameboy 布局中放置了额外的热键按钮）&lt;/p&gt;
&lt;p&gt;七、电路板打样&lt;/p&gt;
&lt;p&gt;将生产文件 Gerber 给到 PCB 制板厂商打样。&lt;/p&gt;
&lt;p&gt;Gerber 文件在本项目文件库中可以下载：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://make.quwj.com/project/385&quot;&gt;https://make.quwj.com/project/385&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;八、焊接 PCB 板&lt;/p&gt;
&lt;p&gt;1.SMD 元件焊接&lt;/p&gt;
&lt;p&gt;将焊膏涂到待焊焊盘上，摆放好元件后，将 PCB 板放到 SMD 加热板上进行回流焊，加热板从底部向上加热 PCB 板至焊膏熔化，焊膏熔化时从加热板上取下 PCB 板冷却 3-4 分钟。之后按照正确的元件焊接顺序，依次重复此操作。&lt;/p&gt;
&lt;p&gt;2.THT 元件焊接&lt;/p&gt;
&lt;p&gt;将 USB 端口、按键和 CON2 连接器添加到 PCB 板，然后焊接。&lt;/p&gt;
&lt;p&gt;最后焊接排母。PCB 板的焊接就大功告成了，接上锂离子电池，在 USB 端口处接 USB 电源监视器，检查 USB 的输出是否为 5V，没问题后接入 Raspberry Pi Zero W（已安装 RecalBox，并完成配置文件的更改）。&lt;/p&gt;
&lt;p&gt;九、整体组装&lt;/p&gt;
&lt;p&gt;利用 3D 打印的支撑部件将显示器固定到 3D 打印的外壳上，用四个 M2 螺丝将电路板安装到外壳上，并将显示器的 VCC、GND 和 PAL 端口与 Raspberry Pi 和电路板连接起来。完成后就可以通过 ON – OFF 垂直按钮启动 Recalbox 了。&lt;/p&gt;
&lt;p&gt;3D 打印文件在本项目文件库中可以下载：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://make.quwj.com/project/385&quot;&gt;https://make.quwj.com/project/385&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;完成！开启你的复古游戏之旅吧！&lt;/p&gt;
&lt;p&gt;十、添加更多游戏&lt;/p&gt;
&lt;p&gt;添加游戏只需下载相应 ROM 即可，具体操作如下：&lt;/p&gt;
&lt;p&gt;进入 Settings &amp;gt; Network Settings，添加路由器的 SSID 和密码，将树莓派连网，可以在网络设置菜单的顶部看到树莓派的 IP 地址。&lt;/p&gt;
&lt;p&gt;然后在电脑上打开 Win SCP，输入树莓派的 IP 地址，用户名是 root，pi 的密码是 recalboxroot。&lt;/p&gt;
&lt;p&gt;进入 recalbox &amp;gt; share &amp;gt; rom，这里包含所有的 ROM 文件，如果想玩其中一款游戏，只需将其 ROM 文件复制粘贴到 GBA 文件夹中即可。&lt;/p&gt;
&lt;p&gt;重启树莓派后打开 GBA 模拟器，就可以看到新添加的游戏了。&lt;/p&gt;
&lt;p&gt;希望你可以从这个项目中得到一些制作复古游戏机的启发。&lt;/p&gt;
&lt;p&gt;项目所用的代码在本项目文件库中可以下载：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://make.quwj.com/project/385&quot;&gt;https://make.quwj.com/project/385&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;via&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>使用静态 IP 地址</title><link>https://wuming.si/blog/3439/</link><guid isPermaLink="true">https://wuming.si/blog/3439/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MCC 172是一个两通道DAQ HAT，用于通过IEPE传感器（如加速度计和麦克风）进行声音和振动测量。它的每个通道具有24位A/D，最大采样率为51.2 kS/s/Ch。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>启用树莓派 CM4 CM4IO 的双路摄像头</title><link>https://wuming.si/blog/3429/</link><guid isPermaLink="true">https://wuming.si/blog/3429/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;树莓派 CM4IO 底板上提供双 MIPI CSI-2 摄像头接口，但系统默认是没有开启的。本文将介绍如何配置和使用双摄像头。&lt;/p&gt;
&lt;p&gt;硬件准备&lt;/p&gt;
&lt;p&gt;CM4 + CM4IO 开发板 X 1&lt;/p&gt;
&lt;p&gt;树莓派摄像头模块 X 2&lt;/p&gt;
&lt;p&gt;摄像头模块软排线 X 2&lt;/p&gt;
&lt;p&gt;这里使用官方 800 万像素摄像头模块，默认配的软排线不兼容 CM4IO，所以需要自己配一条软排线如图所示。&lt;/p&gt;
&lt;p&gt;使用软排线将摄像头模块与 CM4IO 板上的 CAM0、CAM1 接口连接，注意排线银色亮面朝下。&lt;/p&gt;
&lt;p&gt;如果只使用其中一个摄像头，则不需要特别的硬件设置。如果要启用多个摄像头，需要使用跳线帽将 CM4IO 板上的 J6 短接。如下图所示。&lt;/p&gt;
&lt;p&gt;系统设置&lt;/p&gt;
&lt;p&gt;运行下面的命令更新 dt-blob.bin 文件。&lt;/p&gt;
&lt;p&gt;sudo wget &lt;a href=&quot;https://datasheets.raspberrypi.org/cmio/dt-blob-dualcam.bin&quot;&gt;https://datasheets.raspberrypi.org/cmio/dt-blob-dualcam.bin&lt;/a&gt; -O /boot/dt-blob.bin&lt;/p&gt;
&lt;p&gt;运行命令开启摄像头接口。&lt;/p&gt;
&lt;p&gt;sudo raspi-config&lt;/p&gt;
&lt;p&gt;选择 Interface Options -&amp;gt; Camera -&amp;gt; Enable，设置完后按提示重启系统（有图形界面的系统可以在设置里直接设置开启 Camera）。&lt;/p&gt;
&lt;p&gt;使用方法&lt;/p&gt;
&lt;p&gt;重启后进入终端输入命令。&lt;/p&gt;
&lt;p&gt;ls /dev&lt;/p&gt;
&lt;p&gt;确认一下是否出现了 video0、video1 两个设备。&lt;/p&gt;
&lt;p&gt;然后就可以开始使用了。至于如何指定某个摄像头，可以使用 -cs 参数。&lt;/p&gt;
&lt;h1&gt;打开第一个摄像头拍照和录制视频 sudo raspistill -o test.jpg -cs 0 sudo raspivid -o test.flv -cs 0 # 打开第二个摄像头拍照和录制视频 sudo raspistill -o test.jpg -cs 1 sudo raspivid -o test.flv -cs 1&lt;/h1&gt;
&lt;p&gt;如有问题欢迎在小组讨论~&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://talk.quwj.com/&quot;&gt;https://talk.quwj.com/&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>在树莓派上安装 PyCharm</title><link>https://wuming.si/blog/3435/</link><guid isPermaLink="true">https://wuming.si/blog/3435/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;PyCharm 是一种 Python IDE（Integrated Development Environment，集成开发环境），带有一整套可以帮助用户在使用 Python 语言开发时提高其效率的工具，比如调试、语法高亮、项目管理、代码跳转、智能提示、自动完成、单元测试、版本控制。本教程为你介绍如何在树莓派上安装 PyCharm。本教程作者 @Tony 源自趣小组。&lt;/p&gt;
&lt;p&gt;运行 PyCharm 需要 Java 环境，如果树莓派上还没有安装过 JRE，可以使用以下命令安装即可：&lt;/p&gt;
&lt;p&gt;sudo apt install default-jre -y&lt;/p&gt;
&lt;p&gt;下载安装&lt;/p&gt;
&lt;p&gt;前往 JetBrains 官网下载 PyCharm 社区版（Community）：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jetbrains.com/pycharm/download/#section=linux&quot;&gt;https://www.jetbrains.com/pycharm/download/#section=linux&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;#解压，命令行中的文件名以实际下载的为准。 tar -zxvf pycharm-community-2020.2.1.tar.gz #运行，命令行中的目录名以实际下载的为准。 cd pycharm-community-2020.2.1/bin ./pycharm.sh&lt;/p&gt;
&lt;p&gt;创建快捷方式&lt;/p&gt;
&lt;p&gt;为了方便起见，可以添加快捷方式到启动器和桌面。&lt;/p&gt;
&lt;p&gt;在启动器中添加图标，编辑文件。&lt;/p&gt;
&lt;p&gt;sudo nano /usr/share/applications/pycharm.desktop&lt;/p&gt;
&lt;p&gt;输入以下内容&lt;/p&gt;
&lt;p&gt;[Desktop Entry] Name=PyCharm Type=Application Exec=/home/pi/pycharm-community-2020.2/bin/pycharm.sh Icon=/home/pi/pycharm-community-2020.2/bin/pycharm.png Categories=Development&lt;/p&gt;
&lt;p&gt;然后你就会看到树莓派菜单中的 PyCharm 图标。&lt;/p&gt;
&lt;p&gt;在桌面上添加图标，编辑文件。&lt;/p&gt;
&lt;p&gt;nano ~/Desktop/pycharm.desktop&lt;/p&gt;
&lt;p&gt;输入以下内容&lt;/p&gt;
&lt;p&gt;[Desktop Entry] Type=Link Name=PyCharm Icon=/home/pi/pycharm-community-2020.2/bin/pycharm.png URL=/usr/share/applications/pycharm.desktop&lt;/p&gt;
&lt;p&gt;然后你就会在桌面上看到 PyCharm 图标。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>在树莓派登录信息中加入温度 CPU、内存硬盘占用率信息</title><link>https://wuming.si/blog/3433/</link><guid isPermaLink="true">https://wuming.si/blog/3433/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MCC 172是一个两通道DAQ HAT，用于通过IEPE传感器（如加速度计和麦克风）进行声音和振动测量。它的每个通道具有24位A/D，最大采样率为51.2 kS/s/Ch。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>基于树莓派和 ESP32 制造一台掌上电脑</title><link>https://wuming.si/blog/3423/</link><guid isPermaLink="true">https://wuming.si/blog/3423/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;MAKER:rahmanshaber／译：趣无尽（转载请注明出处）&lt;/p&gt;
&lt;p&gt;因为树莓派本身就是一台微型的电脑，大家对于如何使用树莓派来实现一台多功能的电脑一直在做不同的尝试。&lt;/p&gt;
&lt;p&gt;在前面的教程中我们曾发表过《Pi-Micro：用树莓派ZeroW制作的掌上电脑》，如手机般大小的掌上电脑；还有《Arduino制作：支持Basic和VGA显示器的复古计算机》基于 Arduino 的掌上电脑。&lt;/p&gt;
&lt;p&gt;今天我们给大家带来一个基于 ESP32-S2 ，附带 56 个键盘，4 英寸显示屏以及扩展接口的树莓派掌上 PC。整个项目所需材料不多，易于构建，且外壳使用 3D 打印，整体造价不高。&lt;/p&gt;
&lt;p&gt;项目特点：&lt;/p&gt;
&lt;p&gt;– ESP32-S2，易于焊接且损耗小。&lt;/p&gt;
&lt;p&gt;– 4 英寸 IPS 显示屏，4GB RAM，2 个 HDMI 输出端口。&lt;/p&gt;
&lt;p&gt;– 56 个键可定制键盘，2 个用于导航的侧边按钮。&lt;/p&gt;
&lt;p&gt;– 带有 RTC 时钟、蜂鸣器、振动、光敏传感器和红外发射模块。&lt;/p&gt;
&lt;p&gt;– 支持红外遥控；支持自动息屏；支持显示新旧加载项；支持鼠标左右键。&lt;/p&gt;
&lt;p&gt;– 支持 MPU6050 陀螺仪模块、BME280 压力模块和温湿度模块。&lt;/p&gt;
&lt;p&gt;– 支持键盘按钮关闭模块、显示器以及附加组件；支持使用操作系统和键盘键关闭电源。&lt;/p&gt;
&lt;p&gt;– 带有 USB type C 型号的 Nurolink / Dock 端口，支持 UART、I2C 和供电。&lt;/p&gt;
&lt;p&gt;– 支持读取电池电量并在电池电量不足时关闭设备电源。&lt;/p&gt;
&lt;p&gt;– 支持 ESP32-S2 与树莓派通过 Neopixel 库双向连接。&lt;/p&gt;
&lt;p&gt;– 18650 锂电池，带充放电保护。&lt;/p&gt;
&lt;p&gt;– 改进版的外壳使其更好地贴合 PCB。&lt;/p&gt;
&lt;p&gt;组件清单&lt;/p&gt;
&lt;p&gt;Esp32-S2 × 1&lt;/p&gt;
&lt;p&gt;树莓派 × 1&lt;/p&gt;
&lt;p&gt;3D 打印部件 × 若干&lt;/p&gt;
&lt;p&gt;USB C 到 USB C 转换器 × 1&lt;/p&gt;
&lt;p&gt;4 x 2 公头排针 × 1&lt;/p&gt;
&lt;p&gt;2 x 20 母头排针 × 1&lt;/p&gt;
&lt;p&gt;4 英寸 IPS 显示屏 × 1&lt;/p&gt;
&lt;p&gt;FPC 连接器 × 1&lt;/p&gt;
&lt;p&gt;锂电池 × 1&lt;/p&gt;
&lt;p&gt;PCB 板 × 1&lt;/p&gt;
&lt;p&gt;GPS 模块 × 1&lt;/p&gt;
&lt;p&gt;DS3231 × 1&lt;/p&gt;
&lt;p&gt;蜂鸣器 × 1&lt;/p&gt;
&lt;p&gt;TP4056 × 1&lt;/p&gt;
&lt;p&gt;RTC 时钟 × 1&lt;/p&gt;
&lt;p&gt;按键 × 56&lt;/p&gt;
&lt;p&gt;USB C 接口 × 2&lt;/p&gt;
&lt;p&gt;螺丝螺母 × 若干&lt;/p&gt;
&lt;p&gt;电阻 × 若干&lt;/p&gt;
&lt;p&gt;电容 × 若干&lt;/p&gt;
&lt;p&gt;LED 灯 × 1&lt;/p&gt;
&lt;p&gt;接线 × 若干&lt;/p&gt;
&lt;p&gt;贴片按键 × 2&lt;/p&gt;
&lt;p&gt;吸锡带 × 1&lt;/p&gt;
&lt;p&gt;电烙铁 × 1&lt;/p&gt;
&lt;p&gt;焊锡丝 × 1&lt;/p&gt;
&lt;p&gt;剪线钳 × 1&lt;/p&gt;
&lt;p&gt;助焊剂 × 1&lt;/p&gt;
&lt;p&gt;修边刀 × 1&lt;/p&gt;
&lt;p&gt;PCB 功能介绍&lt;/p&gt;
&lt;p&gt;我将提供完整的清单，它包含所有的功能，但你可以根据自己的需求来组装。&lt;/p&gt;
&lt;p&gt;组件功能介绍如下（参考）：&lt;/p&gt;
&lt;p&gt;S – 1 开关电源模块，用于控制设备开关（必备）。&lt;/p&gt;
&lt;p&gt;S – 2 连续充电模块 ，断开充电器，防止设备重启&lt;/p&gt;
&lt;p&gt;S – 3 升压模， 将电池出来的电压升至 5 V&lt;/p&gt;
&lt;p&gt;S – 4 ESP32-S2 + 复位模块，控制器 （必备）。&lt;/p&gt;
&lt;p&gt;S – 5 振动模块 提供振动反馈（可选）。&lt;/p&gt;
&lt;p&gt;S – 6 时钟模块 断电后保持计时（可选）。&lt;/p&gt;
&lt;p&gt;S – 7 MP6050 / BME280 陀螺仪 / 温湿度（可选）。&lt;/p&gt;
&lt;p&gt;S – 8 扩展电源 设备关闭时可保持显示模块 S – 7 的功能（可选）。&lt;/p&gt;
&lt;p&gt;S – 9 红外模块 红外遥控器 （可选）。&lt;/p&gt;
&lt;p&gt;我列出两个版本供大家参考，一个精简版和一个完整版。你可以根据自己的需求对组件进行了删减。&lt;/p&gt;
&lt;p&gt;PCB 与 3D 外壳打印&lt;/p&gt;
&lt;p&gt;PCB 选定好功能后就可以开始搭建以及 3D 打印外壳。外壳总有五个部分。&lt;/p&gt;
&lt;p&gt;关于外壳 3D 打印文件可以在本项目文件库中下载：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://make.quwj.com/project/401&quot;&gt;https://make.quwj.com/project/401&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;PCB 总有三个部分。&lt;/p&gt;
&lt;p&gt;关于 PCB 3D 打印文件可以在本项目文件库中下载：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://make.quwj.com/project/401&quot;&gt;https://make.quwj.com/project/401&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;焊接部分&lt;/p&gt;
&lt;p&gt;在 PCB 中一共有八个模块，我将分模块进行焊接，先焊接三个模块的零件，这样有助于分步查错。&lt;/p&gt;
&lt;p&gt;焊接顺序如下：&lt;/p&gt;
&lt;p&gt;– USB C 端口 &amp;gt; S – 2 &amp;gt; 电池连接器 &amp;gt;&amp;gt; 查看电池是否充电。&lt;/p&gt;
&lt;p&gt;– S-4 （暂不焊接 FPC 连接器） &amp;gt; 4 x 2 公头排针 &amp;gt;&amp;gt; 测试能否能够上传固件。&lt;/p&gt;
&lt;p&gt;– S-1 &amp;gt; S – 3 &amp;gt;&amp;gt; 连接电池并按 S-3 模块中的按钮，查看指示灯是否亮起。&lt;/p&gt;
&lt;p&gt;– 焊接 FPC 连接器（外接显示器用）和 2 x 20 母头排针 &amp;gt; S-8 &amp;gt;&amp;gt; 键盘 &amp;gt;&amp;gt; 连接树莓派和电池，按住电源按钮查看树莓派是否启动。&lt;/p&gt;
&lt;p&gt;– S-6 &amp;gt; S-9 &amp;gt; S-7 &amp;gt;&amp;gt; 查看在 OS 的指导下，整个 PCB 板的是否工作正常。&lt;/p&gt;
&lt;p&gt;组装部分&lt;/p&gt;
&lt;p&gt;螺丝的型号如图所示。&lt;/p&gt;
&lt;p&gt;3D 打印外壳所需的螺丝的型号为：&lt;/p&gt;
&lt;p&gt;– 22mm x 2&lt;/p&gt;
&lt;p&gt;– 9mm x 2&lt;/p&gt;
&lt;p&gt;– 6mm x 4&lt;/p&gt;
&lt;p&gt;– 8mm x 1&lt;/p&gt;
&lt;p&gt;– 10mm x 1&lt;/p&gt;
&lt;p&gt;– 16mm x 1&lt;/p&gt;
&lt;p&gt;尝试将 PCB 放入，毛边的地方需要用砂纸打磨。&lt;/p&gt;
&lt;p&gt;线路连接及其他功能的扩展（可选）&lt;/p&gt;
&lt;p&gt;在 PCB 有一个扩展端口（2 x 10 pin 的母头排针），接入后可扩展一下六个功能：&lt;/p&gt;
&lt;p&gt;– 无线电广播, LoRa 通信&lt;/p&gt;
&lt;p&gt;– 自定义 Wifi&lt;/p&gt;
&lt;p&gt;– GPS&lt;/p&gt;
&lt;p&gt;– Micro SD&lt;/p&gt;
&lt;p&gt;– 蓝牙&lt;/p&gt;
&lt;p&gt;– 压力+湿度+温度模块&lt;/p&gt;
&lt;p&gt;– 自定义专属模块&lt;/p&gt;
&lt;p&gt;Nurolink / Dock 端口的扩展&lt;/p&gt;
&lt;p&gt;该设备的 Nurolink / Dock 对接端口，可以用来供电、连接外部设备或外部电路。&lt;/p&gt;
&lt;p&gt;如图所示，可连接到 Nurolink / Dock 端口的 GPS 模块。&lt;/p&gt;
&lt;p&gt;同时也可通过 Nurolink / Dock 端口连接两个接口，需要将 USB C 和 USB C 的电缆进行改动，需要交换 D+ 和 D- 线，因为 TX 要连接至 RX，没有改动的线是 Tx 是连接到 TX。&lt;/p&gt;
&lt;p&gt;Nurolink / Dock 端口的 6 个引脚如下：&lt;/p&gt;
&lt;p&gt;-2 个引脚分别为 UART / TTL 引脚：Tx 和 Rx&lt;/p&gt;
&lt;p&gt;-2 个引脚分别为电源引脚：3v 和接地&lt;/p&gt;
&lt;p&gt;-2 个引脚分别为 I2c 引脚：SDL 和 SCL&lt;/p&gt;
&lt;p&gt;可升级的地方&lt;/p&gt;
&lt;p&gt;DIY PC 的过程不会一帆风顺，仍有许多改进的地方，可以不断的优化项目。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;p&gt;Arduino 编码，可在固件中添加功能。&lt;/p&gt;
&lt;p&gt;Fusion 360，可改进支持且更容易构建。&lt;/p&gt;
&lt;p&gt;Python，可使附件充分发挥作用，例如基于 Lora 的通信应用程序能够与其他 PC 用户进行通信。&lt;/p&gt;
&lt;p&gt;FreeCAD，将其移植到 freeCAD，能够更加开源。&lt;/p&gt;
&lt;p&gt;KiCAD，能够更加开源。&lt;/p&gt;
&lt;p&gt;via&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>基于树莓派的 WebDAQ 504 应用案例</title><link>https://wuming.si/blog/3421/</link><guid isPermaLink="true">https://wuming.si/blog/3421/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;WebDAQ 504 是 MCC 基于树莓派设计的数据采集产品，与我们之前推荐的 MCC DAQ HATs 系列一样，被用于解决工业数据采集的需求。下面这一则案例带你进一步了解 WebDAQ 504 以及树莓派在工业中的应用。&lt;/p&gt;
&lt;p&gt;风力涡轮机变速箱故障测试系统&lt;/p&gt;
&lt;p&gt;上海朋禾智能科技有限公司是一家专注于工业设备健康评估和故障诊断的定制解决方案提供商。他们专注于工业物联网（IIoT）平台设计，边缘计算系统架构和工业级硬件平台设计作。&lt;/p&gt;
&lt;p&gt;风力发电行业的客户需要风力涡轮机的测试解决方案。通过收集和分析轴承和其它组件的振动数据，对涡轮传动系统齿轮箱进行诊断。&lt;/p&gt;
&lt;p&gt;预测风力涡轮机中的齿轮和轴承故障可以降低维护成本，缩短停机时间并防止绿色风电能源生产中断。&lt;/p&gt;
&lt;p&gt;挑战&lt;/p&gt;
&lt;p&gt;动力传动系统是风力涡轮机的心脏，包含发电机和齿轮箱，可以将叶片的旋转转换为电能。风力涡轮机变速箱可能会经受极端应变和扭矩使用场景，这使组件的耐用性成为一个挑战。&lt;/p&gt;
&lt;p&gt;当典型的涡轮机仅以10~18RPM转速旋转时，感应发电机的转速可以达到1000RPM以上。这样的速度会产生了巨大的应力，给每个部件带来了沉重的负担。由于风力涡轮机的远距离监控的特点，传动系统的可靠性是重中之重。&lt;/p&gt;
&lt;p&gt;所需的变速箱故障测试系统可模拟常见的轴承故障情况。通过模拟不同的运行和负载条件，它可以帮助检测可能发生轴承和齿轮故障的情况。&lt;/p&gt;
&lt;p&gt;客户需要一种振动测量和分析解决方案，该解决方案不仅要功能强大，而且还要足够灵活以满足他们定制化算法的要求。该解决方案将需要在两种模式下使用。在独立模式下，该解决方案将收集数据作为测试平台的一部分，就地完成实时数据查看和分析。在IIoT模式下，数据采集解决方案将与涡轮机一起部署，远程实现实时记录和分析数据。&lt;/p&gt;
&lt;p&gt;解决方案&lt;/p&gt;
&lt;p&gt;MCC的WebDAQ 504被选为系统的核心数据采集部分。两个基于IEPE的加速度计直接连接到WebDAQ，且无需增加信号调理电路。这两个通道是同步采样的，还使用了内置的抗混叠滤波功能。水平和垂直加速度传感器已预先安装在测试台的齿轮箱轴承上，并且每个通道的采样速率高达25kS/s作为核心。&lt;/p&gt;
&lt;p&gt;WebDAQ 504包含一个易于使用的基于Web浏览器的应用程序，该应用程序允许用户设置采集，记录, 分析和显示数据，启用警报以及将数据导出到第三方应用程序。&lt;/p&gt;
&lt;p&gt;WebDAQ 504的计算引擎是树莓派计算模块（Raspberry Pi computer module）。通过使用WebDAQ开源代码，程序员可以直接访问树莓派计算模块，这使该系统成为真正的边缘计算解决方案。随附的Python示例程序允许程序员快速构建其应用程序，并连接程序的自定义显示和分析部分。&lt;/p&gt;
&lt;p&gt;执行&lt;/p&gt;
&lt;p&gt;在独立模式下，通过WebDAQ内置的应用程序进行配置及数据采集，实时分析不同工况（负载和速度）下的振动数据。&lt;/p&gt;
&lt;p&gt;在IIoT模式下，WebDAQ 504与涡轮一起部署，并通过以太网进行通信。该系统支持将WebDAQ 504连接到公有云或私有云。通过自定义界面，可以实现诸如设备管理，远程监视和健康评估之类的应用程序，这些应用程序适合于分布式在线监视和健康管理应用程序场景。数据还可以通过MQTT协议上载，与辅助数据应用程序一起使用。&lt;/p&gt;
&lt;p&gt;测试台可模拟多种操作条件，为操作员提供有价值的反馈。负载分析数据使工程师能够检测轴承和齿轮故障情况。&lt;/p&gt;
&lt;p&gt;成果&lt;/p&gt;
&lt;p&gt;完整的系统是一个灵活的平台，可以收集和分析振动数据。它满足了客户的专用软件需求，为客户提高了设备可靠性并减少了设备维护时间。&lt;/p&gt;
&lt;p&gt;崔鹏&lt;/p&gt;
&lt;p&gt;总经理&lt;/p&gt;
&lt;p&gt;上海朋禾智能科技有限公司&lt;/p&gt;
&lt;p&gt;上海市浦东新区纳贤路800号科海大楼&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://www.phengine.com&quot;&gt;http://www.phengine.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;获取更多关于Measurement Computing的信息，请访问官网：china.mccdaq.com&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>工业树莓派结合USB摄像头实现远程网络监控</title><link>https://wuming.si/blog/3425/</link><guid isPermaLink="true">https://wuming.si/blog/3425/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;背景&lt;/p&gt;
&lt;p&gt;工业环境下为了更好地查看设备附近的运行状况，一般都会安装摄像头达到监控的效果，工业树莓派适用于工业环境，本次测试就使用工业树莓派和USB摄像头结合达到远程网络监控的效果。本次测试采用的远程网络监控工具是MJPG-streame，它是一款免费基于IP地址的视频流服务器，它的输入插件从摄像头读取视频数据，这个输入插件产生视频数据并将视频数据复制到内存中，它有多个输出插件将这些视频数据经过处理，其中最重要的输出插件是网站服务器插件，它将视频数据传送到用户浏览器中，MJPG-streamer的工作就是将其中的一个输入插件和多个输出插件绑定在一起，所有的工作都是通过它的各个插件完成的。&lt;/p&gt;
&lt;p&gt;准备工作&lt;/p&gt;
&lt;p&gt;1台工业树莓派&lt;/p&gt;
&lt;p&gt;1个USB摄像头：普通摄像头即可，本次测试采用的是无需驱动的USB摄像头。&lt;/p&gt;
&lt;p&gt;安装步骤&lt;/p&gt;
&lt;p&gt;1、检查是否存在USB摄像头设备。&lt;/p&gt;
&lt;p&gt;1）方法一：lsusb&lt;/p&gt;
&lt;p&gt;2）方法二：ls /dev&lt;/p&gt;
&lt;p&gt;可以看到video0就表示有USB摄像头设备在运行&lt;/p&gt;
&lt;p&gt;2、安装MJPG-Streamer。&lt;/p&gt;
&lt;p&gt;依次按照以下命令安装：&lt;/p&gt;
&lt;p&gt;1）sudo apt-get install cmake libjpeg8-dev&lt;/p&gt;
&lt;p&gt;2）wget &lt;a href=&quot;https://github.com/Five-great/mjpg-streamer/archive/master.zip&quot;&gt;https://github.com/Five-great/mjpg-streamer/archive/master.zip&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;（注意：如果无法连接到github，可以用PC端下载压缩包，然后通过FinalShell或者FTP服务器上传到树莓派。）&lt;/p&gt;
&lt;p&gt;unzip mjpg-streamer-master.zip&lt;/p&gt;
&lt;p&gt;3）&lt;/p&gt;
&lt;p&gt;cd mjpg-streamer-master cd mjpg-streamer-experimental&lt;/p&gt;
&lt;p&gt;4）make&lt;/p&gt;
&lt;p&gt;5）sudo make install&lt;/p&gt;
&lt;p&gt;6）cd /home&lt;/p&gt;
&lt;p&gt;3、启动MJPG-Streamer&lt;/p&gt;
&lt;p&gt;/usr/local/bin/mjpg_streamer -i “/usr/local/lib/mjpg-streamer/input_uvc.so -n -f 30 -r 1280×720” -o “/usr/local/lib/mjpg-streamer/output_http.so -p 8080 -w /usr/local/share/mjpg-streamer/www”&lt;/p&gt;
&lt;p&gt;4、实时视频接收&lt;/p&gt;
&lt;p&gt;用浏览器打开网址：http://&amp;lt;树莓派IP地址&amp;gt;:8080/javascript.html&lt;/p&gt;
&lt;p&gt;5、总结&lt;/p&gt;
&lt;p&gt;如此便可以实现远程网络监控，适用的场景可以是监控树莓派控制的电机的运行状态或者传感器的运行状态。在一定程度上也降低了成本，相较于以太网接口摄像头，USB摄像头更加实惠。&lt;/p&gt;
&lt;p&gt;关于虹科&lt;/p&gt;
&lt;p&gt;虹科是一家在工业物联网IIoT行业经验超过3年的高科技公司，虹科与世界领域顶级公司包括EXOR、Eurotech、Unitronics、Matrikon、KUNBUS等合作，提供一系列先进的高端的工业4.0 HMI、高端边缘计算机、IoT开发方案、PLC与HMI一体机、OPC UA、工业级树莓派等解决方案。物联网事业部所有成员都受过国内外专业培训，并获得专业资格认证，平均3年+的技术经验和水平一致赢得客户极好口碑。&lt;/p&gt;
&lt;p&gt;我们积极参与行业协会的工作，为推广先进技术的普及做出了重要贡献。至今，虹科已经为行业内诸多用户提供从硬件到软件的不同方案，并参与和协助了众多OEM的设备研发和移植项目，以及终端用户的智能工厂和工业4.0升级改造项目。我们致力于为中国乃至全球的万物互联、智能制造贡献自己的力量。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>工业树莓派零代码实现工业协议与MQTT互转</title><link>https://wuming.si/blog/3415/</link><guid isPermaLink="true">https://wuming.si/blog/3415/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在以往的文章中，我们已经介绍了虹科的工业树莓派与Node-RED之间配合的种种可能性。今天，我们将通过实操案例，来看看如何使用Node-RED，以拖拽式低代码，甚至是无代码编程的方式，轻松实现工业协议与MQTT互转以及简单的HMI数据面板，帮助您有效降低在OT协议方面的学习成本，在低经验，甚至是无OT编程经验的情况下，快速在OT协议中的数据与云端数据之间建立联系。&lt;/p&gt;
&lt;p&gt;准备工作&lt;/p&gt;
&lt;p&gt;1.硬件准备&lt;/p&gt;
&lt;p&gt;PC一台&lt;/p&gt;
&lt;p&gt;工业树莓派一台&lt;/p&gt;
&lt;p&gt;24V直流电源&lt;/p&gt;
&lt;p&gt;网线&lt;/p&gt;
&lt;p&gt;相关协议网关模块（可选，根据实际需求添加）&lt;/p&gt;
&lt;p&gt;2.软件准备&lt;/p&gt;
&lt;p&gt;PC上：&lt;/p&gt;
&lt;p&gt;Chrome内核的浏览器&lt;/p&gt;
&lt;p&gt;树莓派上：&lt;/p&gt;
&lt;p&gt;Node-RED、Node-RED RevPi Nodes Server（默认已预装，若发现预装缺失，或需要版本更新可联系广州虹科获取相关文档）&lt;/p&gt;
&lt;p&gt;node-red-dashboard（可选，用于制作简单的HMI数据面板）&lt;/p&gt;
&lt;p&gt;操作步骤&lt;/p&gt;
&lt;p&gt;1.树莓派上电，连好网线，使用Advanced IP Scanner或类似软件扫描工业树莓派的IP地址，或在路由器后台中直接查看DHCP客户端列表，从而确定树莓派所在IP地址。&lt;/p&gt;
&lt;p&gt;2.浏览器打开树莓派所在IP地址，出现登录画面，账号为admin，默认密码会印在机身侧面的贴纸上，每台机子默认密码各不相同。&lt;/p&gt;
&lt;p&gt;3.登录后打开SERVICES选项卡，分别将以下三项设为Enable，最后点SAVE ALL保存设置。&lt;/p&gt;
&lt;p&gt;Modbus Master（本文以Modbus为例，因此需要将Modbus主站功能使能，若不使用Mobus协议则无需Enable此项）&lt;/p&gt;
&lt;p&gt;Node-RED&lt;/p&gt;
&lt;p&gt;Node-RED RevPi Nodes Server&lt;/p&gt;
&lt;p&gt;4.回到APPS选项卡，点击PiCtory右边的START进入树莓派的模块配置界面——PiCtory。&lt;/p&gt;
&lt;p&gt;5.首先把主模块按实际情况从左边设备列表中拖到右边（作为例子，本文使用Core作为主模块），把其它IO模块和协议网关模块按实际情况中的左右物理位置，按顺序把模块拖到主模块两边（本文没有使用额外模块）。最后，若要使用主模块的Modbus RTU/TCP主站功能，则在Virtual Devices中找到相应的虚拟模块，拖动到右边，如下图所示。&lt;/p&gt;
&lt;p&gt;6.设置相应的协议模块，本文以设置Modbus TCP主站为例：选定要设置的Modbus TCP主站虚拟模块，在右下角变量表中拖到最下方，在slave_IP_address中填入Modbus TCP从站所在IP地址，slave_TCP_port中填入Modbus TCP通讯设置的端口号，默认是502。&lt;/p&gt;
&lt;p&gt;7.对着Modbus TCP主站虚拟模块右键点击，打开Extended Data设置界面，并按实际情况设置Modbus变量地址等相关参数，输入完成后点OK确定，附上本文Modbus TCP从站相关参数表：&lt;/p&gt;
&lt;p&gt;8.配置完成后点上方菜单的File -&amp;gt; Save as Start-Config. 保存到启动方案，然后点Tools -&amp;gt; Reset Driver立即使配置生效。&lt;/p&gt;
&lt;p&gt;9.开一个新标签页，再次输入树莓派IP打开工业树莓派设置主页，点击Node-RED右边的START，或直接访问 [树莓派IP]:1880 进入Node-RED编程界面。&lt;/p&gt;
&lt;p&gt;10.在左边的节点列表中找到Revolution Pi分类，此分类底下的节点即为专为工业树莓派设计的节点。拖动一个revpi-multiple-input节点到右边网格区域，然后双击打开对其进行配置。先点击Server右边的铅笔案例，保持默认的localhost地址和8000端口，其它参数保持留空，然后点击右上角的添加，最后在Input Pins列表中找到以下3个变量（该列表只能找到输入变量，输入是对树莓派而言），分别勾上然后点右上角的完成。&lt;/p&gt;
&lt;p&gt;Input_Word_1&lt;/p&gt;
&lt;p&gt;Input_Word_2&lt;/p&gt;
&lt;p&gt;Input_Word_3&lt;/p&gt;
&lt;p&gt;11.拖一个Debug节点到revpi-multiple-input节点的右边，简单的把两个模块连上，点整个网页右上角的“部署”。&lt;/p&gt;
&lt;p&gt;12.把Modbus TCP从站中的变量值稍作更改，这里我把从站中地址1变量的值改成了321（revpi-multiple-input节点需要它监视的变量中至少一个值有变化，才会向后面的节点输出信息），此时在Debug信息区中找到如下信息表示Modbus TCP数据获取成功。（变量属性是数字形式或者字符串形式都没关系，不影响后续使用）。&lt;/p&gt;
&lt;p&gt;13.按下图方式配置节点，其中MQTT输入输出节点仅需配置MQTT地址、端口、Topic等基本信息即可，本文把树莓派的发布Topic设为了RevPiPub，订阅Topic设为了RevPiSub。&lt;/p&gt;
&lt;p&gt;分割消息和消息分流节点配置分别如下。&lt;/p&gt;
&lt;p&gt;分流节点右边的3个节点为revpi – output，默认会选定前面设置过的localhost:8000作为Server，这部分不需要更改，在下方Pin中分别选择3个输出变量名Output_Word_1、Output_Word_2、Output_Word_3即可。&lt;/p&gt;
&lt;p&gt;14.修改完成后，别忘了点击整个网页右上角的“部署”。然后我们尝试把Modbus TCP从站中地址2变量的值改成44555，此时即能通过MQTT获取到一条记录Modbus变量的信息。&lt;/p&gt;
&lt;p&gt;15.随后我们通过MQTT发送指令，分别通过JSON中out1、out2、out3三个字段，修改3个Modbus写入变量的值，效果如下图。&lt;/p&gt;
&lt;p&gt;16.至此，MQTT和工业通讯协议中的变量交互已经完成，我们既实现了MQTT数据上传，也实现了数据下发。接下来，我们可以尝试创建一个简单的HMI数据面板。首先，在Node-RED右上角的“三”菜单中找到“节点管理”，然后进入“安装”选项卡，搜索node-red-dashboard并安装（安装速度取决于网络环境和树莓派工作负载）。&lt;/p&gt;
&lt;p&gt;17.我们在前面的基础上，额外加上按下图形式编排的一些节点。其中左边一列节点均为inject节点，被设为每秒为周期重复注入字符串（变量名），中间一列节点全是工业树莓派专用节点中的revpi – getpin，用以通过变量名获取值，右边全是dashboard分类下的gauge。&lt;/p&gt;
&lt;p&gt;18.点击部署后，浏览器访问 访问 [树莓派IP]:1880/ui 即可看到由Node-RED自动生成的简易HMI数据面板。&lt;/p&gt;
&lt;p&gt;总结&lt;/p&gt;
&lt;p&gt;通过以上的简单步骤，我们可以轻松实现Modbus变量数据与MQTT的交互，中途没有输过任何一行代码，甚至没有进入过命令行界面，全部都是浏览器上的设置操作，其它工业树莓派所支持的通讯协议的配置也是大同小异。总的思路就是给树莓派设置好相应的参数，树莓派自动从相应协议中获取值并存到变量中，我们只需要在Node-RED中拖拽编程，把变量送到MQTT发送组件即可。这种编程方式大幅降低了非OT从业人员开发相关应用的学习成本，用户也不用担心之前从未使用过树莓派，甚至完全没有Linux系统使用经验也没关系。此外，利用Node-RED上的其它开源节点，我们也能够以MQTT以外的多种方式传输数据、以丰富多样方式的展示数据。有编程经验的用户，也可以通过function节点在发送前对数据进行运算、过滤等多种处理，甚至利用exec节点接入其它应用，进行更为复杂的边缘运算。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>树莓派4 UART 多串口配置通信</title><link>https://wuming.si/blog/3409/</link><guid isPermaLink="true">https://wuming.si/blog/3409/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;相比树莓派零、1、2 以及 3 的双串口 UART0（PL011）和 UART1（mini UART），树莓派4 中新增了 4 个 PL011 串口共计有 6 个 UART，整理此笔记用作记录和配置参考。&lt;/p&gt;
&lt;p&gt;注意，目前搜到的大多数描述树莓派 4 串口的文章，大多数开头都是禁用下蓝牙，这个做法针对树莓派0-3 是必须的，因为本身串口不够用，但对树莓派 4 来说并不需要，因为有额外 4 个串口可以利用，默认配置好的两串口一个用于蓝牙（UART0）另一个是 miniUART 可以保留设置。此方面的文章大多都是一个流程，原因是参考的最初版本是树莓派 3 的设置；树莓派 4 的额外串口设置在树莓派论坛中可以看到相关的介绍，外面的文章不太多。&lt;/p&gt;
&lt;p&gt;UART 配置&lt;/p&gt;
&lt;p&gt;1. 展示所有串口命令&lt;/p&gt;
&lt;p&gt;$ dtoverlay -a | grep uart&lt;/p&gt;
&lt;p&gt;展示 pi4 中所有串口&lt;/p&gt;
&lt;p&gt;pi@raspberrypi:~ $ dtoverlay -a | grep uart midi-uart0 midi-uart1 miniuart-bt uart0 uart1 uart2 uart3 uart4 uart5&lt;/p&gt;
&lt;p&gt;2. 查看特定串口信息&lt;/p&gt;
&lt;p&gt;$ dtoverlay -h uart2&lt;/p&gt;
&lt;p&gt;查看 UART2 的配置信息等：&lt;/p&gt;
&lt;p&gt;pi@raspberrypi:~ $ dtoverlay -h uart2 Name: uart2 Info: Enable uart 2 on GPIOs 0-3 Usage: dtoverlay=uart2, Params: ctsrts Enable CTS/RTS on GPIOs 2-3 (default off)&lt;/p&gt;
&lt;p&gt;相关信息会展现 GPIOs 与新的 UART 串口的分配：0-3 对应 UART2， 4-7 对应 UART3，8-11 对应 UART 4，以及 12-15 对应 GUIO 5&lt;/p&gt;
&lt;p&gt;关于 4 个针脚中后两位对应的 CTS/RTS，可参考：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/zeroboundary/article/details/8966586&quot;&gt;https://blog.csdn.net/zeroboundary/article/details/8966586&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;3. 配置开启串口 UART2-5&lt;/p&gt;
&lt;p&gt;执行编辑 config.txt 命令：&lt;/p&gt;
&lt;p&gt;sudo nano /boot/config.txt&lt;/p&gt;
&lt;p&gt;在文件结尾添加如下：&lt;/p&gt;
&lt;p&gt;dtoverlay=uart2 dtoverlay=uart3 dtoverlay=uart4 dtoverlay=uart5&lt;/p&gt;
&lt;p&gt;保存（Write Out）并退出（Exit）&lt;/p&gt;
&lt;p&gt;重启后查看是否生效：&lt;/p&gt;
&lt;p&gt;$ ls /dev/ttyAMA*&lt;/p&gt;
&lt;p&gt;结果显示如下：&lt;/p&gt;
&lt;p&gt;pi@raspberrypi:~ $ ls /dev/ttyAMA* /dev/ttyAMA0 /dev/ttyAMA1 /dev/ttyAMA2 /dev/ttyAMA3 /dev/ttyAMA4&lt;/p&gt;
&lt;p&gt;各 UART 串口与 GPIO 对应关系：&lt;/p&gt;
&lt;p&gt;GPIO14 = TXD0 -&amp;gt; ttyAMA0 GPIO0 = TXD2 -&amp;gt; ttyAMA1 GPIO4 = TXD3 -&amp;gt; ttyAMA2 GPIO8 = TXD4 -&amp;gt; ttyAMA3 GPIO12 = TXD5 -&amp;gt; ttyAMA4 GPIO15 = RXD0 -&amp;gt; ttyAMA0 GPIO1 = RXD2 -&amp;gt; ttyAMA1 GPIO5 = RXD3 -&amp;gt; ttyAMA2 GPIO9 = RXD4 -&amp;gt; ttyAMA3 GPIO13 = RXD5 -&amp;gt; ttyAMA4&lt;/p&gt;
&lt;p&gt;4. 测试&lt;/p&gt;
&lt;p&gt;4.1 串口自发自收测试&lt;/p&gt;
&lt;p&gt;现在我们先测试 UART2 是否启用成功，比较简单的测试方式是将其 TXD 和 RXD 相连，自发自收。&lt;/p&gt;
&lt;p&gt;根据上方对应关系，UART2 对应 TXD2 和 RXD2，对应 GPIO0 和 GPIO1，对应 ttyAMA1&lt;/p&gt;
&lt;p&gt;注：UART0 对应的 ttyAMA0，UART1 对应的 ttyS0，UART2 到 UART5 对应的 ttyAMA1 到 ttyAMA4。&lt;/p&gt;
&lt;p&gt;找到对应的 GPIO0 和 GPIO1 针脚连起来：&lt;/p&gt;
&lt;p&gt;在树莓派端命令行进入 Python3 环境，通过如下模块和命令自发自收：&lt;/p&gt;
&lt;p&gt;pi@raspberrypi:~ $ python3 Python 3.7.3 (default, Jul 25 2020, 13:03:44) [GCC 8.3.0] on linux Type “help”, “copyright”, “credits” or “license” for more information. &amp;gt;&amp;gt;&amp;gt; import serial &amp;gt;&amp;gt;&amp;gt; ted = serial.Serial(port=”/dev/ttyAMA1″, baudrate=9600) &amp;gt;&amp;gt;&amp;gt; ted.write(“Hello World”.encode(“gbk”)) 11 &amp;gt;&amp;gt;&amp;gt; ted.read(11) b’Hello World’ &amp;gt;&amp;gt;&amp;gt;&lt;/p&gt;
&lt;p&gt;同理，我们可以继续用跳线帽将 GPIO4 和 5 相连测试 UART3；GPIO8 和 9 相连测试 UART4； GPIO12 和 13 相连测试 UART5。&lt;/p&gt;
&lt;p&gt;pi@raspberrypi:~ $ python3 Python 3.7.3 (default, Jul 25 2020, 13:03:44) [GCC 8.3.0] on linux Type “help”, “copyright”, “credits” or “license” for more information. &amp;gt;&amp;gt;&amp;gt; import serial &amp;gt;&amp;gt;&amp;gt; ted = serial.Serial(port=”/dev/ttyAMA1″, baudrate=9600) &amp;gt;&amp;gt;&amp;gt; ted.write(“Hello World”.encode(“gbk”)) 11 &amp;gt;&amp;gt;&amp;gt; ted.read(11) b’Hello World’ &amp;gt;&amp;gt;&amp;gt; ted3 = serial.Serial(port=”/dev/ttyAMA2″, baudrate=9600) &amp;gt;&amp;gt;&amp;gt; ted3.write(“Hello No.3”.encode(“gbk”)) 10 &amp;gt;&amp;gt;&amp;gt; ted3.read(10) b’Hello No.3′ &amp;gt;&amp;gt;&amp;gt; ted4 = serial.Serial(port=”/dev/ttyAMA3″, baudrate=9600) &amp;gt;&amp;gt;&amp;gt; ted4.write(“Hello No.4”.encode(“gbk”)) 10 &amp;gt;&amp;gt;&amp;gt; ted4.read(10) b’Hello No.4′ &amp;gt;&amp;gt;&amp;gt; ted5 = serial.Serial(port=”/dev/ttyAMA4″, baudrate=9600) &amp;gt;&amp;gt;&amp;gt; ted5.write(“Hello No.5”.encode(“gbk”)) 10 &amp;gt;&amp;gt;&amp;gt; ted5.read(10) b’Hello No.5′ &amp;gt;&amp;gt;&amp;gt;&lt;/p&gt;
&lt;p&gt;4.2 串口间通信测试&lt;/p&gt;
&lt;p&gt;接下来测试 UART2 和 UART3 间的通信，将 TXD2 连接 RXD3 即 GPIO0 与 GPIO5 相连；将 TXD3 与 RXD2 连接即 GPIO4 与 GPIO1 相连。&lt;/p&gt;
&lt;p&gt;GPIO0 = TXD2 -&amp;gt; ttyAMA1 GPIO4 = TXD3 -&amp;gt; ttyAMA2 GPIO1 = RXD2 -&amp;gt; ttyAMA1 GPIO5 = RXD3 -&amp;gt; ttyAMA2&lt;/p&gt;
&lt;p&gt;pi@raspberrypi:~ $ python3 Python 3.7.3 (default, Jul 25 2020, 13:03:44) [GCC 8.3.0] on linux Type “help”, “copyright”, “credits” or “license” for more information. &amp;gt;&amp;gt;&amp;gt; import serial &amp;gt;&amp;gt;&amp;gt; ted = serial.Serial(port=”/dev/ttyAMA1″, baudrate=9600) &amp;gt;&amp;gt;&amp;gt; ted3 = serial.Serial(port=”/dev/ttyAMA2″, baudrate=9600) &amp;gt;&amp;gt;&amp;gt; ted.write(“Msg from UART2…”.encode(“gbk”)) 17 &amp;gt;&amp;gt;&amp;gt; ted3.read(17) b’Msg from UART2…’ &amp;gt;&amp;gt;&amp;gt; ted3.write(“Msg from UART3…”.encode(“gbk”)) 17 &amp;gt;&amp;gt;&amp;gt; ted.read(17) b’Msg from UART3…’ &amp;gt;&amp;gt;&amp;gt;&lt;/p&gt;
&lt;p&gt;OK 挺顺利，UART2 和 UART3 间通信正常。&lt;/p&gt;
&lt;p&gt;参考&lt;/p&gt;
&lt;p&gt;官方 UART 配置文档：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.raspberrypi.org/documentation/configuration/uart.md&quot;&gt;https://www.raspberrypi.org/documentation/configuration/uart.md&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;论坛关于多串口的命令与指引：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.raspberrypi.org/forums/viewtopic.php?t=244827#p1493698&quot;&gt;https://www.raspberrypi.org/forums/viewtopic.php?t=244827#p1493698&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本文遵循CC 4.0 BY-SA版权协议，原文出处：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_40796925/article/details/107907991&quot;&gt;https://blog.csdn.net/weixin_40796925/article/details/107907991&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>用树莓派改造猫厕所：智能通风，告别异味</title><link>https://wuming.si/blog/3437/</link><guid isPermaLink="true">https://wuming.si/blog/3437/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;之前我们有介绍过「激光逗猫神器」来增加猫主子的运动量。今天要介绍的项目则可以解决铲屎官们痛点之一：猫主子粑粑的气味问题。每次铲屎的时候，猫砂的气味就会飘散出来，那是一种巨酸爽的味道。&lt;/p&gt;
&lt;p&gt;这个猫咪个智能厕所使用一块树莓派就能将猫砂盆改造成自动通风的猫厕，每当猫主子「出恭」完毕，它就能自行启动风扇，进行散味。&lt;/p&gt;
&lt;p&gt;硬件组成&lt;/p&gt;
&lt;p&gt;组件清单如下：&lt;/p&gt;
&lt;p&gt;树莓派 Zero W&lt;/p&gt;
&lt;p&gt;80mm×80mm 风扇（CPU散热器）&lt;/p&gt;
&lt;p&gt;12V 电源&lt;/p&gt;
&lt;p&gt;1 截空调通风管&lt;/p&gt;
&lt;p&gt;PIR 运动传感器（热释电传感器）若干&lt;/p&gt;
&lt;p&gt;1k 欧姆电阻若干&lt;/p&gt;
&lt;p&gt;三极管&lt;/p&gt;
&lt;p&gt;1 个降压-升压变换器&lt;/p&gt;
&lt;p&gt;这些硬件的总成本在 400 元左右。其中，10 美元的树莓派 Zero W 是整个装置的核心，用于控制电路什么时候运行。&lt;/p&gt;
&lt;p&gt;作者表示，树莓派也可以用更便宜的树莓派 Pico 来代替（5美元），但他想了解自家猫咪的具体拉粑粑时间……因此选择了贵一点的树莓派 Zero W，可以远程登录查看数据。&lt;/p&gt;
&lt;p&gt;风扇用于散味，空调通风管则用于排气。&lt;/p&gt;
&lt;p&gt;PIR（红外）运动传感器，则用来检测猫咪什么时候进猫砂盆、什么时候出去，以决定风扇通风的时间。&lt;/p&gt;
&lt;p&gt;此外，还需要一个降压-升压变换器（此处用了汽车 USB 充电器代替），因为风扇的供电电压需要达到 12V，但树莓派的额定运行电压却在 5V。&lt;/p&gt;
&lt;p&gt;同时，树莓派的输出电压只有 3.3V，因此在树莓派输出 3.3V 信号的时候，还需要一个三极管来控制 12V 电路的开启和关闭。&lt;/p&gt;
&lt;p&gt;整体搭建电路图如下（其中333Ω的电阻，由3个1kΩ的电阻并联获得）：&lt;/p&gt;
&lt;p&gt;图中，PIR 指运动传感器，Pz 指树莓派（输出通过 333 欧姆电阻与三极管相连），fan 指风扇，而风扇和树莓派之间的就是三极管。&lt;/p&gt;
&lt;p&gt;至于图中的 5V 电压，可以直接用降压变换器，也可以对汽车 USB 充电器进行一个改装：先将它拆开，再把充电器的金属尖端用电线焊接替代，最后连接到主电路中。&lt;/p&gt;
&lt;p&gt;到这里，硬件部分的组装就已经完成，搭建到猫砂盆上方后，看起来是这个样子的：&lt;/p&gt;
&lt;p&gt;再用胶带或者热熔胶枪组装起来就行。&lt;/p&gt;
&lt;p&gt;当然，这也只是完成硬件部分的制作，软件上还需要对树莓派进行设置。&lt;/p&gt;
&lt;p&gt;软件和程序的安装&lt;/p&gt;
&lt;p&gt;首先，需要给树莓派的SD卡安装系统，然后连接电源启动等。&lt;/p&gt;
&lt;p&gt;再就可以开始部署程序了。由于作者想记录猫咪的拉粑粑时间，因此还安装了 Python 和数据库 MariaDB：&lt;/p&gt;
&lt;p&gt;sudo apt install mariadb-server-10.0 python3-pip pip3 install mariadb&lt;/p&gt;
&lt;p&gt;除了数据库以外，还需要一个可以控制树莓派 GPIO引脚的 Python 库 GPIO ZERO（GPIO 引脚指图中这部分）：&lt;/p&gt;
&lt;p&gt;sudo apt install python3-gpiozero&lt;/p&gt;
&lt;p&gt;安装后，就可以运行主要的程序了，这部分的原理，是在传感器触发后，将风扇启动10分钟：&lt;/p&gt;
&lt;p&gt;FAN_ON_DURATION = 10 * 60 # 10 min outpin = LED(22) pir = MotionSensor(27, sample_rate=1) # I used the GPIO pin 27 while True: pir.wait_for_motion() outpin.on() time.sleep(FAN_ON_DURATION) outpin.off()&lt;/p&gt;
&lt;p&gt;同时，作者还希望能在整个系统启动时，自动运行程序，因此还写了另一个文件，将它命名为.service，确保它在 mysqld 启动后就能运行：&lt;/p&gt;
&lt;p&gt;[Unit] Description=Motion detection service for cat litter After=mysqld.service [Service] ExecStart=/usr/bin/python3 -u motion_detection.py WorkingDirectory=/home/pi/ StandardOutput=append:/home/pi/execution.log StandardError=append:/home/pi/execution.log Restart=alwaysUser=pi [Install] WantedBy=multi-user.target&lt;/p&gt;
&lt;p&gt;放进/etc/systemd/system文件夹，并允许跟随系统启动：&lt;/p&gt;
&lt;p&gt;sudo systemctl enable .service&lt;/p&gt;
&lt;p&gt;完成！现在软件部分也搞定了，可以愉快地给自家猫用上了，据作者表示，打开风扇时，即使站在旁边，也基本闻不到味道。&lt;/p&gt;
&lt;p&gt;解决更多铲屎官难题&lt;/p&gt;
&lt;p&gt;这个智能“无臭”猫砂盆的作者，目前在硅谷工作，自我介绍是一名“攻城狮”。&lt;/p&gt;
&lt;p&gt;事实上，作者自己家里就有猫，还是两只：&lt;/p&gt;
&lt;p&gt;除了这个猫砂盆，作者此前还自己做过家用版大数据处理平台、以及树莓派运行的GPS跟踪器。作者表示，下一步计划研发一个新的装置，阻止猫咪到桌上和自己抢饭。&lt;/p&gt;
&lt;p&gt;GitHub：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/xinixgit/rpi/tree/main/pi-cat-litter&quot;&gt;https://github.com/xinixgit/rpi/tree/main/pi-cat-litter&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://xinxindai.medium.com/build-a-ventilated-cat-litter-box-with-pi-zero-ce943d55b446&quot;&gt;https://xinxindai.medium.com/build-a-ventilated-cat-litter-box-with-pi-zero-ce943d55b446&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>虹科工业树莓派 VNC Viewer 远程桌面的配置</title><link>https://wuming.si/blog/3431/</link><guid isPermaLink="true">https://wuming.si/blog/3431/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一、什么是 VNC Viewer&lt;/p&gt;
&lt;p&gt;VNC(Virtual Network Console)是虚拟网络控制台的缩写，VNC 是在基于 UNIX 和 Linux 操作系统的免费的开源软件，远程控制能力强大，高效实用，其性能可以和 Windows 和 MAC 中的任何远程控制软件媲美。 在 Linux 中，VNC 包括以下四个命令：VNCserver，VNCviewer，VNCpasswd，和 VNCconnect。大多数情况下用户只需要其中的两个命令：VNCserver 和 VNCviewer。&lt;/p&gt;
&lt;p&gt;VNC viewer是一款远程控制的软件，一般用于远程解决电脑故障或软件调试。&lt;/p&gt;
&lt;p&gt;二、准备工作&lt;/p&gt;
&lt;p&gt;工业树莓派1台&lt;/p&gt;
&lt;p&gt;VNC Viewer（根据计算机环境选择性下载，下载链接：&lt;a href=&quot;https://www.realvnc.com/en/connect/download/viewer/%EF%BC%89&quot;&gt;https://www.realvnc.com/en/connect/download/viewer/）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;三、配置步骤&lt;/p&gt;
&lt;p&gt;1、SSH连接登录&lt;/p&gt;
&lt;p&gt;2、在终端输入以下命令进入配置界面：&lt;/p&gt;
&lt;p&gt;sudo raspi-config&lt;/p&gt;
&lt;p&gt;3、启动VNC服务（根据以下路径依次进入：Interfacing Options -&amp;gt; VNC -&amp;gt; Yes）&lt;/p&gt;
&lt;p&gt;4、返回配置页面首页，启动Desktop Autologin服务，该服务在不同核心模块的路径不同，如果是消费级树莓派请按照根据处理器型号对应到工业树莓派核心模块的路径。&lt;/p&gt;
&lt;p&gt;核心模块：RevPi Core3、RevPi Connect&lt;/p&gt;
&lt;p&gt;路径：Select Boot Options-&amp;gt;Desktop/CII-&amp;gt;Desktop Autologin&lt;/p&gt;
&lt;p&gt;核心模块：RevPi Core3+、RevPi Connect+&lt;/p&gt;
&lt;p&gt;路径：System Options-&amp;gt;Boot/Auto Login-&amp;gt;Desktop Autologin&lt;/p&gt;
&lt;p&gt;5、退出配置界面，选择重启，拔掉电源。&lt;/p&gt;
&lt;p&gt;6、给工业树莓派重新上电，重启工业树莓派，再进行SSH登录&lt;/p&gt;
&lt;p&gt;7、VNC Viewer远程访问，输入相应的IP地址、SHH登录账号和密码。&lt;/p&gt;
&lt;p&gt;8、完成VNC远程访问，可以根据需求进行配置更新。&lt;/p&gt;
&lt;p&gt;关于虹科&lt;/p&gt;
&lt;p&gt;虹科是一家在工业物联网IIoT行业经验超过3年的高科技公司，虹科与世界领域顶级公司包括EXOR、Eurotech、Unitronics、Matrikon、KUNBUS等合作，提供一系列先进的高端的工业4.0 HMI、高端边缘计算机、IoT开发方案、PLC与HMI一体机、OPC UA、工业级树莓派等解决方案。物联网事业部所有成员都受过国内外专业培训，并获得专业资格认证，平均3年+的技术经验和水平一致赢得客户极好口碑。&lt;/p&gt;
&lt;p&gt;我们积极参与行业协会的工作，为推广先进技术的普及做出了重要贡献。至今，虹科已经为行业内诸多用户提供从硬件到软件的不同方案，并参与和协助了众多OEM的设备研发和移植项目，以及终端用户的智能工厂和工业4.0升级改造项目。我们致力于为中国乃至全球的万物互联、智能制造贡献自己的力量。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>虹科工业树莓派：根据天气控制温室窗户开合</title><link>https://wuming.si/blog/3417/</link><guid isPermaLink="true">https://wuming.si/blog/3417/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;1.简介&lt;/p&gt;
&lt;p&gt;在本案例中，虹科工业树莓派RevPi Core3通过Node-RED编程获取并处理荷兰在线天气门户网站Buienradar的实时天气数据，并根据获取的天气信息对温室的窗户进行控制，从而实现温室的IoT改造。&lt;/p&gt;
&lt;p&gt;2.所需器件&lt;/p&gt;
&lt;p&gt;虹科RevPi Core3&lt;/p&gt;
&lt;p&gt;虹科RevPi DIO模块&lt;/p&gt;
&lt;p&gt;继电器&lt;/p&gt;
&lt;p&gt;3.实现方式&lt;/p&gt;
&lt;p&gt;在本案例中，温室的IoT改造主要集中在两个方面：温室窗户的自动开闭以及自动获取天气数据并分析。&lt;/p&gt;
&lt;p&gt;3.1温室窗户开闭控制&lt;/p&gt;
&lt;p&gt;温室的窗户通过行程长度为500 mm的线性驱动器执行打开和关闭动作。此线性驱动器配备有24V电机，可以通过虹科RevPi Core3和DIO扩展模块进行控制。&lt;/p&gt;
&lt;p&gt;所使用的执行器在冲程结束时自动停止电动机，无需其他传感器或开关。由于执行器需要3A的驱动电流，而DIO模块最多只能输出500 mA的电流，因此这里需要一个额外的继电器来控制24V电机的开闭。&lt;/p&gt;
&lt;p&gt;3.2天气数据的获取及分析&lt;/p&gt;
&lt;p&gt;为了实现根据天气情况自动打开和关闭窗户的功能，本案例采用可从网络上免费获取的天气数据。此类数据可以采用Node-RED获取。&lt;/p&gt;
&lt;p&gt;Node-RED具有用户友好的图形开发界面，可以通过添加HTTP请求节点以及功能节点从Buienradar的网站上获取预计降水量数据。&lt;/p&gt;
&lt;p&gt;通过链接 &lt;a href=&quot;https://gpsgadget.buienradar.nl/data/raintext?lat=52.08&amp;lon=5.47&quot;&gt;https://gpsgadget.buienradar.nl/data/raintext?lat=52.08&amp;amp;lon=5.47&lt;/a&gt; 可以文本形式请求未来两个小时的预期降水量。“lat”和“lon”之后的数字对应于需要降水数据的地方的GPS坐标。这些数据由Node-RED每五分钟检索一次。如果预计在15分钟内下雨，窗户将自动关闭。但是，如果在接下来的15分钟内仍保持干燥，则系统将再次打开窗户。&lt;/p&gt;
&lt;p&gt;4.更进一步&lt;/p&gt;
&lt;p&gt;当前，RevPi只区分“窗口打开”以及“窗口关闭”两个状态，要获取更多设置，可以进一步开发。另外，也可以连接温度和湿度传感器，准确获取当前天气状况，以进一步提高窗户自动开闭的准确性。还可以通过仪表盘节点进一步升级，通过web浏览器显示当前降雨预报以及窗口状态等。&lt;/p&gt;
&lt;p&gt;总而言之，虹科工业树莓派RevPi具有极大的灵活性和无限的潜力，并且软件硬件完全开源，您可以以最大的自由度在RevPi上进行开发。&lt;/p&gt;
&lt;p&gt;5.本文使用的模块&lt;/p&gt;
&lt;p&gt;RevPi Core 3具有定制的Raspbian系统，预先安装了RT补丁，配备Raspberry Pi计算模块3，内置Broadcom的多核处理器具有足够的能力来执行诸如图像处理之类的复杂任务，可以完成高性能要求的任务。&lt;/p&gt;
&lt;p&gt;处理器博通BCM2837&lt;/p&gt;
&lt;p&gt;核心数4&lt;/p&gt;
&lt;p&gt;时钟频率1.2 GHz&lt;/p&gt;
&lt;p&gt;内存1 GB&lt;/p&gt;
&lt;p&gt;eMMC闪存4GB&lt;/p&gt;
&lt;p&gt;RevPi Core 3使用了最先进的高效DC-DC转换器（总体效率大于80％），从而可以有效减少发热。模块不仅可以在20.4 V至28.8 V标准化的电源电压范围运行，也可以在10.2 V输入电压下运行，这意味着您甚至可以使用汽车电池或太阳能电池板作为电源。先进的保护电路可确保即使在输入电源线上受到大量电磁干扰的情况下，模块也能连续运行（前提是正确连接了功能性接地）。&lt;/p&gt;
&lt;p&gt;像Raspberry Pi一样，RevPi Core 3也配备了通用接口：&lt;/p&gt;
&lt;p&gt;1个RJ45以太网插座&lt;/p&gt;
&lt;p&gt;2个USB 2.0插座&lt;/p&gt;
&lt;p&gt;1个Micro HDMI插座&lt;/p&gt;
&lt;p&gt;1个Micro USB 2.0插槽&lt;/p&gt;
&lt;p&gt;2 个PiBridge（用于RevPi模块扩展）&lt;/p&gt;
&lt;p&gt;RevPi Core 3模块可以适应恶劣的工业环境：&lt;/p&gt;
&lt;p&gt;电源：12-24 VDC -15％/ +20％，max.10W&lt;/p&gt;
&lt;p&gt;工作温度：-40°C至55°C&lt;/p&gt;
&lt;p&gt;ESD保护：符合EN 61131-2和IEC 61000-6-2的4 kV / 8 kV&lt;/p&gt;
&lt;p&gt;浪涌/冲击测试：根据EN 61131-2和IEC 61000-6-2进行&lt;/p&gt;
&lt;p&gt;EMI测试：根据EN 61131-2和IEC 61000-6-2&lt;/p&gt;
&lt;p&gt;UL认证（UL文件编号E494534）&lt;/p&gt;
&lt;p&gt;I/O模块有三种版本，均可以通过PiBridge与Core模块直接相连。它们的前端都具有相同的28针I/O连接器。除了具有14个数字输入和14个数字输出的标准版本外，还有两个特殊版本，它们仅具有16个数字输入或16个数字输出。根据EN 61131-2的要求，所有版本都有抗干扰保护，并且可以在-40°C到55°C的温度范围，相对湿度高达93%的环境内运行。此外，所有数字I/O模块都具有UL认证（UL文件号E494534）。&lt;/p&gt;
&lt;p&gt;输入和输出的开关状态通过PiBridge与RevPi Core的中央处理映像进行周期性数据交换。除了切换状态之外，还可以周期性地传输可能的错误状态（诊断数据）和配置数据。RevPi DIO模块还配备了PWM（脉冲宽度调制）和计数器输入，可以分别为14个输出中的每个激活PWM功能，为14个输入通道中的每个通道激活计数器功能。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>透过树莓派为 Pico 添加 WiFi 功能</title><link>https://wuming.si/blog/3413/</link><guid isPermaLink="true">https://wuming.si/blog/3413/</guid><pubDate>Mon, 25 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;树莓派 Pico 是一款很棒的设备。它具有出彩的特性，拥有强大的 SDK 和文档，而且成本很低。美中不足的是，它没有网络功能。&lt;/p&gt;
&lt;p&gt;几款基于 RP2040 的 MCU 目前已上市。但是与 Pico 的价格相比，这几款开发板比较贵，而且它们也不具有网络功能。&lt;/p&gt;
&lt;p&gt;本文将向你介绍如何通过几个简单的步骤，使用树莓派为树莓派 Pico 添加网络功能。我们将使用 Telemetrix 树莓派 Pico 包和两个开源网络实用程序，无需额外的硬件。&lt;/p&gt;
&lt;p&gt;你只需按照下面列出的步骤操作即可。另外，你也可以使用 Python 脚本来控制和监视 Pico 的 GPIO 引脚。&lt;/p&gt;
&lt;p&gt;硬件部分&lt;/p&gt;
&lt;p&gt;树莓派 Pico&lt;/p&gt;
&lt;p&gt;支持 WiFi 或以太网的树莓派&lt;/p&gt;
&lt;p&gt;USB-A 转 Micro USB 数据线&lt;/p&gt;
&lt;p&gt;一台 Linux、macOS 或 Windows 10 PC&lt;/p&gt;
&lt;p&gt;用到的软件和在线服务&lt;/p&gt;
&lt;p&gt;Raspberry Pi OS&lt;/p&gt;
&lt;p&gt;Telmetrix4RpiPico（适用于 树莓派 Pico 的开源服务器固件）&lt;/p&gt;
&lt;p&gt;Telemetrix-rpi-pico（适用于 PC 的开源 Python 客户端 API）&lt;/p&gt;
&lt;p&gt;ser2net（适用于树莓派点免费开源串行端口网络代理）&lt;/p&gt;
&lt;p&gt;sosat（适用于 PC 的开源免费多功能中继工具）&lt;/p&gt;
&lt;p&gt;原理&lt;/p&gt;
&lt;p&gt;Telemetrix 项目允许你通过 USB/串行 将消息从 PC 中上传到 Pico 的固件中，从而监控和控制树莓派 Pico 的 GPIO。&lt;/p&gt;
&lt;p&gt;如果我们可以将串行数据流转换为 TCP/IP 流，通过 TCP 中继串行数据会怎样？&lt;/p&gt;
&lt;p&gt;没错，这正是我们将要做的，所以让我们现在就开始吧。&lt;/p&gt;
&lt;p&gt;第 1 步：给 Pico 上传 Telemetrix&lt;/p&gt;
&lt;p&gt;安装步骤很简单，只需按照 Telemetrix 用户指南上描述的步骤操作即可：&lt;/p&gt;
&lt;p&gt;1、下载 Telemetrix4RpiPico.uf2 文件。&lt;/p&gt;
&lt;p&gt;2、按住 Pico 开发板上的 BOOTSEL 按钮，然后将 Pico 插入树莓派或 PC 的 USB 接口，然后松开 BOOTSEL 按钮。&lt;/p&gt;
&lt;p&gt;3、Pico 会被识别为大容量存储设备。&lt;/p&gt;
&lt;p&gt;4、将下载的 MicroPython UF2 文件放入 RPI-RP2 卷上。你的 Pico 将自动重启，然后程序就开始运行了。&lt;/p&gt;
&lt;p&gt;第 2 步：更新 Raspberry Pi OS&lt;/p&gt;
&lt;p&gt;在树莓派上打开一个终端窗口，并执行以下命令：&lt;/p&gt;
&lt;p&gt;sudo apt-get update&lt;/p&gt;
&lt;p&gt;然后运行命令（如果系统已经是最新的话，可选，运行这个命令会很耗时）：&lt;/p&gt;
&lt;p&gt;sudo apt-get upgrade&lt;/p&gt;
&lt;p&gt;完成后，可以继续下面的步骤。&lt;/p&gt;
&lt;p&gt;第 3 步：查找树莓派的 IP 地址&lt;/p&gt;
&lt;p&gt;打开终端窗口并输入以下命令：&lt;/p&gt;
&lt;p&gt;ifconfig&lt;/p&gt;
&lt;p&gt;eth0是为以太网分配的 IP 地址，wlan0是为 WiFi 分配的 IP 地址。由于我用网线把树莓派连接到路由器，并将树莓派连接到 WiFi，因此这两个条目都分配了 IP 地址。当然，我可以选择使用这两个地址中的任何一个，但是由于我想在本演示中使用 WiFi，所以我使用了wlan0地址192.168.2.126。&lt;/p&gt;
&lt;p&gt;请记下树莓派列出的 IP 地址，我们在后面的步骤中会用到。&lt;/p&gt;
&lt;p&gt;第 4 步：在树莓派上安装配置 ser2net&lt;/p&gt;
&lt;p&gt;ser2net 是一个串行转网络的代理。它将 IP 数据流转换为串行数据，并将数据重定向到特定的串行端口。它还把来自串行端口的数据转换为 TCP/IP 流。ser2net 作为守护进程运行，安装之后，它会在每次启动树莓派时自动运行。&lt;/p&gt;
&lt;p&gt;要安装 ser2net，请在终端中输入以下命令：&lt;/p&gt;
&lt;p&gt;sudo apt-get install ser2net&lt;/p&gt;
&lt;p&gt;接下来，我们需要通过修改配置文件来配置 ser2net。首先，在终端中输入以下命令来停止守护进程：&lt;/p&gt;
&lt;p&gt;sudo systemctl stop ser2net&lt;/p&gt;
&lt;p&gt;然后，在终端中输入以下命令来修改 /etc/ser2net.config 文件：&lt;/p&gt;
&lt;p&gt;sudo nano /etc/ser2net.config&lt;/p&gt;
&lt;p&gt;请将以下内容添加到文件的最后：&lt;/p&gt;
&lt;p&gt;3333:raw:0:/dev/ttyACM0:115200,remctl&lt;/p&gt;
&lt;p&gt;保存文件并退出编辑器。&lt;/p&gt;
&lt;p&gt;添加的这行内容告诉 ser2net 将在 IP 端口 3333 上接收到的 IP 数据重定向到 COM 端口 /dev/ttyACM0 上的串行设备。要了解 ser2net 的更多信息，请输入以下内容来参考其手册页：&lt;/p&gt;
&lt;p&gt;man ser2net&lt;/p&gt;
&lt;p&gt;现在，让我们重启 ser2net 守护进程。在终端窗口中，输入：&lt;/p&gt;
&lt;p&gt;sudo systemctl start ser2net&lt;/p&gt;
&lt;p&gt;第 5 步：用 USB 数据线连接 Pico 和树莓派&lt;/p&gt;
&lt;p&gt;连接两个设备之间的数据线后，然后输入：&lt;/p&gt;
&lt;p&gt;ls -l /dev/ttyA*&lt;/p&gt;
&lt;p&gt;你应该会看到列出的 ttyACM0。&lt;/p&gt;
&lt;p&gt;ls -l /dev/ttyA* crw-rw—- 1 root dialout 166, 0 Jun 12 19:17 /dev/ttyACM0 crw-rw—- 1 root dialout 204, 64 Jun 12 19:40 /dev/ttyAMA0&lt;/p&gt;
&lt;p&gt;第 6 步：你的客户端计算机是否基于 Windows？&lt;/p&gt;
&lt;p&gt;如果你使用 Linux 或 macOS，请跳转到第 7 步。&lt;/p&gt;
&lt;p&gt;如果你使用 Windows，你需要在计算机上安装适用于 Linux 的 Windows 子系统 (WSL)。&lt;/p&gt;
&lt;p&gt;WSL 可以让开发人员直接在 Windows 上运行 GNU/Linux 环境——包括大多数命令行工具、实用工具和应用程序，免去了使用传统虚拟机或配置双引导的麻烦。&lt;/p&gt;
&lt;p&gt;要安装 WSL，请按照此教程进行操作。在选择 Linux 版本时，请选择 Ubuntu 20.04。&lt;/p&gt;
&lt;p&gt;安装后，请转到第 7 步。使用 WSL 终端窗口执行所有后续步骤。&lt;/p&gt;
&lt;p&gt;第 7 步：安装 Socat&lt;/p&gt;
&lt;p&gt;Socat 是基于命令行的实用程序，它建立两个双向字节流并在它们之间传输数据。Socat 是树莓派服务器计算机上使用的 ser2net 的对等点。我们将把它安装在客户端 PC 上。&lt;/p&gt;
&lt;p&gt;在客户端计算机的终端窗口中，键入：&lt;/p&gt;
&lt;p&gt;sudo apt-get install socat&lt;/p&gt;
&lt;p&gt;第 8 步：安装 Telemetrix 客户端&lt;/p&gt;
&lt;p&gt;请检查你的计算机上是否安装了 Python3。&lt;/p&gt;
&lt;p&gt;python3 -V&lt;/p&gt;
&lt;p&gt;如果没有，请先安装。&lt;/p&gt;
&lt;p&gt;接下来，通过输入以下命令来检查是否已安装 pip3：&lt;/p&gt;
&lt;p&gt;pip3 -V&lt;/p&gt;
&lt;p&gt;pip3 的版本会显示在终端中。如果显示空行或有关安装 pip3 的错误消息，请输入以下命令来安装 pip3：&lt;/p&gt;
&lt;p&gt;sudo apt-get install python3-pip&lt;/p&gt;
&lt;p&gt;我们现在准备从 PyPi 安装 Telemetrix。请在终端窗口中，输入：&lt;/p&gt;
&lt;p&gt;sudo pip3 install telemetrix-rpi-pico&lt;/p&gt;
&lt;p&gt;第 9 步：下载并修改 Telemetrix 示例脚本&lt;/p&gt;
&lt;p&gt;访问 GitHub 项目：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/MrYsLab/telemetrix-rpi-pico&quot;&gt;https://github.com/MrYsLab/telemetrix-rpi-pico&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下载源码。&lt;/p&gt;
&lt;p&gt;保存并解压缩 .zip 文件。解压后进入examples目录，编辑 blink.py。我们之所以使用这个例子，是因为它不需要任何额外的硬件。&lt;/p&gt;
&lt;p&gt;请将文件中的以下几行&lt;/p&gt;
&lt;h1&gt;Create a Telemetrix instance. board = telemetrix_rpi_pico.TelemetrixRpiPico()&lt;/h1&gt;
&lt;p&gt;更改为：&lt;/p&gt;
&lt;h1&gt;Create a Telemetrix instance. board = telemetrix_rpi_pico.TelemetrixRpiPico(com_port=’/home/afy/MyProxySerialPort’)&lt;/h1&gt;
&lt;p&gt;修改后的行包含在一行代码中。然后保存文件。&lt;/p&gt;
&lt;p&gt;正如我们将在第 10 步中看到的那样，Socat 将创建一个名为 /home/afy/MyProxySerialPort 的虚拟 com 端口。&lt;/p&gt;
&lt;p&gt;在这里，我们告诉 Python 脚本使用该 COM 端口。&lt;/p&gt;
&lt;p&gt;第 10 步：启动 Socat 并运行 Blink 示例&lt;/p&gt;
&lt;p&gt;在终端窗口中，复制并粘贴以下命令。在执行命令之前，请把其中的 IP 地址改为你的树莓派的 IP 地址。&lt;/p&gt;
&lt;p&gt;socat pty,link=$HOME/MyProxySerialPort,waitslave tcp:192.168.2.126:3333,forever,reuseaddr,keepalive &amp;amp;&lt;/p&gt;
&lt;p&gt;该命令包含在一行中。在该行的末尾，&amp;amp; 符号使命令在后台运行，以便你可以在同一终端窗口中运行 blink.py 脚本。&lt;/p&gt;
&lt;p&gt;现在运行修改后的 blink.py，你会看到 Pico 上的 LED 开始闪烁。&lt;/p&gt;
&lt;p&gt;python3 blink.py&lt;/p&gt;
&lt;p&gt;总结&lt;/p&gt;
&lt;p&gt;恭喜你，完成了！现在，你可以编写自己的程序了。如果你愿意，也可以运行一些其他的 demo。请记住一定要像第 9 步中那样指定 com_port 参数。&lt;/p&gt;
&lt;p&gt;另外，如果你想编写自己的脚本，请参阅 Telemetrix API 和用户指南。&lt;/p&gt;
&lt;p&gt;via&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>優やさしい詩</title><link>https://wuming.si/blog/1338/</link><guid isPermaLink="true">https://wuming.si/blog/1338/</guid><pubDate>Fri, 16 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;優やさしい詩うた – RSP&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;広ひろい世界せかいでたったひとつの&lt;/li&gt;
&lt;li&gt;在這廣闊世界僅有的一首&lt;/li&gt;
&lt;li&gt;小ちいさなあたしの願ねがい詩うた&lt;/li&gt;
&lt;li&gt;屬於我小小的祈願詩歌&lt;/li&gt;
&lt;li&gt;笑わらって欲ほしいどこか遠とおくの&lt;/li&gt;
&lt;li&gt;想要微笑好像又漸行漸遠&lt;/li&gt;
&lt;li&gt;名前なまえも知しらないあなたにも&lt;/li&gt;
&lt;li&gt;連你的名字都不曾知曉&lt;/li&gt;
&lt;li&gt;うまく言いえたら辛つらくならない&lt;/li&gt;
&lt;li&gt;能好好表達的話就不會心酸不已&lt;/li&gt;
&lt;li&gt;うまく泣なけたなら苦くるしくもない&lt;/li&gt;
&lt;li&gt;能好好哭出來的話就不會飽受痛苦&lt;/li&gt;
&lt;li&gt;うつむいたままそんなあなたに 安やすらぎのひとつ与あたえられたら…&lt;/li&gt;
&lt;li&gt;低下頭 若是能授予這樣的你一份安逸的話…&lt;/li&gt;
&lt;li&gt;空そらはいつでも誰だれの上うえでも&lt;/li&gt;
&lt;li&gt;就算天空總是一直在我們上方&lt;/li&gt;
&lt;li&gt;青色あおいろしてた見上みあげてごらん&lt;/li&gt;
&lt;li&gt;抬頭觀賞那抹蔚藍色彩&lt;/li&gt;
&lt;li&gt;優やさしい詩うたを唄うたっていたい&lt;/li&gt;
&lt;li&gt;想要歌唱那首温柔的歌謠&lt;/li&gt;
&lt;li&gt;弱よわい人ひとにも強つよい人ひとにも&lt;/li&gt;
&lt;li&gt;不管是對柔弱的人 還是對堅强的人&lt;/li&gt;
&lt;li&gt;嘘うそやいいわけ心こころ隠かくして&lt;/li&gt;
&lt;li&gt;謊言或藉口隐藏心中&lt;/li&gt;
&lt;li&gt;傷付きずつくことをずっと避さけていた&lt;/li&gt;
&lt;li&gt;一直都避免受傷&lt;/li&gt;
&lt;li&gt;隣となりに座すわるほんとの自分じぶん&lt;/li&gt;
&lt;li&gt;一旁坐著真正的自己&lt;/li&gt;
&lt;li&gt;あなたはそれに気付きづかなくて&lt;/li&gt;
&lt;li&gt;而你也没有發現到這一點&lt;/li&gt;
&lt;li&gt;人ひとの痛いたみを感かんじた時ときも&lt;/li&gt;
&lt;li&gt;當感覺到為人之痛的時候&lt;/li&gt;
&lt;li&gt;幸しあわせな人ひとを見みつけた時ときも&lt;/li&gt;
&lt;li&gt;當發現幸福的人的時候&lt;/li&gt;
&lt;li&gt;同おなじ気持きもちで涙なみだしたのも&lt;/li&gt;
&lt;li&gt;也都帶著同樣地心情落下淚水&lt;/li&gt;
&lt;li&gt;あなたの心こころ忘わすれないよう&lt;/li&gt;
&lt;li&gt;也不能忘却你的真心&lt;/li&gt;
&lt;li&gt;空そらはいつでも誰だれの上うえでも&lt;/li&gt;
&lt;li&gt;就算天空總是一直在我們上方&lt;/li&gt;
&lt;li&gt;青色あおいろしてた見上みあげてごらん&lt;/li&gt;
&lt;li&gt;抬頭觀賞那抹蔚藍色彩&lt;/li&gt;
&lt;li&gt;優やさしい詩うたを唄うたっていたい&lt;/li&gt;
&lt;li&gt;想要歌唱那首温柔的歌謠&lt;/li&gt;
&lt;li&gt;弱よわい人ひとにも強つよい人ひとにも&lt;/li&gt;
&lt;li&gt;不管是對柔弱的人 還是對堅强的人&lt;/li&gt;
&lt;li&gt;空そらはいつでも誰だれの上うえでも&lt;/li&gt;
&lt;li&gt;就算天空總是一直在我們上方&lt;/li&gt;
&lt;li&gt;青色あおいろしてたそれが嬉うれしい&lt;/li&gt;
&lt;li&gt;那抹蔚藍讓人歡心喜悦&lt;/li&gt;
&lt;li&gt;優やさしい詩うたを唄うたっていたい&lt;/li&gt;
&lt;li&gt;想要歌唱那首温柔的歌謠&lt;/li&gt;
&lt;li&gt;弱よわい人ひとにも強つよい人ひとにも&lt;/li&gt;
&lt;li&gt;不管是對柔弱的人 還是對堅强的人&lt;/li&gt;
&lt;li&gt;どこで生うまれて どんな色いろ持もって&lt;/li&gt;
&lt;li&gt;在某處被降生下來 不管帶著什麼色彩&lt;/li&gt;
&lt;li&gt;たったひとりのあなたいるから&lt;/li&gt;
&lt;li&gt;你是只此唯一的存在&lt;/li&gt;
&lt;li&gt;今日きょうも生いきてる 人ひとがいること&lt;/li&gt;
&lt;li&gt;今天也安然活著 生命的存在&lt;/li&gt;
&lt;li&gt;忘わすれないでよ 強つよいあなたへ&lt;/li&gt;
&lt;li&gt;别忘記了哦 獻給堅强的你&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;歌词来源：&lt;a href=&quot;http://blog.xuite.net/l42934293/blog/213750290&quot;&gt;http://blog.xuite.net/l42934293/blog/213750290&lt;/a&gt; 及 &lt;a href=&quot;https://www.jpmarumaru.com/tw/JPSongPlay-2781.html&quot;&gt;https://www.jpmarumaru.com/tw/JPSongPlay-2781.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;距离上一篇博文已经3年多了，一直没有静下心来写点东西的机会。&lt;/p&gt;
&lt;p&gt;上一篇博文还在而立，这篇已经到了不惑了。终于还是活成了自己讨厌的人。在年轻的时候觉得以自己方式活着就好，到了临近中年才发现，终究还是要碰壁的，这确实够讽刺的。&lt;/p&gt;
&lt;p&gt;但是生活还是要继续，面对现实，一生都要抱着学习的态度提高自己，保持自己的竞争力。&lt;/p&gt;
</content:encoded><category>Life</category></item><item><title>Ubuntu 16.04 升级OPENSSL</title><link>https://wuming.si/blog/1327/</link><guid isPermaLink="true">https://wuming.si/blog/1327/</guid><pubDate>Wed, 07 Apr 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Ubuntu 16.04 系统默认带的是1.0.2的openssl，而openssl暴露的漏洞越来越多，而ubuntu似乎也不打算在升级包中包含它，只能手动升级它了。&lt;/p&gt;
&lt;p&gt;首先下载最新的openssl源码包&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;wget &lt;a href=&quot;https://www.openssl.org/source/openssl-1.1.1k.tar.gz&quot;&gt;https://www.openssl.org/source/openssl-1.1.1k.tar.gz&lt;/a&gt;&lt;br&gt;tar xvfz openssl-1.1.1k.tar.gz&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;cd openssl-1.1.1k/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;./config&lt;br&gt;make&lt;br&gt;make test&lt;br&gt;make install&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;创建openssl软链接&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mv /usr/bin/openssl /root/&lt;br&gt;ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;链接加密缺失的so文件&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ln -s /usr/local/lib/libssl.so.1.1 /usr/lib/&lt;br&gt;ln -s /usr/local/lib/libcrypto.so.1.1 /usr/lib/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;来试试看效果
openssl version
OpenSSL 1.1.1k  25 Mar 2021
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Technology</category></item><item><title>以太坊入门（三）用web3j进行以太转账及代币转账</title><link>https://wuming.si/blog/1316/</link><guid isPermaLink="true">https://wuming.si/blog/1316/</guid><pubDate>Mon, 01 Jul 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上章讲到账户的查询，本章讲述账户转账。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以太坊转账 &lt;code&gt;We3j web3j = Web3j.build(new HttpService(ConstantLibs.WEB3_ADDRESS)); Credentials credentials = WalletTool.loadCredentials(fromAddress); EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount( fromAddress, DefaultBlockParameterName.LATEST).sendAsync().get(); BigInteger nonce = ethGetTransactionCount.getTransactionCount(); RawTransaction rawTransaction = RawTransaction.createEtherTransaction( nonce, Convert.toWei(&amp;quot;18&amp;quot;, Convert.Unit.GWEI).toBigInteger(), Convert.toWei(&amp;quot;45000&amp;quot;, Convert.Unit.WEI).toBigInteger(), toAddress, new BigInteger(amount)); byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials); String hexValue = Numeric.toHexString(signedMessage); EthSendTransaction ethSendTransaction = web3.ethSendRawTransaction(hexValue).sendAsync().get(); if (ethSendTransaction.hasError()) { log.info(&amp;quot;transfer error:&amp;quot;, ethSendTransaction.getError().getMessage()); } else { String transactionHash = ethSendTransaction.getTransactionHash(); log.info(&amp;quot;Transfer transactionHash:&amp;quot; + transactionHash); }&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;以太坊代币转账 &lt;code&gt;Web3j web3j = Web3j.build(new HttpService(ConstantLibs.WEB3_ADDRESS)); Credentials credentials = WalletTool.loadCredentials(fromAddress); EthGetTransactionCount ethGetTransactionCount = web3j.ethGetTransactionCount( fromAddress, DefaultBlockParameterName.LATEST).sendAsync().get(); BigInteger nonce = ethGetTransactionCount.getTransactionCount(); Function function = new Function( &amp;quot;transfer&amp;quot;, Arrays.asList(new Address(toAddress), new Uint256(new BigInteger(amount))), Arrays.asList(new TypeReference&amp;lt;Type&amp;gt;() { })); String encodedFunction = FunctionEncoder.encode(function); RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, Convert.toWei(&amp;quot;18&amp;quot;, Convert.Unit.GWEI).toBigInteger(), Convert.toWei(&amp;quot;100000&amp;quot;, Convert.Unit.WEI).toBigInteger(), contractAddress, encodedFunction); byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials); String hexValue = Numeric.toHexString(signedMessage); log.debug(&amp;quot;transfer hexValue:&amp;quot; + hexValue); EthSendTransaction ethSendTransaction = web3.ethSendRawTransaction(hexValue).sendAsync().get(); if (ethSendTransaction.hasError()) { log.info(&amp;quot;transfer error:&amp;quot;, ethSendTransaction.getError().getMessage()); } else { String transactionHash = ethSendTransaction.getTransactionHash(); log.info(&amp;quot;Transfer transactionHash:&amp;quot; + transactionHash); }&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代币转账和以太转账的区别在于，to地址是合约地址，而input是有三部分数据构成：transfer方法的哈希+收款人的地址+转账金额。此处比较难理解的正是Function部分，设置好参数以后，调用rawTransaction就可以了。&lt;/p&gt;
&lt;p&gt;本站始发，转载于 &lt;a href=&quot;https://www.jianshu.com/p/8ae984e6bafc&quot;&gt;https://www.jianshu.com/p/8ae984e6bafc&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>以太坊入门（二）用web3j进行以太查询及通证查询</title><link>https://wuming.si/blog/1314/</link><guid isPermaLink="true">https://wuming.si/blog/1314/</guid><pubDate>Fri, 01 Mar 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;以太坊的开发，基本都是go语言和nodejs的天下，web3j出现给java开发人员提供了很大的便利。本文会对一些以太坊的基本操作用java语言来实现。&lt;/p&gt;
&lt;p&gt;本章会讲述通过web3j进行账户余额的查询。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;以太余额查询&lt;br&gt;以太的余额查询比较简单，直接调用web3j的ethGetBalance就可以。 &lt;code&gt;Web3j web3j = Web3j.build(new HttpService(ConstantLibs.WEB3_ADDRESS)); EthGetBalance ethGetBalance = web3j.ethGetBalance( address, DefaultBlockParameterName.LATEST).sendAsync().get(); BigInteger balance = ethGetBalance.getBalance();&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通证的余额查询&lt;br&gt;代币的查询就比较复杂一些，研究了好长时间，最后发现每个代币合约都会实现balanceOf方法，可以通过这个方法来查询通证的余额。&lt;code&gt; Web3j web3j = Web3j.build(new HttpService(ConstantLibs.WEB3_ADDRESS)); Function function = new Function( &amp;quot;balanceOf&amp;quot;, Arrays.asList(new Address(address)), // Solidity Types in smart contract functions Arrays.asList(new TypeReference&amp;lt;Type&amp;gt;() { })); String encodedFunction = FunctionEncoder.encode(function); org.web3j.protocol.core.methods.response.EthCall response = web3j.ethCall( org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction(address, contract, encodedFunction), DefaultBlockParameterName.LATEST) .sendAsync().get(); String returnValue = response.getValue(); //返回16进制余额 returnValue = returnValue.substring(2); BigInteger balance = new BigInteger(returnValue, 16);&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本站始发，转载于&lt;a href=&quot;https://www.jianshu.com/p/0f3e65622d65&quot;&gt;https://www.jianshu.com/p/0f3e65622d65&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>以太坊入门（一）账户和nonce的关系</title><link>https://wuming.si/blog/1308/</link><guid isPermaLink="true">https://wuming.si/blog/1308/</guid><pubDate>Mon, 11 Jun 2018 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;&lt;strong&gt;什么是nonce？&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;在以太坊的交易数据中，大家都可以看到一个数字，nonce，从0开始，一直向上递增，这个代表什么意思呢？&lt;/p&gt;
&lt;p&gt;以太坊所有的交易都是基于account的，不同于基于utxo的比特币，因此需要对每次交易都按顺序记录，nonce值就是这个顺序，主要用来防止重放攻击。&lt;/p&gt;
&lt;p&gt;每&lt;strong&gt;发起&lt;/strong&gt; 一笔交易，nonce就会加一。对于&lt;strong&gt;发起&lt;/strong&gt; 的解释：&lt;/p&gt;
&lt;p&gt;1.外部账户每发送一笔交易；&lt;/p&gt;
&lt;p&gt;2.合约账户每创建一个合约&lt;/p&gt;
&lt;p&gt;而转入交易、合约调用其他合约等属于内部调用，因此nonce值不变。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;如何使用nonce&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;发起转账或者创建合约的时候，通过web3从以太坊网络查询当前的nonce（ethGetTransactionCount）值，使用此值作为当前交易的nonce值，发送到以太坊网络即可。&lt;/p&gt;
&lt;h2&gt;&lt;strong&gt;nonce使用的几条规则&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;1. 当nonce太小（小于当前的nonce值），交易会被直接拒绝，Transactions with too low a nonce get immediately rejected；&lt;/p&gt;
&lt;p&gt;2. 当nonce太大，大于当前nonce，交易会一直处于队列之中，Transactions with too high a nonce get placed in the transaction pool queue；&lt;/p&gt;
&lt;p&gt;3.当发送一个比较大的nonce值，然后补齐开始nonce到那个值之间的nonce，那么交易依旧可以被执行，If transactions with nonces that fill the gap between the last valid nonce and the too high nonce are sent and the nonce sequence is complete, all the transactions in the sequence will get processed and mined.&lt;/p&gt;
&lt;p&gt;4. 交易队列只保存最多64个从同一个账户发出的交易，The transaction pool queue will only hold a maximum of 64 transactions with the same From:address with nonces out of sequence. 也就是说，如果要批量转账，同一节点不要发出超过64笔交易。&lt;/p&gt;
&lt;p&gt;5.当某节点queue中还有交易，但此时停止geth客户端，queue中的交易会被清除掉，When the geth instances are shut down and restarted, transactions in the transaction pool queue disappear.&lt;/p&gt;
&lt;p&gt;6.当前nonce合适，但是账户余额不足时，会被以太坊拒绝；&lt;/p&gt;
&lt;p&gt;7.如果发起一笔交易，但是因为gwei比较低或者网络比较忙的时候，该交易还没矿工挖出，可以通过使用相同的nonce和较高的gas费用，从而“覆盖”前一笔交易；&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>树莓派充当airplay接收器</title><link>https://wuming.si/blog/1305/</link><guid isPermaLink="true">https://wuming.si/blog/1305/</guid><pubDate>Wed, 30 May 2018 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;iOS升级以后，新版本经常不兼容xbian之类自带的airplay功能，目前来Shairport的支持还是不错的。我在新的树梅派3b+上编译了一下，支持ios 11.0.4版本。&lt;/p&gt;
&lt;p&gt;首先升级树莓派&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
sudo apt-get upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后安装依赖包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install autoconf automake avahi-daemon build-essential git libasound2-dev libavahi-client-dev libconfig-dev libdaemon-dev libpopt-dev libssl-dev libtool xmltoman
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载shairport源码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/mikebrady/shairport-sync.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd shairport-sync
autoreconf -i -f
./configure --with-alsa --with-avahi --with-ssl=openssl --with-systemd --with-metadata
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置为自动启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable shairport-sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo service shairport-sync start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以在手机里面找到airplay设备，raspberrypi了。&lt;/p&gt;
</content:encoded><category>Raspberry Pi</category><category>Technology</category></item><item><title>Nginx反向代理跨域option问题解决</title><link>https://wuming.si/blog/1299/</link><guid isPermaLink="true">https://wuming.si/blog/1299/</guid><pubDate>Mon, 26 Mar 2018 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;CORS on Nginx&lt;/h1&gt;
&lt;p&gt;The following Nginx configuration enables CORS, with support for preflight requests.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#
# Wide-open CORS config for nginx
#
location / {
     if ($request_method = &amp;#39;OPTIONS&amp;#39;) {
        add_header &amp;#39;Access-Control-Allow-Origin&amp;#39; &amp;#39;*&amp;#39;;
        add_header &amp;#39;Access-Control-Allow-Methods&amp;#39; &amp;#39;GET, POST, OPTIONS&amp;#39;;
        #
        # Custom headers and headers various browsers *should* be OK with but aren&amp;#39;t
        #
        add_header &amp;#39;Access-Control-Allow-Headers&amp;#39; &amp;#39;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&amp;#39;;
        #
        # Tell client that this pre-flight info is valid for 20 days
        #
        add_header &amp;#39;Access-Control-Max-Age&amp;#39; 1728000;
        add_header &amp;#39;Content-Type&amp;#39; &amp;#39;text/plain; charset=utf-8&amp;#39;;
        add_header &amp;#39;Content-Length&amp;#39; 0;
        return 204;
     }
     if ($request_method = &amp;#39;POST&amp;#39;) {
        add_header &amp;#39;Access-Control-Allow-Origin&amp;#39; &amp;#39;*&amp;#39;;
        add_header &amp;#39;Access-Control-Allow-Methods&amp;#39; &amp;#39;GET, POST, OPTIONS&amp;#39;;
        add_header &amp;#39;Access-Control-Allow-Headers&amp;#39; &amp;#39;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&amp;#39;;
        add_header &amp;#39;Access-Control-Expose-Headers&amp;#39; &amp;#39;Content-Length,Content-Range&amp;#39;;
     }
     if ($request_method = &amp;#39;GET&amp;#39;) {
        add_header &amp;#39;Access-Control-Allow-Origin&amp;#39; &amp;#39;*&amp;#39;;
        add_header &amp;#39;Access-Control-Allow-Methods&amp;#39; &amp;#39;GET, POST, OPTIONS&amp;#39;;
        add_header &amp;#39;Access-Control-Allow-Headers&amp;#39; &amp;#39;DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range&amp;#39;;
        add_header &amp;#39;Access-Control-Expose-Headers&amp;#39; &amp;#39;Content-Length,Content-Range&amp;#39;;
     }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First is tricking Nginx that a 405 status is actually a 200 OK and then proxy_pass it to your HAProxy like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error_page 405 =200 @405;
location @405 {
    root /;
    proxy_pass http://yourproxy:8080;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second solution is just to catch the OPTIONS request and build a response for those requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;location / {
    if ($request_method = OPTIONS ) {
        add_header Content-Length 0;
        add_header Content-Type text/plain;
        return 200;
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>Technology</category></item><item><title>春风十里</title><link>https://wuming.si/blog/1287/</link><guid isPermaLink="true">https://wuming.si/blog/1287/</guid><pubDate>Wed, 03 Jan 2018 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;春风十里&lt;br&gt; 作曲作词 倍倍&lt;/p&gt;
&lt;p&gt;我在二环的里边 想着你&lt;br&gt; 你在远方的山上 春风十里&lt;br&gt; 今天的风吹向你 下了雨&lt;br&gt; 我说所有的酒 都不如你&lt;/p&gt;
&lt;p&gt;我在鼓楼的夜色中 为你唱花香自来&lt;br&gt; 在别处 沉默相遇和期待&lt;br&gt; 飞机飞过 车水马龙的城市&lt;br&gt; 千里之外 不离开&lt;/p&gt;
&lt;p&gt;把所有的春天 都揉进了一个清晨&lt;br&gt; 把所有停不下来的言语变成秘密 关上了门&lt;br&gt; 莫名的情愫啊 请问 谁来将它带走呢&lt;br&gt; 只好把岁月化成歌 留在山河&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://s3.wo.ai/upload/mp3/2661437940.mp3&quot;&gt;http://s3.wo.ai/upload/mp3/2661437940.mp3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;时间过得很快，2017年就这么过去了。&lt;/p&gt;
&lt;p&gt;这两天一直在循环播放《春风十里》这首歌。最喜欢的一句就是：我说所有的酒，都不如你，千里之外 不离开。&lt;/p&gt;
&lt;p&gt;酒，对于每个人来说，都是不同的东西，有喜、有恶。正如小站的名字，酒趣琴音，达是酒中趣，琴上偶然音。前两天朋友圈有人晒出在米国时候的照片，一想已经毕业了七年。三五好友凑一起，小酌，谈谈古，论论今，甚是怀念。当年春田还是留下了太多的身影，十一叔，River，Candy，十三姨，老姑…回国以后，各奔东西，即使是同一城市，也难得碰一次。虽然联系很少，偶尔在异地出差，聚在一起，总还是有说不完的话题。从城市各个角落聚拢，陪吃，陪喝，陪聊，还有陪压马路，当今的快节奏，每个人都很忙，花时间陪伴，也只有真切的同学之间才可能做到了。鸿鹄也好，鹡鸰也好，缘分让大家聚在春田，也让大家留下了这份纯真。&lt;/p&gt;
&lt;p&gt;对我来说2017，算是比较动荡的一年，在某个字母公司待了8年，终于下定决心离开了，跑到另外一座城市，迎接未知的挑战。虽然和来之前的初衷有很大的偏差，人性好的、坏的都见识和领教过，至少不算好也不算坏，这一年就这么过去了。生活不只有眼前苟且，还有诗远方。要用眼睛看到诗和远方，还要尝试把诗与远方变成生活，2018谈谈理想吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wuming.si/images/2018/01/WechatIMG548-225x300.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;所有的酒，都不如你；千里之外，不曾离开。&lt;/p&gt;
</content:encoded><category>Life</category></item><item><title>区块链翻译文章系列（一）初学者指南 Blockchain For Dummies: A Beginner’s Guide</title><link>https://wuming.si/blog/1278/</link><guid isPermaLink="true">https://wuming.si/blog/1278/</guid><pubDate>Mon, 20 Nov 2017 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Blockchain For Dummies: A Beginner’s Guide&lt;/h2&gt;
&lt;p&gt;Blockchain: the single most confusing term since Bitcoin. Everyone has a vague idea of what it does. It’s either the ultimate evolution of financial technologies, or a silly fad that can be summed up in the disconcerting phrase: “dogechain.” In reality, major companies around the world have already shown favor to the burgeoning money exchange system and it may become harder and harder to stay away from the financial dark art.&lt;br&gt;In reality, it is all relatively easy to understand. The Blockchain is a public ledger where transactions are recorded and confirmed anonymously. It’s a record of events that is shared between many parties. More importantly, once information is entered, it cannot be altered. So, if the blockchain is the public record, what is being recorded? What are all of these “transactions”?&lt;/p&gt;
&lt;p&gt;Cryptocurrencies, like bitcoin, are currencies that exist solely in digital. There are no physical golden coins with a big “B” on them. Moreover, owning these non-real coins entails a new idea of “ownership.” You don’t literally have it in your hands, or even in your bank account, but you have the ability to transfer “ownership” to someone else simply by creating a record in the blockchain. Rather than using bills, your transfer is pure data.&lt;/p&gt;
&lt;p&gt;Where exactly is this chain located? Due to the open nature of cryptocurrencies, and the importance of the public having access to other blocks, the blockchain isn’t located on just one guy’s large computer. For example, the bitcoin blockchain is actually managed by distributed nodes. These nodes all have a copy of the entire blockchain. Nodes will forever come and go, synchronizing their own copies of the chain with those of other users. By distributing copies and access, the chain can’t simply “go down,” or disappear. It’s a decentralized system that is both sturdy and secure.&lt;/p&gt;
&lt;p&gt;All of your dogecoins are in a row, but what do you do with them? Whether you’re using them IRL or online, the blockchain makes it happen. There are many reasons people are falling in love with cryptocurrencies: it’s anonymous, decentralized, and there are no fees or third parties trying to grab a percentage. However, if there were absolutely no regulations in place, the new currency would quickly become a greedfest of users trying to screw each other over. The public nature of the blockchain means that anyone can check it. It is effectively anonymous, yet public, simultaneously, and it is in the best interest of users if it remains so.&lt;br&gt;You can accept and trade coins, or you can mine for them. Miners can spend thousands of dollars on the right equipment just to mine coin. But what do they really do? What miners do is quite similar to real-world miners in that they are actively looking for something. Their computer repeatedly works through complex calculations to find a very specific answer.&lt;/p&gt;
&lt;p&gt;Miners solve problems, but how in the world is that helpful? Short story, miners are actually verifying that transactions posted by other users are legitimate, and the numbers all add up. Long story…&lt;/p&gt;
&lt;p&gt;Miners collect transactions and put them into a single block. A block generally contains four pieces of information: a reference to the previous block, a summary of included transaction, a time stamp, and Proof of Work that went into creating the secure block. The blocks are strung together into a chain—a fluid chain that does not allow for any inconsistencies; this means there are no “bad cheques” in the system, and transactions entered are necessarily valid and can be processed. By checking the blockchain and confirming transactions, the entire system is effectively self-regulated and fully secure. No, that doesn’t mean some kid cooped up in a basement can just click “okay” and confirm a billion dollar transfer. Blocks generally need numerous independent confirmations, and the equations are intended to be hard to crack. Not to mention, the hardware required is far more specialized than the average laptop. Finally, what’s to stop someone from simply going back and editing existing blocks? Each block is securely hashed—meaning it is rendered into seeming gibberish and nearly impossible to invert or undo. Once it’s in the blockchain, it’s there forever.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://byteacademy.co/images/2016/09/roughblockchain-300x223.png&quot; alt=&quot;rough idea of blockchain&quot;&gt;A rough idea of what a block chain may look like, courtesy of Yevgeniy Brikman&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://byteacademy.co/images/2016/09/roughblockchain-300x223.png&quot; alt=&quot;python for dummies pdf&quot;&gt;&lt;br&gt;A rough idea of what a block chain may look like, courtesy of Yevgeniy Brikman&lt;/p&gt;
&lt;p&gt;So, why waste time and resources helping other people, or the blockchain? Why not let someone else do all that “confirmation stuff,” while you just mine? Because, you don’t necessarily have a choice. Confirmation of the blockchain is central to mining. It’s part of the actual mining process; however, miners are generally given incentives. For example, after solving a problem (and creating a new hash) they are rewarded with coins.&lt;/p&gt;
&lt;p&gt;Will you be seeing a blockchain-styled ledger in your future? Short answer: oh yes. Blockchain and cryptocurrencies have caused quite a stir over the past years. However, it seems their real importance has yet to be fully realized. The future isn’t just in businesses around the globe sporting happy “Now Accepting Bitcoins” signs, but rather emerging companies (and revolutionary existing ones, too) finding new uses for the cutting edge technology. VC firms and investors are placing their bets on the blockchain because there is untapped potential. Identity management, international contracts, and all sorts of complicated bank transactions can be greatly altered with the public ledger system. The process could (in an ideal world) work seamlessly, crossing boundaries where banks, logistics or a plethora of other obstacles once existed. They could be combined with the Internet of Things to create a more connected and automated world. Future companies may be able to absorb mountains of new data, or even digitize real-world things that are hard to quantify. Unfortunately, many big companies are remaining mum on the studies in the blockchain field for obvious reasons.&lt;/p&gt;
&lt;p&gt;However, it is public knowledge that nine major banks (including JP Morgan and Goldman Sachs) recently joined a partnership to develop blockchain technologies. That’s not to say major companies are getting in on the cryptocurrency game; rather, they realize that the blockchain system, itself, could be a powerful tool for efficiency. With a system as versatile and secure as the blockchain, there may many unexpected innovations in the coming months and years.&lt;/p&gt;
&lt;p&gt;Origin post:&lt;/p&gt;
&lt;p&gt;Blockchain For Dummies: A Beginner’s Guide&lt;/p&gt;
&lt;p&gt;BY: HANNAH AUGUR&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://byteacademy.co/blockchain-for-dummies-a-beginners-guide/&quot;&gt;http://byteacademy.co/blockchain-for-dummies-a-beginners-guide/&lt;/a&gt;&lt;/p&gt;
</content:encoded><category>Technology</category></item><item><title>[转]古诗中的”斜“究竟怎么读</title><link>https://wuming.si/blog/1275/</link><guid isPermaLink="true">https://wuming.si/blog/1275/</guid><pubDate>Mon, 16 Oct 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;《小学语文教师》2000年第5期上刊有胡孟铭老师的文章《“斜”在〈山行〉中还是读xia好》，主张将“斜”字还其旧读、读古音xia，理由是：“既然课题为古诗，就应读古韵，取古义，才能字义准确，和谐动听”。本刊2000年第10期上又刊有史南先生的文章《“斜”不可读xia》，予以反对，理由是：1985年国家公布的《普通话异读词审音表》“不管那些当时不存在异读的词；如‘斜’字在普通话字（词）典上早已取消了古读音，因此在《审音表》中没有收列它，表明它除了读xie之外，普通话中不存在别的读音”“小学语文教学大纲要求小学生能用普通话朗读课文……再坚持读古音，就违背了国家有关语言文字的政策法规”。  &lt;/p&gt;
&lt;p&gt;问题至此似乎已有了答案。但笔者的看法却与以上两位老师皆有所不同。语言文字的使用，的确如史老师所说，应当以国家的政策法规为依据。而最高的依据则是2000年10月31日九届人大常委会通过并颁布的《中华人民共和国国家通用语言文字法》（以下简称《语言文字法》）。两位老师讨论时，本法尚未公布。但本法自2001年1月1日实施以后，“斜”的读音问题理当有新的答案。&lt;/p&gt;
&lt;p&gt;“斜”字确实存在着旧读音xia，后来演变出xie音，但意义没变（胡老师所谓“‘斜’的读音发生了变化，词义的范围也大大缩小了。倘若将‘斜’还其旧韵xia，（则有）古义‘倾侧或曲折向前延伸’”是无根据的，《康熙字典》《辞源》等古今字书辞书足以为训）。但在古诗教学中究竟应读古音xia，还是今音xie？则应视情形而论。&lt;br&gt;《语言文字法》规定：“本法所称的国家通用语言文字是普通话和规范汉字”，我们在教学中就应当以国家通用语言文字为准，这是毫无疑义的。所以，教材在古诗《山行》中将“斜”注音为xie是正确的，不但如此，“斜”在本篇课文中还是个生字，教师在教学“斜”这个单字时，也应当训为xie。胡老师对这一点提出异议，笔者不能苟同。但在读本诗的时候也读为xie，又确实拗口。本诗的“斜”字出现在首句，似乎还可将就一二，很多唐诗中“斜”字都出现在偶句末尾。我们知道，古诗的平仄、对仗、押韵，以押韵为第一，当时这些诗都是严格押韵的。若诵读这些诗时也读该字为xie，古诗固有的韵律美被破坏，更让人难于接受。如：&lt;br&gt;刘禹锡《乌衣巷》&lt;br&gt;朱雀桥边野草花，乌衣巷口夕阳斜。旧时王谢堂前燕，飞入寻常百姓家。&lt;br&gt;刘方平《夜月》&lt;br&gt;更深月色半人家，北斗阑干南斗斜。今夜偏知春气暖，虫声新透绿窗纱。&lt;br&gt;张泌《寄人》&lt;br&gt;别梦依依到谢家，小廊回合曲阑斜。多情只有春庭月，犹为离人照落花。&lt;br&gt;韩君平《寒食》&lt;br&gt;春城无处不飞花，寒食东风御柳斜。日暮汉宫传蜡烛，轻烟散入五侯家。&lt;br&gt;元稹《菊花》&lt;br&gt;秋丛绕舍似陶家，遍绕篱边日渐斜。不是花中偏爱菊，此花开尽更无花。&lt;br&gt;皎然《寻陆鸿渐不遇》&lt;br&gt;移家虽带郭，野径入桑麻。近种篱边菊，秋来未着花。扣门无犬吠，，欲去问西家。报道山中去，归来每日斜。&lt;br&gt;张若虚《春江花月夜》（节录）&lt;br&gt;昨夜闲潭梦落花，可怜春半不还家。江水流春去欲尽，江潭落月复西斜。&lt;br&gt;张祜《吴兴新堤》（节录）&lt;br&gt;春堤一望思无涯，树势还同水势斜。深映菰蒲三十里，晴分功利几千家。&lt;/p&gt;
&lt;p&gt;史老师在文章最后说：“但为了让学生知道原来是押韵的，在教学时不妨告诉学生‘斜’在古代的韵母就读ia，直到今天，吴方言中还保留它的古读音，读zia”。笔者认为这样还不够，在诵读这些古诗时就应当读‘斜’为xia。因为教学语言文字与欣赏文学艺术是两回事。我们教学古诗时，这两种活动是同时存在的，但也是完全能够而且应该把二者区分开来的。有的老师就是这样教的：领着学生读“远上寒山石径斜xia……”，然后跟学生讲：“‘斜’字课本上注音为xie，就是‘歪斜’的意思，平时就应当读xie，为什么在诗里读xia呢？这是因为这个字在以前的韵母就是ia，这首诗当时是很押韵的。我们读诗时，为了体会诗的韵律美，也为了更上口，就暂时读xia”。可以确信，只要老师教得明白，学生是不会在其他场合、在说话和写作应用中也读为xia的。这样，不但不会影响到语言文字规范化纯洁化的问题，相反学生还会多学到一点知识，知道“斜”字在古音韵母读ia，知道古人作诗是很重押韵的，更能体会到古诗的韵律美，更容易体会到前人的诗歌艺术才华和成就。岂不更好？重要的是，这样做也有法律的依据。《语言文字法》明确规定：“有下列情形的，可以使用方言：……三戏曲影视等艺术形式中需要使用的；四出版、教学、研究中确需使用的”。这里所谓的“方言”，当然也包括像“斜xia”这样的以方言形式存在的古音韵母。&lt;/p&gt;
&lt;p&gt;当然，这种“暂时地读为古音”也是有条件的：必须是“斜”字作韵脚，在韵律的“胁迫”下自然而然地读为xia；如果“斜”虽在古诗中，但不作韵脚，则须还其正读xie音。&lt;/p&gt;
&lt;p&gt;对于这种处理，我们还可从对古字形的处理当中找到类似的旁证。比如胡、史二位老师讨论的“鹿柴”。“鹿柴”中“柴”之所以读zhai，是因为“柴”是“寨”的通假字。但是在现代汉语用字标准中，“寨”是不能再写作“柴”了。课本之所以仍保留原来的字形“柴”，无非也是让学生多一点知识，知道“寨”在古代可通写作“柴”，为以后直接阅读古书提供方便而已。《语言文字法》也有相应的规定：“有下列情形的，可以保留或使用繁体字、异体字：……三书法、篆刻等艺术作品；……五出版、教学、研究中需要使用的”。&lt;br&gt;至于汉匈奴单于“伊稚斜”仍读cha，不读xie，则是因为《审音表》“对大量的人名、地名中的异读字未加审订，这些人名、地名中的异读字照样异读”。&lt;/p&gt;
&lt;p&gt;在欣赏性的唐诗吟诵中，为求合于韵律、为求顺口而需要暂时恢复原来读音的字，还有很多。除“斜”字外，另一个比较常见的是“回”字。古诗中“回”作韵脚时，应读古音huai，而不读hui。如：&lt;br&gt;李白《望天门山》&lt;br&gt;天门中断楚江开，碧水东流至此回。两岸青山相对出，孤帆一片日边来。&lt;br&gt;杜甫《登高》（节录）&lt;br&gt;风急天高猿啸哀，渚清沙白鸟飞回。无边落木潇潇下，不尽长江滚滚来。&lt;br&gt;刘禹锡《戏赠看花诸君子》&lt;br&gt;紫陌红尘拂面来，无人不道看花回。玄都观里桃千树，尽是刘郎去后栽。&lt;br&gt;刘禹锡《石头城》&lt;br&gt;山围故国周遭在，潮打空城寂寞回。淮水东边旧时月，夜深还过女墙来。&lt;br&gt;杜牧《早雁》&lt;br&gt;金河秋半虏弦开，云外惊飞四散哀。仙掌月明孤影过，长门灯暗数声来。须知胡骑纷纷在，岂逐春风一一回。莫厌潇湘少人处，水多菰米岸莓苔。&lt;/p&gt;
&lt;p&gt;“回”在唐诗韵脚中读huai，当无疑义。不过这又牵涉到另一个“衰”的读音。《长歌行》有“阳春布德泽，万物生光辉。长恐秋节至，焜黄华叶衰。百川东到海，何时复西归。”；贺知章《回乡偶书》有“少小离家老大回，乡音无改鬓毛衰。儿童相见不相识，笑问客从何处来。”－－“衰”当读何音？历来说法不一：或有前首训为shuai的，或有后首训为cui的。笔者的意见则是：“衰”在前诗中训为cui，或有后诗中训为shuai。因为：“衰”字，作“形容萎缩、体量递减”时读cui，如“等衰”；作“气色衰老”时读shuai，如“衰草”。因此，“焜黄华叶衰”中读cui，是指鲜亮的花和叶子萎缩脱落、是形体之变；而“鬓毛衰”中读shuai，是两鬓斑白之意，是气色之变，不是形体之变，不是鬓毛萎缩脱落之意（因为人的鬓毛是头发中最不易脱落的）。诗中的韵脚“辉、归”“回、来”等字可作旁证。&lt;/p&gt;
</content:encoded><category>Poem &amp; Reading</category><category>Uncategorized</category></item><item><title>CentOS 7 下 firewalld的基本操作</title><link>https://wuming.si/blog/1257/</link><guid isPermaLink="true">https://wuming.si/blog/1257/</guid><pubDate>Thu, 07 Sep 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这两天服务器升级，升级后发现有些服务不能用了。后来研究了一下，是因为防火墙的原因。之前调试，为了偷懒，把防火墙关了，就没启动过，现在想想裸奔了几个月，心真是大。&lt;/p&gt;
&lt;p&gt;对外增加服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --zone=public --add-port=80/tcp --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对内设置安全区&lt;/p&gt;
&lt;p&gt;firewall-cmd –permanent –zone=internal –change-interface=enp03s&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --zone=public --add-port=3306/tcp --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，再重新加载firwalld策略&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下转载:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/linux-centos-7-fang-huo-qiang-zhi-ju-you-ming-ling-xing-de-firewalldde-ji-ben-cao-zuo/&quot;&gt;http://qianxunclub.com/linux-centos-7-fang-huo-qiang-zhi-ju-you-ming-ling-xing-de-firewalldde-ji-ben-cao-zuo/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;启动FirewallD服务：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1 2&lt;/th&gt;
&lt;th&gt;&lt;code&gt;systemctl ``enable&lt;/code&gt; &lt;code&gt;firewalld.service ``#设置开机启动&lt;/code&gt; &lt;code&gt;systemctl start firewalld.service ``#开启服务&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;查看防火墙状态：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;systemctl status firewalld&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;1. 区域管理&lt;/p&gt;
&lt;p&gt;1.1. 网络区域简介&lt;/p&gt;
&lt;p&gt;通过将网络划分成不同的区域，制定出不同区域之间的访问控制策略来控制不同程序区域间传送的数据流。例如，互联网是不可信任的区域，而内部网络是高度信任的区域。网络安全模型可以在安装，初次启动和首次建立网络连接时选择初始化。该模型描述了主机所连接的整个网络环境的可信级别，并定义了新连接的处理方式。有如下几种不同的初始化区域：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻塞区域（block）：任何传入的网络数据包都将被阻止。&lt;/li&gt;
&lt;li&gt;工作区域（work）：相信网络上的其他计算机，不会损害你的计算机。&lt;/li&gt;
&lt;li&gt;家庭区域（home）：相信网络上的其他计算机，不会损害你的计算机。&lt;/li&gt;
&lt;li&gt;公共区域（public）：不相信网络上的任何计算机，只有选择接受传入的网络连接。&lt;/li&gt;
&lt;li&gt;隔离区域（DMZ）：隔离区域也称为非军事区域，内外网络之间增加的一层网络，起到缓冲作用。对于隔离区域，只有选择接受传入的网络连接。&lt;/li&gt;
&lt;li&gt;信任区域（trusted）：所有的网络连接都可以接受。&lt;/li&gt;
&lt;li&gt;丢弃区域（drop）：任何传入的网络连接都被拒绝。&lt;/li&gt;
&lt;li&gt;内部区域（internal）：信任网络上的其他计算机，不会损害你的计算机。只有选择接受传入的网络连接。&lt;/li&gt;
&lt;li&gt;外部区域（external）：不相信网络上的其他计算机，不会损害你的计算机。只有选择接受传入的网络连接。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注：FirewallD的默认区域是public。&lt;/p&gt;
&lt;p&gt;1.2. 显示支持的区域列表&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --get-zones&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;1.3. 设置为家庭区域&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --``set``-default-zone=home&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;1.4. 查看当前区域&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --get-active-zones&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;1.5. 设置当前区域的接口&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --get-zone-of-interface=enp03s&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;1.6. 显示所有公共区域（public）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --zone=public --list-all&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;1.7. 临时修改网络接口（enp0s3）为内部区域（internal）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --zone=internal --change-interface=enp03s&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;1.8. 永久修改网络接口enp03s为内部区域（internal）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --permanent --zone=internal --change-interface=enp03s&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;2. 服务管理&lt;/p&gt;
&lt;p&gt;2.1. 显示服务列表&lt;/p&gt;
&lt;p&gt;Amanda, FTP, Samba和TFTP等最重要的服务已经被FirewallD提供相应的服务，可以使用如下命令查看：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --get-services&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;2.2. 允许SSH服务通过&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --``enable&lt;/code&gt; &lt;code&gt;service=``ssh&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;2.3. 禁止SSH服务通过&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --disable service=``ssh&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;2.4. 打开TCP的8080端口&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --``enable&lt;/code&gt; &lt;code&gt;ports=8080``/tcp&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;2.5. 临时允许Samba服务通过600秒&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --``enable&lt;/code&gt; &lt;code&gt;service=samba --timeout=600&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;2.6. 显示当前服务&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --list-services&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;2.7. 添加HTTP服务到内部区域（internal）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1 2&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --permanent --zone=internal --add-service=http&lt;/code&gt; &lt;code&gt;firewall-cmd --reload ``#在不改变状态的条件下重新加载防火墙&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;3. 端口管理&lt;/p&gt;
&lt;p&gt;3.1. 打开端口&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1 2&lt;/th&gt;
&lt;th&gt;&lt;code&gt;#打开443/TCP端口&lt;/code&gt; &lt;code&gt;firewall-cmd --add-port=443``/tcp&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1 2 3 4 5&lt;/th&gt;
&lt;th&gt;&lt;code&gt;#永久打开3690/TCP端口&lt;/code&gt; &lt;code&gt;firewall-cmd --permanent --add-port=3690``/tcp&lt;/code&gt; &lt;code&gt;#永久打开端口好像需要reload一下，临时打开好像不用，如果用了reload临时打开的端口就失效了&lt;/code&gt; &lt;code&gt;#其它服务也可能是这样的，这个没有测试&lt;/code&gt; &lt;code&gt;firewall-cmd --reload&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1 2&lt;/th&gt;
&lt;th&gt;&lt;code&gt;#查看防火墙，添加的端口也可以看到&lt;/code&gt; &lt;code&gt;firewall-cmd --list-all&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;4. 直接模式&lt;/p&gt;
&lt;p&gt;FirewallD包括一种直接模式，使用它可以完成一些工作，例如打开TCP协议的9999端口&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1 2&lt;/th&gt;
&lt;th&gt;&lt;code&gt;firewall-cmd --direct -add-rule ipv4 filter INPUT 0 -p tcp --dport 9000 -j ACCEPT&lt;/code&gt; &lt;code&gt;firewall-cmd --reload&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;p&gt;5. 关闭服务的方法&lt;/p&gt;
&lt;p&gt;你也可以关闭目前还不熟悉的FirewallD防火墙，而使用iptables，命令如下：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://qianxunclub.com/blog?id=67e0d709-ba9f-4ce2-a2f7-128c2cf5cf8b&quot;&gt;?&lt;/a&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1 2 3 4 5&lt;/th&gt;
&lt;th&gt;&lt;code&gt;systemctl stop firewalld&lt;/code&gt; &lt;code&gt;systemctl disable firewalld&lt;/code&gt; &lt;code&gt;yum ``install&lt;/code&gt; &lt;code&gt;iptables-services&lt;/code&gt; &lt;code&gt;systemctl start iptables&lt;/code&gt; &lt;code&gt;systemctl ``enable&lt;/code&gt; &lt;code&gt;iptables&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
</content:encoded><category>Technology</category></item><item><title>擦干眼泪，继续工作</title><link>https://wuming.si/blog/1241/</link><guid isPermaLink="true">https://wuming.si/blog/1241/</guid><pubDate>Tue, 05 Sep 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;昨天注定是个悲伤的日子。&lt;/p&gt;
&lt;p&gt;网上流传不会炒币的码农不是一个好码农，我也加入了，昨天中央发文严管ICO和虚拟货币，所有虚拟货币在半小时内价格一落千丈，跌掉20%以上的比比皆是，我相信是哀鸿遍野了。韭菜党估计已经早就开始割了。今早还有同学发来贺电，说老外昨晚好不容易把价格救上来一些，昨晚哭了一晚的韭菜党早上又开始割了，价格继续下跌，慢慢接着哭吧。&lt;/p&gt;
&lt;p&gt;还有一件事就是昨天发现博客不能访问了，到邮箱里面看到催缴费的邮件，续费完发现还是找不到我的服务器，提了ticket后，工作人员说服务器已经删除，无法再恢复。虽然一个人絮絮叨叨的自说自话，也没多少人看，这可是俺写了10多年的博客啊。翻箱倒柜，总算找到一个3月份的数据备份，最后几篇也找不到了。前不久博客被鬼子黑过，插入了木马，google的所有的链接都被变成了他们的广告，正好备份了网站文件。把这两个加一起基本恢复到了3月份的状态，也算是不幸中的万幸。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wuming.si/images/2017/09/yukinoka.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://s3.wo.ai/upload/mp3/977922946.mp3&quot;&gt;http://s3.wo.ai/upload/mp3/977922946.mp3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;放上中岛美嘉的雪の華、经历了多少不甘之后还能站出来在舞台上用自己沙哑的声音重新演绎自己的前半生。不幸又能怎么样，擦干眼泪，大不了重新再来。&lt;/p&gt;
&lt;p&gt;雪の華-中島美嘉&lt;/p&gt;
&lt;p&gt;のびた人陰(かげ)を舗道にならべ&lt;br&gt;不断延伸的影子 在红砖道上并列&lt;br&gt;夕闇のなかを君と歩いてる&lt;br&gt;在深夜里与你并肩走着&lt;br&gt;手をつないでいつまでもずっと&lt;br&gt;永远紧紧牵着手&lt;br&gt;そばにいれたなら&lt;br&gt;只要能在你身旁&lt;br&gt;泣けちゃうくらい&lt;br&gt;我就感动得快要哭了&lt;br&gt;風が冷たくなって&lt;br&gt;风儿变得寒冷&lt;br&gt;冬の匂いがした&lt;br&gt;有了冬天的味道&lt;br&gt;そろそろこの街に&lt;br&gt;这条街也即将到了&lt;br&gt;君と近付ける季節がくる&lt;br&gt;能和你接近的季节来临&lt;br&gt;今年、最初の雪の華を&lt;br&gt;今年 最初的雪花&lt;br&gt;ふたり寄り添って&lt;br&gt;向我俩靠近&lt;br&gt;眺めているこの瞬間(とき)に&lt;br&gt;在眺望着的这个瞬间里（时间）&lt;br&gt;幸せがあふれだす&lt;br&gt;充满了幸福的喜悦&lt;br&gt;甘えとか弱さじゃない&lt;br&gt;没有撒娇和脆弱&lt;br&gt;ただ、君を愛してる&lt;br&gt;只是 爱你&lt;br&gt;心からそう思った&lt;br&gt;打从心底爱你&lt;br&gt;君がいるとどんなことでも&lt;br&gt;只要有你在 无论发生什么&lt;br&gt;乗りきれるような気持ちになってる&lt;br&gt;都会有可以克服的心情&lt;br&gt;こんな日々がいつまでもきっと&lt;br&gt;我祈祷着 这样的日子&lt;br&gt;続いてくことを祈っているよ&lt;br&gt;一定会直到永远&lt;br&gt;風が窓を揺らした&lt;br&gt;风儿吹的窗摇&lt;br&gt;夜は揺り起こして&lt;br&gt;把夜晚摇醒&lt;br&gt;どんな悲しいことも&lt;br&gt;无论多么悲伤的事情&lt;br&gt;僕が笑顔へと変えてあげる&lt;br&gt;我用笑容为你改变&lt;br&gt;舞い落ちてきた雪の華が&lt;br&gt;雪花飞舞飘落&lt;br&gt;窓の外ずっと&lt;br&gt;在窗外一定&lt;br&gt;降りやむことを知らずに&lt;br&gt;不知何时雪已停&lt;br&gt;僕らの街を染める&lt;br&gt;把我们的街道染色&lt;br&gt;誰かのために何かを&lt;br&gt;为某人想做&lt;br&gt;したいと思えるのが&lt;br&gt;些什么事&lt;br&gt;愛ということを知った&lt;br&gt;原来 这就是爱&lt;br&gt;もし、君を失ったとしたなら&lt;br&gt;如果 失去了你&lt;br&gt;星になって君を照らすだろう&lt;br&gt;我会变成星星照亮你&lt;br&gt;笑顔も涙に濡れてる夜も&lt;br&gt;微笑或被泪水沾湿的夜晚&lt;br&gt;いつもいつでもそばにいるよ&lt;br&gt;我会永远在你身旁&lt;br&gt;今年、最初の雪の華を&lt;br&gt;今年 最初的雪花&lt;br&gt;ふたり寄り添って&lt;br&gt;向我俩靠近&lt;br&gt;眺めているこの瞬間(とき)に&lt;br&gt;在眺望着的这个瞬间里&lt;br&gt;幸せがあふれだす&lt;br&gt;充满了幸福的喜悦&lt;br&gt;甘えとか弱さじゃない&lt;br&gt;没有撒娇和脆弱&lt;br&gt;ただ、君とずっと&lt;br&gt;只是 想永远地&lt;br&gt;このまま一緒にいたい&lt;br&gt;就这么一直在一起&lt;br&gt;素直にそう思える&lt;br&gt;我真心地这么想&lt;br&gt;この街に降り積もってく&lt;br&gt;在这条街上堆积的&lt;br&gt;真っ白な雪の華&lt;br&gt;纯白雪花&lt;br&gt;ふたりの胸にそっと想い出を描くよ&lt;br&gt;悄悄地在我俩胸口画上回忆&lt;br&gt;これからも君とずっと…&lt;br&gt;从今而后也要永远和你在一起&lt;/p&gt;
</content:encoded><category>Life</category><category>Music &amp; Movie</category></item><item><title>候鸟模式</title><link>https://wuming.si/blog/1247/</link><guid isPermaLink="true">https://wuming.si/blog/1247/</guid><pubDate>Mon, 08 May 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在一个字母公司一待就是8年，8年没有感情是不可能的，但是上升无望，如果继续无忧无虑的下去，不知道前途在何方。在去年年底终于下定决心离开，来到另外一家字母公司。&lt;/p&gt;
&lt;p&gt;也许本身对这家字母公司也是短期跳板的期望，没有过多融入，在三观还没有完全毁尽之前快速离开，来到现在这家研究院。&lt;/p&gt;
&lt;p&gt;这项技术目前处于风口，是否能够让我成功转型尚属未知。未知的自己，未知的明天。也正因为如此，抛妻弃子，开启了周末候鸟模式，铁老大该发笑了。&lt;/p&gt;
&lt;p&gt;在公寓里还能听取外边还能蛙声一片，但愿今年能够稻花香里说丰年吧。&lt;/p&gt;
</content:encoded><category>Life</category></item><item><title>Auto start a java Swing GUI program when raspberry boots</title><link>https://wuming.si/blog/1237/</link><guid isPermaLink="true">https://wuming.si/blog/1237/</guid><pubDate>Tue, 07 Mar 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I planed make an self-desgin photo or movie player base on Raspberry. Also I can use it as photo frame. If I need improve the performance of the PI, I need write it with Python, I think.&lt;/p&gt;
&lt;p&gt;Part 1 – Build the Foundation&lt;/p&gt;
&lt;p&gt;In this part, we will focus on preparing Raspbian Lite.&lt;/p&gt;
&lt;p&gt;1. Download the latest Raspbian Lite image.&lt;br&gt;2. Format the SD / microSD card with Raspbian Lite (Plenty of guides out there on how to do this. For macOS, Linux, and Windows users, Etcher is an easy to use application that can help you do this.)&lt;br&gt;3. Insert the SD / microSD card into the Pi.&lt;br&gt;4. Connect the Pi to the Internet using an Ethernet cable. If you want to use Wi-Fi instead, you will have to read on how to configure your wireless receiver using the command line after your Pi has finished booting.&lt;br&gt;5. Connect your TV / Monitor and keyboard. (Mouse is optional at this time.) Turn on the Pi. The Pi should boot up successfully and a prompt to log in will appear.&lt;br&gt;6. Log into Raspbian. The username is pi and the password is raspberry.&lt;/p&gt;
&lt;p&gt;7. We will install Xorg. To do this type in:&lt;/p&gt;
&lt;p&gt;sudo apt-get install –no-install-recommends xserver-xorg&lt;/p&gt;
&lt;p&gt;sudo apt-get install –no-install-recommends xinit&lt;/p&gt;
&lt;p&gt;now, you can write you java program now. For example, I wrote a test program with a button in the center of screen. once I click the button, the window will change to the full size of the screen.&lt;/p&gt;
&lt;p&gt;import java.awt.FlowLayout;&lt;br&gt;import java.awt.event.ActionEvent;&lt;br&gt;import java.awt.event.ActionListener;&lt;/p&gt;
&lt;p&gt;import javax.swing.JButton;&lt;br&gt;import javax.swing.JFrame;&lt;/p&gt;
&lt;p&gt;public class FullScreenTest {&lt;br&gt;public static void main(String[] args) {&lt;br&gt;final JFrame f = new JFrame(“FullScreenTest”);&lt;br&gt;final JButton btn = new JButton(“FullScreen”);&lt;br&gt;btn.addActionListener(new ActionListener() {&lt;br&gt;public void actionPerformed(ActionEvent e) {&lt;br&gt;if (btn.getText().equals(“FullScreen”)) {&lt;br&gt;f.dispose();&lt;br&gt;f.setUndecorated(true);&lt;br&gt;f.getGraphicsConfiguration().getDevice().setFullScreenWindow(f);&lt;br&gt;f.setVisible(true);&lt;br&gt;btn.setText(“NormalMode”);&lt;br&gt;} else {&lt;br&gt;f.dispose();&lt;br&gt;f.setUndecorated(false);&lt;br&gt;f.getGraphicsConfiguration().getDevice().setFullScreenWindow(null);&lt;br&gt;f.setVisible(true);&lt;br&gt;btn.setText(“FullScreen”);&lt;br&gt;}&lt;br&gt;}&lt;br&gt;});&lt;/p&gt;
&lt;p&gt;f.getContentPane().setLayout(new FlowLayout());&lt;br&gt;f.getContentPane().add(btn);&lt;br&gt;f.pack();&lt;br&gt;f.setLocationRelativeTo(null);&lt;br&gt;f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);&lt;br&gt;f.setVisible(true);&lt;br&gt;}&lt;br&gt;}&lt;/p&gt;
&lt;p&gt;Pack the file into a jar file like GUI.jar.&lt;/p&gt;
&lt;p&gt;In order to have a command or program run when the Pi boots, you can add commands to the &lt;code&gt;rc.local&lt;/code&gt; file. This is especially useful if you want to be able to plug your Pi in to power headless, and have it run a program without configuration or a manual start.&lt;/p&gt;
&lt;h2&gt;EDITING RC.LOCAL&lt;/h2&gt;
&lt;p&gt;On your Pi, edit the file &lt;code&gt;/etc/rc.local&lt;/code&gt; using the editor of your choice. You must edit with root, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/rc.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add commands below the comment, but leave the line &lt;code&gt;exit 0&lt;/code&gt; at the end, then save the file and exit.&lt;/p&gt;
&lt;p&gt;sudo xinit /usr/local/jdk1.8.0_77/bin/java -jar /usr/local/mypi/GUI.jar — :1 &amp;amp;&lt;/p&gt;
&lt;p&gt;Reboot your PI, then done!&lt;/p&gt;
&lt;h3&gt;&lt;/h3&gt;
</content:encoded><category>Raspberry Pi</category><category>Technology</category></item><item><title>防范 CSRF 跨站请求伪造</title><link>https://wuming.si/blog/1235/</link><guid isPermaLink="true">https://wuming.si/blog/1235/</guid><pubDate>Wed, 01 Mar 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;CSRF（Cross-site request forgery，中文为&lt;strong&gt;跨站请求伪造&lt;/strong&gt; ）是一种利用网站可信用户的权限去执行未授权的命令的一种恶意攻击。通过&lt;strong&gt;伪装可信用户的请求来利用信任该用户的网站&lt;/strong&gt; ，这种攻击方式虽然不是很流行，但是却难以防范，其危害也不比其他安全漏洞小。&lt;/p&gt;
&lt;p&gt;本文将简要介绍CSRF产生的原因以及利用方式，然后对如何避免这种攻击方式提供一些可供参考的方案，希望广大程序猿们都能够对这种攻击方式有所了解，避免自己开发的应用被别人利用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CSRF也称作&lt;strong&gt;one-click attack&lt;/strong&gt; 或者&lt;strong&gt;session riding&lt;/strong&gt; ，其简写有时候也会使用&lt;strong&gt;XSRF&lt;/strong&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;什么是CSRF？&lt;/h2&gt;
&lt;p&gt;简单点说，CSRF攻击就是 &lt;strong&gt;攻击者利用受害者的身份，以受害者的名义发送恶意请求&lt;/strong&gt; 。与XSS（Cross-site scripting，跨站脚本攻击）不同的是，XSS的目的是获取用户的身份信息，攻击者窃取到的是用户的身份（session/cookie），而CSRF则是利用用户当前的身份去做一些未经过授权的操作。&lt;/p&gt;
&lt;p&gt;CSRF攻击最早在2001年被发现，由于它的请求是从用户的IP地址发起的，因此在服务器上的web日志中可能无法检测到是否受到了CSRF攻击，正是由于它的这种隐蔽性，很长时间以来都没有被公开的报告出来，直到2007年才真正的被人们所重视。&lt;/p&gt;
&lt;h2&gt;CSRF有哪些危害&lt;/h2&gt;
&lt;p&gt;CSRF可以盗用受害者的身份，完成受害者在web浏览器有权限进行的任何操作，想想吧，能做的事情太多了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;以你的名义发送诈骗邮件，消息&lt;/li&gt;
&lt;li&gt;用你的账号购买商品&lt;/li&gt;
&lt;li&gt;用你的名义完成虚拟货币转账&lt;/li&gt;
&lt;li&gt;泄露个人隐私&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;产生原理以及利用方式&lt;/h2&gt;
&lt;p&gt;要完成一个CSRF攻击，必须具备以下几个条件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;受害者已经登录到了目标网站（你的网站）并且没有退出&lt;/li&gt;
&lt;li&gt;受害者有意或者无意的访问了攻击者发布的页面或者链接地址&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://segmentfault.com/img/remote/1460000008505620?w=1190&amp;h=832&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;（图片来自网络，出处不明，百度来的😂）&lt;/p&gt;
&lt;p&gt;整个步骤大致是这个样子的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户小明在你的网站A上面登录了，A返回了一个session ID（使用cookie存储）&lt;/li&gt;
&lt;li&gt;小明的浏览器保持着在A网站的登录状态，事实上几乎所有的网站都是这样做的，一般至少是用户关闭浏览器之前用户的会话是不会结束的&lt;/li&gt;
&lt;li&gt;攻击者小强给小明发送了一个链接地址，小明打开了这个地址，查看了网页的内容&lt;/li&gt;
&lt;li&gt;小明在打开这个地址的时候，这个页面已经自动的对网站A发送了一个请求，这时候因为A网站没有退出，因此只要请求的地址是A的就会携带A的cookie信息，也就是使用A与小明之间的会话&lt;/li&gt;
&lt;li&gt;这时候A网站肯定是不知道这个请求其实是小强伪造的网页上发送的，而是误以为小明就是要这样操作，这样小强就可以随意的更改小明在A上的信息，以小明的身份在A网站上进行操作&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;利用方式&lt;/h3&gt;
&lt;p&gt;利用CSRF攻击，主要包含两种方式，一种是基于GET请求方式的利用，另一种是基于POST请求方式的利用。&lt;/p&gt;
&lt;h4&gt;GET请求利用&lt;/h4&gt;
&lt;p&gt;使用GET请求方式的利用是最简单的一种利用方式，其隐患的来源主要是由于在开发系统的时候没有按照HTTP动词的正确使用方式来使用造成的。&lt;strong&gt;对于GET请求来说，它所发起的请求应该是只读的，不允许对网站的任何内容进行修改&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;但是事实上并不是如此，很多网站在开发的时候，研发人员错误的认为GET/POST的使用区别仅仅是在于发送请求的数据是在Body中还是在请求地址中，以及请求内容的大小不同。对于一些危险的操作比如删除文章，用户授权等允许使用GET方式发送请求，在请求参数中加上文章或者用户的ID，这样就造成了只要请求地址被调用，数据就会产生修改。&lt;/p&gt;
&lt;p&gt;现在假设攻击者（用户ID=121）想将自己的身份添加为网站的管理员，他在网站A上面发了一个帖子，里面包含一张图片，其地址为&lt;code&gt;http://a.com/user/grant_super_user/121&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;img src=&amp;quot;http://a.com/user/grant_super_user/121&amp;quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设想管理员看到这个帖子的时候，这个图片肯定会自动加载显示的。于是在管理员不知情的情况下，一个赋予用户管理员权限的操作已经悄悄的以他的身份执行了。这时候攻击者121就获取到了网站的管理员权限。&lt;/p&gt;
&lt;h4&gt;POST请求利用&lt;/h4&gt;
&lt;p&gt;相对于GET方式的利用，POST方式的利用更加复杂一些，难度也大了一些。攻击者需要伪造一个能够自动提交的表单来发送POST请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要想办法实现用户访问的时候自动提交表单就可以了。&lt;/p&gt;
&lt;h2&gt;如何防范&lt;/h2&gt;
&lt;h3&gt;防范原理&lt;/h3&gt;
&lt;p&gt;防范CSRF攻击，其实本质就是要求网站&lt;strong&gt;能够识别出哪些请求是非正常用户主动发起的&lt;/strong&gt; 。这就要求我们&lt;strong&gt;在请求中嵌入一些额外的授权数据，让网站服务器能够区分出这些未授权的请求&lt;/strong&gt; ，比如说在请求参数中添加一个字段，这个字段的值从登录用户的Cookie或者页面中获取的（这个字段的值必须对每个用户来说是随机的，不能有规律可循）。攻击者伪造请求的时候是无法获取页面中与登录用户有关的一个随机值或者用户当前cookie中的内容的，因此就可以避免这种攻击。&lt;/p&gt;
&lt;h3&gt;防范技术&lt;/h3&gt;
&lt;h4&gt;Synchronizer token pattern&lt;/h4&gt;
&lt;p&gt;令牌同步模式（Synchronizer token pattern，简称STP）是在用户请求的页面中的所有表单中嵌入一个token，在服务端验证这个token的技术。token可以是任意的内容，但是一定要保证无法被攻击者猜测到或者查询到。攻击者在请求中无法使用正确的token，因此可以判断出未授权的请求。&lt;/p&gt;
&lt;h4&gt;Cookie-to-Header Token&lt;/h4&gt;
&lt;p&gt;对于使用Js作为主要交互技术的网站，将CSRF的token写入到cookie中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Set-Cookie: CSRF-token=i8XNjC4b8KVok4uw5RftR38Wgp2BFwql; expires=Thu, 23-Jul-2015 10:25:33 GMT; Max-Age=31449600; Path=/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后使用javascript读取token的值，在发送http请求的时候将其作为请求的header&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X-CSRF-Token: i8XNjC4b8KVok4uw5RftR38Wgp2BFwql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后服务器验证请求头中的token是否合法。&lt;/p&gt;
&lt;h4&gt;验证码&lt;/h4&gt;
&lt;p&gt;使用验证码可以杜绝CSRF攻击，但是这种方式要求每个请求都输入一个验证码，显然没有哪个网站愿意使用这种粗暴的方式，用户体验太差，用户会疯掉的。&lt;/p&gt;
&lt;h3&gt;简单实现STP&lt;/h3&gt;
&lt;p&gt;首先在index.php中，创建一个表单，在表单中，我们将session中存储的token放入到隐藏域，这样，表单提交的时候token会随表单一起提交&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
$token = sha1(uniqid(rand(), true));
$_SESSION[&amp;#39;token&amp;#39;] = $token;
?&amp;gt;
&amp;lt;form action=&amp;quot;buy.php&amp;quot; method=&amp;quot;post&amp;quot;&amp;gt;
    &amp;lt;input type=&amp;quot;hidden&amp;quot; name=&amp;quot;token&amp;quot; value=&amp;quot;&amp;lt;?=$token; ?&amp;gt;&amp;quot; /&amp;gt;
    ... 表单内容
&amp;lt;/form&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在服务端校验请求参数的&lt;code&gt;buy.php&lt;/code&gt;中，对表单提交过来的token与session中存储的token进行比对，如果一致说明token是有效的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;code&amp;gt;
  &amp;lt;?php if ($_POST[&amp;#39;token&amp;#39;] != $_SESSION[&amp;#39;token&amp;#39;]) {
    // TOKEN无效
    throw new \Exception(&amp;#39;Token无效，请求为伪造请求&amp;#39;);
}
// TOKEN有效，表单内容处理
&amp;lt;/code?&amp;gt;
&amp;lt;/code&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于攻击者来说，在伪造请求的时候是无法获取到用户页面中的这个&lt;code&gt;token&lt;/code&gt;值的，因此就可以识别出其创建的伪造请求。&lt;/p&gt;
&lt;h3&gt;解析Laravel框架中的VerifyCSRFToken中间件&lt;/h3&gt;
&lt;p&gt;在Laravel框架中，使用了&lt;code&gt;VerifyCSRFToken&lt;/code&gt;这个中间件来防范CSRF攻击。&lt;/p&gt;
&lt;p&gt;在页面的表单中使用&lt;code&gt;{{ CSRF_field() }}&lt;/code&gt;来生成token，该函数会在表单中添加一个名为&lt;code&gt;_token&lt;/code&gt;的隐藏域，该隐藏域的值为Laravel生成的token，Laravel使用随机生成的40个字符作为防范CSRF攻击的token。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$this-&amp;gt;put(&amp;#39;_token&amp;#39;, Str::random(40));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果请求是ajax异步请求，可以在&lt;code&gt;meta&lt;/code&gt;标签中添加token&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta name=&amp;quot;CSRF-token&amp;quot; content=&amp;quot;{{ CSRF_token() }}&amp;quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;jquery&lt;/code&gt;作为前端的框架时候，可以通过以下配置将该值添加到所有的异步请求头中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$.ajaxSetup({
    headers: {
        &amp;#39;X-CSRF-TOKEN&amp;#39;: $(&amp;#39;meta[name=&amp;quot;CSRF-token&amp;quot;]&amp;#39;).attr(&amp;#39;content&amp;#39;)
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在启用session的时候，Laravel会生成一个名为&lt;code&gt;_token&lt;/code&gt;的值存储到session中。而使用前面两种方式在页面中加入的token就是使用的这一个值。在用户请求到来时，&lt;code&gt;VerifyCSRFToken&lt;/code&gt;中间件会对符合条件的请求进行CSRF检查&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (
  $this-&amp;gt;isReading($request) ||
  $this-&amp;gt;runningUnitTests() ||
  $this-&amp;gt;shouldPassThrough($request) ||
  $this-&amp;gt;tokensMatch($request)
) {
  return $this-&amp;gt;addCookieToResponse($request, $next($request));
}

throw new TokenMismatchException;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;if&lt;/code&gt;语句中有四个条件，只要任何一个条件结果为&lt;code&gt;true&lt;/code&gt;则任何该请求是合法的，否则就会抛出&lt;code&gt;TokenMismatchException&lt;/code&gt;异常，告诉用户请求不合法，存在CSRF攻击。&lt;/p&gt;
&lt;p&gt;第一个条件&lt;code&gt;$this-&amp;gt;isReading($request)&lt;/code&gt;用来检查请求是否会对数据产生修改&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected function isReading($request)
{
    return in_array($request-&amp;gt;method(), [&amp;#39;HEAD&amp;#39;, &amp;#39;GET&amp;#39;, &amp;#39;OPTIONS&amp;#39;]);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里判断了请求方式，如果是&lt;code&gt;HEAD&lt;/code&gt;，&lt;code&gt;GET&lt;/code&gt;，&lt;code&gt;OPTIONS&lt;/code&gt;这三种请求方式则直接放行。你可能会感到疑惑，为什么GET请求也要放行呢？这是因为Laravel认为这三个请求都是请求查询数据的，&lt;strong&gt;如果一个请求是使用GET方式，那无论请求多少次，无论请求参数如何，都不应该最数据做任何修改&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;第二个条件顾名思义是对单元测试进行放行，第三个是为开发者提供了一个可以对某些请求添加例外的功能，最后一个&lt;code&gt;$this-&amp;gt;tokensMatch($request)&lt;/code&gt;则是真正起作用的一个，它是Laravel防范CSRF攻击的关键&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$sessionToken = $request-&amp;gt;session()-&amp;gt;token();
$token = $request-&amp;gt;input(&amp;#39;_token&amp;#39;) ?: $request-&amp;gt;header(&amp;#39;X-CSRF-TOKEN&amp;#39;);

if (! $token &amp;amp;&amp;amp; $header = $request-&amp;gt;header(&amp;#39;X-XSRF-TOKEN&amp;#39;)) {
  $token = $this-&amp;gt;encrypter-&amp;gt;decrypt($header);
}

if (! is_string($sessionToken) || ! is_string($token)) {
  return false;
}

return hash_equals($sessionToken, $token);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel会从请求中读取&lt;code&gt;_token&lt;/code&gt;参数的的值，这个值就是在前面表单中添加的&lt;code&gt;CSRF_field()&lt;/code&gt;函数生成的。如果请求是异步的，那么会读取&lt;code&gt;X-CSRF-TOKEN&lt;/code&gt;请求头，从请求头中读取token的值。&lt;/p&gt;
&lt;p&gt;最后使用&lt;code&gt;hash_equals&lt;/code&gt;函数验证请求参数中提供的token值和session中存储的token值是否一致，如果一致则说明请求是合法的。&lt;/p&gt;
&lt;p&gt;你可能注意到，这个检查过程中也会读取一个名为&lt;code&gt;X-XSRF-TOKEN&lt;/code&gt;的请求头，这个值是为了提供对一些javascript框架的支持（比如Angular），它们会自动的对异步请求中添加该请求头，而该值是从Cookie中的&lt;code&gt;XSRF-TOKEN&lt;/code&gt;中读取的，因此在每个请求结束的时候，Laravel会发送给客户端一个名为&lt;code&gt;XSRF-TOKEN&lt;/code&gt;的Cookie值&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$response-&amp;gt;headers-&amp;gt;setCookie(
    new Cookie(
        &amp;#39;XSRF-TOKEN&amp;#39;, $request-&amp;gt;session()-&amp;gt;token(), time() + 60 * $config[&amp;#39;lifetime&amp;#39;],
        $config[&amp;#39;path&amp;#39;], $config[&amp;#39;domain&amp;#39;], $config[&amp;#39;secure&amp;#39;], false
    )
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;本文只是对CSRF做了一个简单的介绍，主要是侧重于CSRF是什么以及如何应对CSRF攻击。有一个事实是我们无法回避的：&lt;strong&gt;没有绝对安全的系统&lt;/strong&gt; ，你有一千种防御对策，攻击者就有一千零一种攻击方式，但不管如何，我们都要尽最大的努力去将攻击者拦截在门外。如果希望深入了解如何发起一个CSRF攻击，可以参考一下这篇文章 从零开始学CSRF。&lt;/p&gt;
&lt;p&gt;作为一名web方向的研发人员，无论你是从事业务逻辑开发还是做单纯的技术研究，了解一些安全方面的知识都是很有必要的，多关注一些安全方向的动态，了解常见的攻击方式以及应对策略，必将在你成长为一名大牛的路上为你“推波助澜”。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Cross-site_request_forgery&quot;&gt;wikipedia: Cross-site request forgery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://shiflett.org/articles/cross-site-request-forgeries&quot;&gt;Cross-Site Request Forgeries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.cnblogs.com/hyddd/archive/2009/04/09/1432744.html&quot;&gt;浅谈CSRF攻击方式&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.jobbole.com/110398/&quot;&gt;防范 CSRF 跨站请求伪造&lt;/a&gt;，首发于&lt;a href=&quot;http://blog.jobbole.com&quot;&gt;文章 – 伯乐在线&lt;/a&gt;。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>想写无Bug的安全代码？看防御性编程的艺术</title><link>https://wuming.si/blog/1234/</link><guid isPermaLink="true">https://wuming.si/blog/1234/</guid><pubDate>Tue, 21 Feb 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;为什么开发者不编写安全的代码？我们在这并不是要再一次讨论「整洁代码」。我们要从纯粹的实用观点出发，讨论软件的安全性和保密性。是的，因为不安全的软件不仅无用，而且还可怕。我们来看看什么是不安全的软件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1996年6月4日，欧洲航天局的 Ariane 5 Flight 501 在起飞后 40 秒被引爆。因为导航软件里的一个 bug，这个价值 10 亿美金的运载火箭不得不自毁。&lt;br&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/d418b3a7c8_7cc829d3gw1f6r1wqy5zdj20b408cwem.jpg&quot; alt=&quot;&quot;&gt;&lt;/li&gt;
&lt;li&gt;1991年2月25日，MIM-104 Patriot（爱国者）里的一个软件错误使它的系统每一百小时有三分之一秒的时钟偏移，导致定位拦截入侵导弹失败。结果伊拉克的飞毛腿导弹击中宰赫兰（沙特阿拉伯东北部城市）的一个美军军营，28 人死亡，100 多人受伤。&lt;/li&gt;
&lt;li&gt;其他案例，请参见《&lt;a href=&quot;http://blog.jobbole.com/104583/&quot; title=&quot;Bug 引发的 18 次重大事故&quot;&gt;Bug 引发的 18 次重大事故&lt;/a&gt;》。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这应该能够说明编写安全软件的重要性了，尤其在特定的环境中。当然也包括其他用例中，我们也应该意识到我们的软件 bug 会导致什么后果。&lt;/p&gt;
&lt;h2&gt;防御性编程初窥&lt;/h2&gt;
&lt;p&gt;为什么我认为在特定种类的工程中，防御性编程是解决这些问题好办法？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抵御那些不可能的事，因为看似不可能的事也会发生。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;防御性编程中有很多防御方式，这也取决于你的软件项目所需的「安全」级别和资源级别。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;防御性编程是防御式设计的一种形式，用来确保软件在未知的环境中能继续运行。防御性编程的实践往往用于需要高可用性、安全性、保密性的地方。—— 维基百科&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我个人相信这种方法适合很多人参与的大型、长期的项目。例如，一个需要大量维护的开源项目。&lt;/p&gt;
&lt;p&gt;我们来探索一下我提出的关键点，来完成一个防御性编程的实现。&lt;/p&gt;
&lt;h2&gt;永远不要相信用户输入&lt;/h2&gt;
&lt;p&gt;设想你总是获取到你不想要的东西。因为像我们说过的，我们预期的是异常情况的出现，（所以）要时刻防备用户输入以及通常会传入你系统的东西，这是你成为一个防御性程序员的方法。试着做到尽可能的严格，确保输入的值就是你所期望的值。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/0ae4be85f5_1-2AwJBEFQ8XcNR7RzlMnTF-fw.png&quot; alt=&quot;&quot;&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;进攻是最好的防守&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;设置白名单而不是黑名单。举个例子，当你验证图像扩展名时，不要检查非法的类型，而是检查合法的类型并排除其他类型。在 PHP 有无数的开源校验库可以让你的工作变简单。&lt;/p&gt;
&lt;p&gt;进攻是最好的防守。共勉&lt;/p&gt;
&lt;h2&gt;数据库抽象化&lt;/h2&gt;
&lt;p&gt;在 &lt;a href=&quot;https://www.veracode.com/directory/owasp-top-10&quot;&gt;OWASP Top 10 Security Vulnerabilities&lt;/a&gt; 排首位的是注入攻击。这意味着有些人（很多人）还没有使用安全的工具来查询数据库。请使用数据库抽象包或库。在 PHP 里你可以使用 &lt;a href=&quot;http://php.net/manual/en/book.pdo.php&quot;&gt;PDO&lt;/a&gt; 来&lt;a href=&quot;http://stackoverflow.com/questions/134099/are-pdo-prepared-statements-sufficient-to-prevent-sql-injection&quot;&gt;确保基本的注入攻击防范&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;不要重复发明轮子&lt;/h2&gt;
&lt;p&gt;你不用框架（或微框架）吗？好吧恭喜你，你喜欢毫无理由地做额外的工作。这并不仅跟框架有关，也意味着你可以方便地&lt;a href=&quot;https://packagist.org/&quot;&gt;使用已经存在的、经过测试的、受万千开发者信任的、稳定的&lt;/a&gt;新特性，而不是你只为了自己从中受益而制作的东西。你自己创建方法的唯一原因是你需要的东西不存在，或存在但不符合你的需求（性能差、缺失特性等等）。&lt;/p&gt;
&lt;p&gt;这就是所谓的智能代码重用。拥抱它吧。&lt;/p&gt;
&lt;h2&gt;不要相信开发者&lt;/h2&gt;
&lt;p&gt;防御性编程与防御性驾驶相关联。在防御性驾驶中，我们假设周围的每个人都可能犯错。所以我们要留意别人的行为。相同概念也适用于防御性编程，我们作为开发者不要相信其他开发者的代码。我们同样也不要相信我们的代码。&lt;/p&gt;
&lt;p&gt;在很多人参与的大型项目中，我们有许多方式编写并组织代码。这也导致混乱甚至更多的 bug。这也是我们需要加强规范代码风格和代码检查的原因，让生活更轻松。&lt;/p&gt;
&lt;h2&gt;编写符合 SOLID 原则的代码&lt;/h2&gt;
&lt;p&gt;这是（防御性）编程最困难的部分——&lt;a href=&quot;https://medium.com/web-engineering-vox/how-to-write-solid-code-that-doesnt-suck-2a3416623d48&quot;&gt;编写不糟糕的代码&lt;/a&gt;。这也是很多人知道并一直在讨论的，但没有人真正关心或将注意力和精力放在实现符合 SOLID 原则的代码上。&lt;/p&gt;
&lt;p&gt;让我们看一些糟糕的例子&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避免：未初始化的属性&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

class BankAccount
{
    protected $currency = null;
    public function setCurrency($currency) { ... }
    public function payTo(Account $to, $amount)
    {
        // sorry for this silly example
        $this-&amp;gt;transaction-&amp;gt;process($to, $amount, $this-&amp;gt;currency);
    }
}

// I forgot to call $bankAccount-&amp;gt;setCurrency(&amp;#39;GBP&amp;#39;);
$bankAccount-&amp;gt;payTo($joe, 100);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，我们需要牢记签发付款前要先调用 setCurrency。这是很糟糕的事情，一个像这样的改变状态的操作（签发付款）不应该分两步来完成，且使用两个公开的方法。我们还可以用很多方法付款，但我们必须只有一个公开的方法来改变状态（对象不应该存在不一致的状态）。&lt;/p&gt;
&lt;p&gt;在这个例子中，我们把它改进，将未初始化的属性封装进 Money 对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

class BankAccount
{
    public function payTo(Account $to, Money $money) { ... }
}

$bankAccount-&amp;gt;payTo($joe, new Money(100, new Currency(&amp;#39;GBP&amp;#39;)));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使它万无一失。不要使用未初始化的对象属性。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;避免：在类的作用域外泄露状态&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

class Message
{
    protected $content;
    public function setContent($content)
    {
        $this-&amp;gt;content = $content;
    }
}

class Mailer
{
    protected $message;
    public function __construct(Message $message)
    {
        $this-&amp;gt;message = $message;
    }
    public function sendMessage(
    {
        var_dump($this-&amp;gt;message);
    }
}

$message = new Message();
$message-&amp;gt;setContent(&amp;quot;bob message&amp;quot;);
$joeMailer = new Mailer($message);

$message-&amp;gt;setContent(&amp;quot;joe message&amp;quot;);
$bobMailer = new Mailer($message);

$joeMailer-&amp;gt;sendMessage();
$bobMailer-&amp;gt;sendMessage();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，Message 是通过引用传递的，两个实例的输出都是 “joe message”。一个解决方案是复制 Mailer 构造函数中的 message 对象。但是我们应该做的是试着使用（不可变的）&lt;a href=&quot;https://en.wikipedia.org/wiki/Value_object&quot;&gt;值对象&lt;/a&gt;，而不是简单可变的 Message 对象。尽可能使用不可变的对象。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

class Message
{
    protected $content;
    public function __construct($content)
    {
        $this-&amp;gt;content = $content;
    }
}

class Mailer 
{
    protected $message;
    public function __construct(Message $message)
    {
        $this-&amp;gt;message = $message;
    }
    public function sendMessage()
    {
        var_dump($this-&amp;gt;message);
    }
}

$joeMailer = new Mailer(new Message(&amp;quot;bob message&amp;quot;));
$bobMailer = new Mailer(new Message(&amp;quot;joe message&amp;quot;));

$joeMailer-&amp;gt;sendMessage();
$bobMailer-&amp;gt;sendMessage();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编写测试&lt;/h2&gt;
&lt;p&gt;这点我们很还需要再说吗？编写单元测试可以帮助你秉承一般的原则，比如高内聚、单一职责、低耦合和正确的对象组合。它不仅帮助你测试小的单元用例，也能测试你组织对象的方式。确实，当测试你的小功能时，你会清晰的看到你需要测试多少情况和需要模拟多少对象，来达到 100% 的覆盖率。&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;希望你喜欢这篇文章。记住这些仅仅是建议而已，由你决定何时、何处以及是否应用。&lt;/p&gt;
&lt;p&gt;感谢阅读！&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.jobbole.com/110123/&quot;&gt;想写无Bug的安全代码？看防御性编程的艺术&lt;/a&gt;，首发于&lt;a href=&quot;http://blog.jobbole.com&quot;&gt;文章 – 伯乐在线&lt;/a&gt;。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>百亿互金平台救火故事</title><link>https://wuming.si/blog/1233/</link><guid isPermaLink="true">https://wuming.si/blog/1233/</guid><pubDate>Mon, 20 Feb 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;多年前，又是周六客服打电话过来，平台官网不能访问，app完全无法打开，客户在QQ群和微信群中各种反馈，说平台是不是跑路了？客服的多条400热线完全被打爆，电话已经接不过来…&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;一直以来总是想以什么方式去记录下自己在互金行业的这段经历，趁着自己还记得清楚，还能找到一些资料原型，一方面可以分享出来供大家参考，但是更重要就是多年以后我可以根据这些文章回忆起来自己的那段激情岁月。&lt;/p&gt;
&lt;p&gt;想了很久但一直没有实施，后来觉得应该从架构的角度来梳理一篇文章，就写《&lt;a href=&quot;http://blog.jobbole.com/110234/&quot;&gt;从零到百亿互联网金融架构发展史&lt;/a&gt;》这篇文章；最后认为只有实战出来的东西以及解决问题的过程，才是工作中最宝贵的经验，应该把它分享出来，在梳理的过程中觉得有三起事故比较有代表性就整理出了下面这三篇文章，本篇文章从整体来回忆一下一路走过来所经历过的救火故事。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://blog.jobbole.com/110152/&quot;&gt;一次生产事故的优化经历&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://blog.jobbole.com/110165/&quot;&gt;一次 DNS 缓存引发的惨案&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://blog.jobbole.com/110225/&quot;&gt;一个脚本引发的血案&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作为一个互联网金融平台，涉及到用户资金，任何的服务（资金）差错用户都是不可容忍的，用户不懂什么是数据库，不知道什么网络不通，就是一会看不到钱在app里面展示都会觉得不安。在已经有很多P2P公司跑路的前提下，用户个个已经被锻炼成为福尔摩斯侦探，每天打开app查看收益，监控着平台一切，甚至半夜升级断网十分钟，也会被用户察觉，直接就发到群里面，更有甚者直接在QQ群或者微信群中你们的技术行不行!&lt;/p&gt;
&lt;p&gt;我们常说的互联网工作经验，一方面是开发经验，但其实更重要的是处理问题的能力。那么处理问题的能力怎么来呢，就是不断的去解决问题，不断的去总结经验，其中处理生产环境中问题的经验更甚,因为在处理生产环境中对个人的压力和临危应变的能力要求最高，你不但需要面临千万个用户反馈，客服不时得催促而且旁边可能就站了N个领导在看着你，一副你行不行的样子要求立刻马上解决问题！这个时候你的操作就非常重要，稍有不慎便会引发二次生产事故。&lt;/p&gt;
&lt;p&gt;说了这么多，只是想说明，生产事故对技术综合能力要求颇高，更是锻炼处理问题能力最佳时机！下面给大家介绍我们从零开发到现在百亿交易量所遇到的几次关键事故，有大有小挑出一些比较有代表性的事件来分享。&lt;/p&gt;
&lt;h2&gt;并发满标&lt;/h2&gt;
&lt;p&gt;公司系统刚上线的时候，其实没有经历过什么大量用户并发的考验，结果公司做了一个大的推广，涌入了一批用户来抢标，共1000万的标的几乎都在10秒之内搞定，大概会有上万左右的用户会同时去抢标，平均每秒大概有千人左右的并发，满标控制这块没有经过大的并发测试，上来之后就被打垮了，导致得结果是什么呢，1000万的标的，有可能到一千零几万满标，也有可能会九百多万就满标，也就说要不就是多了一些，要不就是少了一些，就满标了。&lt;/p&gt;
&lt;p&gt;这就会很尴尬，因为借款用户就借款一千万整，那么多出来的钱既不能给用户退回了，因为用户好不容易才抢上了，无端退了用户也闹；少了也是问题，用户借款一千万，少了几十万也不行，如果短得少了可以想办法找一些有钱的客户直接给买了，多了就必须重新放出来让用户投资，非常影响士气，这个问题困扰了我们有一段时间。&lt;/p&gt;
&lt;p&gt;购买标的流程图，不知道大家是否能根据此图发现问题呢？&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/5ecf0cf72b_buy.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;超募&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为何会产生超募？在最早前的版本中没有使用乐观锁来控制，如果在最后购买的用户一单出现并发，就会出现超募，比如最后剩余30000份的购买份额，因为并发量特别大，可能同时会有十几个用户拿到了剩余30000份余额的可购买额度，有的买1000份、有的买上3000份、有的买上20000份都会驱动满标，所以最后导致了超募。&lt;/p&gt;
&lt;p&gt;针对这个问题，主要是引入了memcached乐观锁的概念（底层主要是&lt;code&gt;cas&lt;/code&gt;、&lt;code&gt;gets&lt;/code&gt;两个命令），在发标的时候存入标的总份额，当用户购买的时候首先去锁定用户购买的份额，因为乐观锁的原因，如果同时有两个用户拿到份额的时候保证只有一个最后可以更新成功（锁定份额），（锁定份额）失败直接返回，这样就保证了在入口的时候就直接屏蔽了部分并发的请求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;少募&lt;/strong&gt;&lt;br&gt;为何产生少募？少募是可能1000万的标的突然到980万就给满标了，这是因为在超募情况下我们完善了代码，用户一进来首先就是锁定购买份额，只有锁定购买份额才能进行下面的流程，如果锁定购买份额失败直接返回，这样虽然保证了在1000万份额在购买初期必须每一个用户只能锁定一份，但是在高并发的情况下，因为购买流程中有十几个分支，每一个分支失败就会退回锁定的份额，这样就会导致这样的现象，就是可能是并发一上来，马上就满标了，过了一会进度就回退回来了。&lt;/p&gt;
&lt;p&gt;少募主要是因为分支失败回退导致的，一方面我们分析了容易导致回退热点，因为在用户抢标的时候会给用户实时的展示标的进度，在很早的版本中直接就是存入到一个标的进度表里面，并且采用了乐观锁，如果并发一高就频繁的更新失败导致回退，因此优化了标的进度这块，直接去掉了标的进度表，实时根据查询来展示标的进度（可以有延迟，有缓存）；另一方面在回退份额的时候在次判断试下memcached的份额和标的的状态，如果份额不为零并且标的状态是满标，马上自动更新状态保证后续用户可以立即购买再次驱动满标。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;做了以上的两种优化后，我们还遇到了其它的一些小问题，在不断的优化过程中，终于稳定下来；在后期版本中将考虑使用MQ队列或者redis队列来处理抢标更合理对用户也更公平一些。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;黑客攻击&lt;/h2&gt;
&lt;p&gt;2015年应该是互联网行业受黑客攻击最多的一年吧，各互金公司都深受其害，其中我就记得网贷之家有一段时间被黑客攻击的太厉害，连续几天网站都无法打开。当然了我们也未能幸免，什么DDOS攻击、SQL注入、寻找系统漏洞等都几乎都经历过了，有的黑客还比较好，应该是出于善意或者展示自己，将漏洞放到乌云上面或者漏洞盒子里面让厂商来修复。但更多的是一些黑产完全就是威胁、敲诈想捞一笔钱，先看看下面这位吧：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f9811b8c8c_hacker.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个家伙潜伏到我们公司的客户群里面，冒充我们的客户代表将头像和资料替换成一样，然后给群里所有的客服让他们发送我们内部的后台地址，想通过这种方式来寻找突破口，当然了这个算是里面的小菜鸟吧。&lt;/p&gt;
&lt;h3&gt;DDOS攻击&lt;/h3&gt;
&lt;p&gt;DDOS攻击我们也是遇到了很多次，确实也没有比较好办法，最后都是通过一些笨办法来尽量的避免，先说说我们的经历吧。有一次我正在敲代码，客服QQ又闪烁了起来，还没来得及打开查看信息，客服的经理电话就直接打了过来，我立刻就有一种不祥的预感，说官网打不开了，后台也登录不了。&lt;/p&gt;
&lt;p&gt;挂了电话，我在本机进行了测试果然不行，立刻准备登录VPN查看服务器各项指标，结果登录不上去，马上上楼找运维经理，他也登录不上，刚准备给机房打电话的时候，机房来电话了，说我们的一个IP正经历着1G多的流量访问，问我们是否正在做什么活动，刚话没有说完就说流量已经到5G，不到一分钟之后流量已经到达18G之多。因为我们的机房和集团公用了一个宽带入口，结果陆续的集团上面反馈他们的网站、服务也都出现了问题，机房方面害怕引起更大的冲击，直接把我们官网对外的IP封掉，集团的其它业务也才慢慢都恢复了过来，我们也紧急的更换了外网IP，重新切换了域名解析才恢复。&lt;/p&gt;
&lt;p&gt;事后我们根据apache分析了日志，流量来自N多个不同的IP地址根本无法应对，也正式因为这次攻击也才让我们领导重视了起来，将我们公司的机房网络层和公司集团彻底分离，这样的话不管那方受到大流量攻击都不会相互影响，我们也想了一些笨办法，因为上次我们更换了外网IP之后攻击也就停止了，那么我们认为肯定是针对我们外网来攻击的，所有我们就多准备了6个外网IP，当监控到对某一个外网进行攻击的时候马上切换到另一个外网地址，就这样跟他们玩，可以起到非常有限的一点作用，如果黑客真的想跟我们玩，这个办法就像是小孩子捉迷藏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;周年庆的DDOS攻击&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;还有一次我们正在做周年庆活动，突然有人在QQ群里面给我们客服说了一句，叫你们的技术负责人来找我，然后我们的网站就挂了，我还保留了当时的一个截图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f9811b8c8c_hacker02.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;完了之后客服就来找我，然后按照往常的策略处理完之后，我根据客服给我的QQ号码加上了那个人，开口就来吓我，我依稀记当年的对话如下：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;黑客：你是平台的技术负责人吗？&lt;br&gt; 我：算是吧&lt;br&gt; 黑客：你信不信我可以让你们官网在5秒之内挂掉?&lt;br&gt; 我：…（沉默，还真害怕又把官网搞挂了）&lt;br&gt; 黑客：你们的官网漏洞很大&lt;br&gt; 我：如果有好的建议请您赐教&lt;br&gt; 黑客：你们的服务器是不是什么防护软件都没有装？&lt;br&gt; 我：…（继续沉默，这会在想不会是那个安全厂商来推广产品的吧，当然我们基础的防护肯定有）&lt;br&gt; 黑客：我们有非常多的肉鸡，想攻击谁，几秒之内肯定搞定&lt;br&gt; 我：…&lt;br&gt; 黑客：我们已经给很多互联网金融行业做了渗透测试，花点钱帮买你们平安，保证以后不会在出事情&lt;br&gt; 我：…&lt;br&gt; 黑客：免费的策略也有很多，比如360、百度云的安全产品可以免费低档10G左右的流量&lt;/p&gt;
&lt;p&gt;……(中间省略)&lt;/p&gt;
&lt;p&gt;黑客：我说了这多，你们也是不是给包烟钱，表示表示。&lt;/p&gt;
&lt;p&gt;……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;后来也和领导进行了商议，坚决不能给他们钱，不能助涨这种嚣张气焰，实在不行就报警！&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;曝光一下当年使用的假QQ号，刚查了下变了个头像和描述，如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f9811b8c8c_hacker01.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;后来我一直在想为什么DDOS攻击总是喜欢根据外网IP来攻击呢，慢慢好像是理解了如果针对域名来攻击的话，那不就是攻击到域名商的服务器了吗,一般域名商比较强大，黑客不太搞的定，也确实没有必要。当然记的前一段时间，某著名域名服务商被攻击，导致国外twitter等著名的互联网公司访问不断到达半天以上，还是很严重的。但是对于我们这些小公司，倒不至于搞这么大的动作。&lt;/p&gt;
&lt;p&gt;到底如何正确的防止DDOS攻击：&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第一种方案，隐藏服务器外网地址，服务器前端加CDN中转，免费的有百度云加速、360网站卫士、加速乐、安全宝等，如果资金充裕的话，可以购买高防的盾机，用于隐藏服务器真实IP，域名解析使用CDN的IP，所有解析的子域名都使用CDN的IP地址。此外，服务器上部署的其他域名也不能使用真实IP解析，全部都使用CDN来解析。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第二种方案，买一些安全产品来进行流量清洗，主要是阿里云、腾讯云这种大厂商提供的一种服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;第三种方案，有很多的防火墙产品声称可以防止Ddos攻击，但是我个人使用感觉效果非常有限。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;SQL注入&lt;/h3&gt;
&lt;p&gt;我们的官网使用的是PHP开发，因为框架比较老旧的原因，存在着一些SQL注入的点，我们发现了一些进行了修补，没想到还是被一些黑客找到了突破点，这块还是比较感谢这些黑客的在漏洞盒子上面提交了bug(如下图)，最后我们根据提示进行了紧急修复，后来我们也在WAF防火墙配置了一些拦截SQL注入的策略，起到双保险的作用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f9811b8c8c_sql.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我一直在想为什么PHP一般比较容易出现SQL注入呢，而Java较少的暴漏出来SQL漏洞的情况，我估摸着有两方面的原因：第一，PHP一般会在前端使用的较多，受攻击的机会更多一些，Java一般做为后端服务攻击的可能性会比较少；第二，PHP框架较多而且很多早期的框架并没有特别考虑SQL注入的情况，Java大量普及了mybaits\hibernate这种orm框架，框架本身对常见的SQL注入有防止的功能，但不是说mybaits/hibernate框架就没有被sql注入的可能，大部分场景下是OK的。另外参数化查询可以有效的避免SQL注入。&lt;/p&gt;
&lt;p&gt;通过一段时间的学习，我发黑客一般先使用工具对网站做整体的扫描类似Acunetix，在根据扫描出来的漏洞做个大概的分析，但是比较深入的漏洞都需要根据网站的业务在进行调整，比如sql注入会根据页面的查询使用sqlmap等工具来进一步的渗透。当然我对这方面还是外行，描述的可能不够清晰。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;其它攻击&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;其它方面的攻击，主要是在业务方面，比如我们当初有一个很小的失误，有一个程序员在H5的小网页中将发送短信验证码返回了前端，最后被haker发现了，利用这个漏洞可以给任意的用户重置登录密码；短信攻击，现在的网站几乎都有发送短信或者短信验证码的功能，如果前端不做校验，haker会随便写一个for循环来发短信，一般系统的短信会进行全方位的防控，比如：1、前端加验证（字符验证码，有的是拖拽的动画）;2、后端根据用户或者IP加限制，比如用户一分钟只可以发送一条短信，忘记密码的短信一天只能发送10条、一个IP地址限制每天只能发送100条短信等。&lt;/p&gt;
&lt;h2&gt;BUG&lt;/h2&gt;
&lt;h3&gt;重复派息&lt;/h3&gt;
&lt;p&gt;15年的某一天看到一个新闻说是陆金所的一个用户发现自己银行里面突然多了很多钱，没过多久又被扣走了，然后收到陆金所那边的解释，说是给用户还本派息的时候程序出现了问题导致还本派息两次，当他们程序员发现了此问题后紧急进行了处理，用户当然闹了呀，就上了新闻，当然陆金所通道能力确实比较强可以直接从用户卡里面扣，当大家都兴致勃勃的谈论这个话题的时候，我却有一股淡淡的忧伤，为什么呢?因为这个错误我们也犯过，具体说就是我搞的，大家可不知道当时的心里压力有多大！&lt;/p&gt;
&lt;p&gt;事情是这样子的，我们使用的第三方支付的扣款接口不是特别的稳定，于是我们前期就对接了两种不通的扣款接口，平时前端投资的时候走一个接口，后端派息或者还本的时候走另外的一个接口，在初期的时候扣款接口不稳定，因此在给用户跑批的时候经常会有个别用户失败，需要手动给失败的用户二次派息。做为一个有志向的程序员当然觉得这种方式是低效的，于是将程序改造了一下，在后端派息的时候当第一种扣款失败的时候，自动再次调用第二种扣款接口进行扣款，当时想着这种方式挺好的，各个环境测试也没有问题，上线之后监控过一段时间也运行稳定。&lt;/p&gt;
&lt;p&gt;当我感觉一切都很美妙的时候，事故就来了，突然有一天客服反馈说有的用户说自己收到的利息感觉不对，好像是多了（真的是太感谢这个用户了），我登录后台看了一下派息的流水复核了一遍，果然利息被重复派了，一股冷水从头而下，把当天所有的用户派息记录和到期记录都进行了检查，影响了70多个用户，导致多派息了6万多元，幸亏只是派息出了问题，如果是到期的话金额会翻N倍，其中70多个人里面有几个进行了提现、几个进行了再次投资，绝大部分用户在我们发现的时候还不知情，金额也没有动。&lt;/p&gt;
&lt;p&gt;怎么处理呢，当然不能直接就动用户的钱了，给每个重复派息的用户打电话，说明原因赠送小礼物，请求谅解后我们把重复派过的利息在次调回来。大部分用户进行了核对之后都还是比较配合的，但是肯定有一些用户不干了，当然也不能怪客户，都是我的原因，有的客户需要上门赔礼道歉，有的客户需要公司出具证明材料，我们的老板亲自给客户打了N个电话被客户骂了N遍，我心里压力可想而知，其中有一个客户特别难缠，各种威胁说既然到了我的账户里面肯定是我的，你们的失误不应该让他来承担，折腾了很久，还是不能怪客户。可能会说有的互联网公司经常出现这种问题后就送给客户了，哎，我们是小公司呀！这个噱头玩不起。&lt;/p&gt;
&lt;p&gt;到底是什么原因呢，事后进行了复盘也给领导做了汇报，平时都是首先进行派息的定时任务，过一个小时之后进行到期的定时任务，当天的派息标的比较多，跑了一个半小时，就导致了派息和到期的两个定时任务同时进行，转账有了并发，第三方支付的接口不稳定给我们返回的失败，其实有的是成功的，就导致了我们进行了二次的扣款尝试引发了此问题。这个事情给我带来了非常大的教训，对于金融扣款的这种事情一定需要谨慎，哪怕付款引发报警之后在人工处理，也不能盲目重试可能引发雪崩效应。&lt;/p&gt;
&lt;h3&gt;杂七杂八&lt;/h3&gt;
&lt;p&gt;还有就是其它一些零碎的问题了，记的有一次对用户的登录过程进行优化，导致有一块判断少了一个括号结果用户在那两个小时内，只要输入账户，任意密码就可以登录了，幸好及时发现这个问题，正是这个问题才导致了我们正式确立了规范的上线流程，为以后的上线制度建定了基础。&lt;/p&gt;
&lt;p&gt;还有一次我们在模拟用户投资一种标的时候，留了一个入口通过http就可以调用，测试也没有问题，有一天正好给领导演示呢，就在次用http请求的方式在浏览器执行了一下，前端就会看到自动投标的过程，因为生产的数据有点多，投标的过程有点长，我们为了加快进度，找了好几个人同时来执行这http请求，导致最后出现了问题，最后发现写测试脚本的这个同事根本就没有考虑并发的情况，才导致出现了问题。&lt;/p&gt;
&lt;p&gt;也做了很多的活动，记得做一个网贷之家的一个活动的时候，活动上线比较紧张，我们团队曾经连续工作超过30个小时（一天一夜再一天），当天晚上我2点左右写完程序，测试从2两点测试到早上9点，最终确认没有任何问题，才进行投产。半夜公司没有暖气，我们实在冻的不行了，就在办公室跑步，从这头跑到那头，第二天上线之后，又害怕出现问题，监控了一天，确认没有任何问题，才到下午正常下班回家，那时候真是激情满满呀。&lt;/p&gt;
&lt;p&gt;说到做活动肯定少了羊毛党，说哪一家互金公司没有遇到过羊毛党那很少见，而且现在的羊毛党规模简直逆天了，我们用户里面就有一个羊毛党在两三天之内邀请了六七千位用户，如果说邀请一个用户送1元，那这个用户就可以搞几千块一次，而且有很多专业的网站、QQ群、微信公共账号都是他们的聚集地，那天那个平台有活动门清，他们写的淘羊毛操作手册有时候比我们官网的帮助文档还清晰，所以做活动的时候要考虑特别周全，各种限制，有封定、有预案、讲诚信，只要是符合我们活动规则的坚决按照流程走。&lt;/p&gt;
&lt;p&gt;还有一个有趣的事情，app推送，一次我在公交车上就看到xx盒子app弹出hhhhh的推送，这个事情我们也搞过，因为在调试的时候生产和测试就差了一个参数，有时候开发人员不注意就把生产参数部署到uat环境了，测试一发送就跑到生产了，这方面只能严格流产管理来防止了。&lt;/p&gt;
&lt;p&gt;其实还很多问题：mongodb集群和mysql的同步出现的一些状况、后台大量数据查询下的sql优化、golang使用mapreduce碰到的问题… 限于篇幅这里就不一一清晰的描述了。&lt;/p&gt;
&lt;p&gt;其实每次的出现问题都是对团队一次非常好的锻炼机会，通过发现问题，定位问题，解决问题，再次回过头来反思这些问题；重新梳理整个环节， 举一反三避免下次再次出现类似的问题。正是因为经历这些种种的困难、考验才让团队变的更强大更稳定，也更体现了流程的重要性，更是避免再次发生类似问题。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;古代对将军的要求是，心有万马奔腾而过，而面平静如湖水可照镜，在互联网行业对大牛的要求也同如此，特别是技术的负责人，在面对生产事故的时候，一定首先是安抚同事，静下下心来找到问题本质在去解决，而不是不断去施加压力催促解决，重压之下很多心里承受能力稍弱的队友，更加慌乱而不利于解决问题或者引发二次事故。&lt;/p&gt;
&lt;p&gt;在看淘宝双十一视频中，有一段特别受到感触，在双十一初期，虽然技术团队做了很多的准备，但是在零点过后流量瞬间涌入，服务被打垮，部分用户投诉刷新不出网页，紧接着隔壁同事也都反馈网站打不开，在大家都在慌乱中，xx一拍桌子大喊一声，大家都别动，三分钟之后再说，过了几分钟之后服务慢慢部分恢复了正常。后来回忆说，当时虽然服务瘫痪，但是监控还是有部分得业务成功，说明系统并没有被压垮，而此时的任何操作都有可能引发更大的问题，从此之后此人一战成名，成为阿里大将。&lt;/p&gt;
&lt;p&gt;互联网平台发展大抵都会经历三个阶段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1、上线初期，此阶段问题最为繁多，生产事故不断，系统快速迭代优化。有人说为什么不测试到完全没有问题在投产吗？说实话在互联网行业这个很难，第一小公司很难做到生产环境和测试环境一致，成本太高；时间紧迫，一般都是很短的时间内要求上线，上线之后在快速迭代。另外互联网本就是一个快速试错的行业，错过半年时间可能风口早过；&lt;/li&gt;
&lt;li&gt;2、发展期，此阶段主要业务模式已经得到验证，系统出现问题的频繁度较少，低级错误减少，但此时是用户量和交易量不断爆发的时候，对系统性能、高并发的要求又上来了，所以此时出现的问题大多都是性能的问题；&lt;/li&gt;
&lt;li&gt;3、成熟期，发展期过后系统相对比较平稳，用户量和交易量都已经慢慢稳定下来，生产问题越来越少，出现问题几乎都是细小的bug，这个阶段也是公司最忽略技术得阶段，恰好我们公司就处于这个阶段，在这个阶段就需要静下心里，组织架构升级，补齐在初期和发展起所欠的技术债务，做好公司在升下一个量级的技术准备。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;所有的这些问题几乎都集中在14年底到15年初的这个阶段，15年后半年开始到现在平台慢慢稳定了下来，到现在几乎没有再出现过类似的问题，也因为几乎都是两年前的事情，有很多记的不是特别清楚了，写的比较粗糙望见谅。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.jobbole.com/110255/&quot;&gt;百亿互金平台救火故事&lt;/a&gt;，首发于&lt;a href=&quot;http://blog.jobbole.com&quot;&gt;文章 – 伯乐在线&lt;/a&gt;。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item><item><title>如何用 OllyDbg 的跟踪功能分析虚拟机保护</title><link>https://wuming.si/blog/1232/</link><guid isPermaLink="true">https://wuming.si/blog/1232/</guid><pubDate>Sat, 14 Jan 2017 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;虚拟机保护已经是现代保护壳不可缺少的一环，虽然逆向方也发展出各种插件帮助分析，但只针对特定某款，通用性的方法却不多见。我总在想，既然虚拟机的结构是固定的，如果有一款工具能够记录指令流，那么按图索骥，也许能发展出一套通用的分析方法来。其实OD（OllyDbg）就有记录指令流的功能，叫跟踪（trace），也许是效果不好或者操作不便，用的人甚至知道的人不多。先介绍下怎么用。&lt;/p&gt;
&lt;p&gt;OD的跟踪功能原理很简单，就是每一步都自动下单步断点，然后记录断下来的指令信息。这项功能涉及到几项设置，第一项是缓存大小，不难想象，跟踪得到的这一些列的指令记录是需要占地方存储的，占多大可以设置，位置在调试选项（Debugging options）-&amp;gt;跟踪（Trace），如图1。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/eb2510c4c8_1a016cb4c1c92a441a0fd4ab9e6bd879.jpg&quot; alt=&quot;file0001&quot;&gt;&lt;/p&gt;
&lt;p&gt;图1&lt;/p&gt;
&lt;p&gt;第一项就是缓存的大小，内存允许的话，自然是多多益善，毕竟缓存越大，允许记录的信息越多。第二项是记录的内容，跟踪会自动记录地址模块等信息，此外可以选择是否记录指令、ESP和标志位的信息。设置位置紧接着缓存大小，见图2，可以按需勾选，本文只需要记录指令即可。最后一项是在调试（DEBUG）菜单中打开Trace。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/4e35159f3f_461237d368474b64ecfb6a277b900835.jpg&quot; alt=&quot;file0002&quot;&gt;&lt;/p&gt;
&lt;p&gt;图2&lt;/p&gt;
&lt;p&gt;现在Trace已经设置完毕了，按下Ctrl+F12，查看Trace窗口，应该已经开始记录执行过的指令。否则请检查前述设置和操作是否正确。&lt;/p&gt;
&lt;p&gt;那么，虚拟机保护要怎么入手分析呢？前面我提到，虚拟机是有固定结构的，既然要分析，那对应的找到这些结构应该就可以了。传统保护虚拟机的结构其实很简单，大致可以看成一只章鱼，有三个部分，分别是init（头），Dispatch（身）和Handle（触须），如图3：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/f248e63eb1_a0ec974c5655b05c9920257d38c5ff95.jpg&quot; alt=&quot;file0003&quot;&gt;&lt;/p&gt;
&lt;p&gt;图3&lt;/p&gt;
&lt;p&gt;Init主要完成虚拟机初始化工作，例如申请内存填写初始值之类，每次进入虚拟机，这个“头部”通常只执行一次。Dispatch是虚拟机的主体，可以看成一个主循环，它是每一条虚拟机指令的开始之处，也是结束之处，负责读取虚拟机指令，进入具体handle解释等工作。Handle就是虚拟机的“指令”了，实际完成各项虚拟机指令的功能。 我曾写过一篇《基于虚拟机的软件保护技术》较为详细的介绍过虚拟机保护技术，对基本结构还不太熟的同学，此文会对上述概念有更详细的说明。&lt;/p&gt;
&lt;p&gt;现在，我们就要在具体的软件中找这只“章鱼”了。以一个CrackMe为例，首先清理所有断点，打开Trace，Ctrl+F12跟踪步过运行，看到程序跑起来了，F12暂停，看Trace的窗口如下（图4）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/8ca497f3c3_54c7c30af4f097638622028ca5ae901b.jpg&quot; alt=&quot;file0004&quot;&gt;&lt;/p&gt;
&lt;p&gt;图4&lt;/p&gt;
&lt;p&gt;记录是从下往上看的，可以看出，在程序空间的最后一条支流，是00401534的一个call，调用了DialogBoxInDirectParamA，这是一个调出系统对话框的API，其中有一个参数DlgProc用来指明消息回调函数的位置，我们直接在反汇编窗口查看这个API，发现回调函数是0x401572（图5）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/b5846b312e_d965452de3ca2c5d42de23efbedbbbf7.jpg&quot; alt=&quot;file0005&quot;&gt;&lt;/p&gt;
&lt;p&gt;图5&lt;/p&gt;
&lt;p&gt;0x401572处代码不长，有好几条Call，但大部分都是系统Call，只有一处调用了程序空间的函数，这个函数就是虚拟机的入口。到这里，我们对虚拟机的分析的工作才刚刚开始。&lt;/p&gt;
&lt;p&gt;首先对虚拟机的入口下断，然后重新运行程序。目的是保证能够正确找到init。现在应该端在虚拟机的入口处，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/b5846b312e_d125fc8a809b6de97cbdba3d35762398.jpg&quot; alt=&quot;file0006&quot;&gt;&lt;/p&gt;
&lt;p&gt;图6&lt;/p&gt;
&lt;p&gt;这是个非常简单的虚拟机，有经验的同学也许可以一眼就看出来图6包含了Init和Dispatch分别在哪里。当然也可以用Trace快速找出虚拟机的各个结构。现在去掉断点，打开Trace，Ctrl+F12跟踪步过，这时程序会跑起来，多点击几下按钮，目的是让主要分支得到更充分的执行（即增加获得执行的次数），然后F12暂停。回到Trace窗口，对着任意一行程序空间的指令点击右键，选择模块统计，结果如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://blog.wo.ai/wp-content/plugins/wp-o-matic/cache/858acb4c47_a4069cd955169adc70fa5ef0bc894680.jpg&quot; alt=&quot;file0007&quot;&gt;&lt;/p&gt;
&lt;p&gt;图7&lt;/p&gt;
&lt;p&gt;统计是以代码段来划分的，第一栏显示的是这段代码在刚才的跟踪执行中执行的次数，第二栏显示了某个代码段的首地址。我们先找执行了一次的指令首地址。可以找到第5行的地址就是虚拟机的入口地址，点击在反汇编窗口跟随，可以看到这段代码是从0x00401060到0x004010B9，这就是init：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00401060 $ 55 push ebp
00401061 . 8BEC mov ebp, esp
00401063 . 81C4 D0FEFFFF add esp, -0x130
00401069 . C745 E4 00000&amp;gt;mov dword ptr [ebp-0x1C], 0x0
00401070 . C745 E8 00000&amp;gt;mov dword ptr [ebp-0x18], 0x0
00401077 . C745 F1 00000&amp;gt;mov dword ptr [ebp-0xF], 0x0
0040107E . C645 FD 00 mov byte ptr [ebp-0x3], 0x0
00401082 . C645 FE 00 mov byte ptr [ebp-0x2], 0x0
00401086 . C745 F5 00000&amp;gt;mov dword ptr [ebp-0xB], 0x0
0040108D . 8D85 D0FEFFFF lea eax, dword ptr [ebp-0x130]
00401093 . 8945 F1 mov dword ptr [ebp-0xF], eax
00401096 . 8B45 14 mov eax, dword ptr [ebp+0x14]
00401099 . 8945 E0 mov dword ptr [ebp-0x20], eax
0040109C . 8B45 08 mov eax, dword ptr [ebp+0x8]
0040109F . 8945 D0 mov dword ptr [ebp-0x30], eax
004010A2 . 8B45 0C mov eax, dword ptr [ebp+0xC]
004010A5 . 8945 D8 mov dword ptr [ebp-0x28], eax
004010A8 . C745 DC 00000&amp;gt;mov dword ptr [ebp-0x24], 0x0
004010AF . C745 D4 00000&amp;gt;mov dword ptr [ebp-0x2C], 0x0
004010B6 . 8B45 10 mov eax, dword ptr [ebp+0x10]
004010B9 . 8945 EC mov dword ptr [ebp-0x14], eax
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着找Dispatch，刚才说过，它既是虚拟机指令的开始，又是结束，它得到的执行次数一定也最多。可以看到第三行的0x004010B9，这个地址在虚拟机入口地址之后，执行次数最多，同样的办法可以看到这段代码的终止位置是0x004010D9：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;004010BC &amp;gt; /FF45 EC inc dword ptr [ebp-0x14]
004010BF . |8B45 EC mov eax, dword ptr [ebp-0x14]
004010C2 . |8A00 mov al, byte ptr [eax]
004010C4 . |8845 F0 mov byte ptr [ebp-0x10], al
004010C7 . |B8 00204000 mov eax, 00402000
004010CC . |0FB65D F0 movzx ebx, byte ptr [ebp-0x10]
004010D0 . |C1E3 02 shl ebx, 0x2
004010D3 . |03C3 add eax, ebx
004010D5 . |FF20 jmp dword ptr [eax]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后是找这次执行虚拟机用到的handle。这个不难，虚拟机入口地址之后的代码段除了init和dispatch，其它都是handle，所有执行过的handle都会在里面出现。当然了，某条handle的具体作用，以及没有执行过的handle，就只能靠人肉分析了。还有就是，就分析虚拟机保护来说，了解执行了哪些handle，以及哪些handle更常用，这些信息都是十分有用的。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://blog.jobbole.com/109803/&quot;&gt;如何用 OllyDbg 的跟踪功能分析虚拟机保护&lt;/a&gt;，首发于&lt;a href=&quot;http://blog.jobbole.com&quot;&gt;文章 – 伯乐在线&lt;/a&gt;。&lt;/p&gt;
</content:encoded><category>Uncategorized</category></item></channel></rss>