1use std::{collections::HashMap, fmt};
2
3use serde::{Deserialize, Serialize};
4use serde_yaml_ng::Value;
5
6#[derive(Debug, Deserialize, Serialize)]
7pub struct EspforgeConfiguration {
8 pub espforge: EspforgeConfig,
9 #[serde(default)]
10 pub example: Option<ExampleConfig>,
11 pub esp32: Option<Esp32Config>,
12 pub components: Option<HashMap<String, ComponentConfig>>,
13 pub devices: Option<HashMap<String, DeviceConfig>>,
14 pub app: Option<AppConfig>,
15}
16
17#[derive(Debug, Deserialize, Serialize)]
18pub struct DeviceConfig {
19 pub using: String,
20 #[serde(default)]
21 pub with: HashMap<String, Value>,
22}
23
24#[derive(Debug, Deserialize, Serialize)]
25pub struct EspforgeConfig {
26 pub name: String,
27 pub platform: PlatformConfig,
28 #[serde(default)]
29 pub wokwi_board: Option<WokwiBoard>,
30 #[serde(default)]
31 pub wokwi: Option<WokwiConfig>,
32 #[serde(default)]
33 pub enable_async: bool,
34
35}
36
37#[derive(Debug, Deserialize, Serialize)]
38pub struct WokwiConfig {
39 pub diagram: Option<String>,
40 pub config: Option<String>,
41}
42
43#[derive(Debug, Deserialize, Serialize)]
44#[serde(rename_all = "kebab-case")]
45pub enum WokwiBoard {
46 BoardEsp32DevkitCV4,
47 BoardEsp32S2Devkitm1,
48 BoardEsp32S3Devkitc1,
49 BoardEsp32C3Devkitm1,
50 BoardEsp32C6Devkitc1,
51 BoardEsp32H2Devkitm1,
52 BoardXiaoEsp32C3,
53 BoardXiaoEsp32C6,
54 BoardXiaoEsp32S3,
55}
56
57#[derive(Debug, Deserialize, Serialize)]
58pub struct ExampleConfig {
59 pub name: String,
60 #[serde(flatten)]
61 pub example_properties: HashMap<String, Value>,
62}
63
64#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
65#[serde(rename_all = "lowercase")]
66pub enum PlatformConfig {
67 ESP32,
68 ESP32C2,
69 ESP32C3,
70 ESP32C6,
71 ESP32H2,
72 ESP32S2,
73 ESP32S3,
74}
75
76impl PlatformConfig {
77 pub fn target(&self) -> &str {
78 match self {
79 PlatformConfig::ESP32 => "xtensa-esp32-none-elf",
80 PlatformConfig::ESP32C2 => "riscv32imc-unknown-none-elf",
81 PlatformConfig::ESP32C3 => "riscv32imc-unknown-none-elf",
82 PlatformConfig::ESP32C6 => "riscv32imac-unknown-none-elf",
83 PlatformConfig::ESP32H2 => "riscv32imac-unknown-none-elf",
84 PlatformConfig::ESP32S2 => "xtensa-esp32s2-none-elf",
85 PlatformConfig::ESP32S3 => "xtensa-esp32s3-none-elf",
86 }
87 }
88}
89
90impl EspforgeConfiguration {
91 pub fn get_name(&self) -> &str {
92 &self.espforge.name
93 }
94
95 pub fn get_platform(&self) -> String {
96 self.espforge.platform.to_string()
97 }
98
99 pub fn get_template(&self) -> Option<String> {
100 self.example.as_ref().map(|e| e.name.clone())
101 }
102}
103
104impl fmt::Display for PlatformConfig {
105 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106 match self {
107 PlatformConfig::ESP32 => write!(f, "esp32"),
108 PlatformConfig::ESP32C2 => write!(f, "esp32c2"),
109 PlatformConfig::ESP32C3 => write!(f, "esp32c3"),
110 PlatformConfig::ESP32C6 => write!(f, "esp32c6"),
111 PlatformConfig::ESP32H2 => write!(f, "esp32h2"),
112 PlatformConfig::ESP32S2 => write!(f, "esp32s2"),
113 PlatformConfig::ESP32S3 => write!(f, "esp32s3"),
114 }
115 }
116}
117
118#[derive(Debug, Deserialize, Serialize)]
119pub struct Esp32Config {
120 #[serde(default)]
121 pub gpio: HashMap<String, GpioPinConfig>,
122 #[serde(default)]
123 pub spi: HashMap<String, SpiConfig>,
124 #[serde(default)]
125 pub i2c: HashMap<String, Value>,
126 #[serde(default)]
127 pub uart: HashMap<String, Value>,
128}
129
130impl Esp32Config {
131 pub fn contains_resource(&self, name: &str) -> bool {
133 self.gpio.contains_key(name) ||
134 self.spi.contains_key(name) ||
135 self.i2c.contains_key(name) ||
136 self.uart.contains_key(name)
137 }
138}
139
140#[derive(Debug, Deserialize, Serialize)]
141pub struct SpiConfig {
142 pub miso: u8,
143 pub mosi: u8,
144 pub sck: u8,
145 #[serde(default)]
146 pub cs: Option<u8>,
147}
148
149#[derive(Debug, Deserialize, Serialize)]
150pub struct GpioPinConfig {
151 pub pin: u8,
152 pub direction: PinDirection,
153 #[serde(default)]
154 pub pullup: bool,
155 #[serde(default)]
156 pub pulldown: bool,
157}
158
159#[derive(Debug, Deserialize, Serialize, PartialEq)]
160#[serde(rename_all = "lowercase")]
161pub enum PinDirection {
162 Input,
163 Output,
164}
165
166#[derive(Debug, Deserialize, Serialize)]
167pub struct ComponentConfig {
168 pub using: String,
169 #[serde(default)]
170 pub with: HashMap<String, Value>,
171}
172
173#[derive(Debug, Deserialize, Serialize)]
174pub struct AppConfig {
175 #[serde(default)]
176 pub variables: HashMap<String, VariableConfig>,
177
178 #[serde(default)]
179 pub setup: Vec<HashMap<String, Value>>,
180
181 #[serde(default, rename = "loop")]
182 pub loop_fn: Vec<HashMap<String, Value>>,
183}
184
185#[derive(Debug, Deserialize, Serialize)]
186pub struct VariableConfig {
187 #[serde(rename = "type")]
188 pub type_name: String,
189 pub initial: Value,
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use serde_yaml_ng;
196
197 #[test]
198 fn parse_minimal() {
199 let yaml = r#"
200 espforge:
201 name: minimum
202 platform: esp32c3
203 "#;
204
205 let config: EspforgeConfiguration =
206 serde_yaml_ng::from_str(yaml).expect("YAML parse failed");
207
208 assert_eq!(config.espforge.name, "minimum");
209 assert_eq!(config.espforge.platform, PlatformConfig::ESP32C3);
210 assert!(config.example.is_none());
211 }
212
213 #[test]
214 fn parse_wokwi_config() {
215 let yaml = r#"
216 espforge:
217 name: wokwi_test
218 platform: esp32c3
219 wokwi:
220 diagram: diagram.json
221 config: wokwi.toml
222 "#;
223
224 let config: EspforgeConfiguration =
225 serde_yaml_ng::from_str(yaml).expect("YAML parse failed");
226
227 let wokwi = config.espforge.wokwi.expect("Wokwi config should exist");
228 assert_eq!(wokwi.diagram, Some("diagram.json".to_string()));
229 assert_eq!(wokwi.config, Some("wokwi.toml".to_string()));
230 }
231
232 #[test]
233 fn invalid_platform() {
234 let yaml = r#"
235 espforge:
236 name: minimum
237 platform: invalid
238 "#;
239
240 let result: Result<EspforgeConfiguration, _> = serde_yaml_ng::from_str(yaml);
241 assert!(
242 result.is_err(),
243 "expected invalid_platform to fail deserialization"
244 );
245 }
246
247 #[test]
248 fn parse_blink_example() {
249 let yaml = r#"
250 espforge:
251 name: blink
252 platform: esp32c3
253 example:
254 name: blink
255 "#;
256
257 let config: EspforgeConfiguration =
258 serde_yaml_ng::from_str(yaml).expect("YAML parse failed");
259
260 assert_eq!(config.espforge.name, "blink");
261 if let Some(example) = config.example {
262 assert_eq!(example.name, "blink");
263 } else {
264 panic!("Example was None.");
265 }
266 }
267}