diff --git a/.github/workflows/build-jc-esp32p4-m3.yml b/.github/workflows/build-jc-esp32p4-m3.yml new file mode 100644 index 00000000..264b8afa --- /dev/null +++ b/.github/workflows/build-jc-esp32p4-m3.yml @@ -0,0 +1,46 @@ +name: Build JC ESP32-P4 M3 firmware + +on: + workflow_dispatch: + pull_request: + branches: + - main + push: + branches: + - agent/jc-esp32p4-m3-audio + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Build firmware + uses: espressif/esp-idf-ci-action@v1 + with: + esp_idf_version: v6.1-beta1 + target: esp32p4 + path: esp32 + command: python ../moondeck/build/build_esp32.py --firmware esp32p4-jc-m3-eth --release jc-m3-test + + - name: Stage flash package + run: | + set -euo pipefail + B=build/esp32-esp32p4-jc-m3-eth + mkdir -p dist/bootloader dist/partition_table + cp "$B/projectMM.bin" dist/ + cp "$B/bootloader/bootloader.bin" dist/bootloader/ + cp "$B/partition_table/partition-table.bin" dist/partition_table/ + cp "$B/ota_data_initial.bin" dist/ + cp "$B/flasher_args.json" dist/ + cp "$B/flash_args" dist/ + + - uses: actions/upload-artifact@v4 + with: + name: projectMM-esp32p4-jc-m3-eth + path: dist/ diff --git a/docs/architecture.md b/docs/architecture.md index c114328f..6534972d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -238,7 +238,7 @@ Three distinct things, kept distinct in the vocabulary: - **deviceModel** — the whole assembled product, identified by its catalog name (`Olimex ESP32-Gateway Rev G`). This is *which hardware this is*. It is distinct from **`deviceName`**, *which individual unit this is* (per-unit identity the user sets — see [§ Device name](#device-name-one-identity-every-network-name-derives-from-it)); a **device** (the umbrella term) has a `deviceName` and a `deviceModel`. - **board** — the bare PCB *only*. The word survives in its literal sense: **on-board** LED, **on-board** peripherals, board-soldered pins — things physically *on the PCB*. (A deviceModel is a board plus whatever is wired onto it.) -**Firmware** is the compiled binary: chip target plus which radios/peripherals/sdkconfig fragments are included. Today's variants: `esp32` (classic, WiFi **and** RMII Ethernet in one binary — Ethernet comes up only when a PHY is present, pins/PHY per deviceModel), `esp32-eth` (classic, Ethernet only, WiFi excluded), `esp32-16mb` (classic with 16 MB flash, WiFi + Ethernet), `esp32s3-n16r8` / `esp32s3-n8r8` (S3 with WiFi + W5500 SPI Ethernet), `esp32p4-eth` (Waveshare ESP32-P4-NANO, Ethernet only), `esp32p4-eth-wifi` (the same P4 hardware with WiFi via its on-board ESP32-C6 over esp_hosted). Each chip's firmware carries the Ethernet *driver(s)* it can host (RMII EMAC for classic/P4, W5500 SPI for S3); which PHY/pins a deviceModel uses is runtime config. Selected by `build_esp32.py --firmware `, reported by `SystemModule.firmware`, used as the contract target key in scenarios. +**Firmware** is the compiled binary: chip target plus which radios/peripherals/sdkconfig fragments are included. Today's variants: `esp32` (classic, WiFi **and** RMII Ethernet in one binary; Ethernet comes up only when a PHY is present, pins/PHY per deviceModel), `esp32-eth` (classic, Ethernet only, WiFi excluded), `esp32-16mb` (classic with 16 MB flash, WiFi + Ethernet), `esp32s3-n16r8` / `esp32s3-n8r8` (S3 with WiFi + W5500 SPI Ethernet), `esp32p4-eth` (Waveshare ESP32-P4-NANO, Ethernet only), `esp32p4-jc-m3-eth` (JC-ESP32P4-M3-DEV, Ethernet + ES8311 codec), `esp32p4-eth-wifi` (the P4-NANO with WiFi via its on-board ESP32-C6 over esp_hosted), and `esp32s31` (S31 CoreBoard with WiFi, RGMII Ethernet, and ES8311 codec). Each chip's firmware carries the Ethernet *driver(s)* it can host (RMII EMAC for classic/P4, W5500 SPI for S3); which PHY/pins a deviceModel uses is runtime config. Selected by `build_esp32.py --firmware `, reported by SystemModule's `firmware` control, used as the contract target key in scenarios. **deviceModel** is the physical hardware: chip + PCB + on-board peripherals (PHY, USB-serial, PSRAM, antenna), identified by its product name. Examples: `Olimex ESP32-Gateway Rev G`, `LOLIN D32`, `Generic ESP32 Dev`. A unit cannot identify its own deviceModel (no readable PCB ID on classic ESP32), so MoonDeck deduces it from the firmware where unambiguous (`esp32-eth*` ⇒ Olimex) and otherwise lets the user pick. It is stored on the unit as SystemModule's `deviceModel` Text control (display-only in the UI; HTTP `/api/control` writes still apply). MoonDeck mirrors the picked / deduced value to the unit via `POST /api/control` after each discover and after every dropdown change. The catalog of valid deviceModels lives at [web-installer/deviceModels.json](../web-installer/deviceModels.json), shared between MoonDeck and the web installer: MoonDeck reads it for its dropdown and HTTP push (plain REST on the LAN); the web installer reads it for its picker and pushes the whole entry — deviceModel plus every module/control — over serial during provisioning as REST ops (**"Improv = REST over serial"**, the `APPLY_OP` vendor RPC; see [ImprovProvisioningModule.md](moonmodules/core/moxygen/ImprovProvisioningModule.md)). Pushing over serial sidesteps the mixed-content block that stops an HTTPS installer page from POSTing to an `http://` device; an already-running device is re-configured via MoonDeck on the LAN. diff --git a/docs/assets/deviceModels/jc-esp32p4-m3-dev.jpg b/docs/assets/deviceModels/jc-esp32p4-m3-dev.jpg new file mode 100644 index 00000000..f115fb20 Binary files /dev/null and b/docs/assets/deviceModels/jc-esp32p4-m3-dev.jpg differ diff --git a/docs/history/plans/Plan-20260726 - JC ESP32-P4 M3 audio.md b/docs/history/plans/Plan-20260726 - JC ESP32-P4 M3 audio.md new file mode 100644 index 00000000..72fd5c6d --- /dev/null +++ b/docs/history/plans/Plan-20260726 - JC ESP32-P4 M3 audio.md @@ -0,0 +1,26 @@ +# JC ESP32-P4 M3 audio + +## Goal + +Create a dedicated firmware and device-model configuration for the JC-ESP32P4-M3-DEV so its onboard ES8311 microphone feeds AudioService and the audio-reactive effects reliably. + +## Hardware contract + +- ES8311 control address: `0x18`. +- I2C: SDA GPIO7, SCL GPIO8. +- I2S: BCLK GPIO12, WS GPIO10, MCLK GPIO13. +- Mic data defaults to GPIO48 from the published schematic. GPIO11 remains testable through AudioService's live `sdPin` control because the supplied ESPHome example disagrees with the schematic. +- Default sample rate: 22,050 Hz. + +## Implementation + +1. Add an `esp32p4-jc-m3-eth` firmware variant based on the existing P4 configuration. +2. Extend the existing Espressif `esp_codec_dev` ES8311 platform path to the JC P4 target. +3. Configure record-only ES8311 operation with a 256× MCLK ratio and a 32-bit I2S word matching the receive channel. +4. Add the JC device model with its audio pins so audio-reactive effects can consume the local source. +5. Add a hardware reference page and focused manifest/configuration tests. +6. Build the desktop target, run focused checks, and build the JC firmware. + +## Validation boundary + +The build and host-side checks prove the configuration and code paths. The product owner verifies real capture on the JC board, first with GPIO48 and then GPIO11 if GPIO48 produces silence. diff --git a/docs/reference/jc-esp32p4-m3-dev.md b/docs/reference/jc-esp32p4-m3-dev.md new file mode 100644 index 00000000..46bec882 --- /dev/null +++ b/docs/reference/jc-esp32p4-m3-dev.md @@ -0,0 +1,51 @@ +# JC-ESP32P4-M3-DEV hardware reference + +Audio and network wiring for the Guition **JC-ESP32P4-M3-DEV**. The dedicated projectMM firmware is `esp32p4-jc-m3-eth`. + +![JC-ESP32P4-M3-DEV](../assets/deviceModels/jc-esp32p4-m3-dev.jpg) + +## Sources + +- [Community hardware repository](https://github.com/p1ngb4ck/unofficial_guition_esp32p4_repo/tree/main/JC-ESP32P4-M3-Dev) +- [ES8311 product brief](https://www.everest-semi.com/pdf/ES8311%20PB.pdf) +- [Espressif `esp_codec_dev`](https://components.espressif.com/components/espressif/esp_codec_dev) + +## Audio + +The onboard analog microphone connects to an ES8311 mono codec. The P4 is the I2S master and drives MCLK, BCLK, and WS. The codec control bus responds at `0x18` with SDA GPIO7 and SCL GPIO8. + +| Signal | GPIO | Role | +|---|---:|---| +| I2C SDA | 7 | ES8311 register control | +| I2C SCL | 8 | ES8311 register control | +| I2S WS / LRCLK | 10 | word select | +| I2S BCLK | 12 | bit clock | +| I2S MCLK | 13 | master clock, 256 × sample rate | +| I2S ADC data | 48 | codec microphone data to the P4, schematic mapping | +| I2S DAC data | 9 | P4 playback data to the codec | +| PA control | 53 | speaker amplifier enable | + +The supplied ESPHome example assigns microphone data to GPIO11 and uses 16-bit samples. The published schematic routes the codec's `ES7210_SDOUT` net to GPIO48. ProjectMM defaults `sdPin` to GPIO48 because it follows the schematic, while the AudioService control stays editable live so GPIO11 can be tested without reflashing. + +The codec path explicitly selects its analog microphone input and converts the scanned 7-bit address (`0x18`) to the 8-bit wire-address form expected by `esp_codec_dev`. The codec and receive channel use a 32-bit standard-I2S word. This keeps the ES8311 ADC result aligned with the `int32_t` sample path used by AudioService. The default 22,050 Hz sample rate yields a Nyquist frequency of 11,025 Hz and keeps a 512-sample analysis block near 23 ms. + +## Ethernet + +The board uses the P4 on-chip EMAC with an IP101-compatible RMII PHY, matching the common P4 Ethernet configuration: + +| Signal | GPIO | +|---|---:| +| MDC | 31 | +| MDIO | 52 | +| PHY reset / power | 51 | +| RMII reference clock input | 50 | + +The device-model entry supplies these runtime values to NetworkModule. + +## Hardware validation + +1. Flash `esp32p4-jc-m3-eth` and apply the `JC-ESP32P4-M3-DEV` device model. +2. Confirm Audio reports no codec initialization error. +3. Play music near the onboard microphone and inspect `level RMS`, `peakHz`, and an audio-reactive effect. +4. If the readings stay silent, change Audio `sdPin` from 48 to 11. The I2S channel rebuilds immediately. +5. To recheck the control bus, disable or remove Audio before running I2cScan because the active codec owns I2C port 0. diff --git a/esp32/main/Kconfig.projbuild b/esp32/main/Kconfig.projbuild index f8833387..a7c701cb 100644 --- a/esp32/main/Kconfig.projbuild +++ b/esp32/main/Kconfig.projbuild @@ -22,4 +22,12 @@ menu "projectMM" path on an eth-only build (link up but no IP). Only the WiFi build (esp32p4-eth-wifi) sets this, via sdkconfig.defaults.esp32p4-eth-wifi. + config MM_JC_P4_M3 + bool "JC-ESP32P4-M3-DEV: include the on-board ES8311 microphone codec" + default n + help + Selects the JC board's ES8311 control wiring and pulls the + esp_codec_dev managed component. The dedicated esp32p4-jc-m3-eth + firmware sets this via sdkconfig.defaults.esp32p4-jc-m3. + endmenu diff --git a/esp32/main/idf_component.yml b/esp32/main/idf_component.yml index 7a27dd59..d6f6ba2a 100644 --- a/esp32/main/idf_component.yml +++ b/esp32/main/idf_component.yml @@ -37,18 +37,20 @@ dependencies: # only from platform_esp32_i2s.cpp's audioFft. espressif/esp-dsp: version: "^1.5.0" - # esp_codec_dev — Espressif's audio-codec driver library; we use its ES8311 part - # (es8311_codec_new) to configure the ESP32-S31 CoreBoard's onboard mic, which is - # an analog mic behind an ES8311 I2S codec (I2C addr 0x18) rather than a direct - # I2S MEMS mic. Referenced only from platform_esp32_es8311.cpp, behind the codec - # gate. The `rules` gate scopes it to the S31 — the only board with the codec — - # so other targets don't resolve a component they never compile (same pattern as - # ip101/w5500 above). A managed component outside mainline v6.0, like the P4 - # esp_hosted exception (docs/building.md § ESP-IDF version). + # esp_codec_dev: Espressif's audio-codec driver library; we use its ES8311 + # part (es8311_codec_new) for the S31 CoreBoard and the dedicated + # JC-ESP32P4-M3-DEV firmware. Both put an analog mic behind an ES8311 at 0x18. + # Keep the validated minor line stable: its high-level device API requires a + # data interface, while ProjectMM intentionally uses the codec interface + # directly because its I2S channel is owned by the platform audio path. + # `matches` is the component-manager OR form: include the dependency for the + # S31 target or when the JC firmware's Kconfig flag is set, while other targets + # avoid resolving a component they never compile. espressif/esp_codec_dev: - version: "^1.3.0" - rules: + version: "~1.6.2" + matches: - if: "target == esp32s31" + - if: "$CONFIG{MM_JC_P4_M3} == True" # esp_wifi_remote + esp_hosted — WiFi for the ESP32-P4 via the on-board ESP32-C6 # co-processor over SDIO. The P4 has no native radio; these present the C6's # radio through the standard esp_wifi_* API (API-compatible, so the WiFi seam in diff --git a/esp32/sdkconfig.defaults.esp32p4-jc-m3 b/esp32/sdkconfig.defaults.esp32p4-jc-m3 new file mode 100644 index 00000000..7154a352 --- /dev/null +++ b/esp32/sdkconfig.defaults.esp32p4-jc-m3 @@ -0,0 +1,6 @@ +# JC-ESP32P4-M3-DEV board features layered onto the common P4 configuration. +# +# The board has an ES8311 codec at I2C address 0x18. The Kconfig symbol selects +# its fixed control/MCLK wiring in platform_config.h and includes Espressif's +# esp_codec_dev managed component through main/idf_component.yml. +CONFIG_MM_JC_P4_M3=y diff --git a/moondeck/build/build_esp32.py b/moondeck/build/build_esp32.py index 0127af2f..4f284cd0 100644 --- a/moondeck/build/build_esp32.py +++ b/moondeck/build/build_esp32.py @@ -193,6 +193,15 @@ def check_idf_pin(idf_path: Path) -> None: "WiFi-less fallback; esp32p4-eth-wifi adds the C6 radio.", "ships": True, }, + "esp32p4-jc-m3-eth": { + "chip": "esp32p4", + "fragments": ["sdkconfig.defaults", "sdkconfig.defaults.esp32p4-eth", + "sdkconfig.defaults.esp32p4-jc-m3"], + "eth_only": True, + "description": "JC-ESP32P4-M3-DEV with Ethernet + on-board ES8311 microphone " + "(I2C 0x18, dedicated codec configuration).", + "ships": True, + }, "esp32p4-eth-wifi": { "chip": "esp32p4", "fragments": ["sdkconfig.defaults", "sdkconfig.defaults.esp32p4-eth", diff --git a/src/platform/esp32/platform_config.h b/src/platform/esp32/platform_config.h index 2f2d4511..c0f323de 100644 --- a/src/platform/esp32/platform_config.h +++ b/src/platform/esp32/platform_config.h @@ -141,9 +141,9 @@ constexpr bool hasI2sMic = false; // Some boards put the mic behind an I2S audio codec configured over I2C (vs a -// direct I2S MEMS mic). The codec type + its control pins are a fixed board -// property, so they live here per-target (like ethConfigDefault), not as -// AudioService controls — the I2S data pins (ws/sd/sck) stay user controls. +// direct I2S MEMS mic). The codec type + its control pins are fixed by the +// firmware's hardware target, so they live here (like ethConfigDefault), not as +// AudioService controls. The I2S data pins (ws/sd/sck) stay user controls. // `audioCodecInit` (platform.h) consumes these; CodecType is neutral so a second // codec is just another enum value + a backend branch. enum class CodecType : uint8_t { None = 0, Es8311 = 1 }; @@ -151,16 +151,17 @@ struct AudioCodecPins { uint16_t i2cSda; uint16_t i2cScl; uint16_t mclk; // I2S master clock the codec needs (separate from BCLK/WS) - uint8_t i2cAddr; // codec I2C address (ES8311 default 0x18) + uint8_t i2cAddr; // 7-bit codec I2C address (ES8311 default 0x18) }; -// Default None; the ESP32-S31 Function-CoreBoard has an ES8311 (addr 0x18, I2C -// SDA on GPIO51 / SCL on GPIO50, MCLK on GPIO52 — bench-confirmed by I2C scan; the -// schematic net labels read SDA/SCL the other way round. See -// docs/reference/esp32-s31-coreboard.md.). +// The ESP32-S31 Function-CoreBoard and the dedicated JC-ESP32P4-M3-DEV firmware +// carry an ES8311. Other firmware variants keep the direct-mic/no-codec path. #ifdef CONFIG_IDF_TARGET_ESP32S31 constexpr CodecType audioCodecType = CodecType::Es8311; constexpr AudioCodecPins audioCodecPins = { /*sda*/ 51, /*scl*/ 50, /*mclk*/ 52, /*addr*/ 0x18 }; +#elif defined(CONFIG_MM_JC_P4_M3) +constexpr CodecType audioCodecType = CodecType::Es8311; +constexpr AudioCodecPins audioCodecPins = { /*sda*/ 7, /*scl*/ 8, /*mclk*/ 13, /*addr*/ 0x18 }; #else constexpr CodecType audioCodecType = CodecType::None; constexpr AudioCodecPins audioCodecPins = { 0, 0, 0, 0 }; diff --git a/src/platform/esp32/platform_esp32_es8311.cpp b/src/platform/esp32/platform_esp32_es8311.cpp index a46a18bf..8f1960ba 100644 --- a/src/platform/esp32/platform_esp32_es8311.cpp +++ b/src/platform/esp32/platform_esp32_es8311.cpp @@ -1,14 +1,14 @@ // ES8311 audio-codec init — the I2C control half of the microphone path on boards -// whose mic is an analog part behind an ES8311 I2S codec (the ESP32-S31 -// Function-CoreBoard), rather than a direct digital I2S MEMS mic. The I2S *read* +// whose mic is an analog part behind an ES8311 I2S codec, rather than a direct +// digital I2S MEMS mic. The I2S *read* // stays in platform_esp32_i2s.cpp (audioMic*); this file only brings the codec up // over I2C so it streams its ADC (mic) onto the I2S bus the read then drains. So // the audio domain code (AudioService) is unchanged — it calls audioCodecInit (a // no-op on direct-mic boards) before audioMicInit, and reads samples as always. // -// Uses Espressif's esp_codec_dev managed component (the recognised ES8311 driver), -// gated to the S31 in main/idf_component.yml. This is the platform layer's first -// I2C master bus — owned here, behind the boundary. +// Uses Espressif's esp_codec_dev managed component (the recognized ES8311 +// driver), gated to codec-bearing firmware in main/idf_component.yml. This is +// the platform layer's first I2C master bus, owned here behind the boundary. // // Compiles on every ESP32 chip: the codec path is under SOC_I2S_SUPPORTED and the // esp_codec_dev availability gate; everything else gets an inert stub (audioCodecInit @@ -40,13 +40,12 @@ namespace { const char* ES_TAG = "mm_es8311"; -// The codec device + the interfaces and I2C bus it sits on, kept alive between -// init and deinit (the codec keeps streaming once opened; the I2S read drains it). +// The codec interface + its control interface and I2C bus, kept alive between +// init and deinit (the codec keeps streaming once enabled; the I2S read drains it). struct CodecState { i2c_master_bus_handle_t i2cBus = nullptr; const audio_codec_ctrl_if_t* ctrl = nullptr; const audio_codec_if_t* codec = nullptr; - esp_codec_dev_handle_t dev = nullptr; }; CodecState* g_codec = nullptr; @@ -54,7 +53,6 @@ CodecState* g_codec = nullptr; // Tear down a partially- or fully-built CodecState in reverse order. void deinitState(CodecState* st) { if (!st) return; - if (st->dev) { esp_codec_dev_close(st->dev); esp_codec_dev_delete(st->dev); } if (st->codec) audio_codec_delete_codec_if(st->codec); if (st->ctrl) audio_codec_delete_ctrl_if(st->ctrl); if (st->i2cBus) i2c_del_master_bus(st->i2cBus); @@ -91,7 +89,10 @@ bool audioCodecInit(CodecType type, const AudioCodecPins& pins, uint32_t sampleR // streams the ADC onto the bus). The mic path is record-only. audio_codec_i2c_cfg_t i2cCtrlCfg = {}; i2cCtrlCfg.port = I2C_NUM_0; - i2cCtrlCfg.addr = pins.i2cAddr; // ES8311 default 0x18 + // esp_codec_dev keeps the legacy 8-bit wire address and shifts it to seven + // bits inside its modern i2c_master adapter. The platform config stores the + // address in the standard i2cdetect shape (0x18), so convert at this seam. + i2cCtrlCfg.addr = pins.i2cAddr << 1; i2cCtrlCfg.bus_handle = st->i2cBus; st->ctrl = audio_codec_new_i2c_ctrl(&i2cCtrlCfg); if (!st->ctrl) { ESP_LOGE(ES_TAG, "codec i2c ctrl failed"); deinitState(st); return false; } @@ -99,7 +100,8 @@ bool audioCodecInit(CodecType type, const AudioCodecPins& pins, uint32_t sampleR es8311_codec_cfg_t es8311Cfg = {}; es8311Cfg.ctrl_if = st->ctrl; es8311Cfg.codec_mode = ESP_CODEC_DEV_WORK_MODE_ADC; // record / mic only - es8311Cfg.use_mclk = true; // MCLK provided to the codec on GPIO52 + es8311Cfg.digital_mic = false; // onboard analog microphone + es8311Cfg.use_mclk = true; // MCLK uses the firmware target's codec pin es8311Cfg.mclk_div = 256; // MCLK = 256 * sample_rate (the standard // I2S ratio; the codec's coeff table is // keyed on it — 0 fails "configure rate"). @@ -107,22 +109,29 @@ bool audioCodecInit(CodecType type, const AudioCodecPins& pins, uint32_t sampleR st->codec = es8311_codec_new(&es8311Cfg); if (!st->codec) { ESP_LOGE(ES_TAG, "es8311_codec_new failed"); deinitState(st); return false; } - esp_codec_dev_cfg_t devCfg = {}; - devCfg.codec_if = st->codec; - devCfg.dev_type = ESP_CODEC_DEV_TYPE_IN; // input (mic) device - st->dev = esp_codec_dev_new(&devCfg); - if (!st->dev) { ESP_LOGE(ES_TAG, "esp_codec_dev_new failed"); deinitState(st); return false; } - esp_codec_dev_sample_info_t fs = {}; fs.sample_rate = sampleRate; fs.channel = 1; - fs.bits_per_sample = 16; - if (esp_codec_dev_open(st->dev, &fs) != ESP_CODEC_DEV_OK) { - ESP_LOGE(ES_TAG, "esp_codec_dev_open failed"); + // The platform RX channel uses a 32-bit standard-I2S slot. Match the codec's + // word length so its 24-bit ADC result stays aligned in the int32_t samples + // AudioService analyzes; a 16-bit codec word in that slot loses precision + // and presents the sample at a different significance. + fs.bits_per_sample = 32; + // ProjectMM owns the I2S channel in platform_esp32_i2s.cpp, so configure the + // codec interface directly. esp_codec_dev_new is the higher-level adapter + // for callers that also give the component a data_if; it rejects a null + // data_if and is therefore the wrong layer for this split-ownership design. + if (!st->codec->set_fs || st->codec->set_fs(st->codec, &fs) != ESP_CODEC_DEV_OK) { + ESP_LOGE(ES_TAG, "es8311 sample format failed"); + deinitState(st); + return false; + } + if (!st->codec->enable || st->codec->enable(st->codec, true) != ESP_CODEC_DEV_OK) { + ESP_LOGE(ES_TAG, "es8311 enable failed"); deinitState(st); return false; } - esp_codec_dev_set_in_gain(st->dev, 30.0f); // mic gain (dB), a reasonable default + if (st->codec->set_mic_gain) st->codec->set_mic_gain(st->codec, 30.0f); g_codec = st; return true; diff --git a/test/python/test_build_esp32_jc_m3.py b/test/python/test_build_esp32_jc_m3.py new file mode 100644 index 00000000..035c3f0c --- /dev/null +++ b/test/python/test_build_esp32_jc_m3.py @@ -0,0 +1,65 @@ +"""Contracts for the JC-ESP32P4-M3-DEV firmware and device model.""" + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "moondeck" / "build")) + +from build_esp32 import FIRMWARES, firmware_cmake_args # noqa: E402 + + +def test_jc_m3_firmware_selects_the_board_fragment(): + firmware = FIRMWARES.get("esp32p4-jc-m3-eth") + assert firmware is not None + assert firmware["chip"] == "esp32p4" + assert firmware["eth_only"] is True + assert firmware["ships"] is True + + fragment = "sdkconfig.defaults.esp32p4-jc-m3" + assert fragment in firmware["fragments"] + assert (ROOT / "esp32" / fragment).read_text(encoding="utf-8").strip().endswith( + "CONFIG_MM_JC_P4_M3=y" + ) + assert fragment in firmware_cmake_args("esp32p4-jc-m3-eth")[0] + + +def test_jc_m3_codec_dependency_is_kconfig_selected(): + manifest = (ROOT / "esp32" / "main" / "idf_component.yml").read_text(encoding="utf-8") + assert "matches:" in manifest + assert '$CONFIG{MM_JC_P4_M3} == True' in manifest + + platform_config = ( + ROOT / "src" / "platform" / "esp32" / "platform_config.h" + ).read_text(encoding="utf-8") + assert "defined(CONFIG_MM_JC_P4_M3)" in platform_config + assert "{ /*sda*/ 7, /*scl*/ 8, /*mclk*/ 13, /*addr*/ 0x18 }" in platform_config + + codec_source = ( + ROOT / "src" / "platform" / "esp32" / "platform_esp32_es8311.cpp" + ).read_text(encoding="utf-8") + assert "i2cCtrlCfg.addr = pins.i2cAddr << 1;" in codec_source + assert "es8311Cfg.digital_mic = false;" in codec_source + assert "fs.bits_per_sample = 32;" in codec_source + assert "st->codec->set_fs(st->codec, &fs)" in codec_source + assert "st->codec->enable(st->codec, true)" in codec_source + assert "esp_codec_dev_new(" not in codec_source + + +def test_jc_m3_device_model_applies_the_schematic_audio_pins(): + models = json.loads( + (ROOT / "web-installer" / "deviceModels.json").read_text(encoding="utf-8") + ) + model = next(entry for entry in models if entry["name"] == "JC-ESP32P4-M3-DEV") + assert model["firmwares"] == ["esp32p4-jc-m3-eth"] + assert "Audio" in model["supported"] + + audio = next(module for module in model["modules"] if module["type"] == "AudioService") + assert audio["parent_id"] == "Services" + assert audio["controls"] == {"sckPin": 12, "wsPin": 10, "sdPin": 48} + + scanner = next( + module for module in model["modules"] if module["type"] == "I2cScanModule" + ) + assert scanner["controls"] == {"sda": 7, "scl": 8} diff --git a/web-installer/deviceModels.json b/web-installer/deviceModels.json index 0532d633..560e9c18 100644 --- a/web-installer/deviceModels.json +++ b/web-installer/deviceModels.json @@ -1018,6 +1018,66 @@ } ] }, + { + "name": "JC-ESP32P4-M3-DEV", + "chip": "ESP32-P4", + "firmwares": [ + "esp32p4-jc-m3-eth" + ], + "image": "assets/deviceModels/jc-esp32p4-m3-dev.jpg", + "url": "https://github.com/p1ngb4ck/unofficial_guition_esp32p4_repo/tree/main/JC-ESP32P4-M3-Dev", + "supported": [ + "Audio", + "Ethernet" + ], + "planned": [ + "Speaker", + "Display", + "Touchscreen", + "Camera", + "MicroSD" + ], + "modules": [ + { + "type": "System", + "id": "System", + "controls": { + "deviceModel": "JC-ESP32P4-M3-DEV" + } + }, + { + "type": "AudioService", + "id": "Audio", + "parent_id": "Services", + "controls": { + "sckPin": 12, + "wsPin": 10, + "sdPin": 48 + } + }, + { + "type": "NetworkModule", + "id": "Network", + "controls": { + "ethType": 2, + "ethPhyAddr": 1, + "ethMdcGpio": 31, + "ethMdioGpio": 52, + "ethRstGpio": 51, + "ethClockGpio": 50, + "ethClockExtIn": true + } + }, + { + "type": "I2cScanModule", + "id": "I2cScan", + "controls": { + "sda": 7, + "scl": 8 + } + } + ] + }, { "name": "MHC-WLED ESP32-P4 shield", "chip": "ESP32-P4", diff --git a/web-installer/firmwares.json b/web-installer/firmwares.json index 09693fcd..cb17e05e 100644 --- a/web-installer/firmwares.json +++ b/web-installer/firmwares.json @@ -49,6 +49,13 @@ "ships": true, "description": "Waveshare ESP32-P4-NANO — Ethernet only (IP101 PHY). The WiFi-less fallback; esp32p4-eth-wifi adds the C6 radio." }, + { + "name": "esp32p4-jc-m3-eth", + "chip": "esp32p4", + "eth_only": true, + "ships": true, + "description": "JC-ESP32P4-M3-DEV with Ethernet + on-board ES8311 microphone (I2C 0x18, dedicated codec configuration)." + }, { "name": "esp32p4-eth-wifi", "chip": "esp32p4",