分类目录归档:笔记

恢复UrBackup数据库

有时候服务器意外关机,会导致UrBackup的sqlite数据库出现无法自动恢复的错误,我这边就出现了这样的提示:

在访问或检查UrBackup内部数据库时发生错误。这表示内部数据库错误或者没有足够的可用空间。如果这个错误发生,请从备份中恢复数据库文件(backup_server.db, backup_server_files.db, backup_server_links.db, backup_server_link_journal.db and backup_server_settings.db)。详细数据请参阅日志文件。

出现这种情况,当然最好的处理方式是从备份恢复这个数据库,但是如果没有数据库备份的话,只能尝试修复现有的数据库。

进行任何修复之前首先关闭urbackup的进程,如果是docker运行的就关闭docker容器。

然后需要确保操作的电脑上有sqlite3命令,没有的话就进行对应的安装。

准备好之后,开始检查所有数据库文件,看看到底哪些坏了,终端命令进入到urbackup数据库文件所在的目录(一条一条执行,以免分不清哪个结果属于哪个检查):

sqlite3 backup_server.db "PRAGMA integrity_check;"
sqlite3 backup_server_files.db "PRAGMA integrity_check;"
sqlite3 backup_server_link_journal.db "PRAGMA integrity_check;"
sqlite3 backup_server_links.db "PRAGMA integrity_check;"
sqlite3 backup_server_settings.db "PRAGMA integrity_check;"

如果返回的是”ok”,那么这个数据库文件就没问题,否则就说明这个文件需要处理。

先备份需要处理的文件,可以创建一个bak目录然后把损坏的数据库文件复制进去,当然想要保险一点也可以把所有数据库文件都备份一遍。

然后把有损坏的数据库文件名后面都加上”.corrupt”,防止一会儿分不清,接着尝试以下方法:

方法一:使用sqlite自带的”.recover”指令尝试导出数据

sqlite3 损坏db.corrupt ".recover" > recovered.sql 2>/dev/null
sqlite3 损坏db < recovered.sql

#把"损坏db"替换成原始的数据库文件名

然后检查生成的数据库文件内部结构和原来的是否一样,可以用一些可视化工具比对起来会方便一点,比如DB Browser for SQLite

有一些表即使无法恢复也不影响urbackup正常运行,比如”backup_server.db”中的”log_data”和”user_tokens”这种表,无法恢复就不用管了。

如果recover命令无法恢复主要内容的话,就尝试方法二。

方法二:我用AI生成了一个从原来数据库文件导出数据的bash脚本

下载这个脚本放在数据库目录: https://gist.github.com/JiaJiaJiang/8baa74abbc320da41259d49c44f293d8

给它执行权限:chmod +x sqlite_rebuild_db.sh

然后执行 ./sqlite_rebuild_db.sh 损坏db.corrupt 损坏db ,看到有报错也没事,直接检查生成的数据库内容是否包含了必要的核心表。我自己用这个方法恢复出来的数据比方法一要多,生成的数据库文件也可以直接正常启动urbackup,报错没有了。

[TureNAS]宿主和虚拟机互相网络不通

我刚刚发现我在TrueNAS里面安装的虚拟机和宿主机无法互相ping通,怪不得把网关设置成里面的openwrt虚拟机一直连不上外网。

查了一下是因为我把TrueNAS宿主和虚拟机的网卡都直接绑定到宿主机的物理网卡上了,导致网卡不知道这些数据包之间互相怎么路由。

需要做的事情是要在宿主机里创建一个网桥充当虚拟交换机,然后把TrueNAS的地址设置到网桥上,把虚拟机的网卡也绑定到网桥上就可以了。

  1. 记住你给TrueNAS物理网卡的设置,然后在物理接口的设置里把地址和其它设置都清空并保存。(注意此时不要点网络主界面上的”测试连接”
  2. 在TrueNAS的网络设置里添加一个网口,设置为网桥,名称必须为”br”开头带数字的名字,比如”br0″。把刚才记住的物理网卡设置完整搬到网桥的设置里然后保存。
  3. 这时候可以点击测试连接,正常情况下应该瞬间会完成测试回到网络界面上,然后点击保存设置即可。
    1. 建议在测试之前关闭所有虚拟机,我就是因为没有关虚拟机导致测试一直无法通过
  4. 然后去每个虚拟机的网络设置里把网卡绑定到网桥上,在启动虚拟机即可解决宿主和虚拟机之间的通信问题。

PVE安装OpenWrt以及Clash-Meta笔记

做个笔记,方便下次再用。

  1. PVE创建OpenWrt虚拟机,操作系统不选择任何安装介质,bios选efi引导,注意不要创建硬盘,其它自定义。
  2. https://firmware-selector.openwrt.org/选择合适的镜像并下载”COMBINED-EFI (EXT4)”的那个,把解压出来的openwrt img镜像文件用pve的ISO上传功能上传到pve里。
  3. 执行以下命令把镜像导入为虚拟机的硬盘
    qm importdisk 虚拟机ID /var/lib/vz/template/iso/openwrt-25.12.5-x86-64-generic-ext4-combined-efi.img(改成你的镜像文件名) local-zfs(存放虚拟硬盘的位置,改成你的存储库名称)
  4. 在pve的openwrt虚拟机硬件配置里把刚刚导入的盘分配到设备上,比如SATA0,然后调整它到你想要的大小。
  5. 在pve的openwrt虚拟机选项页面调整引导顺序,把刚刚分配的硬盘勾上。
  6. 接下来就可以启动虚拟机了,此时openwrt应该已经可以顺利启动。
  7. 从pve打开openwrt的shell界面开始设置。
      1. 执行vi /etc/config/network修改网络配置的以下部分:
        config interface 'lan'
                option ifname 'br-lan'
                option proto 'static'
                option ipaddr '192.168.1.1'   # 按需设置
                option netmask '255.255.255.0'# 按需设置
                option gateway '你的网关IP'    # 如有需要
                list dns '8.8.8.8'            # 如有需要

        改完以后保存退出(按”:wq回车”)。

      2. 执行/etc/init.d/network restart重启网络服务,此时应该可以ping通这个openwrt了。
      3. (可选)执行passwd设置一个root密码,然后用ssh客户端登录进行后续步骤,可以方便粘贴东西(此时的pve vnc界面是没法复制粘贴文字的)。
      4. (可选)替换apk软件源为清华源:
        sed -i 's_https\?://downloads.openwrt.org_https://mirrors.tuna.tsinghua.edu.cn/openwrt_' /etc/apk/repositories.d/distfeeds.list
        apk update
      5. (可选)有需要的话可以扩展一下openwrt的主存储空间,参照另一篇笔记
      6. (可选)安装luci界面中文语言包 apk add luci-i18n-base-zh-cn
      7. (可选)如果需要使用winscp连接,需要安装 apk add openssh-sftp-server
      8. (建议)如果这个openwrt是作为旁路网关使用的话,可以直接删除dnsmasq软件包,它的部分功能和开启tun模式的clash是冲突的,但如果你需要在这个openwrt上开启DHCP功能,则不能删除它,需要对dnsmasq进行具体的设置来消除冲突。
      9. 安装必要的包:apk add curl
      10. 你需要在局域网内准备一个socks5代理,可以开启各种梯子客户端自带的socks5代理并向局域网开放,注意windows系统要在防火墙放行代理端口。
      11. 安装clash-meta客户端(目前常用的是nikki包)
        curl -x socks5h://socks5代理地址:端口 -sSL https://github.com/nikkinikki-org/OpenWrt-nikki/raw/refs/heads/main/feed.sh | ash

        然后在luci的包管理界面安装执行nikki luci-app-nikki luci-i18n-nikki-zh-cn这三个包,注意不要使用apk直接安装,因为luci模块有后处理步骤,直接使用apk安装的话不会在web界面显示出来。

至此在pve上安装openwrt并安装clash的步骤就完成了。

[OpenWrt]扩展分区大小

直接使用磁盘镜像写入的openwrt系统存储区默认很小,需要自己额外运行一个官方脚本来把可用空间扩展到剩余可用大小。

  1. 安装依赖
    opkg install parted losetup resize2fs blkid
    #新版本的openwrt使用apk包管理
    apk add parted losetup resize2fs blkid
  2. 执行脚本
    wget -U "" -O expand-root.sh "https://openwrt.org/_export/code/docs/guide-user/advanced/expand_root?codeblock=0"
    chmod +x expand-root.sh
    . ./expand-root.sh

    注意你可能不会看到任何输出,但是不要多次执行此脚本,执行完之后如果没有自动重启就手动reboot,系统可能会自动重启几次,直到它不再重启就代表空间已经成功扩展好了。

Windows配置网卡开启路由转发

有时候有的设备需要爬墙但又不能指定代理,而且家里又没有单独的可以爬墙的路由器时,可以用这个方法把windows系统的电脑临时变成网关来使用。

要用这个方法需要你的梯子软件开启TUN模式,才能把过来的网络流量转发到梯子去。注意,你可能需要用两张网卡来实现这个功能,否则会出现代理流量在clash虚拟网卡和出口网卡之间回环路由,一张只接受网络中的流量(在这上面配置路由转发),另一张作为网络出口。

此方法适用于windows 10以上的系统,在PowerShell(管理员)中使用以下命令即可在指定网卡上开启路由转发,开启后此网卡上接收到的网络流量会走系统路由表转发出去。

Set-NetIPInterface -InterfaceAlias "网卡接口名称" -AddressFamily IPv4 -Forwarding Enabled
#不指定网卡时将会开启所有网卡的路由转发
Set-NetIPInterface -AddressFamily IPv4 -Forwarding Enabled

使用以下命令查看网卡是否已经打开了路由转发:

Get-NetIPInterface -AddressFamily IPv4 | Format-Table InterfaceAlias, InterfaceIndex, Forwarding -AutoSize

注意此方法在重启系统后会失效,如果需要保持转发效果,可以写一个powershell脚本配合任务计划在开机后自动执行。

[PVE]在Proxmox Virtual Environment中给虚拟机直通PCIE SATA控制器

我在PVE中跑了个TrueNAS系统,不过之前一直是通过软SCSI控制器把硬盘一块块映射到虚拟机里面去的,虽然这样能用,但这样一个问题是有性能开销,truenas也不能直接对硬盘进行最直接的控制,另一个问题是换盘的时候都要重新执行映射指令,很麻烦。

于是我想着还是得实现真正的直通才行,要实现对虚拟机来说能让硬盘即插即用最干脆的方法还是直接把整个SATA控制器映射进去,我刚好有个插在主板m.2插槽上的PCIE转SATA扩展卡,扩出来5个SATA口。

注意主板上的SATA控制器是不可以映射到虚拟机里的,一个原因是其iommu分组一定会绑定主板芯片组里其它兼职的部分无法分开,另一个原因是如果你的PVE系统盘在主板上的SATA接口上或者在通过芯片组的m.2接口上(通常是远离CPU的那个m.2接口),会导致pve启动不了。

这篇文章的主要部分就是以上我踩的坑,分享出来给同样遇到问题的人参考一下,下面是跟着AI的指导操作的步骤。

编辑PVE的配置文件,设置在PVE主机中屏蔽需要直通硬件的驱动:

  1. 编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX_DEFAULT 中添加 intel_iommu=on(Intel)或 amd_iommu=on(AMD)以及 iommu=pt
  2. 如果你发现扩展卡和别的设备被捆绑在一个组里,可以添加 pcie_acs_override=downstream,multifunction
  3. 我这个AMD平台的示例:
    GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt pcie_acs_override=downstream,multifunction
  4. 编辑 /etc/modules,确保里面包含:
    vfio
    vfio_iommu_type1
    vfio_pci
    vfio_virqfd
  5. 执行proxmox-boot-tool refresh,然后重启PVE。
  6. 重启后执行以下命令检查 IOMMU 分组
    for d in /sys/kernel/iommu_groups/*/devices/*; do n=${d#*/iommu_groups/*}; n=${n%%/*}; printf '%s\t%s\n' "$n" "$(lspci -nns ${d##*/})"; done | sort -n

    如果此时你要直通的设备还是和其他设备共用一个组号,那么要考虑更换直通CPU的插槽,换插槽也没用就是没有办法了。

  7. 在刚才命令执行后你还会获得一个PCI地址,就在你设备所在行开头的那一串,类似:26:00.0,然后就要执行lspci -n -s 26:00.0查看你的设备ID(这里面的PCI地址替换成你自己设备的那个),我的设备id返回的是这个样式的:197b:0585
  8. 接下来要用vfio驱动接管你的设备,防止设备被ahci驱动接管。
    1. /etc/modprobe.d/vfio.conf中添加以下内容:options vfio-pci ids=你的设备ID
    2. /etc/modprobe.d/pve-blacklist.conf中添加:softdep ahci pre: vfio-pci
    3. 执行update-initramfs -u -k all
    4. 重启 PVE,重启后执行lspci -k -s 你的设备PCI地址,检查是否有Kernel driver in use: vfio-pci,有的话就是驱动成功被接管了。
  9. 然后就可以在虚拟机设置里添加PCIE设备了,编辑PCIE设备时选择“原始设备”,在里面选择你的SATA控制器,“所有功能”的选项不用勾,然后勾上”Rom-Bar”和”PCI-Express”保存即可。

完成以上步骤直通SATA控制器就设置好了,然后可以打开虚拟机。

[PostgreSQL]pgroonga扩展关闭强制前缀搜索

被这个破问题困扰了好几天,明明我没有在搜索语法中使用前缀搜索,但搜索结果就硬是会出现只有前缀匹配的结果,自己的测试结果和groonga文档里的结果都不一样,搞得自己怀疑是不是我的pgsql服务坏了。

挣扎了三天才发现pgroonga扩展打开了强制前缀搜索,要修改这个参数:

SET pgroonga.match_escalation_threshold = -1;

这个参数默认值为0,表示“如果主关键字的命中数为 0,则升级为松散前缀搜索”,设置为-1之后即可关闭这个功能。

 

 

PostgreSQL Docker从17版本升级到18版本

PostgreSQL Docker从17升级到18版本,有一些大的变动,因此升级过程需要进行一些额外的操作。

首先,17版本默认是不开启数据检验和的,因此需要先把17版本的数据库启用校验和,才能正确迁移升级到18版本。

  1. 开启校验和的第一步,一定要把数据库服务停止
  2. 然后执行:
    • docker run --rm -it \
        --mount "type=bind,src=17版本数据库目录路径,dst=/var/lib/postgresql/data" \
        postgres:17 \
        pg_checksums --enable --verbose --progress -D /var/lib/postgresql/data

      如果出现类似这样的错误:`pg_checksums: error: invalid segment number 0 in file name “/var/lib/postgresql/data/base/24699/pgrn.conf”`,可能是插件生成的文件此工具无法识别,暂时把它们移走,等执行完以上命令之后再移动回来。

  3. 完成后应该看到类似这样的结果提示:
    • Checksum operation completed
      Files scanned:   1991
      Blocks scanned:  7209262
      Files written:  1674
      Blocks written: 7209185
      pg_checksums: syncing data directory
      pg_checksums: updating control file
      Data checksum version: 1
      Checksums enabled in cluster

现在数据库的校验和就开启完成了,然后就是改变数据库的目录结构,17版本以及之前的数据目录在`/var/lib/postgresql/data`中,但18版本开始,数据目录需要在以大版本号命名的子目录中,使用以下流程进行迁移:

  1. 保持数据库服务处于停止的状态
  2. 首先你需要把原来的17版本数据库目录中的内容从`pgsql数据目录/data`中移动到`pgsql数据目录/17/docker`中(部分17版本已经把数据移动到了此目录中,视情况操作),也就是数据不再存放在”data”目录中,而是放在”版本号/docker”目录中。之后18版本的数据库将被生成在`pgsql数据目录/18/docker`目录中。
  3. 创建一个新的18版本空数据库,可以使用你自己的dockerfile或者compose文件创建,把17版本对应的配置文件复制一份改一下版本就行,以下是注意点:
    1. 注意使用”tianon/postgres-upgrade”工具需要确保新老版本的超级用户名一致,否则会报”FATAL: role “xxx” does not exist”的错误,而且我测试出来它没法通过环境变量指定新创建数据库的用户名和密码,只能使用”PGUSER”环境变量指定执行迁移时使用的用户,因此才有这一节独立创建空库的步骤,如果你原本数据库的超级用户是”postgres”,则可以尝试直接跳到下一个大步骤。
    2. 新建一个临时目录”pgsql数据目录/tmp”映射到将启动容器的”/var/lib/postgresql/”,创建完成后再把生成的”18″目录移动出来。(如果不使用临时目录,在创建新的容器时检测到数据目录中有其它包含数据库文件的目录它会拒绝启动)
    3. 创建空数据库示例:
        1. 确保你没有数据库容器在运行。
        2. 启动一个18版本的临时容器来创建数据库文件
          docker run --name temp-pg-18 \
            --mount "type=bind,src=pgsql数据目录/tmp,dst=/var/lib/postgresql/" \
            -e POSTGRES_USER=超级用户名 \
            -e POSTGRES_PASSWORD=超级用户密码 \
            -e POSTGRES_DB=postgres \
            -d postgres:18

          容器成功运行后等十几秒钟,检查”pgsql数据目录/tmp”目录中是否已经生成了数据库文件,有了的话执行命令清除临时容器:

          docker stop temp-pg-18 && docker rm temp-pg-18
        3. 最后把生成的目录从临时目录移出来(如果你刚刚已经手动创建了18目录,就把它先删除,再执行):
          mv tmp/18 ./18
          rm -r tmp
  4. 开始迁移,注意,如果你的17版本数据库使用了额外的扩展库,那么在迁移时你需要确保当前迁移环境中也有对应的扩展库,否则会提示”Your installation references loadable libraries that are missing from the new installation. You can add these libraries to the new installation, or remove the functions using them from the old installation.”。运行以下工具容器进行迁移:
    • docker run --rm \
        --mount "type=bind,src=pgsql数据目录,dst=/var/lib/postgresql" \
        --env "PGDATAOLD=/var/lib/postgresql/17/docker" \
        --env "PGDATANEW=/var/lib/postgresql/18/docker" \
        --env "PGUSER=超级用户名" \
        tianon/postgres-upgrade:17-to-18 \
          /bin/bash -c "apt update && \ #在这里执行你的依赖安装命令,如果不需要安装依赖的话可以把这个apt命令也去掉,仅占位示例用
          /usr/local/bin/docker-upgrade --link"

      不算安装依赖的时间,迁移过程应该很快会完成,并且给出如下提示:

      Upgrade Complete
      ----------------
      Some statistics are not transferred by pg_upgrade.
      Once you start the new server, consider running these two commands:
          /usr/lib/postgresql/18/bin/vacuumdb --all --analyze-in-stages --missing-stats-only
          /usr/lib/postgresql/18/bin/vacuumdb --all --analyze-only
      Running this script will delete the old cluster's data files:
          ./delete_old_cluster.sh
  5. 迁移完成之后使用18版本的数据库容器检查迁移状态:
    • 启动18版本服务之前:
      •  检查校验和(可选,如果是前面17版本刚刚生成的校验和则不用执行):
        docker run --rm -it \
          --mount "type=bind,src=pgsql数据目录,dst=/var/lib/postgresql/data" \
          postgres:18 \
          pg_checksums --check -D /var/lib/postgresql/data/18/docker
    • 启动18版本的服务
      • 重建统计信息,进入容器执行: vacuumdb -U 超级用户名 --all --analyze-in-stages

 

这样基本上就完成了迁移工作。

 

安卓应用脱壳逆向笔记

写一篇小笔记,记录一下第一次脱壳并逆向修改安卓应用的过程,为了避免麻烦,这里不会有被我逆向应用的信息。

为了把一个应用使用的一个库换成opencv,我这个连java都不会写的人硬着头皮消耗了一个月,甚至整个春节假期在做这个事情。

首先需要的一个工具是apktool [https://github.com/iBotPeaches/Apktool],它的作用是把应用安装包apk文件中的各个部分提取出来,变成一个apktool项目目录,且修改完之后我们也需要用它打包回apk。

以下是用ai生成的一个apktool反编译后目录的结构,另外此次逆向修改过程中也大量使用了ai,不然以我这第一次认识这些东西的知识储备,还得更久才能弄好。

your_app/
├── AndroidManifest.xml        # 应用主配置文件,声明组件、权限、包名等
├── apktool.yml                # APKTool 元数据,记录 SDK 版本、资源 ID 偏移、压缩规则等,用于重打包
├── smali/                     # 主 DEX 反编译出的 Dalvik 字节码(.smali),对应 Java/Kotlin 逻辑,如果没有选择反编译资源的话,则不会有这个目录,取而代之的是classes.dex文件
│   └── com/example/...        # 按原始包名组织的类文件
├── smali_classes2/            # (若存在)第二个 DEX 文件的 smali 代码,用于 multidex 应用
├── smali_classes3/            # (若存在)第三个 DEX,依此类推
├── res/                       # 编译后的资源目录,已还原为可读 XML
│   ├── layout/                # 界面布局文件(.xml)
│   ├── values/                # 字符串(strings.xml)、颜色(colors.xml)、样式(styles.xml)等
│   └── drawable-*/            # 图片资源,按屏幕密度分类(如 drawable-xhdpi)
├── assets/                    # (可选)原始资产目录,内容不被编译,通过 AssetManager 访问(如 .json、.db)
├── lib/                       # (可选)原生库目录,包含 .so 文件
│   ├── arm64-v8a/             # 64 位 ARM 架构原生库
│   └── armeabi-v7a/           # 32 位 ARM 架构原生库(其他架构如 x86 等也可能存在)
├── original/                  # (可选)原始 APK 中的 META-INF 目录,含签名文件(重打包时通常会被替换)
└── [resources.arsc]           # (注:此文件在反编译后不直接出现,其内容已解析并融入 res/ 目录中)

要修改应用的逻辑,则需要修改各个smali目录下的smali文件,这些是反编译出来的字节码文件。 继续阅读安卓应用脱壳逆向笔记

frida调试安卓应用时重载loadLibrary导致无法从应用库目录载入库文件

我让ai给我写了段调试安卓应用的frida脚本,里面有一段类似这样的代码:

var SystemClass = Java.use("java.lang.System");
SystemClass.loadLibrary.overload("java.lang.String").implementation = function (libname) {
	console.log("[库加载] System.loadLibrary被调用: " + libname);

	// 所有库都调用原始方法,不做任何拦截
	this.loadLibrary(libname);
};

然后调试应用的时候就发现应用总是只从”/system/lib/”和” /vendor/lib/”中寻找库文件了,结果当时是加载失败,踩了个坑,记录一下。

似乎一旦重载了这种方法,就会影响应用加载库的搜索路径,目前没找到什么好办法解决。

[ESPHOME]自定义开发

esphome提供的框架偏向于让我们尽量在yaml配置文件中定义逻辑,它提供了很多基本的功能,这样可以减少自己需要写的代码,也可以减少出问题的概率。

如果框架提供的部分满足不了自己的全部需求的话,就需要自己写c++代码。另外再强调一下,esphome基于platformIO工具链,再底层可以选Arduino,也可以是esp-idf,在开发时要考虑兼容性的话(比如在esp8266和esp32上都可以运行),就尽量使用esphome和arduino的api。

esphome提供了几种方式混入外部的代码:

  1. 在配置文件中使用”includes”选项直接一同编译指定的文件。
  2. 在配置文件中使用”libraries”选项引入外部的库。
  3. 使用外部组件,外部组件支持直接指定仓库地址,它在编译时会去把仓库克隆下来。当指定的是本地路径时,也就是把本地的一个目录当成一个组件库来处理。

要自定义代码我认为最好的方式就是在项目目录里定义一个本地的外部组件,虽然使用外部组件需要写一个python初始化脚本(这是我极其讨厌的,还好有AI帮我写),但这种组件的形式可以更容易地嵌入和调用我们的代码(因为如果你不用这种形式,就要禁用main.cpp自动生成,然后自己去手写入口文件,否则你的代码就没有地方可以调用。esphome框架默认会生成一份它自己的入口和loop,你就不能再在全局定义你的了。)。

继续阅读[ESPHOME]自定义开发

[ESPHOME]本地部署开发环境

这是一篇笔记,记录我开始尝试使用esphome做一些小工具的过程,之后可能还会有一些相关的笔记。
如果你要问为什么我不用home assistant里的esphome builder插件的话,原因是我需要插入我自己的项目代码,如果使用esphome builder 我就必须先把代码上传到某处然后把部件的地址写在配置文件里才能下载并编译,会很麻烦,所以需要一个本地开发环境用于快速测试更改。

安装环境

使用esphome主要是为了接入home assistant,esphome依赖platformIO(以下都简称为pio)作为底层框架,因此需要先安装它。在windows平台,我们可以直接使用vscode安装platformIO插件,这个插件将会帮我们把pio安装好,在安装pio的时候需要确保网络爬墙,否则大概率会安装失败,在实际使用时我们并不需要直接去用这个插件,之后的步骤都将靠esphome的命令完成。

然后是安装esphome工具链,这其实是一个python包,按照官网的方式安装:https://esphome.io/guides/installing_esphome

pip3 install wheel
pip3 install esphome

然后使用`esphome version`命令检查能否正确显示版本号即可。

项目初始化

接下来进行一个基础的固件编译测试,首先确定要使用的esp8266或者esp32的模块,然后用以下命令来初始化一个配置文件:

esphome wizard 配置文件名字.yaml

执行后将会引导你选择板子以及进行一些设置,这里就不赘述了,完成后会生成一个配置文件。

我在写这篇笔记时使用的测试板是一个esp8266的开发板,选择的是’nodemcuv2’。

固件编译

有了配置文件之后就可以使用以下命令来进行编译,编译出来的固件会在当前目录下的”.esphome\build”文件夹里,这里面还有其它的缓存之类的东西,如果之后的编译出现了奇怪的错误,可以尝试删除这个文件夹。

esphome compile 配置文件名字.yaml

固件烧录

编译好固件之后就可以用以下命令上传你的固件,第一次上传需要使用串口连接,以后的固件就可以通过网络OTA的方式进行更新了(也是这个命令,执行时会让你选择)。

esphome upload 配置文件名字.yaml

固件写完之后重启开发板,它将会开始连接到wifi,此时同一局域网里的home assistant应该就会提示发现了新的设备,问是否要添加,添加时把配置里的api密钥填进去连接即可。

额外测试

接下来配置一个简单的控制开关用来点亮开发板上的led,在配置文件里加入以下顶层项目:

# LED控制配置
output:
- platform: gpio
pin: D4
id: led_gpio
inverted: true #led在引脚低电平时是亮,所以要反转一下

switch:
- platform: output
name: "开发板LED"
output: led_gpio

然后再编译并上传,设备联网后在home assistant的该设备页面上应该会自动多出来一个控制开关,现在就可以通过这个开关控制led的亮灭了。


继续阅读[ESPHOME]本地部署开发环境

[ZFS]给Zpool中的硬盘附加镜像

刚刚在处理truenas里一块问题镜像盘的时候本想把它下线,结果不小心直接把它删除了,于是这个pool直接就被降级成了条带池。

然后我插进另一块盘之后想要再把它添加为镜像盘,Truenas的ui竟然不让,于是只能用命令,这里做个笔记。

首先用 zpool status 池名称 看一下想要镜像的盘在池中的id,这里我的是”df872c84-8689-44f1-8a1d-5f0ee673c34d”。

然后用以下命令直接把新的硬盘添加进池中即可。

zpool attach 池名称 df872c84-8689-44f1-8a1d-5f0ee673c34d /新盘的路径

 

[Zig]使用 std.Thread.Pool 线程池

我发现Zig的官方文档里似乎没有写这个`std.Thread.Pool`的具体用例,所以写一篇笔记给大家参考一下。

这个Zig标准库中的线程池主要用到两个部分,一个是线程池本身的结构体`std.Thread.Pool`,一个是用于等待任务结束的`std.Thread.WaitGroup`(等待组)结构体。等待组的作用是把一组相关的任务放在一起,之后可以使用特定方法等待它们全部完成。

流程:

  • 在线程池初始化好了之后在创建线程任务时给pool.spawnWg方法传入指定的WaitGroup和要执行的函数以及参数
  • 分配任务时线程池就会开始执行任务
  • →随时使用`pool.waitAndWork`方法阻塞当前线程,等待WaitGroup中所有任务完成
  • ↘或者在任何时候使用`wait_group.isDone()`方法检查任务是否全部完成,如果没有完成可以让主线程去做其他事情。

以下直接写一个示例,内容依然是在我前面的笔记中已经出现了好几次的多线程数据竞争示例。

const std = @import("std");
const debug = std.debug;
const Pool = std.Thread.Pool;
const Thread = std.Thread;
const WaitGroup = std.Thread.WaitGroup;

const print = debug.print;

const NUM_THREADS = 2;

var sum: u64 = 0;//让几个线程分别把这个sum自增,完成后sum应当由于非加锁自增导致其值为一个随机数
pub fn sumTask(idx: usize) void {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Thread {} (idx:{}) : starting...\n", .{ tid, idx });
    var timer = std.time.Timer.start() catch unreachable; //获取一个计时器,用于之后计算线程执行时间
    const y = &sum;
    for (0..50000000) |_| {
        y.* += 1;
    }
    print("Thread {} (idx:{}) : done in {}ms.\n", .{ tid, idx, timer.read() / 1000000 });
}
test "threadpool" {
    const tid = Thread.getCurrentId(); // 获取线程真实的id
    print("Main thread {} start\n", .{tid});
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    var thread_pool: Pool = undefined;
    try thread_pool.init(.{ //初始化线程池
        .allocator = allocator,
        .n_jobs = NUM_THREADS, //线程数
    });
    defer thread_pool.deinit();
    var wait_group: WaitGroup = undefined;
    wait_group.reset(); //初始化等待组
    //分配10个任务,线程池内的空闲线程会分别完成这些任务
    for (0..10) |idx| {
        thread_pool.spawnWg(
            &wait_group,//传入等待组
            sumTask,//传入任务函数
            .{idx},//任务函数的参数元组
        );
    }
    thread_pool.waitAndWork(&wait_group); //等待以上分配的任务全部完成
    print("Main: sum = {}\n", .{sum});
    print("Main: program completed. Exiting.\n", .{});
}

需要注意的是,如果在分配完任务后立刻使用`thread_pool.waitAndWork`等待任务完成(像上面的示例中一样,也就是在任务没有全部完成时就开始等待),你会发现虽然设置的线程数是2,理论上任务应该两个两个一起执行,但实际上却是3个任务一起执行的,因为调用这个等待方法的线程也执行了任务。我当时也疑惑了一会儿,然后想明白了,既然等待线程已经等在那里了,那么与其干等不如一起把任务做完,这样的逻辑确实是合理的,但这样实际并行执行的线程就会多一个,这是要注意的地方。

另一个要注意的是,目前的线程池实现中每一个线程启动之后就会开始从任务队列的开头取任务,但添加任务的时候也是添加到队列的开头的,所以虽然不严格保证顺序(因为你后续还可以继续添加任务),但任务队列大致上是后进先出的,所以不要连续不断地添加任务,至少要等线程池任务清空了才可以继续添加新的任务。(我觉得这样不太合理)

[WebAssembly]使用Zig语言制作wasm模块

“前几篇文章你不是还在写C++的wasm笔记,为什么突然开始使用Zig了?”

实际上由于C++古老的语言特性导致有的时候必须把声明和定义分开来写非常繁琐,而且在Emscripten环境下要添加第三方的库比较麻烦,所以我想改到一个自带包管理的语言。于是就首先捡起以前学到一半被劝退的Rust,这个国庆我花了几天时间把rust的教程看完了,但我又一次被这个反人类语言极致的复杂性劝退了,也就有了上一篇文章的内容。

在群友的推荐下我发现Zig这个类C的语言好像还行,于是抱着试一试的心态来学习一下,也就有了这篇笔记。不过不代表我之后就会用这个语言写项目,还有待观察。

这篇笔记的主要内容不是怎么编写Zig语言,而是如何把一个Zig项目编译成wasm模块,因此需要先有一点点的zig基础才能看懂。现在zig的文档和社区生态还不是很完善,且有的东西还在变化,有的东西是查不到或者很难查到的,我花了一些时间摸索才搞明白一些要点,这就是写这篇笔记的原因。

环境准备

如果还没有zig环境,可以用scoop快速部署,只需执行两个脚本即可

安装scoop

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression

用scoop安装最新版的zig,我写这篇笔记的时候zig版本是0.14.0-dev.1798,部分特性有可能在以后改变。

scoop bucket add versions
scoop install versions/zig-dev

以上即可完成zig环境的安装。顺带一提,使用scoop update zig-dev可以更新zig-dev的版本。

简单的示例

最简单快速的方式就是创建一个zig文件,然后使用命令把它编译成wasm文件。

main.zig 文件(由于代码高亮插件没有支持这个语言,而且以下示例看着挺像rust的,所以先用rust高亮来凑活一下)

const std = @import("std");

pub fn main() !void {
    const poi: u16 = 666;

    std.debug.print("poi: {}\n", .{poi});
}

然后使用命令编译

zig build-exe main.zig -target wasm32-wasi

此时目录下就会多出一个main.wasm和一个main.wasm.o文件(以后不同版本可能有所区别),和Emscripten不同的是它不会帮你生成一个胶水js文件供你直接加载。

这里我们使用的是wasi接口的wasm,如果你的运行环境是wasmtime的话,那么就可以直接执行这个wasm文件了。

生成的结果使用nodejs也可以运行,但要在nodejs中运行它,我们得自己为加载wasm写一些代码,还好也不多:

start.js 文件

const { readFile } = require('node:fs/promises');
const { WASI } = require('node:wasi');
const { argv, env } = require('node:process');
const { join } = require('node:path');

const wasi = new WASI({
	version: 'preview1',//该项必须定义,见文档:
	// https://nodejs.org/docs/latest-v20.x/api/wasi.html#new-wasioptions
	args: argv,//该项不是必须的,用于把node运行参数传给wasm程序
	env,//该项不是必须的,用传递环境变量
	
	//其它参数也见上面的文档链接
	//注意这是一个较新的特性,不同nodejs版本的接口可能会有区别,注意选择对应的文档版本
});

(async () => {
	const wasm = await WebAssembly.compile(
		await readFile(join(__dirname, 'main.wasm')),
	);
	const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
	wasi.start(instance);
})();

使用nodejs执行以上js文件将得到输出结果:poi: 666

继续阅读[WebAssembly]使用Zig语言制作wasm模块