小类随手记

esp32-c3 在 NixOS 上自编译 micropython v1.28 固件(esptouch / coretemp)

本文记录在 NixOS 上自编译 esp32-c3 micropython v1.28.0 固件,并内置 esptouch(ESP-Touch + AirKiss)智能配网与 coretemp(芯片内置温度传感器)两个模块的完整过程。

本文主要内容由skill自动创建,你如果觉得本文过度复杂不够条理,可以直接丢给AI参考。

前作 esp32-s2-mpy-smartconfig 基于 ubuntu + micropython 1.20 + esp-idf 4.4,本文把版本栈更新到 v1.28 + ESP-IDF 5.x,平台换成 NixOS,并补上了挖过的几个坑。文末附 flake 开发环境、编译脚本和 C 模块完整源码。

为什么自编译

  • 只加入自己需要的模块,降低资源占用
  • 把 .py 编译成中间码烧进固件,方便量产、一定程度保护代码
  • 内置 smartconfig 类 C 模块,比纯 Python 实现更省内存、更稳定

详细好处见 esp32-s2-mpy-smartconfigesp32-mpy-code-to-bin

版本与结构变化

  • micropython v1.28.0(2026-04-06),ports/esp32/README.md 推荐 ESP-IDF v5.5.1,同时支持 v5.3 / v5.4 / v5.4.1 / v5.4.2;本文使用 v5.3.2。
  • 与老文(v1.20 + IDF v4.4.5)相比,有三个直接影响移植的差异:
    • MICROPY_SOURCE_PORT 列表从 ports/esp32/main/CMakeLists.txt 移到了 ports/esp32/esp32_common.cmake,注入 C 模块的位置变了
    • 端口源码从 STATIC 宏改为普通 static,照抄老代码会编译报错
    • IDF 5.x 的 esp_smartconfig.h 位于 components/esp_wifi/include/,smartconfig 已并入 esp_wifi 组件

NixOS 环境要点

NixOS 不能直接照搬官方 ubuntu 流程,需要处理几个点:

  • ESP-IDF 预编译工具链是动态链接的,依赖 /lib64/ld-linux-x86-64.so.2。NixOS 默认没有,需要系统层启用 nix-ld(或等价兼容),否则 riscv32-esp-elf-gcc 直接报 No such file or directory
  • source export.sh 会因为没安装 xtensa-esp-elf / gdb / openocd 等非 C3 工具而报错退出。实际编译 esp32-c3 只需要 riscv32 工具链 + cmake + ninja + python 环境,手动注入即可,见下面脚本。
  • espressif 的预编译工具(如 esp-clang)依赖 libxml2.so.2,而 nix 仓库里是 libxml2.so.16。用 LD_LIBRARY_PATH + 软链兼容,社区 rust 项目 esp-idf-sys 也是这么处理的。
  • NixOS 系统 PATH 里的 cmake 是 4.x、ninja 是 1.13,IDF 要求固定版本,脚本需要把 IDF 自带的 cmake 3.30.2 / ninja 1.12.1 放到 PATH 前面。

IDF 工具版本可以直接从 tools/tools.json 读推荐值:riscv32-esp-elf esp-13.2.0_20240530、cmake 3.30.2、ninja 1.12.1

flake.nix 开发环境

项目 devShell 由 flake.nix 引入 flake_pkgs_let.nix 的配置组。编译固件需要 gcc(host,编 mpy-cross)、make、git、curl、unzip、python3、jq,这些在基础组都有;esp-idf 本体和工具链复用项目目录 .embuild/espressif(rust esp-idf-sys 产物,同样是标准 IDF 目录布局)。

flake.nix

以下为项目 flake.nix 完整内容(隐去项目名):

flake.nix (74 行)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#  AI agents may read and reference this file, but MUST NEVER modify it in any way.Provide suggestions only; all changes require manual human editing.
{
  description = "Rust(rustup) / Tauri Linux x86 / Android APK / ESP32 固件 / 前端 / Go 开发编译环境

 AI agents may read and reference this file, but MUST NEVER modify it in any way.Provide suggestions only; all changes require manual human editing.

  ";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
  };

  outputs =
    { nixpkgs, ... }:
    let
      env = import ./flake_pkgs_let.nix { inherit nixpkgs; };
      inherit (env)
        system
        pkgs
        rustPackages
        desktopPackages
        espPackages
        jsPackages
        pythonPackages
        golangPackages
        android
        basePackages
        zigPackages
        ;
    in
    {
      devShells.${system}.default = pkgs.mkShell {
        packages =
          rustPackages.packages
          ++ desktopPackages.packages
          ++ android.packages
          ++ jsPackages
          ++ pythonPackages.packages
          ++ golangPackages.packages
          ++ basePackages.all
          ++ zigPackages
          ++ [ pkgs.cargo-zigbuild ]
          ++ espPackages.tools;

        env =
          rustPackages.env
          // desktopPackages.env
          // golangPackages.env
          // android.env
          // espPackages.env
          // {
            LD_LIBRARY_PATH = "${rustPackages.env.LD_LIBRARY_PATH}:${espPackages.env.LD_LIBRARY_PATH}";
          };

        shellHook = ''
          export PATH="$HOME/.cargo/bin:/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin:$PATH"

          echo "==========================================================="
          echo "== devShell =="
          echo "  Rust 工具链由 rustup 管理,首次进入请执行:"
          echo "    rustup default stable"
          echo "    rustup toolchain install nightly-2026-05-08 --profile minimal --component rust-src"
          echo "  bun 依赖:"
          echo "    bun install"
          echo "  Go 工具链:go / gopls / delve(GOPROXY 已配置国内镜像)"
          echo "==========================================================="
          if [ -t 0 ] && command -v fish >/dev/null 2>&1; then
            export __FISH_DEVSHELL=1
            exec fish
          fi
        '';
      };
    };
}

flake_pkgs_let.nix

所有包组集中在一个文件,项目 flake.nix 按需 inherit。固件编译相关的重点是 espPackages(esptool / espflash / mpremote / python3 等)和 basePackages(gcc / make / git / curl / unzip / jq)。完整内容如下:

flake_pkgs_let.nix (317 行)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
{ nixpkgs }:
let
  readme = ''
    # flake_pkgs_let.nix —— 统一开发环境配置全集

    ## 作用
    - 所有项目共享的开发环境包配置全集,项目 flake.nix 按需引入其中的组。
    - 本文件所有副本必须保持完全一致;修改后需同步到:
      - os-config/dev-env/flake_pkgs_let.nix(模板与系统模块的数据源)
      - 各项目根目录的 flake_pkgs_let.nix

    ## 使用方式(项目 flake.nix)
    - import ./flake_pkgs_let.nix 取得 env,按需 inherit 需要的组:
        env = import ./flake_pkgs_let.nix { inherit nixpkgs; };
        inherit (env) system pkgs rustPackages desktopPackages android;
    - devShell 中 packages 组合各组的 .packages,env 组合各组的 .env。

    ## 配置组说明
    - basePackages:基础工具全集 all = utils + net + archive + dev + build + libs
      - utils 通用命令 / net 网络下载 / archive 压缩打包 / dev 开发辅助 / build 编译工具链 / libs 编译链接库
    - jsPackages:nodejs + bun + typescript + tsserver + prettierd(前端)
    - languagePackages:系统级语言运行时、LSP、格式化器与常用开发工具
    - cloudflarePackages:wrangler(Cloudflare Workers)
    - playwrightPackages / playwrightLibPath:chromium、google-chrome 及运行库路径
    - rustPackages:rustup + cargo 工具链 + LLVM;env 含 RUSTFLAGS / sccache 等
    - desktopPackages:Linux 桌面 GUI 依赖(GTK / WebKitGTK / 图形栈),Tauri / Flutter / Electron 通用
    - espPackages:ESP32 工具(espflash / esptool / espup / ldproxy 等)+ esp-idf 动态库与 python 依赖 + pip 国内镜像
    - docsPackages:文档工具 all = hugo + jekyll + mdbook
    - golangPackages:go + gopls + delve;env 含 GOPROXY 国内镜像
    - zigPackages:zig + zls
    - pythonPackages:python313 + uv / ruff / pyright 等
    - flutterPackages:flutter(Android 构建依赖 android 组)
    - android:Android SDK / NDK + jdk17;env 含 ANDROID_HOME / JAVA_HOME 等

    ## 快速上手
    - 复制 os-config/dev-env/flake_tpl_*.nix(按语言组合命名)到项目改名 flake.nix 即可使用。
    - 组合模板:flake_tpl_rust_tauri(Rust + Tauri + Android + Bun)、flake_tpl_all(全量)。

    ## 系统级引入(可选)
    - os-config modules/dev/dev_base_and_import_all.nix 将全部包挂入系统 systemPackages:
      - 防止 nix-collect-garbage 清理开发环境包(devShell 包不在系统闭包内)
      - 仅引入 packages,不注入 env,避免污染日用环境
  '';
  system = "x86_64-linux";
  pkgs = import nixpkgs {
    inherit system;
    config = {
      allowUnfree = true;
      android_sdk.accept_license = true;
    };
  };

  pick =
    set: names:
    map (n: pkgs.lib.getAttrFromPath (pkgs.lib.splitString "." n) set) (pkgs.lib.splitString "|" names);
  pkgList = pick pkgs;

  # 基础工具:按用途细分(按需用 all 或细分字段)
  basePackages = rec {
    utils = pkgList "git|fish|hivemind|tree|ripgrep|jq|file|bc|lsof|dconf|coreutils-full"; # 通用命令行工具
    net = pkgList "curl|wget"; # 网络下载
    archive = pkgList "unzip|zip|xz|zstd|p7zip|upx"; # 压缩打包
    dev = pkgList "shellcheck|shfmt|taplo|pkg-config|perl|dpkg|protobuf"; # 开发辅助
    build = pkgList "gcc|gnumake|cmake|ninja|binutils|patchelf"; # 编译工具链
    libs = pkgList "openssl|openssl.dev|sqlite|zlib|zlib.dev"; # 编译链接库(不放 glibc.static:其 lib 会进 NIX_LDFLAGS,链接器误用 libc.a 导致 Rust 程序崩溃)
    all = utils ++ net ++ archive ++ dev ++ build ++ libs;
  };

  # 前端
  jsPackages = pkgList "nodejs|pnpm|bun|typescript|typescript-language-server|prettierd";

  # Node/npm 全局安装环境:Nix store 内 nodejs 安装目录只读,npm -g 的 prefix
  # 必须指向用户可写目录,否则 install/uninstall -g 都会报 ENOENT。
  nodejs = {
    env = {
      NPM_CONFIG_PREFIX = "/home/y/.npm-global";
    };
  };

  # 语言运行时、LSP、格式化器与编辑器通用开发工具。
  # 系统模块和项目 devShell 共用这组定义,避免维护两套基础开发环境。
  languagePackages = rec {
    python = pkgs.python313.withPackages (
      ps: with ps; [
        pip
        requests
        pyyaml
        toml
        tkinter
        numpy
        pandas
        scipy
        scikit-learn
        matplotlib
        pillow
        beautifulsoup4
        lxml
        httpx
        aiohttp
        flask
        fastapi
        uvicorn
        django
        pytest
        openpyxl
        python-dotenv
        click
        tqdm
        rich
      ]
    );
    packages =
      with pkgs;
      [
        mise
        python
        uv
        ruff
        python3Packages.debugpy
        pyright
        go
        gopls
        golangci-lint
        gofumpt
        delve
        lua-language-server
        stylua
        vscode-langservers-extracted
        vscode-js-debug
        taplo
        yaml-language-server
        kdlfmt
        nixfmt
        nixd
        nil
        nixpkgs-fmt
        shfmt
        bruno
        yaak
        jq
        ripgrep
        tokei
        lsof
        file
        tree
        perl
        bc
        xdg-utils
        sshpass
        xdotool
        pciutils
        usbutils
        vips
        imagemagick
        ffmpeg-full
        blender
        hugo
      ]
      ++ jsPackages;
  };

  # Cloudflare Workers(wrangler 已从 nix 移除,改用 bunx/npx wrangler 按需调用)
  cloudflarePackages = [ ];

  # Playwright
  playwrightLibPath = pkgs.lib.makeLibraryPath (
    pick pkgs "nspr|nss|cups|expat|libxcb|libXcomposite|libXdamage|libgbm|systemd|alsa-lib"
  );
  playwrightPackages = pkgList "chromium|google-chrome";
  # RustBase 使用rustup 虽然需要手动安装,但是nix表达更简单
  rustPackages = rec {
    libPath = pkgs.lib.makeLibraryPath (pkgList "openssl|curl|zlib|stdenv.cc.cc.lib");
    serverLibPath = pkgs.lib.makeLibraryPath (pkgList "openssl|sqlite|zlib|libffi");
    rustFlags = "-Cdebuginfo=1 -Ccodegen-units=1 -Clink-arg=-fuse-ld=lld -Csplit-debuginfo=packed -Clink-arg=-Wl,-rpath,${serverLibPath} -Clink-arg=-Wl,-rpath,${desktopPackages.libPath}";
    packages = pkgList "rustup|cargo-tauri|sccache|mold|cargo-edit|cargo-nextest|cargo-binstall|cargo-release|lldb|llvmPackages_19.clang|llvmPackages_19.libclang|llvmPackages_19.libllvm|llvmPackages_19.lld|gcc|gnumake|cmake|ninja|binutils|patchelf";
    env = {
      RUSTFLAGS = rustFlags;
      RUSTC_WRAPPER = "sccache";
      SCCACHE_CACHE_SIZE = "30G";
      CARGO_BUILD_JOBS = "12";
      CARGO_INCREMENTAL = "0";
      CARGO_PROFILE_DEV_INCREMENTAL = "false";
      CARGO_PROFILE_DEV_DEBUG = "0";
      CARGO_CACHE_RUSTC_INFO = "0";
      RUSTC_CODEGEN_UNITS = "1";
      LIBCLANG_PATH = "${pkgs.llvmPackages_19.libclang.lib}/lib";
      LIBRARY_PATH = "${serverLibPath}:${desktopPackages.libPath}";
      LD_LIBRARY_PATH = libPath;
    };
  };
  # Linux 桌面 GUI 应用依赖(Tauri/Flutter/Electron 通用)
  desktopPackages = {
    libPath = pkgs.lib.makeLibraryPath (
      pkgList "libglvnd|mesa|libgbm|wayland|libx11|libxrandr|libxrender|libxcursor|libxinerama|libxi|libxext|libxfixes|libxcb|libXcomposite|libXdamage|glib|gtk3|cairo|pango|gdk-pixbuf|atk|harfbuzz|pcre|gst_all_1.gstreamer|gst_all_1.gst-plugins-base|gst_all_1.gst-plugins-good|gst_all_1.gst-plugins-bad|gst_all_1.gst-plugins-ugly|gst_all_1.gst-libav|libsoup_3|webkitgtk_4_1|dbus|at-spi2-core|libxkbcommon|libayatana-appindicator|librsvg|nss|nspr|cups|expat|systemd|udev|alsa-lib"
    );
    packages = pkgList "webkitgtk_4_1|webkitgtk_4_1.dev|libsoup_3|libsoup_3.dev|gtk3|gtk3.dev|glib|glib.dev|cairo.dev|pango.dev|gdk-pixbuf.dev|atk.dev|harfbuzz.dev|libepoxy.dev|librsvg|librsvg.dev|at-spi2-core.dev|dbus.dev|libayatana-appindicator|glib-networking|gsettings-desktop-schemas|libglvnd|libglvnd.dev|mesa|libxkbcommon.dev|wayland.dev|libx11.dev|libxext.dev|libxi.dev|libxrandr.dev|libxrender.dev|libxcursor.dev|libxinerama.dev|libxfixes.dev|libxtst|libxxf86vm|libdrm|libffi.dev|pcre.dev|openssl|openssl.dev|sqlite.dev|zlib.dev|expat|nss|nspr|cups|udev|alsa-lib|gst_all_1.gstreamer|gst_all_1.gst-plugins-base|gst_all_1.gst-plugins-good|gst_all_1.gst-plugins-bad|gst_all_1.gst-plugins-ugly|gst_all_1.gst-libav|dconf";
    env = {
      GIO_EXTRA_MODULES = "${pkgs.glib-networking}/lib/gio/modules:${pkgs.dconf.lib}/lib/gio/modules";
      GST_PLUGIN_SYSTEM_PATH_1_0 = (
        pkgs.lib.concatStringsSep ":" (
          map (pkg: "${pkgs.lib.getOutput "out" pkg}/lib/gstreamer-1.0") (
            pick pkgs.gst_all_1 "gstreamer|gst-plugins-base|gst-plugins-good|gst-plugins-bad|gst-plugins-ugly|gst-libav"
          )
        )
      );
      GST_PLUGIN_SCANNER_1_0 = "${pkgs.lib.getOutput "out" pkgs.gst_all_1.gstreamer}/libexec/gstreamer-1.0/gst-plugin-scanner";
      WEBKIT_DISABLE_COMPOSITING_MODE = "1";
    };
  };
  # ESP32 工具与 esp-idf 运行环境
  espPackages = rec {
    libPath = pkgs.lib.makeLibraryPath (pkgList "libxml2|xz|zlib|libffi");
    tools = pkgList "espflash|esptool|espup|ldproxy|mpremote|minicom|picocom|libxml2|xz|cmake|ninja|python313";
    pythonDeps =
      ps: with ps; [
        pyserial
        pyparsing
        click
        packaging
        pyyaml
        setuptools
        pyelftools
        construct
        cryptography
        pygdbmi
        requests
      ];
    env = {
      LD_LIBRARY_PATH = libPath;
      PIP_INDEX_URL = "https://mirrors.aliyun.com/pypi/simple/";
      PIP_EXTRA_INDEX_URL = "https://dl.espressif.com/pypi";
      PIP_TRUSTED_HOST = "mirrors.aliyun.com dl.espressif.com";
    };
  };

  #文档工具
  # 文档工具(hugo / jekyll / mdbook,按需用 all 或细分字段)
  docsPackages = rec {
    hugo = pkgList "hugo|go";
    jekyll = [ (pkgs.ruby.withPackages (_: [ pkgs.bundler ])) ];
    mdbook = pkgList "mdbook";
    all = hugo ++ jekyll ++ mdbook;
  };

  # Go(含国内镜像配置)
  golangPackages = {
    packages = pkgList "go|gopls|delve";
    env = {
      GOPROXY = "https://goproxy.cn,direct";
      GOSUMDB = "sum.golang.org";
    };
  };

  # Zig
  zigPackages = pkgList "zig|zls";

  pythonPackages = {
    packages = [
      (pkgs.python313.withPackages (
        ps: (espPackages.pythonDeps ps) ++ (pick ps "pip|tkinter|json5|protobuf|click|pyyaml")
      ))
    ]
    ++ pkgList "uv|ruff|python3Packages.debugpy|pyright";
  };
  # Flutter(Android 构建依赖 android 组)
  flutterPackages = pkgList "flutter";

  # 安卓编译 tauri 和 flutter必须
  android = rec {
    sdk =
      (pkgs.androidenv.composeAndroidPackages {
        cmdLineToolsVersion = "latest";
        platformToolsVersion = "latest";
        buildToolsVersions = pkgs.lib.splitString "|" "35.0.0|36.1.0";
        platformVersions = pkgs.lib.splitString "|" "34|35|36";
        includeNDK = true;
        ndkVersion = "28.2.13676358";
        includeCmake = true;
        includeEmulator = false;
        includeSources = false;
      }).androidsdk;
    home = "${sdk}/libexec/android-sdk";
    ndk = "${home}/ndk/28.2.13676358";
    packages = [ sdk ] ++ pkgList "jdk17|cargo-ndk|usbutils|scrcpy";
    env = {
      ANDROID_HOME = home;
      ANDROID_SDK_ROOT = home;
      ANDROID_NDK = ndk;
      ANDROID_NDK_HOME = ndk;
      JAVA_HOME = "${pkgs.jdk17}";
      GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${home}/build-tools/35.0.0/aapt2";
    };
  };
in
{
  inherit
    readme
    system
    pkgs
    basePackages
    jsPackages
    nodejs
    languagePackages
    cloudflarePackages
    playwrightLibPath
    playwrightPackages
    rustPackages
    desktopPackages
    espPackages
    docsPackages
    golangPackages
    zigPackages
    pythonPackages
    flutterPackages
    android
    ;
}

编译脚本

脚本做这些事:

  • clone:浅克隆 micropython v1.28.0 到构建目录
  • patch:把 mpy-fw/ 目录下全部 .c 模块复制进 ports/esp32/,并在 esp32_common.cmakeMICROPY_SOURCE_PORT 逐个追加(幂等)
  • 注入 IDF 环境:IDF_PATH / IDF_TOOLS_PATH / IDF_PYTHON_ENV_PATH,PATH 前置 venv + riscv32 + cmake + ninja + $IDF_PATH/tools,LD_LIBRARY_PATH 补 libxml2 兼容
  • make -C mpy-cross(host gcc)
  • make submodules(第一次会拉取网络依赖,包括组件管理器要的 espressif/mdns)
  • make BOARD=ESP32_GENERIC_C3(增量)
  • 复制 firmware.bin / micropython.elf / micropython.map 并打印 SHA-256
  • 校验:ELF 里有各模块 *_user_cmodule 符号(当前 esptouch_native / coretemp)、生成的 genhdr/moduledefs.h 登记了模块、esptool 能解析镜像

下面的脚本是项目脚本的精简版,目录名做了泛化(fw-build / fw-bin / mpy-fw)。

mpy-build.sh (224 行)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
MPY_BUILD_ROOT="$ROOT_DIR/fw-build"
MPY_REPO_DIR="$MPY_BUILD_ROOT/micropython"
MPY_MODULE_DIR="$ROOT_DIR/mpy-fw"
FW_DIR="$ROOT_DIR/fw-bin"

: "${MPY_VERSION:=v1.28.0}"
: "${MPY_VERSION_SHORT:=1.28.0}"
: "${MPY_RELEASE_DATE:=20260406}"
: "${BOARD:=ESP32_GENERIC_C3}"
FIRMWARE_NAME="ESP32_GENERIC_C3-${MPY_RELEASE_DATE}-v${MPY_VERSION_SHORT}-esptouch"

IDF_VERSION="v5.3.2"
: "${MPY_IDF_PATH:=$ROOT_DIR/.embuild/espressif/esp-idf/$IDF_VERSION}"
: "${MPY_IDF_TOOLS_PATH:=$ROOT_DIR/.embuild/espressif}"
: "${MPY_IDF_PYTHON_ENV:=$MPY_IDF_TOOLS_PATH/python_env/idf5.3_py3.13_env}"
: "${JOBS:=$(nproc)}"

do_mpy_build() {
  local action="${1:-build}"
  case "$action" in
    build) mpy_build ;;
    clone) clone_mpy ;;
    patch) apply_patch ;;
    env) print_idf_env ;;
    verify) verify_firmware ;;
    clean)
      echo "==> removing $MPY_BUILD_ROOT"
      rm -rf "$MPY_BUILD_ROOT"
      ;;
    *) die "actions: build|clone|patch|env|verify|clean" ;;
  esac
}

clone_mpy() {
  if [[ -d "$MPY_REPO_DIR/.git" ]]; then
    echo "micropython repo already exists: $MPY_REPO_DIR"
    return 0
  fi
  mkdir -p "$MPY_BUILD_ROOT"
  echo "==> cloning micropython $MPY_VERSION"
  git clone --depth 1 --branch "$MPY_VERSION" https://github.com/micropython/micropython.git "$MPY_REPO_DIR"
}

apply_patch() {
  clone_mpy
  local cmake="$MPY_REPO_DIR/ports/esp32/esp32_common.cmake"
  if ! grep -q '^    modespnow.c$' "$cmake"; then
    die "unexpected esp32_common.cmake: MICROPY_SOURCE_PORT list pattern not found"
  fi
  local src name patched=false
  for src in "$MPY_MODULE_DIR"/*.c; do
    [[ -f "$src" ]] || continue
    name="$(basename "$src")"
    echo "==> copying $src -> ports/esp32/$name"
    cp "$src" "$MPY_REPO_DIR/ports/esp32/$name"
    if grep -q "    $name" "$cmake"; then
      echo "==> $name already in MICROPY_SOURCE_PORT"
      continue
    fi
    sed -i "s|^    modespnow.c$|    modespnow.c\n    $name|" "$cmake"
    echo "==> added $name to MICROPY_SOURCE_PORT"
    patched=true
  done
  $patched || echo "==> $cmake already patched"
}

idf_tool_version() {
  # $1 = tool name,从 ESP-IDF tools.json 读推荐版本
  python3 - "$MPY_IDF_PATH/tools/tools.json" "$1" <<'EOF'
import json, sys

with open(sys.argv[1]) as f:
    data = json.load(f)
for tool in data["tools"]:
    if tool["name"] == sys.argv[2]:
        print(tool["versions"][0]["name"])
        break
EOF
}

setup_idf_export() {
  # export.sh 会因未安装 xtensa/gdb/openocd 等工具报错退出;这里只注入 esp32-c3 构建需要的工具。
  [[ -d "$MPY_IDF_PATH" ]] || die "ESP-IDF not found: $MPY_IDF_PATH (期望 $IDF_VERSION)"
  [[ -d "$MPY_IDF_TOOLS_PATH/tools" ]] || die "ESP-IDF tools not found: $MPY_IDF_TOOLS_PATH"
  [[ -x "$MPY_IDF_PYTHON_ENV/bin/python3" ]] || die "ESP-IDF python env missing: $MPY_IDF_PYTHON_ENV"

  local riscv_ver cmake_ver ninja_ver
  riscv_ver="$(idf_tool_version riscv32-esp-elf)"
  cmake_ver="$(idf_tool_version cmake)"
  ninja_ver="$(idf_tool_version ninja)"

  local riscv_dir="$MPY_IDF_TOOLS_PATH/tools/riscv32-esp-elf/$riscv_ver/riscv32-esp-elf/bin"
  local cmake_dir="$MPY_IDF_TOOLS_PATH/tools/cmake/$cmake_ver/bin"
  local ninja_dir="$MPY_IDF_TOOLS_PATH/tools/ninja/$ninja_ver"
  [[ -d "$riscv_dir" ]] || die "riscv32-esp-elf $riscv_ver missing under $MPY_IDF_TOOLS_PATH/tools"
  [[ -d "$cmake_dir" ]] || die "cmake $cmake_ver missing under $MPY_IDF_TOOLS_PATH/tools"
  [[ -x "$ninja_dir/ninja" ]] || die "ninja $ninja_ver missing under $MPY_IDF_TOOLS_PATH/tools"

  export IDF_PATH="$MPY_IDF_PATH"
  export IDF_TOOLS_PATH="$MPY_IDF_TOOLS_PATH"
  export IDF_PYTHON_ENV_PATH="$MPY_IDF_PYTHON_ENV"
  export IDF_TARGET="esp32c3"
  unset PYTHONPATH
  export PATH="$MPY_IDF_PYTHON_ENV/bin:$riscv_dir:$cmake_dir:$ninja_dir:$MPY_IDF_PATH/tools:$PATH"

  # NixOS 下 espressif 预编译工具依赖系统 libxml2/zlib/gcc 运行库。
  find_nix_lib() {
    local pattern="$1"
    find /nix/store -maxdepth 1 -type d -name "$pattern" -print -quit 2>/dev/null
  }
  local nix_libxml2 nix_gcc_lib nix_zlib_lib
  nix_libxml2="$(find_nix_lib '*-libxml2-*')"
  nix_gcc_lib="$(find_nix_lib '*-gcc-*-lib')"
  nix_zlib_lib="$(find /nix/store -maxdepth 1 -type d -name '*-zlib-[0-9]*' -exec test -f '{}/lib/libz.so.1' \; -print -quit 2>/dev/null)"
  if [[ -n "$nix_libxml2" ]]; then
    export LD_LIBRARY_PATH="$nix_libxml2/lib${nix_gcc_lib:+:$nix_gcc_lib/lib}${nix_zlib_lib:+:$nix_zlib_lib/lib}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
    if [[ ! -f "$nix_libxml2/lib/libxml2.so.2" ]]; then
      local compat_dir="/tmp/esp-libxml2-compat"
      mkdir -p "$compat_dir"
      ln -sf "$nix_libxml2/lib/libxml2.so.16" "$compat_dir/libxml2.so.2" 2>/dev/null || true
      export LD_LIBRARY_PATH="$compat_dir:$LD_LIBRARY_PATH"
    fi
  fi
}

print_idf_env() {
  setup_idf_export
  echo "IDF_PATH=$IDF_PATH"
  echo "IDF_TOOLS_PATH=$IDF_TOOLS_PATH"
  echo "IDF_PYTHON_ENV_PATH=$IDF_PYTHON_ENV_PATH"
  echo "IDF_TARGET=$IDF_TARGET"
  command -v idf.py
  command -v riscv32-esp-elf-gcc
  riscv32-esp-elf-gcc --version | head -1
  command -v cmake
  cmake --version | head -1
  command -v ninja
}

build_mpy_cross() {
  echo "==> building mpy-cross (host compiler)"
  (cd "$MPY_REPO_DIR" && make -C mpy-cross -j"$JOBS")
}

ensure_submodules() {
  echo "==> updating esp32 submodules (first run downloads network deps)"
  (cd "$MPY_REPO_DIR/ports/esp32" && make submodules)
}

build_firmware() {
  local build_dir="$MPY_REPO_DIR/ports/esp32/build-$BOARD"
  if [[ -f "$build_dir/firmware.bin" ]]; then
    echo "==> firmware already built, incremental make"
  else
    echo "==> building MicroPython $MPY_VERSION BOARD=$BOARD (ESP-IDF $IDF_VERSION, first build slow)"
  fi
  (cd "$MPY_REPO_DIR/ports/esp32" && make BOARD="$BOARD" -j"$JOBS")
}

copy_artifacts() {
  local build_dir="$MPY_REPO_DIR/ports/esp32/build-$BOARD"
  [[ -f "$build_dir/firmware.bin" ]] || die "firmware.bin not found in $build_dir"
  mkdir -p "$FW_DIR"
  echo "==> copying artifacts to $FW_DIR"
  cp "$build_dir/firmware.bin" "$FW_DIR/$FIRMWARE_NAME.bin"
  if [[ -f "$build_dir/micropython.elf" ]]; then
    cp "$build_dir/micropython.elf" "$FW_DIR/$FIRMWARE_NAME.elf"
  fi
  if [[ -f "$build_dir/micropython.map" ]]; then
    cp "$build_dir/micropython.map" "$FW_DIR/$FIRMWARE_NAME.map"
  fi
  echo "==> SHA-256:"
  sha256sum "$FW_DIR/$FIRMWARE_NAME"* 2>/dev/null || true
}

verify_firmware() {
  local elf="$FW_DIR/$FIRMWARE_NAME.elf"
  [[ -f "$elf" ]] || die "missing $elf (run build first)"
  local src sym fail=0
  for src in "$MPY_MODULE_DIR"/*.c; do
    [[ -f "$src" ]] || continue
    sym="$(basename "$src" .c)_user_cmodule"
    if grep -a -q "$sym" "$elf"; then
      echo "OK: $sym found in $elf"
    else
      echo "FAIL: $sym missing in $elf" >&2
      fail=1
    fi
  done
  [[ "$fail" -eq 0 ]] || die "firmware module verification failed"
  if command -v esptool.py >/dev/null 2>&1 || command -v esptool >/dev/null 2>&1; then
    local esptool_bin
    esptool_bin="$(command -v esptool.py || command -v esptool)"
    echo "==> firmware image info:"
    "$esptool_bin" image_info "$FW_DIR/$FIRMWARE_NAME.bin" 2>/dev/null | head -8 || true
  fi
}

mpy_build() {
  clone_mpy
  apply_patch
  setup_idf_export
  build_mpy_cross
  ensure_submodules
  build_firmware
  copy_artifacts
  verify_firmware
  echo
  echo "==> done: $FW_DIR/$FIRMWARE_NAME.bin"
}

die() {
  printf '%s\n' "$*" >&2
  exit 1
}

do_mpy_build "$@"

注意首次 make 会通过 IDF 组件管理器拉取 espressif/mdnsmain/idf_component.yml 声明),需要网络;之后都是增量构建。

esptouch_native 模块

Python API

  • esptouch_native.start():开始 ESP-Touch/AirKiss;已在运行则无副作用
  • esptouch_native.stop():请求停止,幂等
  • esptouch_native.success():是否已收到 SSID/密码
  • esptouch_native.info():返回 (ssid, password, type, token)
  • esptouch_native.active():smartconfig 是否运行中
  • 常量:TYPE_UNKNOWN = -1TYPE_ESPTOUCH = 0TYPE_AIRKISS = 1TYPE_ESPTOUCH_AIRKISS = 2

与老文的差异

esp32-s2-mpy-smartconfig 的 smartconfig.c 相比主要改动:

  • 模块名用 esptouch_native,暴露 start/stop/success/info/active
  • STATIC 改成 static(v1.28 已移除 STATIC 宏)
  • 生命周期由 FreeRTOS 事件组管理:start() 建任务,收到凭据或 stop() 时任务内 esp_smartconfig_stop() 并自删,支持反复 start/stop
  • 事件回调和 MicroPython 任务之间用 portMUX_TYPE 自旋锁保护凭据缓冲,任意时刻读 success()/info() 都安全
  • WiFi 未启动时自动以 STA 模式拉起,但建议调用方先 WLAN(STA_IF).active(True)
  • 错误统一转 OSError,详细码打到 ESP_LOG

完整源码

esptouch_native.c (273 行)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/*
 * esptouch_native - ESP-Touch / SmartConfig native module for the MicroPython ESP32 port.
 *
 * Built into a custom MicroPython v1.28.0 firmware for ESP32-C3 (ESP-IDF v5.3.2).
 * The module wraps the ESP-IDF smartconfig component (ESP-Touch + AirKiss) and is
 * compiled into the firmware as a builtin module named "esptouch_native".
 *
 * Build integration (automated by build script):
 *   1. copy this file to <micropython>/ports/esp32/esptouch_native.c
 *   2. add esptouch_native.c to MICROPY_SOURCE_PORT in
 *      <micropython>/ports/esp32/esp32_common.cmake
 *
 * Python API:
 *   esptouch_native.start()              # begin ESP-Touch/AirKiss; no-op if already running
 *   esptouch_native.stop()               # request stop; idempotent
 *   esptouch_native.success() -> bool    # True once SSID/password were received
 *   esptouch_native.info() -> (ssid, password, type, token)
 *   esptouch_native.active() -> bool     # True while smartconfig is running
 *   constants: TYPE_UNKNOWN(-1), TYPE_ESPTOUCH(0), TYPE_AIRKISS(1), TYPE_ESPTOUCH_AIRKISS(2)
 *
 * Threading model:
 *   - the smartconfig task is a plain FreeRTOS task; it never touches MicroPython objects.
 *   - the SC_EVENT handler runs on the default event loop task and stores the latest
 *     credentials in C globals guarded by a FreeRTOS spinlock.
 *   - success()/info() are safe to call from the MicroPython task at any time.
 *
 * WiFi precondition:
 *   STA (or APSTA) must be initialised before start(); if WiFi was never
 *   started, start() initialises it in STA mode.
 */

#include "py/obj.h"
#include "py/runtime.h"
#include "py/mperrno.h"

#include <string.h>
#include <stdlib.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_netif.h"
#include "esp_log.h"
#include "esp_smartconfig.h"

#define TAG "esptouch_native"

#define ESPTOUCH_DONE_BIT BIT0
#define ESPTOUCH_STOP_BIT BIT1

#define ES_TOUCH_TASK_STACK 4096
#define ES_TOUCH_TASK_PRIORITY 3

static EventGroupHandle_t s_evt_group;
static volatile bool s_running;
static volatile bool s_found;
static bool s_handler_registered;
static portMUX_TYPE s_mux = portMUX_INITIALIZER_UNLOCKED;

static uint8_t s_ssid[33];
static uint8_t s_password[65];
static int s_type;
static uint8_t s_token;

static void esptouch_task(void *parm);

static void esptouch_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) {
    if (event_base != SC_EVENT) {
        return;
    }
    if (event_id == SC_EVENT_SCAN_DONE) {
        ESP_LOGI(TAG, "scan done");
    } else if (event_id == SC_EVENT_FOUND_CHANNEL) {
        ESP_LOGI(TAG, "found channel");
    } else if (event_id == SC_EVENT_GOT_SSID_PSWD) {
        smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t *)event_data;
        taskENTER_CRITICAL(&s_mux);
        size_t ssid_len = strlen((const char *)evt->ssid);
        if (ssid_len >= sizeof(s_ssid)) {
            ssid_len = sizeof(s_ssid) - 1;
        }
        memcpy(s_ssid, evt->ssid, ssid_len);
        s_ssid[ssid_len] = 0;
        size_t password_len = strlen((const char *)evt->password);
        if (password_len >= sizeof(s_password)) {
            password_len = sizeof(s_password) - 1;
        }
        memcpy(s_password, evt->password, password_len);
        s_password[password_len] = 0;
        s_type = (int)evt->type;
        s_token = evt->token;
        s_found = true;
        taskEXIT_CRITICAL(&s_mux);
        ESP_LOGI(TAG, "got SSID: %s (type=%d)", s_ssid, s_type);
        if (s_evt_group != NULL) {
            xEventGroupSetBits(s_evt_group, ESPTOUCH_DONE_BIT);
        }
    }
}

static esp_err_t ensure_wifi_ready(void) {
    esp_err_t err = esp_netif_init();
    if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
        return err;
    }
    err = esp_event_loop_create_default();
    if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
        return err;
    }

    // main.c / network_wlan.c of the ESP32 port already initialise WiFi when the
    // WLAN object is created; tolerate "already done" here.
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    err = esp_wifi_init(&cfg);
    if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
        return err;
    }

    wifi_mode_t mode = WIFI_MODE_NULL;
    err = esp_wifi_get_mode(&mode);
    if (err != ESP_OK) {
        return err;
    }
    if (mode == WIFI_MODE_NULL) {
        // WiFi never started: bring up STA so smartconfig can sniff packets.
        err = esp_wifi_set_mode(WIFI_MODE_STA);
        if (err != ESP_OK) {
            return err;
        }
        err = esp_wifi_start();
        if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
            return err;
        }
    }
    return ESP_OK;
}

static void esptouch_task(void *parm) {
    esp_err_t err = esp_smartconfig_set_type(SC_TYPE_ESPTOUCH_AIRKISS);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "set_type failed: 0x%x", err);
        goto exit;
    }
    smartconfig_start_config_t cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
    err = esp_smartconfig_start(&cfg);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "start failed: 0x%x", err);
        goto exit;
    }
    s_running = true;
    while (true) {
        EventBits_t bits = xEventGroupWaitBits(s_evt_group, ESPTOUCH_DONE_BIT | ESPTOUCH_STOP_BIT, pdTRUE, pdFALSE, portMAX_DELAY);
        if (bits & (ESPTOUCH_DONE_BIT | ESPTOUCH_STOP_BIT)) {
            break;
        }
    }
    err = esp_smartconfig_stop();
    if (err != ESP_OK) {
        ESP_LOGW(TAG, "stop failed: 0x%x", err);
    }

exit:
    s_running = false;
    vTaskDelete(NULL);
}

static void raise_esp_error(esp_err_t err) {
    ESP_LOGE(TAG, "smartconfig failed: 0x%x", err);
    mp_raise_OSError(MP_EIO);
}

static mp_obj_t esptouch_native_start(void) {
    if (s_running) {
        return mp_const_none;
    }
    esp_err_t err = ensure_wifi_ready();
    if (err != ESP_OK) {
        raise_esp_error(err);
    }

    s_found = false;
    s_ssid[0] = 0;
    s_password[0] = 0;
    s_type = -1;
    s_token = 0;

    if (s_evt_group == NULL) {
        s_evt_group = xEventGroupCreate();
        if (s_evt_group == NULL) {
            mp_raise_OSError(MP_ENOMEM);
        }
    } else {
        xEventGroupClearBits(s_evt_group, ESPTOUCH_DONE_BIT | ESPTOUCH_STOP_BIT);
    }

    if (!s_handler_registered) {
        err = esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, esptouch_event_handler, NULL);
        if (err != ESP_OK) {
            raise_esp_error(err);
        }
        s_handler_registered = true;
    }

    BaseType_t rc = xTaskCreate(esptouch_task, "esptouch_native", ES_TOUCH_TASK_STACK, NULL, ES_TOUCH_TASK_PRIORITY, NULL);
    if (rc != pdPASS) {
        mp_raise_OSError(MP_ENOMEM);
    }
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_0(esptouch_native_start_obj, esptouch_native_start);

static mp_obj_t esptouch_native_stop(void) {
    if (s_evt_group != NULL) {
        xEventGroupSetBits(s_evt_group, ESPTOUCH_STOP_BIT);
    }
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_0(esptouch_native_stop_obj, esptouch_native_stop);

static mp_obj_t esptouch_native_success(void) {
    return mp_obj_new_bool(s_found);
}
static MP_DEFINE_CONST_FUN_OBJ_0(esptouch_native_success_obj, esptouch_native_success);

static mp_obj_t esptouch_native_info(void) {
    taskENTER_CRITICAL(&s_mux);
    uint8_t ssid[sizeof(s_ssid)];
    uint8_t password[sizeof(s_password)];
    memcpy(ssid, s_ssid, sizeof(ssid));
    memcpy(password, s_password, sizeof(password));
    int type = s_type;
    uint8_t token = s_token;
    taskEXIT_CRITICAL(&s_mux);

    mp_obj_t items[4] = {
        mp_obj_new_str((const char *)ssid, strlen((const char *)ssid)),
        mp_obj_new_str((const char *)password, strlen((const char *)password)),
        mp_obj_new_int(type),
        mp_obj_new_int(token),
    };
    return mp_obj_new_tuple(4, items);
}
static MP_DEFINE_CONST_FUN_OBJ_0(esptouch_native_info_obj, esptouch_native_info);

static mp_obj_t esptouch_native_active(void) {
    return mp_obj_new_bool(s_running);
}
static MP_DEFINE_CONST_FUN_OBJ_0(esptouch_native_active_obj, esptouch_native_active);

static const mp_rom_map_elem_t esptouch_native_module_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_esptouch_native) },
    { MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&esptouch_native_start_obj) },
    { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&esptouch_native_stop_obj) },
    { MP_ROM_QSTR(MP_QSTR_success), MP_ROM_PTR(&esptouch_native_success_obj) },
    { MP_ROM_QSTR(MP_QSTR_info), MP_ROM_PTR(&esptouch_native_info_obj) },
    { MP_ROM_QSTR(MP_QSTR_active), MP_ROM_PTR(&esptouch_native_active_obj) },
    { MP_ROM_QSTR(MP_QSTR_TYPE_UNKNOWN), MP_ROM_INT(-1) },
    { MP_ROM_QSTR(MP_QSTR_TYPE_ESPTOUCH), MP_ROM_INT(SC_TYPE_ESPTOUCH) },
    { MP_ROM_QSTR(MP_QSTR_TYPE_AIRKISS), MP_ROM_INT(SC_TYPE_AIRKISS) },
    { MP_ROM_QSTR(MP_QSTR_TYPE_ESPTOUCH_AIRKISS), MP_ROM_INT(SC_TYPE_ESPTOUCH_AIRKISS) },
};

static MP_DEFINE_CONST_DICT(esptouch_native_module_globals, esptouch_native_module_globals_table);

const mp_obj_module_t esptouch_native_user_cmodule = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&esptouch_native_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_esptouch_native, esptouch_native_user_cmodule);

coretemp 模块(芯片内置温度传感器)

micropython 官方固件没有提供读芯片内置温度(核心温度)的 Python API。参考 esp32 s2 C3 在micropython下使用内置温度传感器,基于 IDF v5.3.2 的 driver/temperature_sensor.h(handle API)实现 coretemp

Python API

  • coretemp.init([min_c, max_c]):安装并启动传感器,量程默认 10~50℃,可选传 min/max;已启用时幂等
  • coretemp.read():返回当前核心温度(float,摄氏度)
  • coretemp.stop():停止传感器,幂等

与老文的差异

  • 模块名 coretemp;错误不再 ESP_ERROR_CHECK(会重启设备),统一转 OSError 并打 ESP_LOG
  • init 支持量程参数、启停幂等,重复调用安全
  • v1.28 端口源码 STATIC 已换成 static

使用方法

1
2
3
4
5
6
7
8
import coretemp
import time

coretemp.init()                 # 默认 10~50℃
for _ in range(10):
    print(coretemp.read())
    time.sleep(1)
coretemp.stop()

内置传感器读数通常比环境温度高 1~3℃,稳定环境可校准后作为参考温度。

完整源码

coretemp.c (106 行)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
/*
 * coretemp - ESP32-C3 built-in temperature sensor module for the MicroPython ESP32 port.
 *
 * Built into a custom MicroPython v1.28.0 firmware for ESP32-C3 (ESP-IDF v5.3.2).
 * Wraps the ESP-IDF temperature sensor driver (esp_driver_tsens) so Python can read
 * the silicon core temperature of ESP32-C3 / S2 / S3 class chips.
 *
 * Reference: https://dev.leiyanhui.com/mcu/esp32-s2-coreTemp/ (IDF v5.0.4 style API;
 * this module follows the same handle-based API of IDF v5.3.2 and adds error handling).
 *
 * Python API:
 *   coretemp.init([min_c, max_c])   # install + enable; no-op if already enabled
 *   coretemp.read() -> float        # current core temperature in Celsius
 *   coretemp.stop()                 # disable; idempotent
 *
 * Notes:
 *   - Built-in temperature sensor typically reads 1-3 °C above ambient; calibrate
 *     in a stable environment when used as a reference.
 *   - Errors are converted to OSError instead of resetting the device.
 */

#include "py/obj.h"
#include "py/runtime.h"
#include "py/mperrno.h"

#include "esp_log.h"
#include "driver/temperature_sensor.h"

#define TAG "coretemp"

#define CORETEMP_DEFAULT_MIN_C 10.0f
#define CORETEMP_DEFAULT_MAX_C 50.0f

static temperature_sensor_handle_t tsens = NULL;
static bool s_enabled;

static void raise_esp_error(esp_err_t err) {
    ESP_LOGE(TAG, "temperature sensor failed: 0x%x", err);
    mp_raise_OSError(MP_EIO);
}

static mp_obj_t coretemp_init(size_t n_args, const mp_obj_t *args) {
    if (tsens == NULL) {
        float min_c = (n_args > 0) ? mp_obj_get_float(args[0]) : CORETEMP_DEFAULT_MIN_C;
        float max_c = (n_args > 1) ? mp_obj_get_float(args[1]) : CORETEMP_DEFAULT_MAX_C;
        if (min_c >= max_c) {
            mp_raise_ValueError(MP_ERROR_TEXT("min must be smaller than max"));
        }
        temperature_sensor_config_t cfg = TEMPERATURE_SENSOR_CONFIG_DEFAULT(min_c, max_c);
        esp_err_t err = temperature_sensor_install(&cfg, &tsens);
        if (err != ESP_OK) {
            tsens = NULL;
            raise_esp_error(err);
        }
        ESP_LOGI(TAG, "installed, range %.1f~%.1f C", (double)min_c, (double)max_c);
    }
    if (!s_enabled) {
        esp_err_t err = temperature_sensor_enable(tsens);
        if (err != ESP_OK) {
            raise_esp_error(err);
        }
        s_enabled = true;
    }
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(coretemp_init_obj, 0, 2, coretemp_init);

static mp_obj_t coretemp_read(void) {
    if (tsens == NULL) {
        mp_raise_OSError(MP_EINVAL);
    }
    float celsius;
    esp_err_t err = temperature_sensor_get_celsius(tsens, &celsius);
    if (err != ESP_OK) {
        raise_esp_error(err);
    }
    return mp_obj_new_float(celsius);
}
static MP_DEFINE_CONST_FUN_OBJ_0(coretemp_read_obj, coretemp_read);

static mp_obj_t coretemp_stop(void) {
    if (s_enabled) {
        esp_err_t err = temperature_sensor_disable(tsens);
        if (err != ESP_OK) {
            raise_esp_error(err);
        }
        s_enabled = false;
    }
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_0(coretemp_stop_obj, coretemp_stop);

static const mp_rom_map_elem_t coretemp_module_globals_table[] = {
    { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_coretemp) },
    { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&coretemp_init_obj) },
    { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&coretemp_read_obj) },
    { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&coretemp_stop_obj) },
};

static MP_DEFINE_CONST_DICT(coretemp_module_globals, coretemp_module_globals_table);

const mp_obj_module_t coretemp_user_cmodule = {
    .base = { &mp_type_module },
    .globals = (mp_obj_dict_t *)&coretemp_module_globals,
};

MP_REGISTER_MODULE(MP_QSTR_coretemp, coretemp_user_cmodule);

编译与验证

1
2
3
bash <dev.sh> <mpy-build>:build
bash <dev.sh> <mpy>:download   # 校验固件存在并打印 sha256
bash <dev.sh> <mpy>:flash /dev/ttyACM0

验证模块确实进固件:

  • build-ESP32_GENERIC_C3/genhdr/qstrdefs.generated.h 出现 QDEF1(MP_QSTR_esptouch_native, ...)QDEF1(MP_QSTR_coretemp, ...)
  • build-ESP32_GENERIC_C3/genhdr/moduledefs.h 的模块注册表里有 MODULE_DEF_ESPTOUCH_NATIVEMODULE_DEF_CORETEMP
  • ELF 里能查到 esptouch_native_user_cmodulecoretemp_user_cmodule 符号
  • esptool.py image_info firmware.bin 能正常解析为 ESP32-C3 镜像

本次构建产物 SHA-256(bin 约 1.73MB):

1
2
3
f7447b2360b66e9c7ed1b26d7fae64ee65721c551513bbb41c694c756e9fc030  ESP32_GENERIC_C3-20260406-v1.28.0-esptouch.bin
ecbf26eb63fa257cd6fb7f154c6f2f3d17f9129e69368ca68e724eff5e9464f4  ESP32_GENERIC_C3-20260406-v1.28.0-esptouch.elf
629fcf9aedb84625c8a1f84e16fe9f79db5b9c66b21bddde96b3d327294eca94  ESP32_GENERIC_C3-20260406-v1.28.0-esptouch.map

相关文章

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Theme Stack