Baked Sweet Potatoes

使用空气炸锅做出好吃的烤红薯,香甜软糯、皮肉分离,记录一下。

材料

  • 红薯:约200g/个,选“蜜薯”品种的长条型

制作

  1. 红薯洗干净(去除表面泥土),清水浸泡15分钟(避免烤出来太干)。
  2. 取出红薯,用叉子随意扎洞。
  3. 选用带架子的空气炸锅,倒入半碗水,架子上平铺放入红薯(不要堆叠)。
  4. 200摄氏度烤20分钟,翻面后再200摄氏度烤20分钟。

总结

  • 空气炸锅选用带架子的,并且可以装水的。
  • 红薯最多铺满空气炸锅,不能堆叠起来。
  • 红薯品种不会买的话,问下摊主(老板)。
  • 长条型的红薯,主要是好烤、容易熟。

Redmi Note 12 Turbo Flashing Lineageos 22

刷上LineageOS 22.2(Android 15)一段时间了,日常使用也基本没大问题。这里记录一下相关信息。

1. 关于LineageOS

LineageOS是一个开源的Android定制ROM,是CyanogenMod(开源的Android定制ROM始祖)的继承者,是很多定制ROM的基础。

LineageOS的红米Note 12 Turbo(Marble)移植版,一开始是非官方版本。后来合并到官方版本,才敢尝试使用。

该ROM的优点:

  • 开源、精简、稳定、每周更新。
  • 体验接近Android原生系统。

目前缺点:

  • 没有自带支持KernelSU。目前刷第三方Kernel解决。
  • 微信和支付宝都不支持指纹支付。据说可以使用Meow模块解决,但未尝试。

2. 刷机准备

下载相关文件:

文件说明:

  • 一般只要刷LineageOS本身即可。建议下载zip包,并使用Recovery进行刷ROM。

    参考文件名: lineage-22.2-20250930-nightly-marble-signed.zip

  • OrangeFox Recovery是为了方便刷ROM和后续管理、升级。

    参考文件名: OFRP-R11.1_7_RECOVERY-Beta-marble.img

  • 刷Kernel是为了支持KernelSU。如果使用Magisk或者不需要root,可以不刷。Glow是编译好的版本,目前建议刷v4.1.2,因为v4.2有bug。

    参考文件名: Glow-Kernel-v4.1.2.zip

  • Gapps采用MindTheGapps,是LineageOS官方推荐,若不需要可以不刷。

    参考文件名: MindTheGapps-15.0.0-arm64-20250214_082511.zip

3. 刷机说明

  • 先做好数据备份。
  • 建议双清(格式化system和data分区),特别是已刷了其它ROM的情况。
  • 进入fastboot模式,刷OrangeFox Recovery。
    • 参考命令sudo fastboot flash recovery OFRP-R11.1_7_RECOVERY-Beta-marble.img
  • 进入Recovery模式,把LineageOS和后续需要刷机的文件,复制到/sdcard/rom目录下。
  • 在OrangeFox Recovery里选择LineageOS的ROM文件,并刷入,然后重新进入Recovery模式。
    • 在A/B分区模式下,LineageOS会自动刷入当前未激活的分区。例如,原来的ROM使用A分区,刷入LineageOS后会刷入B分区,并激活B分区。
    • 在A/B分区模式下,Recovery会在ROM里面。ROM的分区切换并激活后,需要运行新刷入ROM的Recovery。
    • OrangeFox Recovery默认在刷入ROM后,自动替换其自带的Recovery。
  • 在OrangeFox Recovery执行后续输入,包括Kernel、Gapps。
  • 最后清除缓存,并重启进入系统。

4. 问题

4.1. 不支持指纹支付

目前唯一问题是,“支付宝”和“微信”不能开启指纹支付。后面解决后再更新此文档。

4.2. A/B分区相关指令

手机进入Fastboot模式,在电脑端执行如下命令:

# 查看当前激活的分区
fastboot getvar current-slot

# 切换到a分区
fastboot set_active a

Using a Router as a Switch

遇到“主路由”的有线网口不够用时,可以把“旧路由”作为交换机使用,实现扩展有线网口。如果“旧路由”是无线路由,还可以作为AP(Access Point)使用。

参考:

设置概述:

  • 重置(reset)一下“旧路由”。
  • “旧路由”设置静态IP,并与“主路由”在同一网段。
  • “旧路由”关闭DHCP服务,或者设置为DHCP客户端。
  • 重启“旧路由”。
  • “旧路由”通过LAN口接入“主路由”,其他设备通过LAN口接入“旧路由”。
  • 此时如果“旧路由”是无线路由,只要开启WiFi,就是AP模式。

注意:

  • 不要使用太老的旧路由,设置后可能使用集线器模式,即数据广播,会导致整个网络堵塞。
  • 尽量避免把“旧路由”接入在另一个交换机,可能会导致“旧路由”不能使用。

Sago Soup

西米露(Sago soup)是一道很普通平凡,但也很难做的甜品。既要把西米(Sago)煮至透明,也要保持Q弹口感,是技巧所在。

材料

  • 西米:50g,小粒(约3人份量)
  • 水:1500ml(宜多不不宜少)

制作

  1. 1500ml的水,大火烧至沸腾(约3分钟),倒入西米。再大火煮3分钟,熄火焖10分钟,捞出沥水。
  2. 1500ml的水,大火烧至沸腾(约3分钟),倒入西米后,转小火煮15分钟,熄火焖15分钟,捞出沥水。
  3. 如果作为冷甜品享用,建议把西米倒进冰水,过一下“冷河”(让其口感更加Q弹),沥水备用。
  4. 享用时,可以搭配糖水、牛奶、椰奶、红豆沙等,冷热皆可。当然,作为茶饮的小料,也不错。

总结

  • 选小粒的西米,更容易熟。
  • 西米不要泡水,而是直接倒入沸腾的水去煮。
  • 水要够多,让西米有足够的空间翻腾,避免粘锅。
  • 采用煮+焖,避免煮烂。

Electric Mosquito Swatter Repair

N年前,第一次在亲戚家接触到“电蚊拍”,就觉得是个神器,并玩上了一天。后来家里买过几个,电力都不持久。拆开一看,里面是个“三无”电池,据说是铅酸蓄电池,不耐用的。于是找了教程,改装为使用18650电池,且带USB口可充电。

参考教程:电蚊拍啪啪啪不给力,直接改18650电池,加锂电充放电保护板

1. 工具和材料

  • 充放电模块
    • 参考型号:4057锂电池充电模块,18650充电器,TYPE-C接口
    • 参考价:3.90 CNY,包邮
  • 18650电池
    • 参考型号:亿纬 ICR 18650/26V,2550mAh
    • 参考价:5.80 CNY,包邮
  • 工具或材料
    • 电烙铁
    • 焊锡丝
    • 助焊膏
    • 电线两条,最好是红、黑两色

2. 过程

2.1. 电池焊接电线

把电池两极焊上电线,一般红色正极,黑色负极。注意,电烙铁不能接触电池太久!导致电池温度过高的话,轻则损坏电池,重则引起电池爆炸。如果是新手,建议找专业人士焊接。或者改用免焊接的电池盒。

参考视频:「盘丝法」无伤焊接锂电池,3秒搞定,用电烙铁即可

焊接过程简要说明:

  • 电线焊接处,挂上助焊剂,焊上一点焊锡丝。
  • 用砂纸打磨电池的焊接处,依次放上一点助焊剂、一小段盘好的焊锡丝、电线焊接处。
  • 电烙铁调至最大功率(例如450°C),分别点一下电线焊接处左边、右边和中间。注意,每次不能超过1秒,看见焊锡丝熔化即可松开。
  • 焊接牢固即完成,再以此焊接电池的另一端。

2.2. 充放电模块焊接

按照接口提示焊接即可:

  • OUT+,接电蚊拍原来电池的正极线。
  • BAT+,接电池正极线。
  • BAT-,接电池负极线。
  • OUT-,接电蚊拍原来电池的负极线。

2.3. 组装

主要是把电池和充放电模块塞进电蚊拍内部,把充放电模块的USB口露出来。由于不同的电蚊拍,内部结构不同,没有统一的做法。另外,一定要注意电池的安全,包括保护好两极、避免尖利的东西碰到等。

3. 总结

焊接电池时,确实捏了把汗,一是不容易焊上,二是怕电池温度过高。最后总算焊好,电蚊拍也恢复可用了。

Fiil Key Repairing

用了大概一年的Fiil Key蓝牙耳机,“右耳”突然坏了。前一晚还能用,第二天就不工作(灯不亮、不能连接)。于是计划买一个“右耳”。

维修方案:

  1. 换电池。查了下,说是一般电池坏了,换了就好。但这是2022年上市的产品,而且自己维修有风险,还不如直接换一个。
  2. 换个“右耳”。但是查到的信息是,可能会遇到双耳的固件版本(1.5.0.0与1.4.0.0)不一致,导致不能配对互连。

纠结了一下,选择了方案2。“右耳”到手后,“双耳”不能互连,但能各自单独使用。幸好找到了配对互连的操作视频:Fiil Key 重置步骤视频

“双耳”配对互连的操作总结:

  1. 两个耳机充满电。防止操作过程没电。
  2. 两个耳机从充电取出,白灯闪烁,即处于待配对状态。
  3. 两个耳机同时长按触控区域8秒,看到白灯亮3秒后熄灭,即可松手。此时,两个耳机都进入关机状态。
  4. 两个耳机同时长按触控区域15秒左右,直到白灯快速闪烁,即可松手。此操作使耳机开机并进行配对互连。
  5. 两个耳机放回充电仓,可以看到白灯都在同时闪烁,表示配对成功。此时用手机蓝牙进行配对,只看到一个Fiil Key。
  6. 手机蓝牙连上Fiil Key后,使用“Fiil+”app,可以看到两个耳机的固件版本。

Deploy Certbot Using Docker Compose

服务器上,使用Docker Compose部署和管理各种服务,体验相当好。部署Certbot,申请和更新免费SSL证书,有些不同,记录一下相关操作。

首先,很多DNS服务商已经在提供DNS服务的同时,提供了免费SSL服务。如果这种服务能解决SSL免费证书的需求,就没必要部署Certbot了。例如Cloudflare、DNSPod等。

使用Docker Compose部署Certbot的特点:

  • Docker Compose部署和管理容器,比较清晰和方便。
    • 比如直接运行容器(即使用docker run命令运行容器),能记录容器的配置,方便查看和维护。
  • Certbot容器无需一直保持运行,只需启动一下,申请或续签SSL免费证书。
    • 只要得到SSL免费证书的文件,即可停止运行。
    • 需要设置定时运行,执行续签证书。
  • 需要使用SSL免费证书的服务(例如Nginx),需要把证书文件mount到对应的容器。

1. Docker Compose部署Certbot

创建好部署目录,以下例子采用/opt/docker/certbot

先创建Cloudflare的认证配置文件:/opt/docker/certbot/etc/letsencrypt/certbot-dns-cloudflare-credentials.ini。该文件格式如下:

dns_cloudflare_api_token=<your-cloudflare-api-token>

注意,/opt/docker/certbot/etc/letsencrypt最好设置拥有者为root用户,并保证只有root可读写,提高安全性。

sudo chown -R root:root /opt/docker/certbot/etc/letsencrypt

再创建docker-compose.yaml文件,该文件内容如下:

# https://hub.docker.com/r/certbot/dns-cloudflare
services:
  certbot:
    image: certbot/dns-cloudflare:latest
    volumes:
      - ./etc/letsencrypt:/etc/letsencrypt
      - ./log:/var/log/letsencrypt
    command: certonly --agree-tos --non-interactive -m xxx@abc.com --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/certbot-dns-cloudflare-credentials.ini -d example.com,*.example.com

networks:
  default:
    name: default-net
    external: true

注意:

  • 由于采用Cloudflare的DNS服务,所以采用dns-cloudflare的Docker镜像。
  • --agree-tos,同意ACME订阅协议(the ACME server’s Subscriber Agreement)
  • --non-interactive,非交互式运行,即运行过程中不需要询问用户输入。
    • 当客户端发现参数缺失时会给出相应的说明。
  • -m,即--email,设置域名所有者的EMail。
  • -d,设置SSL证书相关域名。
    • 多个域名可以设置多个-d实现。
    • 也可以1个-d设置多个域名,但域名之间需要英文逗号(,)分隔,并且主域名放第一位,子域名随后。
    • 支持泛域名,即*.+主域名,避免设置多个子域名。
  • 最后的networks可以不设。
    • 这里设置了默认加入网络default-net,方便通过网络访问其它容器。

最后启动。由于无需一直保持运行,所以不用添加-d参数。如果无错误输出,则成功运行。

docker compose up

2. 使用SSL证书

对于HTTP服务的容器,只需把SSL证书文件mount到相关容器即可。

比如Nginx,其docker-compose.yamlvolumes配置如下:

volumes:
  - ../certbot/etc/letsencrypt/archive:/etc/letsencrypt/archive:ro
  - ../certbot/etc/letsencrypt/live:/etc/letsencrypt/live:ro

对应网站的server配置SSL证书:

server {
  server_name example.com;
  listen 443 ssl;
  ...
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ...

3. 自动续签

这里使用Systemd Timer实现定时自动renew证书。

创建服务管理单元,文件/opt/docker/certbot/deploy_conf/certbot-renew.service

[Unit]
Description=Certbot renew
Requires=network-online.target

[Service]
Type=oneshot
ExecStart=docker compose -f /opt/docker/certbot/docker-compose.yaml start 

[Install]
WantedBy=multi-user.target

创建定时器单元,文件/opt/docker/certbot/deploy_conf/certbot-renew.timer

[Unit]
Description=Auto run certbot renew
Requires=certbot-renew.service

[Timer]
# 周一和周四的凌晨3点执行
OnCalendar=Mon,Thu *-*-* 03:00:00
# 在5分钟以内,随机延迟执行
RandomizedDelaySec=5min
Unit=certbot-renew.service

[Install]
WantedBy=multi-user.target

部署:

sudo ln -s /opt/docker/certbot/deploy_conf/certbot-renew.service /etc/systemd/system/certbot-renew.service
sudo ln -s /opt/docker/certbot/deploy_conf/certbot-renew.timer /etc/systemd/system/certbot-renew.timer
sudo systemctl daemon-reload 
sudo systemctl enable certbot-renew.timer
sudo systemctl start certbot-renew.timer

注意:这里没实现自动续签成功后(即SSL证书文件更新后),自动更新Nginx。

Simplest HTTP Server for MicroPython

开发过一个项目,在刷了MicroPython的ESP32C3开发板上,运行一个HTTP服务和一个定时提交数据的自动任务。一开始,HTTP服务经常不定时宕掉。后来才发现,是内存不足。由于定时提价数据的自动任务,是通过HTTP协议POST数据,不能再精简。那么,只能对这个HTTP服务“开刀”。

这个HTTP服务,一开始是用TinyWeb开发,升级MicroPython v1.24.1后,改为使用microdot。两个框架的GitHub项目如下:

这些Web框架,功能比较全面,自然占用的内存就比较大。想要最大限度精简内存占用,最好的做法就是不使用Web框架开发,从底层实现HTTP服务。参考了以下代码:

最后编写出可以在MicroPython v1.24.1上运行的HTTP服务:

from micropython import const
import socket
import time
import logging

_logger = None
_RESP_STATUS = {
    200: 'HTTP/1.1 200 OK\n'
    ,302: 'HTTP/1.1 302 Found\n'
    ,400: 'HTTP/1.1 400 Bad Request\n'
    ,403: 'HTTP/1.1 403 Forbidden\n'
    ,500: 'HTTP/1.1 500 Internal Server Error\n'
}

def _getRequest(reqRaw: byte):
    '''获取整理后的请求数据
    
    Args:
        reqRaw (byte): 请求的原始字符串。
    
    Returns:
        dict: 整理后的请求dict对象。method:请求方法;path:请求Path;protocol:请求协议;form:POST的form数据。
	'''
    charset = 'utf-8'
    rArr = reqRaw.split(b'\r\n')
    r = str(rArr[0], charset).split()
    r = {'method':r[0].upper(), 'path':r[1], 'protocol':r[2]}
    if r['method'] == 'POST' and b'Content-Type: application/x-www-form-urlencoded' in rArr:
        r['form'] = dict(
            [tuple(item.split('=')) for item in str(rArr[-1], charset).split('&')]
        )
    return r

def _getPage(html: str, redirectUrl: str = '', redirectSecond: int = 0) -> str:
    '''生成HTML页面。主要是把传入的HTML数据,套上模板,或者实现自动跳转功能
	
    Args:
        html (str): HTML数据。
        redirectUrl (str): 跳转URL。
        redirectSecond (int): 等待指定秒数后自动跳转。

    Returns:
        str: 添加模板后的HTML页面
	'''
    redirectMeta = '' if len(redirectUrl) <= 0 else f'<meta http-equiv="Refresh" content="{redirectSecond}; URL={redirectUrl}" />'
    return f'<html><head><meta name="viewport" content="width=device-width,initial-scale=1">{redirectMeta}<title>Simple Web</title><link rel="icon" href="data:;base64,="></head><body>{html}</body></html>'

def _pageIndex(request):
    '''处理访问首页的请求。
	'''
    html = _getPage('''
<div style="text-align:center;">
<h1>Hello!</h1>
<p>Page index</p>
</div>
''')

    # Send response
    # Response header set boot status, for Shell script reading
    return html, 200

def _pageTips(request):
    '''处理提示页的请求,会在5秒后自动跳转首页。
	'''
    html = _getPage('<div style="text-align:center;">Forbidden.</div>', redirectUrl = 'index', redirectSecond = 3)
    return html, 200

# Start web server application
def run(logger: logging.Logger):
    '''启动HTTP服务

    Args:
        logger (logging.Logger): 传入logger对象。
    '''
    global _logger
    _logger = logger

    # Set up socket and start listening
    try:
        addr = socket.getaddrinfo(conf.WEB_HOST, conf.WEB_PORT)[0][-1]
        s = socket.socket()
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind(addr)
        s.listen()
        _logger.info(f'Web server on {addr}')
    except Exception as e:
        _logger.error(f'Web server start failed: {e}')
        return

    # Main loop to listen for connections
    while True:
        try:
            conn, addr = s.accept()
            _logger.debug(f'Request from: {addr}')
            
            # Receive and parse the request
            reqRaw = conn.recv(1024)
            _logger.debug(f'Request content: {reqRaw}')
            req = _getRequest(reqRaw)
                       
            # Process the request
            if req['path'] in ['/','/index'] and req['method'] == "GET":
                respHtml, respStatus, respHeader = _pageIndex(req)
            elif req['path'] == '/tips' and req['method'] == "GET":
                respHtml, respStatus, respHeader = _pageTips(req)
            else:
                # Url path of request is not support
                _logger.debug(f"Path {req['path']}, 400")
                conn.send(_RESP_STATUS[400])
                conn.close()
                continue

            # Send the HTTP response and close the connection
            conn.send(_RESP_STATUS[respStatus])
            conn.send('Content-Type: text/html\n')
            if respHeader is not None:
                for k,v in respHeader.items():
                    conn.send(f'{k}: {v}\n')
            # Respone set no cache
            # Notice: Two newline characters ("\n") indicate the end of the header.
            conn.send('Cache-Control: no-cache\nExpires: 0\n\n')
            conn.write(respHtml)
        except Exception as e:
            _logger.error(f'WebServer Exception: {e}')
            try:
                conn.send(_RESP_STATUS[500])
            except:
                pass
        finally:
            try:
                if conn is not None:
                    conn.close()
            except:
                pass

注意:

  • 关于节省内存
    • 首先不要import没用到的库。甚至可以import指定库并用完后,执行del
    • 使用byte类型,代替str类型。
    • 适当的时候,手工清理内存,即执行gc.collect()
    • py文件编译为mpy,加载mpy文件更快更省内存。
  • 关于稳定性
    • 使用try ... except捕获所有异常,避免程序因异常而意外终止。

Use Systemd Timers Instead of Cron Jobs

某次逛论坛,发现使用Systemd管理的Linux发行版,应该使用Systemd Timers管理定时任务,并替代原来的Cron Jobs。

1. Systemd Units

Systemd Units(单元),是System的最小功能单位。其按功能划分为12种类型,例如target、service、timer等。“单元”之间互相依赖和调用,组成Systemd这个任务管理系统。

System Units存放路径:

  • /usr/lib/systemd/system,系统默认的单元文件
  • /etc/systemd/system,用户安装的软件的单元文件

注意:

  • Debian 12中,/usr/lib映射到/lib,所以/lib/systemd/system/usr/lib/systemd/system的文件相同。
  • Systemd读取Unit文件时,先读/usr/lib/systemd/system,再读/etc/systemd/system。所以修改原来的Unit文件,或者自定义的Unit文件,建议放在/etc/systemd/system
  • 编辑Unit文件,建议使用命令systemctl edit --full xxx.service,保存后会自动reload。

配置Systemd定时器时,需要创建两个Unit:

  • xxx.service,service类型,用于配置需要执行的任务。如果该任务已配置,则不用创建此文件。
  • xxx.timer,timer类型,对xxx.service执行定时操作。

2. 创建Systemd Service

对于Cron Job,会把定时执行的脚本或命令,与定时配置写在一起。配置是简单直接,但是不好管理。Systemd需要把定时执行的任务,配置为oneshot的Service。

例如创建文件/etc/systemd/system/sample-job.service

[Unit]
Description=sample job

[Service]
Type=oneshot
ExecStart=/bin/bash /path/to/sample-job.sh

[Install]
WantedBy=multi-user.target

注意:

  • Type=oneshot,表示只运行一次,不用长期运行。
  • Requires,配置了需要网络服务联网成功,才能执行。
  • WantedBy,配置了多用户命令状态时,才能执行。

3. 创建Systemd Timer

例如创建文件/etc/systemd/system/sample-job.timer

[Unit]
Description=Auto run sample job

[Timer]
OnCalendar=Mon,Thu *-*-* 01:23:49
Unit=sample-job.service

[Install]
WantedBy=multi-user.target

注意:

  • Unit,配置对应的Service。
  • Requires,需要依赖的Service,可不配。
  • OnCalendar,按配置的日期时间进行定时运行。格式是DOW yyyy-MM-dd HH:mm:ss,其中DOW可以是Mon,Fri(周一和周五这2天)或者Mon..Fri(周一到周五这5天)
    • 详细参考:https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html#Calendar%20Events

4. 管理

# 启动定时器
sudo systemctl start sample-job.timer
# 停止定时器
sudo systemctl stop sample-job.timer
# 查看定时器状态
systemctl status sample-job.timer
# 列出正在运行的定时器
systemctl list-timers
# 设置开机启动
sudo systemctl enable sample-job.timer
# 取消开启启动
sudo systemctl disable sample-job.timer

参考

Detect the Current Public IP

宽带运营商,有的会提供动态公网IP(Dynamic Public IP Address)。用户可以通过DDNS(Dynamic DNS)绑定域名后,就能远程访问自家网络了。这个方案有个最重要的前提,就是要知道当前的公网IP,然后才可以进行DDNS绑定。

以下总结几个检测并获取当前公网IP的方法。另外,为了方便使用,所获取的公网IP地址,都是文本。

1. 请求第三方网站

网站服务基于TCP/IP协议,能获取客户端的公网IP。只要在当前网络请求第三方网站,并且该网站返回客户端的公网IP,就是当前网络的公网IP了。这个方案最常用。

整理了一些网站,能返回当前网络的公网IP,而且返回的IP地址都是文本格式:

2. 自建网站

既然第三方网站能返回当前网络的公网IP,那么可以自己建一个这样网站或者接口。前提是,需要网站服务器,或者小小的VPS。

相关功能,只需要配置Nginx即可:

server {
  ...
  # 请求path为"/ip"的网址,则返回当前网络的公网IP
  location /ip {
    default_type text/plain;
    return 200 $remote_addr;
  }
}

3. Cloudflare pages

如果没有网站服务器(或者VPS),可以利用Cloudflare pages实现相关功能。

首先需要开通Cloudflare的pages功能,然后进入“Workers & Pages” -> “Create”按钮 -> 选“Workers” -> “Start from a template”点“Hello World” -> “worker.js”的代码改为如下 -> 点“Deploy”执行部署。

export default {
  async fetch(request) {
    return new Response(request.headers.get('CF-Connecting-IP'), {
      status: 200,
      headers: {"Content-Type": "text/plain;charset=UTF-8"},
    })
  }
};

4. 从路由器检测

一般家用路由器,能用于网络拨号,那就能检测宽带运营商所分配的动态公网IP。这个方法虽然效率最高,但不同路由器的获取方法不同,甚至不能获取。

这里记录一下K2P刷了官改ROM后,通过telnet执行命令,检测当前公网IP。其中192.168.0.1是路由器IP地址。

(sleep 1; echo "ip addr show dev pppoe-wan"; sleep 1; ) | telnet 192.168.0.1 2>/dev/null | grep -oP '(?<=inet\ )[0-9\.]+'