分类 硬件折腾 下的文章

记录一下最近选购打气筒的总结。

1. 总结

a) 预算充足,选购电动打气机(充气宝)。

  • 省力,全能。
  • 能显示胎压。
  • 适合家用。用在汽车上的话,可以选购能接入点烟器插头的。

b) 普通家用,选购传统自行车打气筒。

  • 推荐自带压力表(单位psibar),能显示胎压,避免打爆轮胎。
  • 可用于摩托车、电动自行车、普通自行车等的轮胎打气。有的声称汽车轮胎也能用,但没试过。

2. 胎压参考

注意:

  • 胎压仅供参考,特别自行车,最好根据实际情况调整。
  • 汽车如果自身没有显示胎压的,最好去专业的店里打气,避免4个轮的胎压不平衡。

胎压参考值:

a) 两轮摩托车

  • 前轮:28~32 psi,2 bar左右
  • 后轮:30~35 psi,2.5 bar左右

b) 两轮自行车

  • 公路自行车:85~130 psi,5.9~9.0 bar。
  • 山地自行车:30~50 psi,2.1-3.4 bar。
  • BMX自行车:35~70 psi,2.4-4.8 bar。
  • 儿童自行车:25~40 psi,1.7-2.8 bar。

3. 参考资料

虽然家里的迷你服务器(锐角云三角主机)跑了N年,但是一直没有搞定USB硬盘在开机后自动挂载。每次断电重启后,都需要手工执行挂载。终于,通过SystemD的配置,解决了这个问题。

Debian 12上实现开机自动挂载USB磁盘,一般可以在系统启动过程或者系统启动后执行。如果在系统启动过程执行挂载,挂载过程出错,有可能导致系统启动失败(导致不能进入系统)。所以系统启动后执行挂载,相对稳妥。其方案很多,我选择了在fstab文件里通过SystemD的配置实现。

修改/etc/fstab文件,添加以下配置即可。

UUID=7d010a88-bc3c-7a2c-0270-3eae70edd016 /media/ud1 ext4 defaults,auto,nofail,x-systemd.automount,x-systemd.device-timeout=10s 0 0

相关参数解析如下:

  • UUID,填写USB磁盘里需要挂载分区的UUID。可以使用以下其中一条命令查看:

    ls -l /dev/disk/by-uuid/
    lsblk -f
    blkid
  • /media/ud1,设置挂载目录。
  • ext4,分区的文件系统类型。
  • defaults,使用默认选项,即rw,suid,dev,exec,auto,nouser,async

    • rw,挂载该文件系统可读写。
    • suid,从该文件系统执行程序时,遵循用户权限和组权限的设置。
    • dev,解释该文件系统上的字符或块特殊设备。
    • exec,允许执行二进制文件或其它可执行文件。
    • auto,标识mount -a命令可以自动挂载该文件系统,实现开机或重启时自动挂载。
    • nouser,普通用户不能执行挂载。
    • async,该文件系统的所有I/O都应该异步完成。
  • auto,同defaultsauto
  • nofail,设备不存在时,不报告错误信息。
  • x-systemd.automount,为该文件系统创建自动挂载单元(automount unit)。
  • x-systemd.device-timeout=10s,SystemD等待该设备可见(show up)的时间,超过该时间则放弃挂载,避免没有接入该USB磁盘而导致开机等太久。

参考

1. 背景

由于公司的办公电脑也用于深度学习的模型训练,该电脑就不能同时大型软件(连Chrome也不能多开标签),避免影响训练。于是就想把新手机“红米Note 12 Turbo”利用起来,处理一些“轻办公”的工作。

找到方案是:

  • 方案1:Android系统利用chroot安装完整Linux系统,运行基于X11的图形化软件(例如Chrome),PC端运行X server
  • 方案2:手机运行Android桌面模式,并投屏到PC。

由于方案1运行不流畅、不能方便利用GPU加速、软件兼容不佳等,只能放弃。方案2整理了一下,能达到比较高的实用性。

2. 原理

据说从Android 10开始,Android系统内置了桌面模式。该模式下,App可以自由拖动显示位置,并调整窗口大小(跟PC操作系统一样)。

有趣的是,“开发者选项”里开启了“模拟辅助显示设备”,就可以启用桌面模式,而且基本不影响手机正常显示模式。可以认为同一个Android手机上同时运行“手机模式”和“桌面模式”,App能在这两个模式之间显示。

最后,利用scrcpy把“模拟辅助显示设备”投屏到PC端,就可以使用大屏幕显示“桌面模式”,并且利用鼠标键盘进行输入。

据说手机直连显示器(需要手机直接输出HDMI功能),或者无线连接Miracast,都可以显示“桌面模式”。但是手上没有相关设备,不能验证。

3. 配置

3.1. PC端软件

3.2. Android必要配置

  • 启用“开发者模式”。
  • 进入“设置” -> “系统” -> “开发者选项”,勾选“启用可自由调整的窗口”、“强制使用桌面模式”。

    • 其中“强制使用桌面模式”,就是“模拟辅助显示设备”启用桌面模式。

3.3. 启动命令

在PC端的命令窗口执行以下命令即可启动。Linux Shell脚本参考如下:

#!/bin/bash

# Params
#  $1: SERIAL|HOST[:PORT]
P_SERIAL=$1

PHONE_NAME="My Phone"
CMD_ADB="/opt/android-sdk/platform-tools/adb"
ADB_SERIAL=
CMD_SCRCPY="/opt/scrcpy/scrcpy"
DISPLAY_ID=0
# Log level
declare -A LOG_LEVEL=([i]=INFO [w]=WARN [e]=ERR)

# Log message
# Params
#  $1: log level
#  $2: message
function func_log() {
    lv=${LOG_LEVEL[$1]}
    if [ -z "$lv" ]; then
        lv=NONE
    fi
    echo [$lv] $2 1>&2
    return
}

function func_init() {
    result=n
    
    # Do connect
    is_connected=$(func_connect)
    if [ "$is_connected" == "y" ]; then
        echo y
        return
    fi
    func_log w "Connect failed. SERIAL or HOST is invalid, or need pairing"
    
    # Do pair
    read -p "Need to pair device? Enter "y" for yes, otherwise exit:" need_pair
    if [ "$need_pair" != "y" ]; then
        echo Exit 1>&2
        echo $result
        return
    fi
    is_paired=$(func_pair)
    if [ "$is_paired" != "y" ]; then
        func_log e "Pair failed, exit"
        echo $result
        return
    else
        func_log i "Pair succeed"
    fi
    is_connected=$(func_connect)
    if [ "$is_connected" != "y" ]; then
        func_log e "Connect failed, exit"
        echo $result
        return
    else
        func_log i "Connect succeed"
    fi
    echo y
    return
}

function func_get_serialno() {
    dev_serialno=$($CMD_ADB get-serialno)
    if [ -z "$dev_serialno" ]; then
        return
    fi
    echo $dev_serialno
    return
}

function func_check_serialno() {
    result=n
    dev_serialno=$($CMD_ADB -s "$P_SERIAL" get-serialno)
    if [ "$P_SERIAL" != "$dev_serialno" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_connect() {
    result=n
    
    # Get serialno of default device
    if [ -z $P_SERIAL ]; then
        dev_serialno=$(func_get_serialno)
        if [ -z "$dev_serialno" ]; then
            func_log e "No connected device"
            echo $result
        else
            echo y
            P_SERIAL=$dev_serialno
        fi
        return
    fi
    
    # Check if the device is connected
    is_connected=$(func_check_serialno)
    if [ "$is_connected" == "y" ]; then
        echo y
        return
    fi
    
    # Connect to the device
    dev_connect=$($CMD_ADB connect "$P_SERIAL")
    if [ "${dev_connect:0:9}" != "connected" ] && [ "${dev_connect:0:17}" != "already connected" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_pair() {
    result=n
    echo Enter "HOST[:PORT]" of the paired device:; read pair_host
    echo Enter "PAIRING CODE":; read pair_code
    dev_pair=$($CMD_ADB pair "$pair_host" pair_code)
    if [ "${dev_pair:0:19}" != "Successfully paired" ]; then
        echo $result
        return
    fi
    echo y
    return
}

function func_tips() {
    echo + Tips ------------------
    echo   - Ctrl + Shift + O, turn ON screen of the connected device
    echo   - Ctrl + O, turn OFF screen of the connected device
    echo + -----------------------
}

function func_start_apps() {
    # --windowingMode <WINDOWING_MODE>: The windowing mode to launch the activity into.
    # --activityType <ACTIVITY_TYPE>: The activity type to launch the activity as.
    $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.LAUNCH -e package cu.axel.smartdock
    # $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.UNFREEZE -e package cu.axel.smartdock
    # sleep 2
    # $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -n cu.axel.smartdock/.activities.MainActivity
}

function func_stop_apps() {
    $CMD_ADB $ADB_SERIAL shell am start-activity --display $DISPLAY_ID -a com.aistra.hail.action.FREEZE -e package cu.axel.smartdock
}

# main function
function func_main() {
    is_init=$(func_init)
    if [ "$is_init" != "y" ]; then
        return
    fi

    # Turn on auxiliary display device
    $CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices 1920x1080/193
    #$CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices 1920x1008/180

    # Get display-id of Simulate secondary displays
    DISPLAY_ID=$($CMD_SCRCPY $ADB_SERIAL --list-displays | $CMD_ADB $ADB_SERIAL shell "grep -o 'display-id=[1-9][0-9]*' | sed 's/display-id=\([1-9][0-9]*\)/\1/'")
    func_log i "Display id: $DISPLAY_ID"

    # Show tips
    func_tips

    # Run apps
    func_start_apps

    # Run scrcpy
    # $CMD_SCRCPY $ADB_SERIAL --display-id=$DISPLAY_ID --keyboard=uhid --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="$PHONE_NAME - Android Desktop Mode" --window-x=0 --window-y=25
    $CMD_SCRCPY $ADB_SERIAL --display-id=$DISPLAY_ID --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="$PHONE_NAME - Android Desktop Mode" --window-x=0 --window-y=25

    # Stop apps
    func_stop_apps

    # Turn off auxiliary display device
    $CMD_ADB $ADB_SERIAL shell settings put global overlay_display_devices null
}

func_main
exit

说明:

  • PHONE_NAME设置手机名称,作为投屏窗口的标题。
  • CMD_ADB设置adb命令所在位置。
  • ADB_SERIAL设置设备序列号,多设备时指定连接的设备。连接无线adb时,可设置IP:端口。可以作为脚本第一个参数传入。
  • CMD_SCRCPY设置scrcpy命令所在位置。
  • DISPLAY_ID是“模拟辅助显示设备”的“display-id”,用于scrcpy投屏。

    • 这里利用了Android自带的grepsed实现字符串匹配提取,避免操作系统缺乏相关命令(例如Windows的CMD)。
    • 如果PC端是Linux设备,可以改为“DISPLAY_ID=$(${CMD_SCRCPY} --list-displays | grep -oP '(?<=display-id=)1-9*')”,简化命令。
  • adb shell settings put global overlay_display_devices用于开启或关闭Android上“模拟辅助显示设备”。

    • 如果在“开发者选项”中开启“模拟辅助显示设备”,没有DPI选项,所以这里通过命令开启。
    • 值为1920x1080/180,设置“模拟辅助显示设备”的分辨率为1920x1080,DPI为180。此值适合针对20吋左右的屏幕。DPI的值,主要影响“模拟辅助显示设备”UI效果,包括字体大小,但不影响手机正常模式。
    • 值为"1920x1080/180\;1920x1080/180",显示两个“模拟辅助显示设备”。多个“模拟辅助显示设备”的配置,需要用双引号(")包裹,并以\;分隔。
    • 值为null,关闭“模拟辅助显示设备”。
    • 对于Windows 11系统,显示器分辨率为1080p时,“模拟辅助显示设备”的分辨率建议为1920x1008。
  • 启动scrcpy

    • 相关参数可以通过scrcpy --help查阅。
  • 关闭“模拟辅助显示设备”。

    • 由于scrcpy运行时,会占着终端,同时暂停了Shell脚本的运行。那么scrcpy停止后,就可以自动执行关闭“模拟辅助显示设备”。

Windows的CMD批处理脚本,参考如下:

@echo off
SETLOCAL ENABLEEXTENSIONS

rem Params
rem  %1: SERIAL|HOST[:PORT]
set P_SERIAL=%1

set PHONE_NAME=My Phone
set CMD_ADB="D:\tools\android-sdk\platform-tools\adb.exe"
set ADB_SERIAL=
set CMD_SCRCPY="D:\tools\scrcpy\scrcpy.exe"
set TEMP_FILE="C:\Users\%username%\AppData\Local\Temp\scrcpy_display_id_%RANDOM%.txt"
set DISPLAY_ID=0

call :func_main
goto end

rem Log message. If error, do exit.
rem Params
rem  %1: log level
rem  %2: message
:func_log
    set lv_index=%~1
    if "%lv_index%" == "i" (
        set lv=INFO
    ) else if "%lv_index%" == "w" (
        set lv=WARN
    ) else if "%lv_index%" == "e" (
        set lv=ERR
    ) else (
        set lv=NONE
    )
    echo [%lv%] %~2
goto :eof

:func_init
    set %~1=n
    
    rem Do connect
    call :func_connect is_connected
    if "%is_connected%" == "y" (
        set %~1=y
        set ADB_SERIAL=-s %P_SERIAL%
        goto :eof
    )
    call :func_log w,"Connect failed. SERIAL or HOST is invalid, or need pairing"
    
    rem Do pair
    set /p need_pair=Need to pair device? Enter "y" for yes, otherwise exit:
    if "%need_pair%" neq "y" (
        echo Exit
        goto :eof
    )
    call :func_pair is_paired
    if "%is_paired%" neq "y" (
        call :func_log e,"Pair failed, exit"
        goto :eof
    ) else (
        call :func_log i,"Pair succeed"
    )
    call :func_connect is_connected
    if "%is_connected%" neq "y" (
        call :func_log e,"Connect failed, exit"
        goto :eof
    ) else (
        call :func_log i,"Connect succeed"
    )
    set ADB_SERIAL=-s %P_SERIAL%
    set %~1=y
goto :eof

:func_get_serialno
    for /f "delims=" %%i in ('%CMD_ADB% get-serialno') do set dev_serialno=%%i
    if "%dev_serialno%" == "" (
        goto :eof
    )
    set %~1=%dev_serialno%
goto :eof

:func_check_serialno
    set %~1=n
    for /f "delims=" %%i in ('%CMD_ADB% -s %P_SERIAL% get-serialno') do set dev_serialno=%%i
    if "%P_SERIAL%" neq "%dev_serialno%" (
        goto :eof
    )
    set %~1=y
goto :eof

:func_connect
    set %~1=n
    
    rem Get serialno of default device
    if "%P_SERIAL%" == "" (
        call :func_get_serialno dev_serialno
    )
    if "%P_SERIAL%%dev_serialno%" == "" (
        call :func_log e,"No connected device"
        goto :eof
    ) else if "%P_SERIAL%%dev_serialno%" == "%dev_serialno%" (
        set %~1=y
        set P_SERIAL=%dev_serialno%
        goto :eof
    )
    
    rem Check if the device is connected
    call :func_check_serialno is_connected
    if "%is_connected%" == "y" (
        set %~1=y
        goto :eof
    )
    
    rem Connect to the device
    for /f "delims=" %%i in ('%CMD_ADB% connect %P_SERIAL%') do set dev_connect=%%i
    if "%dev_connect:~0,9%" == "connected" (
        rem Do nothing
    ) else if "%dev_connect:~0,17%" == "already connected" (
        rem Do nothing
    ) else (
        goto :eof
    )
    set %~1=y
goto :eof

:func_pair
    set %~1=n
    echo Enter "HOST[:PORT]" of the paired device: & set /p pair_host=
    echo Enter "PAIRING CODE": & set /p pair_code=
    for /f "delims=" %%i in ('%CMD_ADB% pair %pair_host% %pair_code%') do set dev_pair=%%i
    if "%dev_pair:~0,19%" neq "Successfully paired" (
        goto :eof
    )
    set %~1=y
goto :eof

:func_tips
    echo + Tips ------------------
    echo   - Ctrl + Shift + O, turn ON screen of the connected device
    echo   - Ctrl + O, turn OFF screen of the connected device
    echo + -----------------------
goto :eof

:func_start_apps
    rem --windowingMode <WINDOWING_MODE>: The windowing mode to launch the activity into.
    rem --activityType <ACTIVITY_TYPE>: The activity type to launch the activity as.
    %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.LAUNCH -e package cu.axel.smartdock
    rem %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.UNFREEZE -e package cu.axel.smartdock
    rem timeout /T 2
    rem %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -n cu.axel.smartdock/.activities.MainActivity
goto :eof

:func_stop_apps
    %CMD_ADB% %ADB_SERIAL% shell am start-activity --display %DISPLAY_ID% -a com.aistra.hail.action.FREEZE -e package cu.axel.smartdock
goto :eof

rem main function
:func_main
    call :func_init is_init
    if "%is_init%" neq "y" (
        goto :eof
    )
    call :func_log i,"Connected device: %P_SERIAL%"

    rem Turn on auxiliary display device
    rem %CMD_ADB% shell settings put global overlay_display_devices 1920x1080/180
    %CMD_ADB% %ADB_SERIAL% shell settings put global overlay_display_devices 1920x1008/180

    rem Get display id of auxiliary display device
    %CMD_SCRCPY% %ADB_SERIAL% --list-displays | %CMD_ADB% %ADB_SERIAL% shell "grep -o 'display-id=[1-9][0-9]*' | sed 's/display-id=\([1-9][0-9]*\)/\1/'" > %TEMP_FILE%
    set /p DISPLAY_ID=<%TEMP_FILE%
    del %TEMP_FILE%
    call :func_log i,"Display id: %DISPLAY_ID%"

    rem Show tips
    call :func_tips

    rem Run apps
    call :func_start_apps

    rem Run scrcpy
    rem %CMD_SCRCPY% --display-id=%DISPLAY_ID% --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="%PHONE_NAME% - Android Desktop Mode" --window-x=0 --window-y=25
    %CMD_SCRCPY% %ADB_SERIAL% --display-id=%DISPLAY_ID% --keyboard=sdk --mouse=sdk --no-audio --power-off-on-close --shortcut-mod="lctrl,rctrl" --stay-awake --turn-screen-off --window-title="%PHONE_NAME% - Android Desktop Mode" --window-x=0 --window-y=25

    rem Stop apps
    call :func_stop_apps

    rem Turn off auxiliary display device
    %CMD_ADB% %ADB_SERIAL% shell settings put global overlay_display_devices null
goto :eof

:end

年初给老婆入手了“红米Note 12 Turbo”,整机较轻,运行速度快,最大惊喜是可以运行“yuzu”(Switch模拟器),可以玩“星之卡比:探索发现”。于是上个月也给自己入手一台(终于脱离“红米K30 5G”的苦海…)。

优点:

  • SoC采用高通的骁龙7+Gen2,高性能。
  • SoC能耗比高,OLED屏幕加持,续航很出色。
  • 塑料机身,取消屏幕支架,整机重量不到190g。
  • X轴线性马达,打字的震动反馈舒适。
  • 双扬声器。

缺点:

  • OLED屏幕,会有烧屏风险。
  • 即使国外有销售(国外名称为Poco F5),LineageOS官方没有支持。
  • Android 14的第三方ROM,自动亮度失效。(注:已修复)

1. 刷机

此机型有很多第三方ROM可选,推荐两个:

本人喜欢“Uvite 14”,下面介绍对应的刷机方法:

1.1. 下载文件

  • Recovery(可选)

  • ROM文件

    • Paranoid官方提供了针对Recovery和Fashboot的刷机包。
    • 个人建议下载Fashboot版,后面安装KernelSU会方便一点。

1.2. 刷ROM

注:

  • 刷机前,需要利用小米手机解锁工具进行解锁(此处略)。
  • 由于使用了A/B分区,Recovery集成在boot分区(跟系统Rom集成),所以刷入的第三方Recovery会被系统Rom所覆盖。
  • 由于Recovery集成在boot分区,所以就建议使用Fastboot模式刷系统Rom。
  • 使用Windows系统的,一定要先装好驱动。

方案1,使用Recovery

注:不推荐这个方案。

手机开机进入Fastboot模式(同时按音量减和开机键),刷入Recovery并进入,参考命令如下:

# 刷入Recovery
fastboot devices
fastboot flash recovery twrp-3.7.1_12-v8.6_A14-marble-skkk.img
fastboot reboot recovery

进入Revocery后,就是常规操作了:

  • 格式化data(首次刷这个系统,需要执行此步骤。记得先备份好文件。)
  • Recovery里刷入“Uvite 14”的Recovery版ROM
  • 重启后完成

方案2,使用Fastboot

注:如果使用Windows系统刷机,要先安装好驱动。具体驱动文件,在官方刷机工具里已提供。

Paranoid官方提供了Fastboot的刷机工具:https://github.com/ghostrider-reborn/aospa-flashing-kit/tree/marble

如果手工执行刷机,可参考以下步骤:

  • 手机开机进入Fastboot模式
  • 电脑端执行以下命令

    # Fastboot模式刷ROM
    fastboot update --skip-reboot aospa-uvite-beta-marble-20240617-image.zip
  • 期间手机会重启进入AOSPA Fastbootd,并执行余下处理,这里不要管
  • 直到电脑端提示Finished. Total time: 238.537s,表示完成刷机。此时,手机选Reboot system now,并按电源键,手机重启进入系统。

2. 获取Root权限

推荐使用KernelSU,天然自带隐藏功能,避免App检测Root。

参考资料:

参考步骤:

  • 在github下载最新的KernelSU apk,并在手机安装。
  • 从“Uvite 14”的Fastboot版的刷机包中,提取boot.img文件,并放到手机上。
  • 手机上使用KernelSU apk对boot.img打补丁。
  • 把打补丁后的boot.img刷入手机。

    • 这一步,我的做法是把打补丁后的boot.img,替换“Uvite 14”的Fastboot版的刷机包对应文件,并使用刷ROM的操作刷入。
  • 重启手机后,再打开KernelSU,就提示正常工作了。

3. 刷成砖

值得记录的是,试过使用Recovery刷Rom导致刷成砖。主要是刷机过程中,Recovery自动重启,导致刷机过程意外终止,手机被刷成砖,不能启动,也不能进入Fastboot模式。

由于采用高通的SoC,这个情况,可以进入9008模式,刷入Rom,实现拯救手机。而Android手机进入9008模式的方式,主要有两个,一个是使用刷机线,另一个是开机过程短接主板上的特定触点。查了下,这个手机只能通过“短接触点”的方式进入9008。由于我直接申请售后,就没有继续深入研究了。

关于9008刷机线(以下内容来源网络,没有实践过):

  • 使用Micro USB数据线,即USB 2.0的数据线。
  • 剥开“绿色”和“黑色”的线,露出金属,用于短接。
  • 使用此数据线连上电脑,另一头接上Type C转接头。
  • 手机关机状态下,短接“绿色”和“黑色”线(用手摁住即可),Type C转接头连上手机,几秒后松开两个金属线,在电脑设备管理器(我的电脑-右键-管理-设备管理器-端口COM和LPT)中可以看到9008端口。
  • 如果嫌麻烦,可以直接购买9008刷机线,有个按钮可以实现短接“绿色”和“黑色”的线。

上个月,发现“cloudflare.com”被解析为“127.0.0.1”,于是研究了一下DNS污染。

1. 检测

可以直接使用相关网站,检查各个DNS针对指定域名的解析是否正确。例如:

也可以使用命令或工具,查询使用指定DNS解析指定域名的结果。nslookup命令的示例如下:

# nslookup 域名 DNS地址
nslookup cloudflare.com 223.5.5.5

2. 解决方案

一般设置DNS为可靠的共用DNS即可。由于是网络运营商的DNS发现的污染,所以不推荐使用三大运营商的DNS。暂时改为使用“阿里公共DNS”。

最简单的是,修改网络出口设备(例如路由器)的DNS,所有网络设备(例如手机、电脑)都使用其默认DNS。比较麻烦的是,各个网络设备各自设置DNS。

国内外的免费公共DNS,可参考:

几个比较有名的DNS如下:

3. 扩展内容

3.1. 关于nslookup命令

一般各大系统都有nslookup命令。对于Debian 11和Ubuntu 22.04,可能没有默认安装nslookup,需要手动安装。

# Debian或Ubuntu,安装nslookup命令
sudo apt install bind9-dnsutils

# 查看域名的DNS A记录解析
nslookup -type=A cloudflare.com 223.5.5.5

该命令的详细说明,可以参考man nslookup或Debian官方文档:nslookup(1) — bind9-dnsutils — Debian bookworm — Debian Manpages

3.2. 如何修改DNS配置

3.2.1. 总结

  • 路由器,进入其管理后台,修改DNS配置。
  • 网络终端设备(电脑,手机等),需要明确配置DNS的范围,一般是:全局、指定网络接口、指定代理服务。
  • 全局DNS,需确定管理DNS服务的程序,再修改其配置文件。
  • 指定网络接口的DNS,一般修改其网卡设置。
  • 代理服务的DNS,一般不走本地设置,需要参考该代理服务的配置说明。

3.2.2. 关于配置全局DNS

对于Linux(例如Debian 12),一般查看/etc/resolv.conf文件,可以了解当前使用什么DNS。直接修改该文件,可以更改当前全局DNS,例如:

# 设置DNS为阿里DNS
nameserver 223.5.5.5

但是,如果有其它程序接管了/etc/resolv.conf文件,比如systemd-resolved服务,系统重启后会该文件被重置,导致设置无效。注意各个系统的情况不同,比如:

  • Debian 11/12,默认没有安装systemd-resolved
  • Ubuntu 22.04,默认安装并启用systemd-resolved

3.2.3. 关于配置网络接口DNS

  • 图形界面,通过网络配置,修改相应的DNS。

    • 例如Ubuntu 22.04,使用NetworkManager管理。网络接口的配置文件在/etc/NetworkManager/system-connections/
  • 命令界面,一般修改配置文件/etc/network/interfaces

    • 例如Debian 11/12,其示例配置如下:
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

source /etc/network/interfaces.d/*

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
allow-hotplug enp3s0
#iface enp3s0 inet dhcp
iface enp3s0 inet static
    address 192.168.0.100
    netmask 255.255.255.0
    gateway 192.168.0.1
    dns-nameservers 223.5.5.5 192.168.0.1

3.3. 管理DNS缓存

3.3.1. Linux的DNS缓存

  1. 使用systemd-resolve,适合Ubuntu 20.04及以下、Debian 11/12等。
# 清除DNS缓存
sudo systemd-resolve --flush-caches

# 查看DNS缓存情况
sudo systemd-resolve --statistics

对于Debian 11/12,需要启用systemd-resolve服务。

sudo systemctl enable systemd-resolved.service
  1. 使用resolvectl,适合Ubuntu 20.04以上。
# 清除DNS缓存
sudo resolvectl flush-caches

# 查看DNS缓存情况
sudo resolvectl statistics
  1. 重启网络服务,清除DNS缓存,适合一般Linux。
# 基于Init.d的系统
sudo /etc/init.d/networking restart

# 基于SystemD的系统
sudo service networking restart

3.3.2. Windows的DNS缓存

rem 清除DNS缓存
ipconfig /flushdns

3.3.3. Chrome浏览器的DNS缓存

Chrome本身建立了自己的DNS缓存,并提供简单的管理功能。打开链接:chrome://net-internals/#dns 即可。

由于需要开发移动端App,有机会用上了Apple M1 CPU的Mac Mini。对比过Windows 11、Ubuntu 22.04,感觉是目前最好的 App开发机。

1. 优点

1.1. 唯一支持两大移动平台

没办法,Build iOS App和提交Apple Store,都只能使用Xcode ,也就离不开Mac OS。

Android就随意了。

1.2. 支持两大平台的模拟器,且很爽

支持iOS和Android的模拟器,除了Android SDK下载镜像慢,创建很快、运行很流畅。能满足大部分需要兼容多个系统版本的开发测试的场景。相比购买和使用一堆实体机器进行开发测试,这个方便太多。

但是模拟器具体有哪些不支持的功能,需要使用实体机验证的,暂时没遇到。

2. 缺点

2.1. 快捷键不习惯

估计Mac OS新手用户,特别是从Windows或Linux切换过来的,都有这个问题吧。网上有解决方案,调整快捷键。但我选择了去适应它。

2.2. 随机弹出屏幕保护程序

这个屏幕保护程序,即使设置了关闭,也会弹出。主要是使用VNC远程时关不掉,导致不能进入桌面。解决方案见“附录”。

2.3. 使用 VNC 远程桌面比较卡

由于使用非Mac的电脑远程过去,只能使用VNC。用Mac远程到Mac,可能是另一种体验。

2.4. 不支持多人同时远程桌面

由于习惯了Windows、Linux的服务器,可以多人同时远程上去,特别是Windows远程桌面可以多人同时使用,所以提出这个问题。某个程序员的社交网站发过贴请教,结果被吐槽为什么不是人手一台Mac……当然,人手一台Mac的话,就不会有这个应用场景和问题了。

另外,Mac的SSH服务是可以多人同时登录。

3. 附录

踩过的坑,参考:Headless Mac Mini 折腾记

3.1 远程访问

最好是同时开启屏幕共享(vnc)远程登录(ssh)。遇到万不得已的情况,可以SSH进去执行sudo reboot重启。

3.2 关闭屏幕保护

在图形界面“设置”关闭了屏幕保护后,仍然会随机运行。可使用以下命令设置关闭:

sudo defaults write /Library/Preferences/com.apple.screensaver loginWindowIdleTime 0

如果屏幕保护程序在运行且不能退出的情况,可以使用以下命令(可以通过SSH执行)去关闭其进程,实现退出屏幕保护:

killall ScreenSaverEngine

1 背景

烘焙咖啡豆的过程中,为了更好地测量和记录温度及其变化,使用ESP32C3制作了一个温度监控模块。

2 需求

如果从烘焙咖啡豆的角度去考虑,参照商用机器的设计,这个温度监测会有很多功能要实现。作为起始的设计,还是先简化需求,逐步实现更多功能。所以第一版先确定以下需求:

  • 计时。这个实现起来简单,记录的温度也需要跟时间关联。
  • 读取测量出的温度值。用于展示、分析、记录等。
  • 展示温度变化。显示当前温度值,和温度与时间的曲线。

3 设计

3.1 元件

  • 主控:ESP32C3-Core,刷上MicroPython固件

    • 此开发板很廉价,最低9.9 RMB包邮。
    • 支持WiFi和蓝牙,数据可以很方便地同步到其它设备。
    • 基于MicroPython开发程序,调试很方便。
  • 温度检测模块:MAX6675,K型热电偶温度传感模块,SPI接口。

    • 廉价。
    • 最低精度为0.25 °C,够用。
  • 温度检测探头:K型铠装热电偶,探头为可弯曲、接壳式、304不锈钢材质。

    • 这个探头比温度检测模块附送的灵敏很多。
    • 一般型号为WRNK191。
    • 参考规格:直径1mm,插深50mm,线长500mm。
  • 显示模块:SSD1306,单色OLED屏,0.96英寸,分辨率128x64,两线I2C接口。

    • 廉价。
    • I2C接口的数据线只有两根,减少GPIO的占用。

3.2 接线

这里忽略电源(VCC 3.3V)和接地(GND)的连接。详细如下:

ESP32C3的接口模块接口
GPIO10MAX6675SO
GPIO02MAX6675SCK
GPIO12MAX6675CS
GPIO05SSD1306SCL
GPIO04SSD1306SDA

接线不是固定的,可以根据实际调整,但是要改main.py的对应配置。

4 程序

相关代码文件:

  • max6675.py:MAX6675的驱动程序。
  • ssd1306.py:SSD1306的驱动程序。
  • series_list.py:温度数据类,方便后面扩展对温度数据的保存。
  • main.py:主程序。

4.1 MAX6675的驱动程序

文件名max6675.py

# from https://github.com/BetaRavener/micropython-hw-lib/blob/master/MAX6675/max6675.py

import time


class MAX6675:
    MEASUREMENT_PERIOD_MS = 220

    def __init__(self, sck, cs, so):
        """
        Creates new object for controlling MAX6675
        :param sck: SCK (clock) pin, must be configured as Pin.OUT
        :param cs: CS (select) pin, must be configured as Pin.OUT
        :param so: SO (data) pin, must be configured as Pin.IN
        """
        # Thermocouple
        self._sck = sck
        self._sck.off()

        self._cs = cs
        self._cs.on()

        self._so = so
        self._so.off()

        self._last_measurement_start = 0
        self._last_read_temp = 0
        self._error = 0

    def _cycle_sck(self):
        self._sck.on()
        time.sleep_us(1)
        self._sck.off()
        time.sleep_us(1)

    def refresh(self):
        """
        Start a new measurement.
        """
        self._cs.off()
        time.sleep_us(10)
        self._cs.on()
        self._last_measurement_start = time.ticks_ms()

    def ready(self):
        """
        Signals if measurement is finished.
        :return: True if measurement is ready for reading.
        """
        return time.ticks_ms() - self._last_measurement_start > MAX6675.MEASUREMENT_PERIOD_MS

    def error(self):
        """
        Returns error bit of last reading. If this bit is set (=1), there's problem with the
        thermocouple - it can be damaged or loosely connected
        :return: Error bit value
        """
        return self._error

    def read(self):
        """
        Reads last measurement and starts a new one. If new measurement is not ready yet, returns last value.
        Note: The last measurement can be quite old (e.g. since last call to `read`).
        To refresh measurement, call `refresh` and wait for `ready` to become True before reading.
        :return: Measured temperature
        """
        # Check if new reading is available
        if self.ready():
            # Bring CS pin low to start protocol for reading result of
            # the conversion process. Forcing the pin down outputs
            # first (dummy) sign bit 15.
            self._cs.off()
            time.sleep_us(10)

            # Read temperature bits 14-3 from MAX6675.
            value = 0
            for i in range(12):
                # SCK should resemble clock signal and new SO value
                # is presented at falling edge
                self._cycle_sck()
                value += self._so.value() << (11 - i)

            # Read the TC Input pin to check if the input is open
            self._cycle_sck()
            self._error = self._so.value()

            # Read the last two bits to complete protocol
            for i in range(2):
                self._cycle_sck()

            # Finish protocol and start new measurement
            self._cs.on()
            self._last_measurement_start = time.ticks_ms()

            self._last_read_temp = value * 0.25

        return self._last_read_temp

4.2 SSD1306的驱动程序

文件名ssd1306.py

这个驱动继承了FrameBuffer类,显示文字或绘图的方法,参考FrameBuffer类的文档即可(代码有说明)。

# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
# from https://github.com/micropython/micropython-lib/blob/master/micropython/drivers/display/ssd1306/ssd1306.py

from micropython import const
import framebuf


# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_IREF_SELECT = const(0xAD)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)

# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        self.buffer = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP,  # display off
            # address setting
            SET_MEM_ADDR,
            0x00,  # horizontal
            # resolution and layout
            SET_DISP_START_LINE,  # start at line 0
            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
            SET_MUX_RATIO,
            self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
            SET_DISP_OFFSET,
            0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV,
            0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL,
            0x30,  # 0.83*Vcc
            # display
            SET_CONTRAST,
            0xFF,  # maximum
            SET_ENTIRE_ON,  # output follows RAM contents
            SET_NORM_INV,  # not inverted
            SET_IREF_SELECT,
            0x30,  # enable internal IREF during display on
            # charge pump
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,  # display on
        ):  # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP)

    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def rotate(self, rotate):
        self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3))
        self.write_cmd(SET_SEG_REMAP | (rotate & 1))

    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width != 128:
            # narrow displays use centred columns
            col_offset = (128 - self.width) // 2
            x0 += col_offset
            x1 += col_offset
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b"\x40", None]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)


class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        import time

        self.res(1)
        time.sleep_ms(1)
        self.res(0)
        time.sleep_ms(10)
        self.res(1)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))
        self.cs(1)

    def write_data(self, buf):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(buf)
        self.cs(1)

4.3 温度数据类

文件名series_list.py

想模拟成时序数据库,目前只是保存了屏幕可以显示的数据数量。

class SeriesList:
    
    def __init__(self, maxLen: int, firstVal: float):
        self._maxLen = 1 if maxLen <= 0 else maxLen
        self._list = [firstVal]
        self._len = len(self._list)
    
    def append(self, val: float):
        self._list.append(val)
        if (self._len + 1) > self._maxLen:
            delVal = self._list.pop(0)
        else:
            self._len += 1
    
    def last(self, index: int = 0) -> int:
        return self._list[self._len - index -1]
    
    def histogram(self, maxRange: int) -> list:
        hList = self._list.copy()
        hMax = int(max(self._list))
        hMin = int(min(self._list))
        hRange = hMax - hMin + 1
        rate = 1 if hRange <= maxRange else (maxRange / hRange)
        if rate == 1:
            for i, v in enumerate(hList):
                hList[i] = int(v) - hMin
        else:
            for i, v in enumerate(hList):
                hList[i] = int((int(v) - hMin) * rate)
        return hList

4.4 主程序

文件名main.py

总结一下:

  • 使用了定时任务(Timer)去探测温度和更新显示,每秒执行一次。
  • 每次执行最大耗时约150毫秒,绘画曲线的代码可以再优化。
  • 绘画曲线时,如果把整个区域置黑,再画曲线,性能比较高,但是会闪屏。目前是逐列置黑,再画,虽然慢了点,但观感良好。
  • 展示的曲线是为了直观看到温度变化,如果显示范围内的温度差超过屏幕区域的分辨率,曲线会按比例压缩。
from micropython import const
import time
from machine import Pin, SoftI2C, Timer
from max6675 import MAX6675
from ssd1306 import SSD1306_I2C
from series_list import SeriesList

UNIT_60 = const(60)
SCREEN_W = const(128)
SCREEN_H = const(64)
HISTOGRAM_X = const(0)
HISTOGRAM_Y = const(10)
HISTOGRAM_W = const(SCREEN_W)
HISTOGRAM_H = const(SCREEN_H - HISTOGRAM_Y)

""" init MAX6675 ################################## """
print('init MAX6675')
so = Pin(10, Pin.IN) # GPIO10
sck = Pin(2, Pin.OUT) # GPIO02
cs = Pin(12, Pin.OUT) # GPIO12
max = MAX6675(sck, cs, so)
time.sleep(1)
curTemp = max.read()

""" init OLED ################################## """
print('init OLED')
i2c = SoftI2C(scl=Pin(5), sda=Pin(4))
oled = SSD1306_I2C(SCREEN_W, SCREEN_H, i2c)

""" init Ticks ################################## """
ticksStart = time.ticks_ms()

""" init Temperature list ################################## """
sList = SeriesList(SCREEN_W, curTemp)

""" init Timer ################################## """
def timerRefresh(t):
    global max, curTemp, oled, timeSec, sList
    
    duration = 0 if ticksStart==None else int(time.ticks_diff(time.ticks_ms(), ticksStart) / 1000)
    dSec = duration % UNIT_60
    dMin = int(duration / UNIT_60)
    
    curTemp = max.read() # Current temperature
    sList.append(curTemp)
    
    oled.fill(0)
    oled.text('{:02d}:{:02d} | {:>6.2f} C'.format(dMin, dSec, curTemp) , 0, 0)
    hList = sList.histogram(HISTOGRAM_H)
    hLen = len(hList)
    hPre = 0
    hCur = 0
    startX = SCREEN_W - hLen
    startY = HISTOGRAM_Y + HISTOGRAM_H - 1
    for i, v in enumerate(hList):
        hPre = v if i == 0 else hCur
        hCur = v
        oled.vline(startX + i, startY - hCur, (hCur - hPre if hPre < hCur else 1), 1)
        if hPre > hCur:
            oled.vline(startX + i - 1, startY - hPre, hPre - hCur, 1)
    oled.show()

timerTemp = Timer(0)
timerTemp.init(period=500, mode=Timer.PERIODIC, callback=timerRefresh) # Every 1 second

print('start run')

春节前,以35rmb入手了Kido x3儿童智能手表,刷上基于官方系统修改的第三方ROM,尝试成为日常通讯设备,以失败告终。

1. 背景

随着时代的变化,现在已经日常使用两个手机SIM卡。一个是“保号卡”,以低价月租套餐保留手机号,一般用于注册、认证、社交等。另一个是“流量卡”,主要使用大流量套餐,子卡互打免费等。这个方案方便更换更优惠的手机流量套餐,同时避免更换手机号。

然后就想,是否可以把“保号卡”插上轻量化设备,比如可插卡的智能手表,避免别人联系不上。此时,“流量卡”可以插上其它智能设备,比如平板、笔记本电脑之类。

决定入手Kido x3,去实行这个想法。

2. 硬件介绍

Kido x3相关介绍视频:

其配置不算突出,吸引我的地方主要是:

  • 可破解,有定制系统。
  • 可插SIM卡,支持4G LTE和WiFi。
  • 价格便宜,当时只要35rmb。
  • 电池容量为900mAh,比同类产品高。
  • 防水级别为IP68,日常生活防水。

其详细配置如下:

  • CPU: Qualcomm® Snapdragon Wear™ 2100
  • 内存: 512 MB
  • 闪存: 4 GB(用户可用不到2GB)
  • 电池: 900 mAh
  • 重量: 57.4g
  • 屏幕: 1.55英寸IPS,320 * 360像素
  • 按键: 两个
  • 摄像头: 侧边500万像素、前置200万像素
  • 移动网络: 4G全网通(可插1个SIM卡)
  • WiFi: 只支持2.4GHz
  • 蓝牙: (未知)
  • GPS: L1+L5 Dual-Frequency GPS
  • 防水级别: IP68
  • 充电: 磁吸线、无线充
  • 系统:Android 7.1.1

3. 刷机

主要思路是,重启进入高通9008模式,利用高通刷机工具,刷入userdata、system、boot三个分区。

刷机前,需要制作数据线,把手表背面的4个触点连上USB 2.0的线。

具体过程,可参照ROM维护者的说明文档。

4. 缺点

  1. 定制系统优化不足。

由于原版系统没有开源,只能在原版ROM基础上修修补补。虽然可以获得root权限,但是改得有限。最致命的缺点是不够省电。虽然其电池容量比同类要大,但是开启4G流量后电量雪崩,而且是关屏待机状态,也不能坚持一天。对比孩子的“小天才”电话手表,启用流量后,能待机两天多。

还出现过致命问题,别人打不进电话!虽然重启后恢复正常,但完全没办法知道什么时候又不行了。

  1. 配套软件不足。

主要是屏幕太小,而一般App都是针对手机设计,用起来就是手机屏幕缩小的效果。缓解的办法是,系统设置三击屏幕后放大显示。

虽然有一些小市场收集了专门的App,但是App数量实在少得可怜。

  1. 抬腕操作很累。

抬腕看手表屏幕的动作,持续太久,手臂会累。另外,手表也有点重,毕竟电池容量大,日常佩戴会感到累。

  1. 一般功能的缺失。

很多传感器,比如计步、GPS等,是没有的。估计是驱动问题吧。

5. 收获

  1. 手环更实用

使用“小米手环3 NFC”已经好几年了。没有连上手机,只是作为手表(看时间)、闹钟、计步、久坐提醒、公交卡等,只需一个月充电一次。相比之下,这个手表相差太远。

  1. 体会到儿童手表的优化

以前一直很鄙视那些系统版本低下的儿童手表,例如某品牌基于Android 4.4改造还卖得贼贵。但用过才知道,儿童手表都做了大量优化,特别是省电方面的。比如我孩子的“小天才 Q2”,开启4G流量后,仍能待机两天多。另外,UI优化、配套App等,就不用多说了。

  1. 表带螺丝的替代

由于到手的kido x3只有本体,没有表带。即使购买了合适的表带,也找到合适的表带螺丝。后来才了解到,可以使用“舌钉”、“乳环”之类代替表带螺丝,确实刷新了对世界的认识(奇怪的知识有增加了)。

  1. 破解系统

kido x3的背面,4个触点,就是对应USB 2.0的四条线。接上USB后,就可以使用ADB连上系统。不少儿童手表都可以用这个方法破解。另外,这个手表是基于高通的CPU,也有专用工具刷入分区镜像,实现破解。

  1. 儿童微信

用过儿童微信,不算好用,但是提供了一个实现微信分身的方法。同一个微信号,可以同时登录手机和儿童微信,而且各不影响接收消息。不过安装儿童微信需要license,可从相关的儿童手表获取,或者直接到“咸鱼”购买。

  1. 涨价

可能由于视频的宣传带动,这货居然涨价了。折腾一番后,仍能高于原价卖出,真好~

采用MicroPython编写的定时任务,特别是在实际环境测试,一般不能看到错误信息。因此,需要做log记录。

找到一个实现Logger功能的项目ulogger,其代码没有依赖其它库,使用方式也符合一般的Logger用法。相关信息如下:

相关代码复制了一份过来,取消了TextIOWrapper的引用:

"""
- project: micropython-ulogger
  https://github.com/whales-chen/micropython-ulogger
- code from
  https://github.com/whales-chen/micropython-ulogger/blob/main/ulogger/__init__.py
- version
  ec4f6b3842c677fbb457f6bc6d88afd8a82eeed6
"""
try:    import time
except: import utime as time

try:    from micropython import const
except: const = lambda x:x # for debug

#from io import TextIOWrapper
import io

__version__ = "v1.2"

DEBUG:    int = const(10)
INFO:     int = const(20)
WARN:     int = const(30)
ERROR:    int = const(40)
CRITICAL: int = const(50)

TO_FILE = const(100)
TO_TERM = const(200)

# fmt map 的可选参数
_level  = const(0)
_msg    = const(1)
_time   = const(2)
_name   = const(3)
_fnname = const(4)


def level_name(level: int, color: bool = False) -> str:
    if not color:
        if level == INFO:
            return "INFO"
        elif level == DEBUG:
            return "DEBUG"
        elif level == WARN:
            return "WARN"
        elif level == ERROR:
            return "ERROR"
        elif level == CRITICAL:
            return "CRITICAL"
    else:
        if level == INFO:
            return "\033[97mINFO\033[0m"
        elif level == DEBUG:
            return "\033[37mDEBUG\033[0m"
        elif level == WARN:
            return "\033[93mWARN\033[0m"
        elif level == ERROR:
            return "\033[35mERROR\033[0m"
        elif level == CRITICAL:
            return "\033[91mCRITICAL\033[0m"


class BaseClock ():
    """
    This is a BaseClock for the logger.
    Please inherit this class by your custom.
    """

    def __call__(self) -> str:
        """
        Acquire the time of now, please inherit this method.
        We will use the return value of this function as the time format of the log,
        such as `2021 - 6 - 13` or `12:45:23` and so on.

        :return: the time string.
        """
        return '%d' % time.time()


class Handler():
    """The Handler for logger.
    """
    _template: str
    _map: bytes
    level: int
    _direction: int
    _clock: BaseClock
    _color: bool
    _file_name: str
    _max_size: int
    #_file = TextIOWrapper
    _file = None

    def __init__(self,
        level: int = INFO,
        colorful: bool = True,
        fmt: str = "&(time)% - &(level)% - &(name)% - &(msg)%",
        clock: BaseClock = None,
        direction: int = TO_TERM,
        file_name: str = "logging.log",
        max_file_size: int = 4096
        ):
        """
        Create a Handler that you can add to the logger later

        ## Options available for fmt.
        - &(level)%  : the log level
        - &(msg)%    : the log message
        - &(time)%   : the time acquire from clock, see `BaseClock`
        - &(name)%   : the logger's name
        - &(fnname)%  : the function name which you will pass on.
        - more optional is developing.

        ## Options available for level.
        - DEBUG
        - INFO
        - WARN
        - ERROR
        - CRITICAL

        ## Options available for direction.
        - TO_FILE : output to a file
        - TO_TERM : output to terminal

        :param level: Set a minimum level you want to be log
        :type level: int(see the consts in this module)

        :param colorful: Whether to use color display information to terminal(Not applicable to files)
        :type colorful: bool

        :param fmt: the format string like: "&(time)% - &(level)% - &(name)% - &(fnname)% - &(msg)%"(default)
        :type fmt: str

        :param clock: The Clock which will provide time str. see `BaseClock`
        :type clock: BaseClock(can be inherit )

        :param direction: Set the direction where logger will output
        :type direction: int (`TO_FILE` or `TO_TERM`)

        :param file_name: available when you set `TO_FILE` to param `direction`. (default for `logging.log`)
        :type file_name: str
        :param max_file_size: available when you set `TO_FILE` to param `direction`. The unit is `byte`, (default for 4k)
        :type max_file_size: str
        """
        #TODO: 文件按日期存储, 最大份数的设置.
        self._direction = direction
        self.level = level
        self._clock = clock if clock else BaseClock()
        self._color = colorful
        self._file_name = file_name if direction == TO_FILE else ''
        self._max_size = max_file_size if direction == TO_FILE else 0

        if direction == TO_FILE:
            self._file = open(file_name, 'a+')

        # 特么的re居然不能全局匹配, 烦了, 只能自己来.
        # m = re.match(r"&\((.*?)\)%", fmt)
        # i = 0
        # while True:
        #     # 由于蛋疼的 ure 不能直接获取匹配结果的数量, 只能使用这种蠢蛋方法来循环.
        #     try:
        #         text = m.group(i)

        #     except:
        #         # 发生错误说明已经遍历完毕
        #         break

        #     # 使用指针代替文本来减少开销
        #     if text == "level":
        #         self._map.append(_level)
        #     elif text == "msg":
        #         self._map.append(_msg)
        #     elif text == "time":
        #         self._map.append(_time)
        #     elif text == "name":
        #         self._map.append(_name)
        #     elif text == "fnname":
        #         self._map.append(_fnname)

        #     i += 1

        # 添加映射
        self._map = bytearray()
        idx = 0
        while True:
            idx = fmt.find("&(", idx)
            if idx >= 0:  # 有找到
                a_idx = fmt.find(")%", idx+2)
                if a_idx < 0:
                    # 没找到后缀, 报错
                    raise Exception(
                        "Unable to parse text format successfully.")
                text = fmt[idx+2:a_idx]
                idx = a_idx+2  # 交换位置
                if text == "level":
                    self._map.append(_level)
                elif text == "msg":
                    self._map.append(_msg)
                elif text == "time":
                    self._map.append(_time)
                elif text == "name":
                    self._map.append(_name)
                elif text == "fnname":
                    self._map.append(_fnname)
            else:  # 没找到, 代表后面没有了
                break

        # 将 template 变成可被格式化的文本
        # 确保最后一个是换行字符

        self._template = fmt.replace("&(level)%", "%s")\
            .replace("&(msg)%", "%s")\
            .replace("&(time)%", "%s")\
            .replace("&(name)%", "%s")\
            .replace("&(fnname)%", "%s")\
            + "\n" if fmt[:-1] != '\n' else ''

    def _msg(self, *args, level: int, name: str, fnname: str):
        """
        Log a msg
        """
        
        if level < self.level:
            return
        # generate msg
        temp_map = []
        text = ''
        for item in self._map:
            if item == _msg:
                for text_ in args:  # 将元组内的文本添加到一起
                    text = "%s%s" % (text, text_)  # 防止用户输入其他类型(int, float)
                temp_map.append(text)
            elif item == _level:
                if self._direction == TO_TERM:  # only terminal can use color.
                    temp_map.append(level_name(level, self._color))
                else:
                    temp_map.append(level_name(level))
            elif item == _time:
                temp_map.append(self._clock())
            elif item == _name:
                temp_map.append(name)
            elif item == _fnname:
                temp_map.append(fnname if fnname else "unknownfn")

        if self._direction == TO_TERM:
            self._to_term(tuple(temp_map))
        else:
            self._to_file(tuple(temp_map))
        # TODO: 待验证: 转换为 tuple 和使用 fromat 谁更快

    def _to_term(self, map: tuple):
        print(self._template % map, end='')

    def _to_file(self, map: tuple):
        fp = self._file
        # 检查是否超出大小限制.
        prev_idx = fp.tell()  # 保存原始指针位置
        # 将读写指针跳到文件最大限制的地方,
        # 如果能读出数据, 说明文件大于指定的大小
        fp.seek(self._max_size)
        if fp.read(1):  # 能读到数据, 说明超出大小限制了
            fp = self._file = open(self._file_name, 'w')  # 使用 w 选项清空文件内容
        else:
            # 没有超出限制
            fp.seek(prev_idx)  # 指针回到原来的地方

        # 检查完毕, 开始写入数据
        fp.write(self._template % map)
        fp.flush()


class Logger():
    _handlers: list

    def __init__(self,
        name: str,
        handlers: list = None,
        ):

        self.name = name
        if not handlers:
            # 如果没有指定处理器, 自动创建一个默认的
            self._handlers = [Handler()]
        else:
            self._handlers = handlers

    @property
    def handlers(self):
        return self._handlers

    def _msg(self, *args, level: int, fn: str):

        for item in self._handlers:
            #try:
            item._msg(*args, level=level, fnname=fn, name=self.name)
            #except:
            #    print("Failed while trying to record")

    def debug(self, *args, fn: str = None):
        self._msg(*args, level=DEBUG, fn=fn)

    def info(self, *args, fn: str = None):
        self._msg(*args, level=INFO, fn=fn)

    def warn(self, *args, fn: str = None):
        self._msg(*args, level=WARN, fn=fn)

    def error(self, *args, fn: str = None):
        self._msg(*args, level=ERROR, fn=fn)

    def critical(self, *args, fn: str = None):
        self._msg(*args, level=CRITICAL, fn=fn)


__all__ = [
    Logger,
    Handler,
    BaseClock,


    DEBUG,
    INFO,
    WARN,
    ERROR,
    CRITICAL,

    TO_FILE,
    TO_TERM,

    __version__
]

近来搞电脑的远程启动搞上瘾了。使用网络启动(Wake on Lan),确实带来很多玩法。为了进一步降低电费,减少电脑非使用时段(例如晚上睡觉时段)待机而产生的功耗,采用ESP32C3(刷上MicroPython)来作为远程开机设备。即:

  • 普通x86电脑,在晚上或无需使用的时间正常关机,并开启网络启动功能。
  • ESP32C3开发板保持24小时开机并联网,可在需要时远程启动所需x86电脑。

于是找了下,在MicroPython上发送Wake on Lan的实现代码。参考了以下文章:

整理出可用代码,保存为文件wol.py,如下:

"""
Small module for use with the wake on lan protocol.

Reference:
- https://pypi.org/project/wakeonlan/
- https://www.cnblogs.com/Yogile/p/16488281.html
"""
import socket
import ubinascii


BROADCAST_IP = "255.255.255.255"
DEFAULT_PORT = 9
SO_BROADCAST = 20

def create_magic_packet(macaddress: str) -> bytes:
    """
    Create a magic packet.

    A magic packet is a packet that can be used with the for wake on lan
    protocol to wake up a computer. The packet is constructed from the
    mac address given as a parameter.

    Args:
        macaddress: the mac address that should be parsed into a magic packet.

    """
    if len(macaddress) == 17:
        sep = macaddress[2]
        macaddress = macaddress.replace(sep, "")
    elif len(macaddress) == 14:
        sep = macaddress[4]
        macaddress = macaddress.replace(sep, "")
    if len(macaddress) != 12:
        raise ValueError("Incorrect MAC address format")
    #return bytes.fromhex("F" * 12 + macaddress * 16)
    return ubinascii.unhexlify("F" * 12 + macaddress * 16)


def send_magic_packet(
    *macs: str,
    ip_address: str = BROADCAST_IP,
    port: int = DEFAULT_PORT,
    interface: str = None
) -> None:
    """
    Wake up computers having any of the given mac addresses.

    Wake on lan must be enabled on the host device.

    Args:
        macs: One or more macaddresses of machines to wake.

    Keyword Args:
        ip_address: the ip address of the host to send the magic packet to.
        port: the port of the host to send the magic packet to.
        interface: the ip address of the network adapter to route the magic packet through.

    """
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 1)
        
        if interface is not None:
            sock.bind((interface, 0))
        sock.setsockopt(socket.SOL_SOCKET, SO_BROADCAST, 1)
        sock.connect((ip_address, port))
        for mac in macs:
            packet = create_magic_packet(mac)
            sock.send(packet)
            print("send magic packet to MAC [%s]" % (mac))
    except:
        print('send magic packet failed')
        pass
    finally:
        sock.close()

使用示例:

import wol

mac = '12:ab:12:ab:12:ab' # 必填参数。要启动电脑的网卡MAC
broadcastIp = '192.168.0.255'  # 可选参数。广播的地址,一般填对应网段的255地址

wol.send_magic_packet(mac, ip_address=broadcastIp)