Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESP32 S3 support #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[build]
target = "xtensa-esp32-espidf"
target = "xtensa-esp32s3-espidf"

[target.xtensa-esp32-espidf]
[target.xtensa-esp32s3-espidf]
linker = "ldproxy"
# runner = "espflash --monitor" # Select this runner for espflash v1.x.x
runner = "espflash flash --monitor" # Select this runner for espflash v2.x.x
Expand All @@ -11,7 +11,7 @@ rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https
build-std = ["std", "panic_abort"]

[env]
MCU="esp32"
MCU="esp32s3"
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
ESP_IDF_VERSION = "v5.1.3"

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/.embuild
/target
/Cargo.lock
/cfg.toml
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ anyhow = "1.0.81"
esp-idf-hal = "0.43.1"
esp-idf-sys = "0.34.1"
heapless = "0.8.0"
toml-cfg = "0.2.0"

[build-dependencies]
embuild = "0.31.3"
anyhow = "=1.0.81"
toml-cfg = "0.2.0"
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
.PHONY: build flash flashm monitor only_build
ELF = target/xtensa-esp32-espidf/release/esp-snapcast
build: only_build
python3 replacer.py ${ELF}
only_build:
.PHONY: build flash flashm monitor
ELF = target/xtensa-esp32s3-espidf/release/esp-snapcast
SERIAL_DEVICE = /dev/ttyACM0

build:
cargo build --release
monitor:
cargo espflash monitor -p /dev/ttyUSB0
espflash monitor -p ${SERIAL_DEVICE}
flash: build
espflash flash -p /dev/ttyUSB0 -f 80mhz -B 921600 --partition-table partitions.csv ${ELF}
espflash flash -p ${SERIAL_DEVICE} -f 80mhz -B 921600 --partition-table partitions.csv ${ELF}
flashm: build
espflash flash -p /dev/ttyUSB0 -f 80mhz -B 921600 --flash-mode dio -M --partition-table partitions.csv ${ELF}
espflash flash -p ${SERIAL_DEVICE} -f 80mhz -B 921600 --flash-mode dio -M --partition-table partitions.csv ${ELF}
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,16 @@ export CC=xtensa-esp32-elf-gcc
export CXX=xtensa-esp32-elf-g++
```

To build the project, run `make only_build`
Create a `cfg.toml` file, using `cfg.toml.example` as a template. Update the settings with your WiFi and Snapcast server details.

Note that mDNS (automatic discovery) is not yet implemented, so you must set the IP for the snapcast server on `src/main.rs`.
To build the project, run `make build`.

Note that mDNS (automatic server discovery) planned, but is not yet implemented.

### Flashing

Requires [espflash](https://github.com/esp-rs/espflash/tree/main/espflash).

You need to provide the `SSID` and `PASS` environment variables, for your wifi settings:

```bash
export SSID=<your wifi name>
export PASS=<your wifi password>
```

These will be embedded into the firmware file with the `replacer.py` script.

To flash the project into an ESP32 you can run `make flashm`

## Hardware
Expand Down
28 changes: 27 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
fn main() {
#[derive(Debug)]
#[toml_cfg::toml_config]
pub struct Config {
#[default("")]
wifi_ssid: &'static str,
#[default("")]
wifi_pass: &'static str,
#[default("")]
server_address: &'static str,
}

fn main() -> anyhow::Result<()> {
if !std::path::Path::new("cfg.toml").exists() {
anyhow::bail!("You need to create a `cfg.toml`! Use `cfg.toml.example` as a template.");
}

// The constant `CONFIG` is auto-generated by `toml_config`.
let app_config = CONFIG;
println!("{:?}", app_config);
if app_config.wifi_ssid == "" || app_config.wifi_pass == "" {
anyhow::bail!("You need to set the Wi-Fi credentials in `cfg.toml`!");
}
if app_config.server_address == "" {
anyhow::bail!("You need to set the Snapcast server address in `cfg.toml`!");
}

embuild::espidf::sysenv::output();
Ok(())
}
4 changes: 4 additions & 0 deletions cfg.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[esp-snapcast]
wifi_ssid = "Tell my WiFi love her"
wifi_pass = "hunter2"
server_address = "192.168.0.2:1704"
32 changes: 0 additions & 32 deletions replacer.py

This file was deleted.

11 changes: 9 additions & 2 deletions sdkconfig.defaults
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
# CHANGED FROM 8K
CONFIG_ESP_MAIN_TASK_STACK_SIZE=4000
CONFIG_ESP_MAIN_TASK_STACK_SIZE=5120
# MIN
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=768
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1024
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_BT_ENABLED=n
CONFIG_ESP32_REV_MIN=3
Expand Down Expand Up @@ -70,3 +70,10 @@ CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM=16
CONFIG_TCP_SND_BUF_DEFAULT=2880
# twice static rx buffer - should not change default?
#CONFIG_ESP_WIFI_RX_BA_WIN=32

# Needed for devices with PSRAM
CONFIG_SPIRAM=y
CONFIG_SPIRAM_TYPE=CONFIG_SPIRAM_TYPE_AUTO
CONFIG_SPIRAM_USE=CONFIG_SPIRAM_USE_CAPS_ALLOC
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_TYPE_AUTO=y
60 changes: 30 additions & 30 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use snapcast_client::decoder::{Decode, Decoder};
use snapcast_client::playback::Player;
use snapcast_client::proto::TimeVal;

use esp_idf_hal::gpio::{Gpio18, Gpio19, Gpio21};
use esp_idf_hal::gpio::{AnyIOPin, AnyOutputPin};
use esp_idf_hal::i2s::I2S0;
use esp_idf_hal::modem::Modem;
use esp_idf_hal::peripherals::Peripherals;
Expand All @@ -20,16 +20,15 @@ mod wifi;

use player::{I2sPlayer, I2sPlayerBuilder};

const SSID: [u8; 32] = [
0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a,
0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x00,
];
const PASS: [u8; 64] = [
0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b,
0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b,
0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b,
0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x4b, 0x00,
];
#[toml_cfg::toml_config]
pub struct Config {
#[default("")]
wifi_ssid: &'static str,
#[default("")]
wifi_pass: &'static str,
#[default("")]
server_address: &'static str,
}

enum Sample {
Data(Vec<u8>),
Expand Down Expand Up @@ -184,30 +183,24 @@ fn main() -> ! {

let mac = setup(&mut peripherals.modem).unwrap();
let i2s = peripherals.i2s0;
let dout = peripherals.pins.gpio19;
let dout = peripherals.pins.gpio11.into();

let bclk = peripherals.pins.gpio12.into();
let ws = peripherals.pins.gpio10.into();

let bclk = peripherals.pins.gpio18;
let ws = peripherals.pins.gpio21;
let nmute = peripherals.pins.gpio13.into();

let res = app_main(mac, i2s, dout, bclk, ws);
let res = app_main(mac, i2s, dout, bclk, ws, nmute);
log::error!("Main returned with {res:?}; will reboot now");
unsafe { esp_restart() };
}

fn setup(modem: &mut Modem) -> anyhow::Result<String> {
let ssid = std::ffi::CStr::from_bytes_until_nul(&SSID)
.expect("Invalid build SSID")
.to_str()
.expect("SSID is not UTF-8");
let pass = std::ffi::CStr::from_bytes_until_nul(&PASS)
.expect("Invalid build PASS")
.to_str()
.expect("PASS is not UTF-8");

log::info!("Connecting to SSID '{ssid}'");
log::info!("Connecting to SSID '{:?}'", CONFIG.wifi_ssid);

let nvsp = EspDefaultNvsPartition::take().unwrap();
let mac = wifi::configure(ssid, pass, nvsp, modem).expect("Could not configure wifi");
let mac = wifi::configure(CONFIG.wifi_ssid, CONFIG.wifi_pass, nvsp, modem)
.expect("Could not configure wifi");

log::info!("Syncing time via SNTP");
let _sntp = start_and_sync_sntp()?;
Expand All @@ -220,8 +213,15 @@ fn setup(modem: &mut Modem) -> anyhow::Result<String> {
Ok(mac)
}

fn app_main(mac: String, i2s: I2S0, dout: Gpio19, bclk: Gpio18, ws: Gpio21) -> anyhow::Result<()> {
let mut player_builder = I2sPlayerBuilder::new(i2s, dout, bclk, ws);
fn app_main(
mac: String,
i2s: I2S0,
dout: AnyIOPin,
bclk: AnyIOPin,
ws: AnyIOPin,
nmute: AnyOutputPin,
) -> anyhow::Result<()> {
let mut player_builder = I2sPlayerBuilder::new(i2s, dout, bclk, ws, nmute);

let name = "esp32";

Expand All @@ -240,7 +240,7 @@ fn app_main(mac: String, i2s: I2S0, dout: Gpio19, bclk: Gpio18, ws: Gpio21) -> a
// TODO: discover stream
//let client = client.connect("192.168.2.131:1704")?;
let client = client
.connect("192.168.2.183:1704")
.connect(CONFIG.server_address)
.context("Could not connect to SnapCast server")?;

let player_2 = player.clone();
Expand Down Expand Up @@ -328,7 +328,7 @@ fn connection_main<
// Delay configuration until player is instantiated
start_vol = s.volume;
if let Some(p) = p.as_mut() {
p.set_volume(s.volume)?;
p.set_volume(if s.muted { 0 } else { s.volume })?;
}
}
Message::Nothing => {
Expand Down
21 changes: 19 additions & 2 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::anyhow;

use esp_idf_hal::delay::TickType;
use esp_idf_hal::gpio;
use esp_idf_hal::gpio::{InputPin, OutputPin};
use esp_idf_hal::gpio::{AnyOutputPin, InputPin, OutputPin, PinDriver};
use esp_idf_hal::i2s;
use esp_idf_hal::i2s::config;
use esp_idf_hal::i2s::I2S0;
Expand All @@ -25,6 +25,7 @@ pub struct I2sPlayerBuilder<
dout: Option<P>,
bclk: Option<Q>,
ws: Option<R>,
nmute_pin: Option<AnyOutputPin>,
}

impl<
Expand All @@ -36,12 +37,19 @@ impl<
R: Peripheral<P = OR> + 'static,
> I2sPlayerBuilder<OP, OQ, OR, P, Q, R>
{
pub fn new(i2s: I2S0, dout: P, bclk: Q, ws: R) -> I2sPlayerBuilder<OP, OQ, OR, P, Q, R> {
pub fn new(
i2s: I2S0,
dout: P,
bclk: Q,
ws: R,
nmute_pin: AnyOutputPin,
) -> I2sPlayerBuilder<OP, OQ, OR, P, Q, R> {
I2sPlayerBuilder {
i2s: Some(i2s),
dout: Some(dout),
bclk: Some(bclk),
ws: Some(ws),
nmute_pin: Some(nmute_pin),
}
}
// Heavily inspired from https://github.com/10buttons/awedio_esp32/blob/main/src/lib.rs#L218
Expand All @@ -67,6 +75,10 @@ impl<
let ws = self.ws.take().ok_or(anyhow!("Initialized twice"))?;
let mut driver = i2s::I2sDriver::new_std_tx(i2s, &i2s_config, bclk, dout, mclk, ws)?;

let nmute_pin = self.nmute_pin.take().ok_or(anyhow!("Initialized twice"))?;
let mut nmute = PinDriver::output(nmute_pin)?;
nmute.set_low()?;

// Clear TX buffers
let data: Vec<u8> = vec![0; 128];
while driver.preload_data(&data)? > 0 {}
Expand All @@ -75,6 +87,7 @@ impl<
d: driver,
is_playing: false,
volume: 0,
nmute: nmute,
sample_rate: ch.metadata.rate() as u16,
};
ret.set_volume(20)?;
Expand All @@ -85,6 +98,7 @@ pub struct I2sPlayer {
d: i2s::I2sDriver<'static, i2s::I2sTx>,
is_playing: bool,
volume: i16,
nmute: PinDriver<'static, AnyOutputPin, gpio::Output>,
sample_rate: u16,
}

Expand Down Expand Up @@ -129,12 +143,15 @@ impl Player for I2sPlayer {
fn latency_ms(&self) -> anyhow::Result<u16> {
Ok(0)
}

fn set_volume(&mut self, val: u8) -> anyhow::Result<()> {
// convert the 0-100 input range to n/VOL_STEP_COUNT
if val == 0 {
self.volume = 0;
self.nmute.set_low()?;
return Ok(());
}
self.nmute.set_high()?;
let volume_float = f64::from(val);
let normalized_volume = (volume_float - 1.0) / 99.0;
let scaled = normalized_volume.powf(2.0) * f64::from(VOL_STEP_COUNT);
Expand Down
Loading