diff --git a/.agents/skills/arduino-code-generator/SKILL.md b/.agents/skills/arduino-code-generator/SKILL.md new file mode 100644 index 0000000..9b319fe --- /dev/null +++ b/.agents/skills/arduino-code-generator/SKILL.md @@ -0,0 +1,130 @@ +--- +name: arduino-code-generator +description: Generate Arduino/embedded C++ code snippets and patterns on demand for UNO/ESP32/RP2040. Use when users request Arduino code for sensors, actuators, communication protocols, state machines, non-blocking timers, data logging, or hardware abstraction. Generates production-ready code with proper memory management, timing patterns, and board-specific optimization. Supports DHT22, BME280, buttons, I2C/SPI, EEPROM, SD cards, WiFi, and common peripherals. +--- + +# Arduino Code Generator + +Generate production-quality Arduino code snippets for sensors, actuators, communication, and embedded patterns. + +## Quick Start + +**Browse example sketches:** +```bash +# See 9 production-ready examples in examples/ folder +ls examples/ +# config-example.ino, filtering-example.ino, buttons-example.ino, +# i2c-example.ino, csv-example.ino, scheduler-example.ino, +# state-machine-example.ino, hardware-detection-example.ino, +# data-logging-example.ino +``` + +**List available patterns:** +```bash +uv run --no-project scripts/generate_snippet.py --list +``` + +**Generate code for specific pattern and board:** +```bash +uv run --no-project scripts/generate_snippet.py --pattern i2c --board esp32 +uv run --no-project scripts/generate_snippet.py --pattern buttons --board uno --output button.ino +``` + +**Interactive mode:** +```bash +uv run --no-project scripts/generate_snippet.py --interactive +``` + +## Resources + +- **examples/** - 9 production-ready example sketches (one per pattern category) +- **examples/README.md** - Detailed documentation for each example with wiring diagrams +- **scripts/generate_snippet.py** - CLI tool for code generation with 9 pattern templates +- **scripts/verify_patterns.ps1** - Compile examples for UNO/ESP32/RP2040 (PowerShell) +- **scripts/verify_patterns.sh** - Compile examples for UNO/ESP32/RP2040 (bash) +- **assets/workflow.mmd** - Mermaid diagram of code generation workflow + +## Supported Patterns + +### Hardware Abstraction +- Multi-board config.h with conditional compilation +- Pin definitions for UNO/ESP32/RP2040 +- Memory budget tracking + +See [patterns-config.md](references/patterns-config.md) | Example: [config-example.ino](examples/config-example.ino) + +### Sensor Reading & Filtering +- ADC noise reduction (moving average, median, Kalman) +- DHT22, BME280, analog sensors +- Data validation and calibration + +See [patterns-filtering.md](references/patterns-filtering.md) | Example: [filtering-example.ino](examples/filtering-example.ino) + +### Input Handling +- Software button debouncing +- Edge detection (PRESSED/RELEASED/LONG_PRESS) +- Multi-button management + +See [patterns-buttons.md](references/patterns-buttons.md) | Example: [buttons-example.ino](examples/buttons-example.ino) + +### Communication +- I2C device scanning and diagnostics +- SPI configuration +- UART/Serial protocols +- CSV data output + +See [patterns-i2c.md](references/patterns-i2c.md) and [patterns-csv.md](references/patterns-csv.md) | Examples: [i2c-example.ino](examples/i2c-example.ino), [csv-example.ino](examples/csv-example.ino) + +### Timing & Concurrency +- Non-blocking millis() patterns +- Task scheduling without delay() +- Priority-based schedulers +- State machines + +See [patterns-scheduler.md](references/patterns-scheduler.md) and [patterns-state-machine.md](references/patterns-state-machine.md) | Examples: [scheduler-example.ino](examples/scheduler-example.ino), [state-machine-example.ino](examples/state-machine-example.ino) + +### Hardware Detection +- Auto-detect boards (UNO/ESP32/RP2040) +- SRAM usage monitoring +- Sensor fallback strategies +- Adaptive configuration + +See [patterns-hardware-detection.md](references/patterns-hardware-detection.md) | Example: [hardware-detection-example.ino](examples/hardware-detection-example.ino) + +### Data Persistence +- EEPROM with CRC validation +- SD card FAT32 logging +- Wear leveling for EEPROM +- Buffered writes + +See [patterns-data-logging.md](references/patterns-data-logging.md) | Example: [data-logging-example.ino](examples/data-logging-example.ino) + +## Code Generation Workflow + +- [ ] **[Identify Pattern Type](workflow/step1-identify-pattern.md)** - Analyze user request to determine core pattern category +- [ ] **[Read Reference Documentation](workflow/step2-read-reference.md)** - Consult pattern-specific reference files for implementation details +- [ ] **[Generate Code](workflow/step3-generate-code.md)** - Create production-ready code following quality standards +- [ ] **[Provide Instructions](workflow/step4-provide-instructions.md)** - Include wiring diagrams and usage guidance +- [ ] **[Mention Integration](workflow/step5-mention-integration.md)** - Suggest combinations with other patterns when relevant + +## Quality Standards & Rules + +- [ ] **[Quality Standards](rules/quality-standards.md)** - Compilation, timing, memory safety, and error handling requirements +- [ ] **[Board Optimization](rules/board-optimization.md)** - UNO, ESP32, and RP2040 specific optimizations and features +- [ ] **[Common Pitfalls](rules/common-pitfalls.md)** - Critical mistakes to avoid in Arduino development + +## Code Output Template + +- [ ] **[Code Template](templates/code-output-template.md)** - Standardized structure for generated Arduino sketches + +## Resources + +- **examples/** - 9 production-ready example sketches (one per pattern category) +- **examples/README.md** - Detailed documentation for each example with wiring diagrams +- **scripts/generate_snippet.py** - CLI tool for code generation with 9 pattern templates +- **assets/workflow.mmd** - Mermaid diagram of code generation workflow +- **workflow/** - Step-by-step code generation process +- **rules/** - Quality standards and board-specific optimizations +- **templates/** - Code output templates and structure guidelines +- **references/** - Detailed pattern documentation and API references +- **references/README.md** - Reference structure and formatting guide diff --git a/.agents/skills/arduino-code-generator/assets/workflow.mmd b/.agents/skills/arduino-code-generator/assets/workflow.mmd new file mode 100644 index 0000000..5c1fcdf --- /dev/null +++ b/.agents/skills/arduino-code-generator/assets/workflow.mmd @@ -0,0 +1,28 @@ +```mermaid +flowchart TD + A["User Request:\nGenerate Arduino Code"] --> B{"Pattern Type?"} + + B -->|config| C1["patterns-config.md"] + B -->|buttons| C2["patterns-buttons.md"] + B -->|i2c| C3["patterns-i2c.md"] + B -->|scheduler| C4["patterns-scheduler.md"] + B -->|filtering| C5["patterns-filtering.md"] + B -->|state-machine| C6["patterns-state-machine.md"] + B -->|csv| C7["patterns-csv.md"] + B -->|data-logging| C8["patterns-data-logging.md"] + B -->|hardware| C9["patterns-hardware.md"] + + C1 & C2 & C3 & C4 & C5 & C6 & C7 & C8 & C9 --> D["Template Engine\ngenerate_snippet.py"] + + D --> E{"Board Type?"} + + E -->|Arduino UNO| F1["2KB SRAM\n9600 baud\nF macro"] + E -->|ESP32| F2["520KB SRAM\nWiFi/BLE\n115200 baud"] + E -->|RP2040| F3["264KB SRAM\nDual-core\n115200 baud"] + + F1 & F2 & F3 --> G["Generated .ino Code"] + + style A fill:#e1f5fe + style G fill:#c8e6c9 + style D fill:#fff3e0 +``` diff --git a/.agents/skills/arduino-code-generator/examples/README.md b/.agents/skills/arduino-code-generator/examples/README.md new file mode 100644 index 0000000..9535c43 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/README.md @@ -0,0 +1,246 @@ +# Arduino Code Generator - Example Sketches + +This directory contains **9 production-ready example sketches** demonstrating each pattern category in the arduino-code-generator skill. All examples follow best practices from `arduino-skills.md`. + +## πŸ“ Available Examples + +### 1. **config-example.ino** - Hardware Configuration Pattern +**Purpose:** Board-agnostic hardware abstraction using compile-time configuration + +**Demonstrates:** +- Multi-board support (UNO/ESP32/RP2040) with automatic pin mapping +- Compile-time board detection using preprocessor directives +- Single codebase for multiple platforms + +**Boards:** Arduino UNO, ESP32, RP2040 + +--- + +### 2. **filtering-example.ino** - ADC Filtering & Signal Processing +**Purpose:** Clean noisy sensor readings using multiple filtering techniques + +**Demonstrates:** +- Moving average filter (10-sample window) +- Exponential moving average (EMA) with alpha = 0.15 +- Median filter (3-sample spike rejection) +- CSV output format for data analysis + +**Boards:** All (uses analog input A0) + +--- + +### 3. **buttons-example.ino** - Debounced Button Input +**Purpose:** Robust button handling with debouncing and event detection + +**Demonstrates:** +- Hardware debouncing (50ms threshold) +- Press/release event detection +- Long-press detection (1 second) +- Non-blocking state machine implementation + +**Boards:** Arduino UNO (pin 2), ESP32 (pin 4), RP2040 (pin 10) + +--- + +### 4. **i2c-example.ino** - I2C Communication & Device Scanner +**Purpose:** I2C bus scanning with device identification + +**Demonstrates:** +- 7-bit address scanning (0x08-0x77) +- Board-specific SDA/SCL pin configuration +- Error detection and reporting +- Common device identification (OLED, RTC, IMU, sensors) + +**Boards:** Arduino UNO (A4/A5), ESP32 (21/22), RP2040 (4/5) + +--- + +### 5. **csv-example.ino** - CSV Data Output Pattern +**Purpose:** Structured data logging in CSV format for analysis + +**Demonstrates:** +- Timestamp + multi-channel sensor data +- F() macro for PROGMEM string storage +- Proper CSV formatting with headers +- Data suitable for Excel/Python analysis + +**Boards:** All (simulated sensors) + +--- + +### 6. **scheduler-example.ino** - Non-Blocking Task Scheduler +**Purpose:** Cooperative multitasking without delay() + +**Demonstrates:** +- Lightweight task scheduler (millis() based) +- 5 independent tasks with different intervals +- Task enable/disable control +- Execution tracking and status reporting + +**Boards:** All + +--- + +### 7. **state-machine-example.ino** - Finite State Machine (FSM) +**Purpose:** Traffic light controller using explicit state machine + +**Demonstrates:** +- State enumeration and transition logic +- Timing-based state changes (non-blocking) +- State-specific behavior and LED outputs +- 4-state cycle: RED β†’ RED+YELLOW β†’ GREEN β†’ YELLOW + +**Boards:** Arduino UNO (pins 9/10/11), ESP32 (pins 25/26/27) + +--- + +### 8. **hardware-detection-example.ino** - Runtime Board Detection +**Purpose:** Detect board capabilities and report system information + +**Demonstrates:** +- Compile-time board identification +- Memory capacity detection (Flash/SRAM) +- Clock speed and ADC resolution reporting +- Feature flags (WiFi, Bluetooth) + +**Boards:** UNO, Mega 2560, ESP32, RP2040, SAMD21 + +--- + +### 9. **data-logging-example.ino** - EEPROM Persistent Data Storage +**Purpose:** Long-term sensor data logging with EEPROM + +**Demonstrates:** +- Circular buffer implementation (20 entries) +- EEPROM wear leveling (write minimization) +- Data retrieval and playback +- Sensor data persistence across reboots + +**Boards:** All (uses EEPROM library) + +--- + +## πŸš€ Quick Start + +### Upload an Example + +1. **Open Arduino IDE** +2. **Select your board:** + - Tools β†’ Board β†’ Arduino UNO / ESP32 / RP2040 +3. **Open example:** + - File β†’ Open β†’ `arduino-code-generator/examples/.ino` +4. **Upload:** + - Click Upload (or Ctrl+U) +5. **Open Serial Monitor:** + - Tools β†’ Serial Monitor (or Ctrl+Shift+M) + - Set baud rate (9600 for UNO, 115200 for ESP32/RP2040) + +### Generate Custom Code + +Use the `generate_snippet.py` script to create custom variations: + +```bash +# Generate config pattern for ESP32 +uv run --no-project scripts/generate_snippet.py --pattern config --board esp32 + +# Generate button handler for UNO on pin 3 +uv run --no-project scripts/generate_snippet.py --pattern buttons --board uno --pin 3 + +# Interactive mode +uv run --no-project scripts/generate_snippet.py --interactive +``` + +--- + +## πŸ“š Pattern Reference Documentation + +Each example corresponds to a detailed reference file in `references/`: + +| Example | Reference File | +|---------|---------------| +| config-example.ino | [patterns-config.md](../references/patterns-config.md) | +| filtering-example.ino | [patterns-filtering.md](../references/patterns-filtering.md) | +| buttons-example.ino | [patterns-buttons.md](../references/patterns-buttons.md) | +| i2c-example.ino | [patterns-i2c.md](../references/patterns-i2c.md) | +| csv-example.ino | [patterns-csv.md](../references/patterns-csv.md) | +| scheduler-example.ino | [patterns-scheduler.md](../references/patterns-scheduler.md) | +| state-machine-example.ino | [patterns-state-machine.md](../references/patterns-state-machine.md) | +| hardware-detection-example.ino | [patterns-hardware-detection.md](../references/patterns-hardware-detection.md) | +| data-logging-example.ino | [patterns-data-logging.md](../references/patterns-data-logging.md) | + +--- + +## πŸ› οΈ Design Principles + +All examples follow arduino-skills best practices: + +βœ… **No `delay()`** - All timing uses `millis()` for non-blocking execution +βœ… **F() macros** - String literals stored in PROGMEM to save RAM +βœ… **Board-agnostic** - Compile for UNO/ESP32/RP2040 without code changes +βœ… **Modular classes** - Reusable components with clear interfaces +βœ… **Serial output** - Verifiable behavior for testing/debugging +βœ… **Production-ready** - Proper error handling and edge cases + +--- + +## πŸ“Š Testing & Verification + +Each example includes: + +- **Serial output** for runtime verification +- **Compile-time board detection** for hardware compatibility +- **Non-blocking loops** for responsive systems +- **Documentation** explaining pattern rationale + +To verify an example compiles for all boards: + +```bash +# Verify UNO compilation +arduino-cli compile --fqbn arduino:avr:uno config-example.ino + +# Verify ESP32 compilation +arduino-cli compile --fqbn esp32:esp32:esp32 config-example.ino + +# Verify RP2040 compilation +arduino-cli compile --fqbn rp2040:rp2040:rpipico config-example.ino +``` + +Automated verification scripts are also available: + +```bash +# Windows PowerShell +../scripts/verify_patterns.ps1 + +# Linux/macOS +../scripts/verify_patterns.sh +``` + +Prerequisites (once per environment): + +```bash +# Install cores before compiling +arduino-cli core install arduino:avr +arduino-cli core install esp32:esp32 +arduino-cli core install rp2040:rp2040 +``` + +--- + +## 🀝 Contributing + +To add a new example: + +1. Follow the template structure (header comment, configuration, classes, setup, loop) +2. Ensure board compatibility (UNO/ESP32/RP2040 minimum) +3. Add documentation to this README +4. Create corresponding reference file in `references/` +5. Test compilation on all target boards + +--- + +## πŸ“„ License + +MIT License - See [LICENSE](../../LICENSE) for details + +**Generated by:** arduino-code-generator v1.3.0 +**Last Updated:** February 2026 diff --git a/.agents/skills/arduino-code-generator/examples/buttons-example.ino b/.agents/skills/arduino-code-generator/examples/buttons-example.ino new file mode 100644 index 0000000..8ad7c47 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/buttons-example.ino @@ -0,0 +1,142 @@ +/* + * Debounced Button Pattern Example + * + * Demonstrates robust button handling with: + * - Debouncing (noise rejection) + * - Press/release event detection + * - Long-press detection (1 second threshold) + * - Non-blocking state machine implementation + * + * Generated by: arduino-code-generator + * Pattern: Button Input with Debouncing + * License: MIT + */ + +// === Configuration === +#if defined(ESP32) + const uint8_t BUTTON_PIN = 4; + const uint32_t SERIAL_BAUD = 115200; +#elif defined(ARDUINO_ARCH_RP2040) + const uint8_t BUTTON_PIN = 10; + const uint32_t SERIAL_BAUD = 115200; +#else + const uint8_t BUTTON_PIN = 2; + const uint32_t SERIAL_BAUD = 9600; +#endif + +// === Debounced Button Class === +class DebouncedButton { +public: + enum Event { + NONE, + PRESSED, + RELEASED, + LONG_PRESS + }; + +private: + const uint8_t pin; + const uint16_t DEBOUNCE_MS = 50; + const uint16_t LONG_PRESS_MS = 1000; + + bool lastReading = HIGH; + bool lastStableState = HIGH; + unsigned long lastDebounceTime = 0; + unsigned long pressStartTime = 0; + bool longPressTriggered = false; + +public: + DebouncedButton(uint8_t buttonPin) : pin(buttonPin) {} + + void begin() { + pinMode(pin, INPUT_PULLUP); + } + + Event update() { + bool reading = digitalRead(pin); + unsigned long now = millis(); + + // Reset debounce timer on state change + if (reading != lastReading) { + lastDebounceTime = now; + } + lastReading = reading; + + // Wait for stable state + if ((now - lastDebounceTime) < DEBOUNCE_MS) { + return NONE; + } + + // State changed + if (reading != lastStableState) { + lastStableState = reading; + + if (reading == LOW) { + // Button pressed + pressStartTime = now; + longPressTriggered = false; + return PRESSED; + } else { + // Button released + return longPressTriggered ? NONE : RELEASED; + } + } + + // Check for long press (while held) + if (lastStableState == LOW && !longPressTriggered) { + if ((now - pressStartTime) >= LONG_PRESS_MS) { + longPressTriggered = true; + return LONG_PRESS; + } + } + + return NONE; + } + + bool isPressed() const { return lastStableState == LOW; } +}; + +// === Application === +DebouncedButton button(BUTTON_PIN); +uint16_t pressCount = 0; +uint16_t longPressCount = 0; + +void setup() { + Serial.begin(SERIAL_BAUD); + button.begin(); + + Serial.println(F("\n=== Debounced Button Example ===")); + Serial.println(F("Events detected:")); + Serial.println(F("- PRESS: Short button press")); + Serial.println(F("- RELEASE: Button released")); + Serial.println(F("- LONG_PRESS: Hold for 1 second")); + Serial.println(); +} + +void loop() { + DebouncedButton::Event event = button.update(); + + switch (event) { + case DebouncedButton::PRESSED: + Serial.println(F("-> PRESSED")); + pressCount++; + break; + + case DebouncedButton::RELEASED: + Serial.print(F("-> RELEASED (count: ")); + Serial.print(pressCount); + Serial.println(F(")")); + break; + + case DebouncedButton::LONG_PRESS: + Serial.println(F("-> LONG_PRESS")); + longPressCount++; + Serial.print(F(" Long press count: ")); + Serial.println(longPressCount); + break; + + case DebouncedButton::NONE: + // No event + break; + } +} diff --git a/.agents/skills/arduino-code-generator/examples/config-example.ino b/.agents/skills/arduino-code-generator/examples/config-example.ino new file mode 100644 index 0000000..11ed447 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/config-example.ino @@ -0,0 +1,83 @@ +/* + * Hardware Configuration Pattern Example + * + * Demonstrates board-agnostic hardware abstraction using compile-time + * configuration. This pattern allows a single codebase to run on multiple + * Arduino boards (UNO, ESP32, RP2040) with automatic pin mapping. + * + * Generated by: arduino-code-generator + * Pattern: Hardware Configuration + * License: MIT + */ + +// === Board Detection & Configuration === +#if defined(ARDUINO_AVR_UNO) + #define BOARD_NAME "Arduino UNO" + #define LED_PIN 13 + #define BUTTON_PIN 2 + #define SERIAL_BAUD 9600 + #define HAS_WIFI false +#elif defined(ESP32) + #define BOARD_NAME "ESP32" + #define LED_PIN 2 + #define BUTTON_PIN 4 + #define SERIAL_BAUD 115200 + #define HAS_WIFI true +#elif defined(ARDUINO_ARCH_RP2040) + #define BOARD_NAME "RP2040" + #define LED_PIN LED_BUILTIN + #define BUTTON_PIN 10 + #define SERIAL_BAUD 115200 + #define HAS_WIFI false +#else + #error "Unsupported board - add configuration above" +#endif + +// === Application Logic === +void setup() { + Serial.begin(SERIAL_BAUD); + while (!Serial && millis() < 3000); // Wait for USB serial + + pinMode(LED_PIN, OUTPUT); + pinMode(BUTTON_PIN, INPUT_PULLUP); + + Serial.println(F("\n=== Hardware Configuration Example ===")); + Serial.print(F("Board: ")); + Serial.println(F(BOARD_NAME)); + Serial.print(F("LED Pin: ")); + Serial.println(LED_PIN); + Serial.print(F("Button Pin: ")); + Serial.println(BUTTON_PIN); + Serial.print(F("Baud Rate: ")); + Serial.println(SERIAL_BAUD); + Serial.print(F("WiFi Available: ")); + Serial.println(HAS_WIFI ? F("Yes") : F("No")); + Serial.println(F("\nPress button to toggle LED")); +} + +void loop() { + static bool lastButtonState = HIGH; + static unsigned long lastDebounce = 0; + const unsigned long DEBOUNCE_MS = 50; + + bool reading = digitalRead(BUTTON_PIN); + + // Simple debounce + if (reading != lastButtonState) { + lastDebounce = millis(); + } + + if ((millis() - lastDebounce) > DEBOUNCE_MS) { + if (reading == LOW) { + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); + Serial.println(digitalRead(LED_PIN) ? F("LED ON") : F("LED OFF")); + + // Wait for release + while (digitalRead(BUTTON_PIN) == LOW) { + // Non-blocking wait + } + } + } + + lastButtonState = reading; +} diff --git a/.agents/skills/arduino-code-generator/examples/csv-example.ino b/.agents/skills/arduino-code-generator/examples/csv-example.ino new file mode 100644 index 0000000..ec78631 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/csv-example.ino @@ -0,0 +1,98 @@ +/* + * CSV Data Output Pattern Example + * + * Demonstrates structured data logging in CSV format with: + * - Timestamp (milliseconds) + * - Multiple sensor channels (temperature, humidity, pressure) + * - F() macro for PROGMEM string storage + * - Proper CSV formatting with headers + * + * Generated by: arduino-code-generator + * Pattern: CSV Data Formatting + * License: MIT + */ + +// === Configuration === +const uint16_t SAMPLE_INTERVAL_MS = 1000; // 1 second +const uint8_t TEMP_PIN = A0; +const uint8_t HUMIDITY_PIN = A1; +const uint8_t PRESSURE_PIN = A2; + +// === Simulated Sensor Class === +class SensorSimulator { +private: + float baseValue; + float noiseLevel; + +public: + SensorSimulator(float base, float noise) + : baseValue(base), noiseLevel(noise) {} + + float read() { + // Simulate sensor with noise and slow drift + float drift = sin(millis() / 10000.0) * (noiseLevel * 2); + float noise = (random(-100, 100) / 100.0) * noiseLevel; + return baseValue + drift + noise; + } +}; + +// === Sensor Instances === +SensorSimulator tempSensor(22.5, 0.5); // 22.5Β°C Β± 0.5Β°C +SensorSimulator humiditySensor(55.0, 2.0); // 55% Β± 2% +SensorSimulator pressureSensor(1013.25, 1.0); // 1013.25 hPa Β± 1 hPa + +// === CSV Output Functions === +void printCSVHeader() { + Serial.println(F("timestamp_ms,temperature_c,humidity_percent,pressure_hpa")); +} + +void printCSVRow(unsigned long timestamp, float temp, float humidity, float pressure) { + Serial.print(timestamp); + Serial.print(F(",")); + Serial.print(temp, 2); + Serial.print(F(",")); + Serial.print(humidity, 1); + Serial.print(F(",")); + Serial.println(pressure, 2); +} + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 3000); + + randomSeed(analogRead(A3)); // Seed for simulation + + Serial.println(F("\n=== CSV Data Logger Example ===")); + Serial.println(F("Simulated environmental monitoring")); + Serial.println(F("Copy output to .csv file for analysis\n")); + + delay(500); + printCSVHeader(); +} + +void loop() { + static unsigned long lastSample = 0; + static uint16_t sampleCount = 0; + + unsigned long now = millis(); + + if (now - lastSample >= SAMPLE_INTERVAL_MS) { + lastSample = now; + + // Read sensors + float temperature = tempSensor.read(); + float humidity = humiditySensor.read(); + float pressure = pressureSensor.read(); + + // Output CSV row + printCSVRow(now, temperature, humidity, pressure); + + sampleCount++; + + // Optional: Stop after 60 samples (1 minute) + if (sampleCount >= 60) { + Serial.println(F("\n# Data logging complete (60 samples)")); + while (1); // Halt + } + } +} diff --git a/.agents/skills/arduino-code-generator/examples/data-logging-example.ino b/.agents/skills/arduino-code-generator/examples/data-logging-example.ino new file mode 100644 index 0000000..99ae97e --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/data-logging-example.ino @@ -0,0 +1,212 @@ +/* + * Data Logging Pattern Example + * + * Demonstrates persistent data storage using EEPROM: + * - Sensor data logging with timestamps + * - Circular buffer implementation + * - EEPROM wear leveling (write minimization) + * - Data retrieval and playback + * + * Generated by: arduino-code-generator + * Pattern: Data Logging & Persistence + * License: MIT + */ + +#include + +// === Configuration === +const uint16_t LOG_INTERVAL_MS = 5000; // 5 seconds +const uint8_t MAX_LOG_ENTRIES = 20; // Circular buffer size + +// === Log Entry Structure === +struct LogEntry { + uint32_t timestamp; // Milliseconds since boot + int16_t temperature; // Β°C * 10 (e.g., 235 = 23.5Β°C) + uint16_t humidity; // % * 10 (e.g., 550 = 55.0%) + uint16_t lightLevel; // Raw ADC value +}; + +// === EEPROM Data Logger === +class EEPROMLogger { +private: + const uint16_t MAGIC_NUMBER = 0xABCD; + const uint16_t ADDR_MAGIC = 0; + const uint16_t ADDR_HEAD = 2; + const uint16_t ADDR_COUNT = 4; + const uint16_t ADDR_DATA_START = 6; + + uint8_t headIndex; + uint8_t entryCount; + + void writeUint16(uint16_t addr, uint16_t value) { + EEPROM.write(addr, value & 0xFF); + EEPROM.write(addr + 1, (value >> 8) & 0xFF); + } + + uint16_t readUint16(uint16_t addr) { + uint8_t low = EEPROM.read(addr); + uint8_t high = EEPROM.read(addr + 1); + return (high << 8) | low; + } + + void writeLogEntry(uint8_t index, const LogEntry& entry) { + uint16_t addr = ADDR_DATA_START + (index * sizeof(LogEntry)); + + const uint8_t* data = (const uint8_t*)&entry; + for (size_t i = 0; i < sizeof(LogEntry); i++) { + EEPROM.write(addr + i, data[i]); + } + } + + LogEntry readLogEntry(uint8_t index) { + uint16_t addr = ADDR_DATA_START + (index * sizeof(LogEntry)); + + LogEntry entry; + uint8_t* data = (uint8_t*)&entry; + for (size_t i = 0; i < sizeof(LogEntry); i++) { + data[i] = EEPROM.read(addr + i); + } + return entry; + } + +public: + void begin() { + // Check if EEPROM is initialized + uint16_t magic = readUint16(ADDR_MAGIC); + + if (magic != MAGIC_NUMBER) { + // First-time initialization + Serial.println(F("Initializing EEPROM logger...")); + writeUint16(ADDR_MAGIC, MAGIC_NUMBER); + writeUint16(ADDR_HEAD, 0); + writeUint16(ADDR_COUNT, 0); + headIndex = 0; + entryCount = 0; + } else { + // Load existing state + headIndex = readUint16(ADDR_HEAD); + entryCount = readUint16(ADDR_COUNT); + Serial.print(F("Loaded ")); + Serial.print(entryCount); + Serial.println(F(" existing log entries")); + } + } + + void logData(const LogEntry& entry) { + writeLogEntry(headIndex, entry); + + headIndex = (headIndex + 1) % MAX_LOG_ENTRIES; + if (entryCount < MAX_LOG_ENTRIES) entryCount++; + + writeUint16(ADDR_HEAD, headIndex); + writeUint16(ADDR_COUNT, entryCount); + } + + void printAllLogs() { + Serial.println(F("\n=== Stored Logs ===")); + Serial.println(F("Index Timestamp Temp(Β°C) Humidity(%) Light")); + Serial.println(F("----- --------- -------- ----------- -----")); + + for (uint8_t i = 0; i < entryCount; i++) { + // Read in circular order (oldest to newest) + uint8_t index = (headIndex - entryCount + i + MAX_LOG_ENTRIES) % MAX_LOG_ENTRIES; + LogEntry entry = readLogEntry(index); + + Serial.print(i); + Serial.print(F(" ")); + Serial.print(entry.timestamp / 1000); + Serial.print(F("s ")); + Serial.print(entry.temperature / 10.0, 1); + Serial.print(F(" ")); + Serial.print(entry.humidity / 10.0, 1); + Serial.print(F(" ")); + Serial.println(entry.lightLevel); + } + Serial.println(); + } + + void clearAll() { + headIndex = 0; + entryCount = 0; + writeUint16(ADDR_HEAD, 0); + writeUint16(ADDR_COUNT, 0); + Serial.println(F("All logs cleared")); + } + + uint8_t getCount() const { return entryCount; } +}; + +// === Application === +EEPROMLogger logger; + +LogEntry readSensors() { + LogEntry entry; + entry.timestamp = millis(); + + // Simulate temperature sensor (22-25Β°C) + entry.temperature = random(220, 250); + + // Simulate humidity sensor (40-60%) + entry.humidity = random(400, 600); + + // Read real light sensor + entry.lightLevel = analogRead(A0); + + return entry; +} + +void setup() { + Serial.begin(115200); + while (!Serial && millis() < 3000); + + Serial.println(F("\n=== EEPROM Data Logger Example ===")); + Serial.print(F("EEPROM Size: ")); + Serial.print(EEPROM.length()); + Serial.println(F(" bytes")); + Serial.print(F("Max Log Entries: ")); + Serial.println(MAX_LOG_ENTRIES); + Serial.print(F("Entry Size: ")); + Serial.print(sizeof(LogEntry)); + Serial.println(F(" bytes")); + Serial.println(); + + logger.begin(); + + Serial.println(F("Commands:")); + Serial.println(F(" 'p' - Print all logs")); + Serial.println(F(" 'c' - Clear all logs")); + Serial.println(); +} + +void loop() { + static unsigned long lastLog = 0; + + // Log data periodically + if (millis() - lastLog >= LOG_INTERVAL_MS) { + lastLog = millis(); + + LogEntry entry = readSensors(); + logger.logData(entry); + + Serial.print(F("Logged: ")); + Serial.print(entry.temperature / 10.0, 1); + Serial.print(F("Β°C, ")); + Serial.print(entry.humidity / 10.0, 1); + Serial.print(F("%, Light=")); + Serial.print(entry.lightLevel); + Serial.print(F(" (")); + Serial.print(logger.getCount()); + Serial.println(F(" total)")); + } + + // Handle serial commands + if (Serial.available()) { + char cmd = Serial.read(); + + if (cmd == 'p') { + logger.printAllLogs(); + } else if (cmd == 'c') { + logger.clearAll(); + } + } +} diff --git a/.agents/skills/arduino-code-generator/examples/filtering-example.ino b/.agents/skills/arduino-code-generator/examples/filtering-example.ino new file mode 100644 index 0000000..85abe56 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/filtering-example.ino @@ -0,0 +1,130 @@ +/* + * ADC Filtering Pattern Example + * + * Demonstrates multiple filtering techniques for noisy sensor readings: + * - Moving average filter (smoothing) + * - Exponential moving average (EMA) + * - Median filter (spike rejection) + * + * Generated by: arduino-code-generator + * Pattern: Filtering & Signal Processing + * License: MIT + */ + +// === Configuration === +const uint8_t SENSOR_PIN = A0; +const uint16_t SAMPLE_INTERVAL_MS = 100; + +// === Moving Average Filter === +template +class MovingAverage { + float buffer[N] = {0}; + size_t index = 0; + size_t count = 0; + +public: + float update(float value) { + buffer[index] = value; + index = (index + 1) % N; + if (count < N) count++; + + float sum = 0; + for (size_t i = 0; i < count; i++) { + sum += buffer[i]; + } + return sum / count; + } + + void reset() { + count = 0; + index = 0; + } +}; + +// === Exponential Moving Average === +class EMA { + float alpha; + float value; + bool initialized = false; + +public: + EMA(float smoothing = 0.1) : alpha(smoothing) {} + + float update(float newValue) { + if (!initialized) { + value = newValue; + initialized = true; + return value; + } + + value = alpha * newValue + (1.0 - alpha) * value; + return value; + } + + void reset() { + initialized = false; + } +}; + +// === Median Filter (3-sample) === +class MedianFilter { + float buffer[3] = {0}; + size_t index = 0; + +public: + float update(float value) { + buffer[index] = value; + index = (index + 1) % 3; + + // Simple 3-value sort + float sorted[3]; + memcpy(sorted, buffer, sizeof(buffer)); + + if (sorted[0] > sorted[1]) { float t = sorted[0]; sorted[0] = sorted[1]; sorted[1] = t; } + if (sorted[1] > sorted[2]) { float t = sorted[1]; sorted[1] = sorted[2]; sorted[2] = t; } + if (sorted[0] > sorted[1]) { float t = sorted[0]; sorted[0] = sorted[1]; sorted[1] = t; } + + return sorted[1]; // Return median + } +}; + +// === Filter Instances === +MovingAverage<10> movingAvg; +EMA ema(0.15); +MedianFilter medianFilter; + +void setup() { + Serial.begin(115200); + pinMode(SENSOR_PIN, INPUT); + + Serial.println(F("\n=== ADC Filtering Example ===")); + Serial.println(F("Comparing 4 filtering techniques:")); + Serial.println(F("RAW, MovingAvg(10), EMA(0.15), Median(3)")); + Serial.println(); +} + +void loop() { + static unsigned long lastSample = 0; + + if (millis() - lastSample >= SAMPLE_INTERVAL_MS) { + lastSample = millis(); + + // Read raw sensor value + int rawADC = analogRead(SENSOR_PIN); + float rawVoltage = rawADC * (5.0 / 1023.0); + + // Apply filters + float avgFiltered = movingAvg.update(rawVoltage); + float emaFiltered = ema.update(rawVoltage); + float medianFiltered = medianFilter.update(rawVoltage); + + // Output CSV format + Serial.print(rawVoltage, 3); + Serial.print(F(",")); + Serial.print(avgFiltered, 3); + Serial.print(F(",")); + Serial.print(emaFiltered, 3); + Serial.print(F(",")); + Serial.println(medianFiltered, 3); + } +} diff --git a/.agents/skills/arduino-code-generator/examples/hardware-detection-example.ino b/.agents/skills/arduino-code-generator/examples/hardware-detection-example.ino new file mode 100644 index 0000000..0c58865 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/hardware-detection-example.ino @@ -0,0 +1,182 @@ +/* + * Hardware Detection Pattern Example + * + * Demonstrates runtime board detection and capability reporting: + * - Compile-time board identification + * - Memory capacity detection + * - Clock speed determination + * - Feature flags (WiFi, Bluetooth, etc.) + * + * Generated by: arduino-code-generator + * Pattern: Hardware Detection + * License: MIT + */ + +// === Board Detection === +struct BoardInfo { + const char* name; + const char* mcu; + uint32_t flashSize; + uint32_t sramSize; + uint32_t clockSpeed; + uint8_t adcBits; + bool hasWiFi; + bool hasBluetooth; + uint32_t serialBaud; +}; + +BoardInfo detectBoard() { + BoardInfo info; + + #if defined(ARDUINO_AVR_UNO) + info.name = "Arduino UNO"; + info.mcu = "ATmega328P"; + info.flashSize = 32768; + info.sramSize = 2048; + info.clockSpeed = 16000000; + info.adcBits = 10; + info.hasWiFi = false; + info.hasBluetooth = false; + info.serialBaud = 9600; + + #elif defined(ARDUINO_AVR_MEGA2560) + info.name = "Arduino Mega 2560"; + info.mcu = "ATmega2560"; + info.flashSize = 262144; + info.sramSize = 8192; + info.clockSpeed = 16000000; + info.adcBits = 10; + info.hasWiFi = false; + info.hasBluetooth = false; + info.serialBaud = 9600; + + #elif defined(ESP32) + info.name = "ESP32"; + info.mcu = "ESP32 Dual-Core"; + info.flashSize = 4194304; // Typical 4MB + info.sramSize = 520000; + info.clockSpeed = 240000000; + info.adcBits = 12; + info.hasWiFi = true; + info.hasBluetooth = true; + info.serialBaud = 115200; + + #elif defined(ARDUINO_ARCH_RP2040) + info.name = "Raspberry Pi Pico (RP2040)"; + info.mcu = "RP2040 Dual-Core ARM"; + info.flashSize = 2097152; // 2MB + info.sramSize = 264000; + info.clockSpeed = 133000000; + info.adcBits = 12; + info.hasWiFi = false; + info.hasBluetooth = false; + info.serialBaud = 115200; + + #elif defined(ARDUINO_SAMD_ZERO) + info.name = "Arduino Zero"; + info.mcu = "SAMD21"; + info.flashSize = 262144; + info.sramSize = 32768; + info.clockSpeed = 48000000; + info.adcBits = 12; + info.hasWiFi = false; + info.hasBluetooth = false; + info.serialBaud = 115200; + + #else + info.name = "Unknown Board"; + info.mcu = "Unknown"; + info.flashSize = 0; + info.sramSize = 0; + info.clockSpeed = 0; + info.adcBits = 10; + info.hasWiFi = false; + info.hasBluetooth = false; + info.serialBaud = 9600; + #endif + + return info; +} + +// === Helper Functions === +void printBoardInfo(const BoardInfo& info) { + Serial.println(F("\n=== Hardware Detection Report ===\n")); + + Serial.print(F("Board: ")); + Serial.println(info.name); + + Serial.print(F("MCU: ")); + Serial.println(info.mcu); + + Serial.print(F("Flash Memory: ")); + Serial.print(info.flashSize / 1024); + Serial.println(F(" KB")); + + Serial.print(F("SRAM: ")); + Serial.print(info.sramSize); + Serial.println(F(" bytes")); + + Serial.print(F("Clock Speed: ")); + Serial.print(info.clockSpeed / 1000000); + Serial.println(F(" MHz")); + + Serial.print(F("ADC Resolution: ")); + Serial.print(info.adcBits); + Serial.println(F(" bits")); + + Serial.println(F("\nCapabilities:")); + Serial.print(F(" WiFi: ")); + Serial.println(info.hasWiFi ? F("Yes") : F("No")); + Serial.print(F(" Bluetooth: ")); + Serial.println(info.hasBluetooth ? F("Yes") : F("No")); + + Serial.println(F("\nCompile-Time Defines:")); + #ifdef ARDUINO_AVR_UNO + Serial.println(F(" - ARDUINO_AVR_UNO")); + #endif + #ifdef ESP32 + Serial.println(F(" - ESP32")); + #endif + #ifdef ARDUINO_ARCH_RP2040 + Serial.println(F(" - ARDUINO_ARCH_RP2040")); + #endif + + Serial.print(F("\nArduino IDE Version: ")); + Serial.println(ARDUINO); +} + +void printMemoryUsage() { + Serial.println(F("\n=== Memory Usage ===")); + + #if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_MEGA2560) + // AVR can report free RAM + extern int __heap_start, *__brkval; + int freeRam = (int)&freeRam - (__brkval == 0 ? (int)&__heap_start : (int)__brkval); + Serial.print(F("Free SRAM: ")); + Serial.print(freeRam); + Serial.println(F(" bytes")); + #else + Serial.println(F("(Memory reporting not available on this platform)")); + #endif +} + +void setup() { + BoardInfo board = detectBoard(); + + Serial.begin(board.serialBaud); + while (!Serial && millis() < 3000); + + printBoardInfo(board); + printMemoryUsage(); + + Serial.println(F("\n=== Board detection complete ===")); +} + +void loop() { + // Periodically print memory usage + static unsigned long lastReport = 0; + if (millis() - lastReport >= 10000) { + lastReport = millis(); + printMemoryUsage(); + } +} diff --git a/.agents/skills/arduino-code-generator/examples/i2c-example.ino b/.agents/skills/arduino-code-generator/examples/i2c-example.ino new file mode 100644 index 0000000..03aa5d0 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/i2c-example.ino @@ -0,0 +1,147 @@ +/* + * I2C Communication Pattern Example + * + * Demonstrates I2C bus scanning and device detection with: + * - 7-bit address scanning (0x08-0x77) + * - Board-specific SDA/SCL pin configuration + * - Error detection and reporting + * - Common device identification + * + * Generated by: arduino-code-generator + * Pattern: I2C Communication + * License: MIT + */ + +#include + +// === Board Configuration === +#if defined(ARDUINO_AVR_UNO) + #define BOARD_NAME "Arduino UNO" + #define SDA_PIN A4 + #define SCL_PIN A5 + #define SERIAL_BAUD 9600 +#elif defined(ESP32) + #define BOARD_NAME "ESP32" + #define SDA_PIN 21 + #define SCL_PIN 22 + #define SERIAL_BAUD 115200 +#elif defined(ARDUINO_ARCH_RP2040) + #define BOARD_NAME "RP2040" + #define SDA_PIN 4 + #define SCL_PIN 5 + #define SERIAL_BAUD 115200 +#else + #define BOARD_NAME "Generic" + #define SERIAL_BAUD 9600 +#endif + +// === Common I2C Device Addresses === +struct KnownDevice { + uint8_t address; + const char* name; +}; + +const KnownDevice knownDevices[] = { + {0x20, "MCP23017 GPIO Expander"}, + {0x27, "PCF8574 LCD Backpack"}, + {0x3C, "SSD1306 OLED Display"}, + {0x3D, "SSD1306 OLED Display (Alt)"}, + {0x48, "ADS1115 ADC"}, + {0x50, "AT24C EEPROM"}, + {0x57, "AT24C32 EEPROM"}, + {0x68, "DS1307 RTC / MPU6050 IMU"}, + {0x76, "BMP280 Sensor"}, + {0x77, "BMP280 Sensor (Alt)"} +}; + +const size_t knownDeviceCount = sizeof(knownDevices) / sizeof(knownDevices[0]); + +// === Helper Functions === +const char* identifyDevice(uint8_t address) { + for (size_t i = 0; i < knownDeviceCount; i++) { + if (knownDevices[i].address == address) { + return knownDevices[i].name; + } + } + return "Unknown Device"; +} + +void scanI2CBus() { + Serial.println(F("\nScanning I2C bus...")); + Serial.println(F("Addr Status Device Name")); + Serial.println(F("---- -------- ---------------------")); + + uint8_t devicesFound = 0; + + for (uint8_t address = 0x08; address < 0x78; address++) { + Wire.beginTransmission(address); + uint8_t error = Wire.endTransmission(); + + if (error == 0) { + // Device found + Serial.print(F("0x")); + if (address < 16) Serial.print(F("0")); + Serial.print(address, HEX); + Serial.print(F(" Found ")); + Serial.println(identifyDevice(address)); + devicesFound++; + } + else if (error == 4) { + // Unknown error + Serial.print(F("0x")); + if (address < 16) Serial.print(F("0")); + Serial.print(address, HEX); + Serial.println(F(" Error Unknown I2C error")); + } + } + + Serial.println(F("---- -------- ---------------------")); + Serial.print(F("Total devices found: ")); + Serial.println(devicesFound); + + if (devicesFound == 0) { + Serial.println(F("\nNo I2C devices detected!")); + Serial.println(F("Check wiring:")); + Serial.print(F(" SDA -> Pin ")); + Serial.println(SDA_PIN); + Serial.print(F(" SCL -> Pin ")); + Serial.println(SCL_PIN); + Serial.println(F(" VCC -> 3.3V or 5V")); + Serial.println(F(" GND -> GND")); + } +} + +void setup() { + Serial.begin(SERIAL_BAUD); + while (!Serial && millis() < 3000); + + Serial.println(F("\n=== I2C Scanner Example ===")); + Serial.print(F("Board: ")); + Serial.println(F(BOARD_NAME)); + + #if defined(ARDUINO_AVR_UNO) + Wire.begin(); + #else + Wire.begin(SDA_PIN, SCL_PIN); + #endif + + Wire.setClock(100000); // 100kHz standard mode + + Serial.print(F("I2C Clock: 100 kHz\n")); + Serial.print(F("SDA Pin: ")); + Serial.println(SDA_PIN); + Serial.print(F("SCL Pin: ")); + Serial.println(SCL_PIN); + + scanI2CBus(); +} + +void loop() { + static unsigned long lastScan = 0; + const unsigned long SCAN_INTERVAL_MS = 10000; // Scan every 10 seconds + + if (millis() - lastScan >= SCAN_INTERVAL_MS) { + lastScan = millis(); + scanI2CBus(); + } +} diff --git a/.agents/skills/arduino-code-generator/examples/scheduler-example.ino b/.agents/skills/arduino-code-generator/examples/scheduler-example.ino new file mode 100644 index 0000000..889ce73 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/scheduler-example.ino @@ -0,0 +1,155 @@ +/* + * Non-Blocking Task Scheduler Pattern Example + * + * Demonstrates cooperative multitasking without delay() using: + * - Lightweight task scheduler (millis() based) + * - Multiple independent tasks with different intervals + * - Task priority and execution tracking + * - No blocking delays - maintains responsiveness + * + * Generated by: arduino-code-generator + * Pattern: Non-Blocking Task Scheduler + * License: MIT + */ + +// === Task Scheduler === +class Task { +public: + using TaskFunction = void (*)(); + +private: + TaskFunction function; + unsigned long interval; + unsigned long lastRun; + bool enabled; + const char* name; + uint32_t executionCount; + +public: + Task(TaskFunction func, unsigned long intervalMs, const char* taskName) + : function(func), interval(intervalMs), lastRun(0), + enabled(true), name(taskName), executionCount(0) {} + + void update() { + if (!enabled) return; + + unsigned long now = millis(); + if (now - lastRun >= interval) { + lastRun = now; + function(); + executionCount++; + } + } + + void setEnabled(bool state) { enabled = state; } + void setInterval(unsigned long ms) { interval = ms; } + + bool isEnabled() const { return enabled; } + const char* getName() const { return name; } + uint32_t getExecutionCount() const { return executionCount; } + unsigned long getInterval() const { return interval; } +}; + +// === Task Scheduler Manager === +template +class Scheduler { +private: + Task* tasks[MAX_TASKS]; + size_t taskCount = 0; + +public: + bool addTask(Task* task) { + if (taskCount >= MAX_TASKS) return false; + tasks[taskCount++] = task; + return true; + } + + void run() { + for (size_t i = 0; i < taskCount; i++) { + tasks[i]->update(); + } + } + + void printStatus() { + Serial.println(F("\n=== Task Status ===")); + for (size_t i = 0; i < taskCount; i++) { + Serial.print(F("Task: ")); + Serial.print(tasks[i]->getName()); + Serial.print(F(" | Interval: ")); + Serial.print(tasks[i]->getInterval()); + Serial.print(F("ms | Executions: ")); + Serial.print(tasks[i]->getExecutionCount()); + Serial.print(F(" | Status: ")); + Serial.println(tasks[i]->isEnabled() ? F("ENABLED") : F("DISABLED")); + } + Serial.println(); + } +}; + +// === Task Functions === +void taskFast() { + static uint16_t counter = 0; + Serial.print(F("[FAST] Execution #")); + Serial.println(++counter); +} + +void taskMedium() { + Serial.print(F("[MEDIUM] Sensor reading: ")); + Serial.println(analogRead(A0)); +} + +void taskSlow() { + Serial.print(F("[SLOW] Uptime: ")); + Serial.print(millis() / 1000); + Serial.println(F(" seconds")); +} + +void taskHeartbeat() { + static bool ledState = false; + ledState = !ledState; + digitalWrite(LED_BUILTIN, ledState); + Serial.println(ledState ? F("[LED] ON") : F("[LED] OFF")); +} + +void taskReport() { + scheduler.printStatus(); +} + +// === Task Definitions === +Task fastTask(taskFast, 500, "Fast Task"); +Task mediumTask(taskMedium, 2000, "Medium Task"); +Task slowTask(taskSlow, 5000, "Slow Task"); +Task heartbeatTask(taskHeartbeat, 1000, "Heartbeat LED"); +Task reportTask(taskReport, 15000, "Status Report"); + +// === Scheduler Instance === +Scheduler<5> scheduler; + +void setup() { + Serial.begin(115200); + pinMode(LED_BUILTIN, OUTPUT); + + Serial.println(F("\n=== Non-Blocking Scheduler Example ===")); + Serial.println(F("Running 5 independent tasks:")); + Serial.println(F("- Fast: 500ms (counter)")); + Serial.println(F("- Medium: 2000ms (sensor read)")); + Serial.println(F("- Slow: 5000ms (uptime)")); + Serial.println(F("- Heartbeat: 1000ms (LED toggle)")); + Serial.println(F("- Report: 15000ms (status)\n")); + + // Register tasks + scheduler.addTask(&fastTask); + scheduler.addTask(&mediumTask); + scheduler.addTask(&slowTask); + scheduler.addTask(&heartbeatTask); + scheduler.addTask(&reportTask); + + Serial.println(F("Scheduler started...\n")); +} + +void loop() { + scheduler.run(); + + // Main loop remains responsive for other operations + // (e.g., button checks, serial input, etc.) +} diff --git a/.agents/skills/arduino-code-generator/examples/state-machine-example.ino b/.agents/skills/arduino-code-generator/examples/state-machine-example.ino new file mode 100644 index 0000000..3000be1 --- /dev/null +++ b/.agents/skills/arduino-code-generator/examples/state-machine-example.ino @@ -0,0 +1,166 @@ +/* + * Finite State Machine Pattern Example + * + * Demonstrates FSM implementation for traffic light control: + * - Explicit state enumeration + * - State transition logic + * - Timing-based state changes (non-blocking) + * - State-specific behavior and outputs + * + * Generated by: arduino-code-generator + * Pattern: State Machine + * License: MIT + */ + +// === Configuration === +#if defined(ESP32) + const uint8_t RED_LED = 25; + const uint8_t YELLOW_LED = 26; + const uint8_t GREEN_LED = 27; +#else + const uint8_t RED_LED = 11; + const uint8_t YELLOW_LED = 10; + const uint8_t GREEN_LED = 9; +#endif + +// === State Machine Definition === +enum TrafficLightState { + STATE_RED, + STATE_RED_YELLOW, + STATE_GREEN, + STATE_YELLOW +}; + +class TrafficLight { +private: + TrafficLightState currentState; + unsigned long stateStartTime; + + // State durations (milliseconds) + static const unsigned long RED_DURATION = 5000; + static const unsigned long RED_YELLOW_DURATION = 2000; + static const unsigned long GREEN_DURATION = 5000; + static const unsigned long YELLOW_DURATION = 2000; + + void setLEDs(bool red, bool yellow, bool green) { + digitalWrite(RED_LED, red ? HIGH : LOW); + digitalWrite(YELLOW_LED, yellow ? HIGH : LOW); + digitalWrite(GREEN_LED, green ? HIGH : LOW); + } + + const char* getStateName(TrafficLightState state) { + switch (state) { + case STATE_RED: return "RED"; + case STATE_RED_YELLOW: return "RED+YELLOW"; + case STATE_GREEN: return "GREEN"; + case STATE_YELLOW: return "YELLOW"; + default: return "UNKNOWN"; + } + } + + void enterState(TrafficLightState newState) { + currentState = newState; + stateStartTime = millis(); + + Serial.print(F("State: ")); + Serial.print(getStateName(newState)); + Serial.print(F(" (duration: ")); + + // Set LEDs and print duration + switch (newState) { + case STATE_RED: + setLEDs(true, false, false); + Serial.print(RED_DURATION / 1000); + break; + + case STATE_RED_YELLOW: + setLEDs(true, true, false); + Serial.print(RED_YELLOW_DURATION / 1000); + break; + + case STATE_GREEN: + setLEDs(false, false, true); + Serial.print(GREEN_DURATION / 1000); + break; + + case STATE_YELLOW: + setLEDs(false, true, false); + Serial.print(YELLOW_DURATION / 1000); + break; + } + + Serial.println(F("s)")); + } + +public: + TrafficLight() : currentState(STATE_RED), stateStartTime(0) {} + + void begin() { + pinMode(RED_LED, OUTPUT); + pinMode(YELLOW_LED, OUTPUT); + pinMode(GREEN_LED, OUTPUT); + + enterState(STATE_RED); + } + + void update() { + unsigned long elapsed = millis() - stateStartTime; + + // State transition logic + switch (currentState) { + case STATE_RED: + if (elapsed >= RED_DURATION) { + enterState(STATE_RED_YELLOW); + } + break; + + case STATE_RED_YELLOW: + if (elapsed >= RED_YELLOW_DURATION) { + enterState(STATE_GREEN); + } + break; + + case STATE_GREEN: + if (elapsed >= GREEN_DURATION) { + enterState(STATE_YELLOW); + } + break; + + case STATE_YELLOW: + if (elapsed >= YELLOW_DURATION) { + enterState(STATE_RED); + } + break; + } + } + + TrafficLightState getState() const { + return currentState; + } +}; + +// === Application === +TrafficLight trafficLight; + +void setup() { + Serial.begin(115200); + + Serial.println(F("\n=== Traffic Light FSM Example ===")); + Serial.println(F("Cycle: RED -> RED+YELLOW -> GREEN -> YELLOW")); + Serial.println(F("Pin Configuration:")); + Serial.print(F(" RED LED: Pin ")); + Serial.println(RED_LED); + Serial.print(F(" YELLOW LED: Pin ")); + Serial.println(YELLOW_LED); + Serial.print(F(" GREEN LED: Pin ")); + Serial.println(GREEN_LED); + Serial.println(); + + trafficLight.begin(); +} + +void loop() { + trafficLight.update(); + + // Main loop remains responsive for other operations +} diff --git a/.agents/skills/arduino-code-generator/references/README.md b/.agents/skills/arduino-code-generator/references/README.md new file mode 100644 index 0000000..40afcd5 --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/README.md @@ -0,0 +1,11 @@ +# Arduino Code Generator β€” Reference Structure + +Each reference file follows the same structured layout to make patterns easy to scan and verify: + +1. **Purpose** β€” What the pattern achieves. +2. **When to Use** β€” Scenarios where the pattern fits best. +3. **Implementation** β€” Code templates and usage (often labeled β€œBasic Template” and β€œUsage”). +4. **Verification** β€” Steps to validate behavior. +5. **Common Pitfalls & Tips** β€” Frequent mistakes and best practices. + +Keep pattern names consistent with `generate_snippet.py` and link to examples in `examples/`. diff --git a/.agents/skills/arduino-code-generator/references/patterns-buttons.md b/.agents/skills/arduino-code-generator/references/patterns-buttons.md new file mode 100644 index 0000000..ca62178 --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-buttons.md @@ -0,0 +1,234 @@ +# Button Debouncing & Input Handling + +## Purpose +- Provide robust button handling without blocking or false triggers. +- Support press, release, and long-press events. + +## When to Use +- Any sketch that reads mechanical buttons or switches. +- UI flows that need reliable event detection. + +## Basic Software Debouncing + +```cpp +class DebouncedButton { +private: + uint8_t pin; + uint8_t lastState; + unsigned long lastDebounceTime; + uint16_t debounceDelay; + +public: + DebouncedButton(uint8_t buttonPin, uint16_t delay = 50) + : pin(buttonPin), lastState(HIGH), lastDebounceTime(0), debounceDelay(delay) { + pinMode(pin, INPUT_PULLUP); + } + + bool isPressed() { + uint8_t reading = digitalRead(pin); + + if (reading != lastState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > debounceDelay) { + if (reading == LOW) { + lastState = reading; + return true; + } + } + + lastState = reading; + return false; + } +}; + +// Usage +DebouncedButton button(2); + +void loop() { + if (button.isPressed()) { + Serial.println(F("Button pressed!")); + } +} +``` + +## Edge Detection (Press/Release Events) + +```cpp +class ButtonWithEvents { +private: + uint8_t pin; + uint8_t currentState; + uint8_t previousState; + unsigned long lastDebounceTime; + uint16_t debounceDelay; + +public: + enum Event { NONE, PRESSED, RELEASED }; + + ButtonWithEvents(uint8_t buttonPin, uint16_t delay = 50) + : pin(buttonPin), currentState(HIGH), previousState(HIGH), + lastDebounceTime(0), debounceDelay(delay) { + pinMode(pin, INPUT_PULLUP); + } + + Event update() { + uint8_t reading = digitalRead(pin); + + if (reading != previousState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > debounceDelay) { + if (reading != currentState) { + currentState = reading; + previousState = reading; + return (currentState == LOW) ? PRESSED : RELEASED; + } + } + + previousState = reading; + return NONE; + } +}; + +// Usage +ButtonWithEvents button(2); + +void loop() { + ButtonWithEvents::Event event = button.update(); + + if (event == ButtonWithEvents::PRESSED) { + Serial.println(F("↓ Button pressed")); + } else if (event == ButtonWithEvents::RELEASED) { + Serial.println(F("↑ Button released")); + } +} +``` + +## Long Press Detection + +```cpp +class ButtonWithLongPress { +private: + uint8_t pin; + uint8_t state; + unsigned long pressStartTime; + unsigned long longPressThreshold; + bool longPressTriggered; + +public: + enum Event { NONE, SHORT_PRESS, LONG_PRESS, RELEASED }; + + ButtonWithLongPress(uint8_t buttonPin, unsigned long threshold = 1000) + : pin(buttonPin), state(HIGH), pressStartTime(0), + longPressThreshold(threshold), longPressTriggered(false) { + pinMode(pin, INPUT_PULLUP); + } + + Event update() { + uint8_t reading = digitalRead(pin); + + if (reading == LOW && state == HIGH) { + // Button just pressed + pressStartTime = millis(); + longPressTriggered = false; + state = LOW; + } + else if (reading == LOW && state == LOW) { + // Button held down + if (!longPressTriggered && (millis() - pressStartTime) >= longPressThreshold) { + longPressTriggered = true; + return LONG_PRESS; + } + } + else if (reading == HIGH && state == LOW) { + // Button released + state = HIGH; + if (!longPressTriggered) { + return SHORT_PRESS; + } + return RELEASED; + } + + return NONE; + } +}; + +// Usage +ButtonWithLongPress button(2, 2000); // 2 second long press + +void loop() { + ButtonWithLongPress::Event event = button.update(); + + if (event == ButtonWithLongPress::SHORT_PRESS) { + Serial.println(F("Short press - Toggle LED")); + } else if (event == ButtonWithLongPress::LONG_PRESS) { + Serial.println(F("Long press - Reset system")); + } +} +``` + +## Multi-Button Manager + +```cpp +class MultiButtonManager { +private: + static const uint8_t MAX_BUTTONS = 8; + DebouncedButton* buttons[MAX_BUTTONS]; + uint8_t buttonCount; + +public: + MultiButtonManager() : buttonCount(0) {} + + void addButton(DebouncedButton* btn) { + if (buttonCount < MAX_BUTTONS) { + buttons[buttonCount++] = btn; + } + } + + int checkButtons() { + for (uint8_t i = 0; i < buttonCount; i++) { + if (buttons[i]->isPressed()) { + return i; + } + } + return -1; + } +}; + +// Usage +DebouncedButton btn1(2); +DebouncedButton btn2(3); +DebouncedButton btn3(4); +MultiButtonManager manager; + +void setup() { + Serial.begin(115200); + manager.addButton(&btn1); + manager.addButton(&btn2); + manager.addButton(&btn3); +} + +void loop() { + int pressed = manager.checkButtons(); + if (pressed >= 0) { + Serial.print(F("Button ")); + Serial.print(pressed); + Serial.println(F(" pressed")); + } +} +``` + +## Verification +- Open Serial Monitor and press/release the button; verify expected events. +- Hold the button past the long-press threshold to confirm long-press events. + +## Common Pitfalls & Tips +- Always debounce buttons (hardware bounce lasts 5–50 ms). +- Use INPUT_PULLUP to avoid external resistors. +- Never use delay() in button checking (blocks other code). +- Edge detection should report PRESS and RELEASE separately. +- Long press requires tracking press duration before release. +- For multi-button setups, scan all buttons each loop. diff --git a/.agents/skills/arduino-code-generator/references/patterns-config.md b/.agents/skills/arduino-code-generator/references/patterns-config.md new file mode 100644 index 0000000..6c8e383 --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-config.md @@ -0,0 +1,74 @@ +# Config.h Hardware Abstraction Pattern + +## Purpose +- Centralize board-specific pins, features, and limits in one header. +- Enable a single codebase to compile across UNO/ESP32/RP2040. + +## When to Use +- Multi-board projects or reusable libraries. +- Any project that needs consistent pin and feature definitions. + +## Basic Template + +```cpp +// config.h +#ifndef CONFIG_H +#define CONFIG_H + +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO) + #define BOARD_NAME "Arduino UNO" + #define LED_PIN 13 + #define BUTTON_PIN 2 + #define I2C_SDA A4 + #define I2C_SCL A5 + #define SRAM_SIZE 2048 + +#elif defined(ESP32) + #define BOARD_NAME "ESP32" + #define LED_PIN 2 + #define BUTTON_PIN 4 + #define I2C_SDA 21 + #define I2C_SCL 22 + #define SRAM_SIZE 520000 + #define HAS_WIFI 1 + +#elif defined(ARDUINO_ARCH_RP2040) + #define BOARD_NAME "RP2040" + #define LED_PIN 25 + #define BUTTON_PIN 14 + #define I2C_SDA 4 + #define I2C_SCL 5 + #define SRAM_SIZE 264000 + +#else + #error "Unsupported board!" +#endif + +// Common constants +#define SERIAL_BAUD 115200 +#define DEBOUNCE_DELAY 50 +#define SENSOR_READ_INTERVAL 1000 + +#endif +``` + +## Usage +```cpp +#include "config.h" + +void setup() { + Serial.begin(SERIAL_BAUD); + pinMode(LED_PIN, OUTPUT); + Wire.begin(I2C_SDA, I2C_SCL); +} +``` + +## Verification +- Compile for UNO, ESP32, and RP2040 to confirm board detection paths. +- Confirm Serial output and I2C initialization succeed on each target. + +## Common Pitfalls & Tips +- Use `#if defined()` for board detection. +- Define all pins in config.h, never hardcode in sketches. +- Include memory limits for adaptive code paths. +- Add feature flags (HAS_WIFI, HAS_BLE) for capabilities. diff --git a/.agents/skills/arduino-code-generator/references/patterns-csv.md b/.agents/skills/arduino-code-generator/references/patterns-csv.md new file mode 100644 index 0000000..cebc70d --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-csv.md @@ -0,0 +1,75 @@ +# CSV Data Output Pattern + +## Purpose +- Emit structured, analysis-ready telemetry over Serial. +- Standardize headers and column formatting for tooling. + +## When to Use +- Logging sensor data for Excel/Python analysis. +- Any workflow that needs consistent, parseable output. + +## Basic CSV Logger + +```cpp +class CSVLogger { +private: + bool headerPrinted; + +public: + CSVLogger() : headerPrinted(false) {} + + void printHeader(const char* header) { + if (!headerPrinted) { + Serial.println(header); + headerPrinted = true; + } + } + + void logData(float val1, float val2, int val3) { + Serial.print(val1, 2); + Serial.print(F(",")); + Serial.print(val2, 2); + Serial.print(F(",")); + Serial.println(val3); + } +}; + +// Usage +CSVLogger logger; + +void setup() { + Serial.begin(115200); + logger.printHeader("Time_ms,Temp_C,Humidity_%,Light"); +} + +void loop() { + static unsigned long lastLog = 0; + + if (millis() - lastLog >= 1000) { + logger.logData(25.5, 60.2, 512); + lastLog = millis(); + } +} +``` + +## With Timestamp + +```cpp +void logWithTimestamp(float temp, float humid) { + Serial.print(millis()); + Serial.print(F(",")); + Serial.print(temp, 2); + Serial.print(F(",")); + Serial.println(humid, 1); +} +``` + +## Verification +- Confirm the header prints once on boot. +- Capture a few lines and import into Excel or Python to validate parsing. + +## Common Pitfalls & Tips +- Use F() macro for strings on UNO (saves SRAM). +- Print the header once at startup. +- Use consistent decimal places (e.g., 2 for 25.50). +- Excel/Python can import CSV directly. diff --git a/.agents/skills/arduino-code-generator/references/patterns-data-logging.md b/.agents/skills/arduino-code-generator/references/patterns-data-logging.md new file mode 100644 index 0000000..a79dd03 --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-data-logging.md @@ -0,0 +1,374 @@ +# Data Logging & Persistence Patterns + +## Purpose +- Persist sensor data across reboots with EEPROM/SD/flash. +- Use buffering and validation to protect data integrity. + +## When to Use +- Long-running data collection or audit trails. +- Systems that must survive power loss or resets. + +## EEPROM Logging with CRC Validation + +```cpp +#include + +struct LogEntry { + uint32_t timestamp; + float temperature; + float humidity; + uint16_t crc; +}; + +class EEPROMLogger { +private: + uint16_t currentAddress; + const uint16_t maxAddress; + + uint16_t calculateCRC(const LogEntry& entry) { + uint16_t crc = 0xFFFF; + const uint8_t* data = (const uint8_t*)&entry; + for (size_t i = 0; i < sizeof(entry) - sizeof(entry.crc); i++) { + crc ^= data[i]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x0001) { + crc = (crc >> 1) ^ 0xA001; + } else { + crc = crc >> 1; + } + } + } + return crc; + } + +public: + EEPROMLogger(uint16_t maxAddr = 1024) + : currentAddress(0), maxAddress(maxAddr) {} + + void writeEntry(uint32_t timestamp, float temp, float humid) { + if (currentAddress + sizeof(LogEntry) > maxAddress) { + Serial.println(F("EEPROM full")); + return; + } + + LogEntry entry; + entry.timestamp = timestamp; + entry.temperature = temp; + entry.humidity = humid; + entry.crc = calculateCRC(entry); + + EEPROM.put(currentAddress, entry); + currentAddress += sizeof(LogEntry); + + Serial.print(F("Logged to EEPROM at ")); + Serial.println(currentAddress - sizeof(LogEntry)); + } + + bool readEntry(uint16_t index, LogEntry& entry) { + uint16_t addr = index * sizeof(LogEntry); + if (addr >= currentAddress) return false; + + EEPROM.get(addr, entry); + + uint16_t calculatedCRC = calculateCRC(entry); + if (calculatedCRC != entry.crc) { + Serial.println(F("CRC mismatch - corrupted data")); + return false; + } + + return true; + } + + void dumpAll() { + Serial.println(F("=== EEPROM Dump ===")); + uint16_t entryCount = currentAddress / sizeof(LogEntry); + + for (uint16_t i = 0; i < entryCount; i++) { + LogEntry entry; + if (readEntry(i, entry)) { + Serial.print(entry.timestamp); + Serial.print(F(",")); + Serial.print(entry.temperature, 2); + Serial.print(F(",")); + Serial.println(entry.humidity, 2); + } + } + } + + void clear() { + currentAddress = 0; + Serial.println(F("EEPROM cleared")); + } +}; + +EEPROMLogger logger; + +void setup() { + Serial.begin(115200); +} + +void loop() { + static EveryMs logTimer(60000); // Log every 60 seconds + + if (logTimer.check()) { + float temp = readTemperature(); + float humid = readHumidity(); + logger.writeEntry(millis(), temp, humid); + } + + if (Serial.available() && Serial.read() == 'd') { + logger.dumpAll(); + } +} +``` + +## SD Card Buffered Logger + +```cpp +#include + +class SDBufferedLogger { +private: + static const uint8_t BUFFER_SIZE = 10; + String buffer[BUFFER_SIZE]; + uint8_t bufferIndex; + const char* filename; + bool sdReady; + +public: + SDBufferedLogger(const char* file) + : bufferIndex(0), filename(file), sdReady(false) {} + + bool begin(uint8_t csPin = 10) { + sdReady = SD.begin(csPin); + if (!sdReady) { + Serial.println(F("SD card init failed")); + return false; + } + + // Write CSV header if file doesn't exist + if (!SD.exists(filename)) { + File dataFile = SD.open(filename, FILE_WRITE); + if (dataFile) { + dataFile.println(F("Timestamp,Temperature,Humidity,Light")); + dataFile.close(); + } + } + + Serial.println(F("SD card ready")); + return true; + } + + void logData(uint32_t timestamp, float temp, float humid, int light) { + // Build CSV line + String line = String(timestamp) + "," + + String(temp, 2) + "," + + String(humid, 2) + "," + + String(light); + + buffer[bufferIndex++] = line; + + // Flush buffer when full + if (bufferIndex >= BUFFER_SIZE) { + flush(); + } + } + + void flush() { + if (!sdReady || bufferIndex == 0) return; + + File dataFile = SD.open(filename, FILE_WRITE); + if (dataFile) { + for (uint8_t i = 0; i < bufferIndex; i++) { + dataFile.println(buffer[i]); + } + dataFile.close(); + Serial.print(F("Flushed ")); + Serial.print(bufferIndex); + Serial.println(F(" entries to SD")); + bufferIndex = 0; + } else { + Serial.println(F("Failed to open SD file")); + } + } + + void dumpFile() { + if (!sdReady) return; + + File dataFile = SD.open(filename, FILE_READ); + if (dataFile) { + Serial.println(F("=== SD Card Contents ===")); + while (dataFile.available()) { + Serial.write(dataFile.read()); + } + dataFile.close(); + } + } +}; + +SDBufferedLogger sdLogger("datalog.csv"); + +void setup() { + Serial.begin(115200); + sdLogger.begin(10); // CS pin 10 +} + +void loop() { + static EveryMs logTimer(5000); // Log every 5 seconds + + if (logTimer.check()) { + float temp = readTemperature(); + float humid = readHumidity(); + int light = analogRead(A0); + sdLogger.logData(millis(), temp, humid, light); + } + + // Manual flush command + if (Serial.available() && Serial.read() == 'f') { + sdLogger.flush(); + } +} +``` + +## Wear Leveling for Flash Storage + +```cpp +#ifdef ESP32 +#include + +class WearLeveledStorage { +private: + Preferences prefs; + const char* namespaceName; + uint8_t currentSlot; + static const uint8_t MAX_SLOTS = 10; + +public: + WearLeveledStorage(const char* ns) : namespaceName(ns), currentSlot(0) {} + + bool begin() { + if (!prefs.begin(namespaceName, false)) { + Serial.println(F("Failed to init Preferences")); + return false; + } + + // Load last used slot + currentSlot = prefs.getUChar("slot", 0); + return true; + } + + void writeValue(const char* key, float value) { + // Rotate through slots to distribute writes + String slotKey = String(key) + String(currentSlot); + prefs.putFloat(slotKey.c_str(), value); + + currentSlot = (currentSlot + 1) % MAX_SLOTS; + prefs.putUChar("slot", currentSlot); + } + + float readValue(const char* key) { + // Read from current slot + uint8_t readSlot = (currentSlot == 0) ? MAX_SLOTS - 1 : currentSlot - 1; + String slotKey = String(key) + String(readSlot); + return prefs.getFloat(slotKey.c_str(), 0.0); + } + + void clear() { + prefs.clear(); + currentSlot = 0; + Serial.println(F("Storage cleared")); + } +}; + +WearLeveledStorage storage("myapp"); + +void setup() { + Serial.begin(115200); + storage.begin(); +} + +void loop() { + static EveryMs saveTimer(10000); + + if (saveTimer.check()) { + float temp = readTemperature(); + storage.writeValue("temp", temp); + Serial.print(F("Saved: ")); + Serial.println(temp); + } +} +#endif +``` + +## Circular Buffer for In-Memory Logging + +```cpp +template +class CircularBuffer { +private: + T buffer[SIZE]; + uint16_t writeIndex; + uint16_t count; + +public: + CircularBuffer() : writeIndex(0), count(0) {} + + void push(const T& value) { + buffer[writeIndex] = value; + writeIndex = (writeIndex + 1) % SIZE; + if (count < SIZE) count++; + } + + T get(uint16_t index) const { + if (index >= count) return T(); + uint16_t actualIndex = (writeIndex - count + index + SIZE) % SIZE; + return buffer[actualIndex]; + } + + uint16_t size() const { return count; } + bool isFull() const { return count == SIZE; } + + void clear() { + writeIndex = 0; + count = 0; + } + + void dump() const { + for (uint16_t i = 0; i < count; i++) { + Serial.println(get(i)); + } + } +}; + +CircularBuffer tempHistory; + +void loop() { + static EveryMs sampleTimer(1000); + + if (sampleTimer.check()) { + float temp = readTemperature(); + tempHistory.push(temp); + + if (tempHistory.isFull()) { + Serial.println(F("Buffer full - oldest data overwritten")); + } + } + + if (Serial.available() && Serial.read() == 'h') { + tempHistory.dump(); + } +} +``` + +## Verification +- Write a few entries, power cycle, and dump to confirm persistence. +- Trigger CRC mismatch by editing data and confirm it is detected. + +## Common Pitfalls & Tips +- EEPROM: use CRC validation to detect corrupted data. +- SD Card: buffer writes to reduce open/close operations (reduces wear). +- Wear leveling: rotate write locations to extend flash lifetime. +- Circular buffer: in-memory logging with automatic overflow handling. +- Always flush buffers before power loss or reset. +- EEPROM has limited write cycles (~100,000 writes per byte). +- F() macro stores strings in flash, not RAM. diff --git a/.agents/skills/arduino-code-generator/references/patterns-filtering.md b/.agents/skills/arduino-code-generator/references/patterns-filtering.md new file mode 100644 index 0000000..93bc0c2 --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-filtering.md @@ -0,0 +1,174 @@ +# Sensor Filtering & ADC Patterns + +## Purpose +- Reduce noise and spikes in analog sensor readings. +- Normalize readings for downstream control logic. + +## When to Use +- Noisy ADC signals or slow-changing sensors. +- Any pipeline that depends on stable sensor values. + +## Moving Average Filter + +```cpp +class MovingAverageFilter { +private: + float* buffer; + uint8_t size; + uint8_t index; + float sum; + +public: + MovingAverageFilter(uint8_t windowSize) : size(windowSize), index(0), sum(0) { + buffer = new float[size]; + for (uint8_t i = 0; i < size; i++) buffer[i] = 0; + } + + ~MovingAverageFilter() { delete[] buffer; } + + float filter(float newValue) { + sum -= buffer[index]; + buffer[index] = newValue; + sum += newValue; + index = (index + 1) % size; + return sum / size; + } +}; + +// Usage +MovingAverageFilter tempFilter(10); + +void loop() { + float raw = analogRead(A0); + float filtered = tempFilter.filter(raw); + Serial.println(filtered); + delay(100); +} +``` + +## Median Filter (Noise Spike Removal) + +```cpp +class MedianFilter { +private: + float* buffer; + uint8_t size; + uint8_t index; + +public: + MedianFilter(uint8_t windowSize) : size(windowSize), index(0) { + buffer = new float[size]; + } + + float filter(float newValue) { + buffer[index++] = newValue; + if (index >= size) index = 0; + + float sorted[size]; + memcpy(sorted, buffer, size * sizeof(float)); + + // Bubble sort + for (uint8_t i = 0; i < size - 1; i++) { + for (uint8_t j = 0; j < size - i - 1; j++) { + if (sorted[j] > sorted[j + 1]) { + float temp = sorted[j]; + sorted[j] = sorted[j + 1]; + sorted[j + 1] = temp; + } + } + } + + return sorted[size / 2]; + } +}; +``` + +## DHT22 Non-Blocking Reader + +```cpp +#include +#include "config.h" + +#define DHT_PIN 2 +#define DHT_TYPE DHT22 + +DHT dht(DHT_PIN, DHT_TYPE); +MovingAverageFilter tempFilter(5); + +void setup() { + Serial.begin(115200); + dht.begin(); +} + +void loop() { + static unsigned long lastRead = 0; + + if (millis() - lastRead >= 2000) { // DHT22 needs 2s between reads + float temp = dht.readTemperature(); + + if (!isnan(temp)) { + float filtered = tempFilter.filter(temp); + Serial.print(F("Temp: ")); + Serial.print(filtered, 1); + Serial.println(F(" Β°C")); + } + + lastRead = millis(); + } +} +``` + +## Calibration Pattern + +```cpp +struct SensorCalibration { + float offset; + float gain; +}; + +float applyCalibration(float rawValue, SensorCalibration cal) { + return (rawValue * cal.gain) + cal.offset; +} + +// Example usage +SensorCalibration tempCal = {-0.5, 1.02}; // -0.5Β°C offset, 2% gain correction +float calibrated = applyCalibration(rawTemp, tempCal); +``` + +## Validation Pattern + +```cpp +bool validateSensorReading(float value, float min, float max) { + if (isnan(value) || isinf(value)) { + Serial.println(F("❌ Invalid reading (NaN/Inf)")); + return false; + } + + if (value < min || value > max) { + Serial.print(F("⚠️ Out of range: ")); + Serial.println(value); + return false; + } + + return true; +} + +// Usage +float temp = dht.readTemperature(); +if (validateSensorReading(temp, -40, 80)) { + // Use validated temperature +} +``` + +## Verification +- Log raw vs filtered values and confirm noise reduction. +- Inject a spike and verify median filter rejects it. + +## Common Pitfalls & Tips +- Always use filters for ADC readings (analog sensors are noisy). +- Moving average smooths readings, best for slow-changing sensors. +- Median filter removes spikes, best for occasional glitches. +- DHT22 requires 2000 ms between reads (hardware limitation). +- Always validate readings (NaN/range checks). +- Prefer fixed-size buffers over heap allocations on small MCUs. +- Calibration: measure known values, calculate offset and gain. diff --git a/.agents/skills/arduino-code-generator/references/patterns-hardware-detection.md b/.agents/skills/arduino-code-generator/references/patterns-hardware-detection.md new file mode 100644 index 0000000..345dc62 --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-hardware-detection.md @@ -0,0 +1,252 @@ +# Hardware Detection & Adaptive Configuration + +## Purpose +- Detect board capabilities and adapt configuration safely. +- Prevent memory and feature mismatches across targets. + +## When to Use +- Multi-board deployments or libraries. +- Systems that scale features based on memory or peripherals. + +## Board Detection Pattern + +```cpp +// config.h - Auto-detect board and set defaults + +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO) + #define BOARD_TYPE "Arduino UNO/Nano" + #define MAX_SRAM 2048 + #define HAS_WIFI false + #define SERIAL_BAUD 9600 + +#elif defined(ARDUINO_ESP32_DEV) || defined(ESP32) + #define BOARD_TYPE "ESP32" + #define MAX_SRAM 327680 + #define HAS_WIFI true + #define SERIAL_BAUD 115200 + +#elif defined(ARDUINO_ARCH_RP2040) + #define BOARD_TYPE "Raspberry Pi Pico" + #define MAX_SRAM 262144 + #define HAS_WIFI false + #define SERIAL_BAUD 115200 + +#else + #define BOARD_TYPE "Unknown Board" + #define MAX_SRAM 2048 + #define HAS_WIFI false + #define SERIAL_BAUD 9600 +#endif + +// Runtime board info +struct BoardInfo { + static const char* getBoardType() { return BOARD_TYPE; } + static uint32_t getMaxSRAM() { return MAX_SRAM; } + static bool hasWiFi() { return HAS_WIFI; } + static uint32_t getSerialBaud() { return SERIAL_BAUD; } +}; + +void printBoardInfo() { + Serial.println(F("=== Board Information ===")); + Serial.print(F("Board: ")); + Serial.println(BoardInfo::getBoardType()); + Serial.print(F("Max SRAM: ")); + Serial.print(BoardInfo::getMaxSRAM()); + Serial.println(F(" bytes")); + Serial.print(F("WiFi: ")); + Serial.println(BoardInfo::hasWiFi() ? F("Yes") : F("No")); +} +``` + +## Memory Monitoring + +```cpp +#ifdef __AVR__ +#include + +int getFreeMemory() { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} +#endif + +class MemoryMonitor { +private: + int minFreeMemory; + unsigned long checkInterval; + unsigned long lastCheck; + +public: + MemoryMonitor(unsigned long intervalMs = 1000) + : minFreeMemory(999999), checkInterval(intervalMs), lastCheck(0) {} + + void update() { + unsigned long now = millis(); + if (now - lastCheck >= checkInterval) { + #ifdef __AVR__ + int freeMem = getFreeMemory(); + if (freeMem < minFreeMemory) { + minFreeMemory = freeMem; + } + + if (freeMem < 200) { + Serial.println(F("WARNING: Low memory!")); + } + #endif + lastCheck = now; + } + } + + void printStats() { + #ifdef __AVR__ + Serial.print(F("Free SRAM: ")); + Serial.print(getFreeMemory()); + Serial.print(F(" bytes (min: ")); + Serial.print(minFreeMemory); + Serial.println(F(")")); + #else + Serial.println(F("Memory monitoring not available")); + #endif + } +}; + +MemoryMonitor memMonitor; + +void setup() { + Serial.begin(SERIAL_BAUD); + printBoardInfo(); +} + +void loop() { + memMonitor.update(); + + // Your code here + + if (Serial.available() && Serial.read() == 'm') { + memMonitor.printStats(); + } +} +``` + +## Adaptive Buffer Sizing + +```cpp +// Adjust buffer size based on available SRAM +#if defined(ARDUINO_AVR_UNO) + #define BUFFER_SIZE 64 // UNO has limited RAM + #define MAX_SAMPLES 50 +#elif defined(ESP32) + #define BUFFER_SIZE 1024 // ESP32 has plenty of RAM + #define MAX_SAMPLES 1000 +#elif defined(ARDUINO_ARCH_RP2040) + #define BUFFER_SIZE 512 + #define MAX_SAMPLES 500 +#else + #define BUFFER_SIZE 64 // Safe default for unknown boards + #define MAX_SAMPLES 50 +#endif + +class AdaptiveLogger { +private: + float samples[MAX_SAMPLES]; + uint16_t sampleCount; + +public: + AdaptiveLogger() : sampleCount(0) {} + + void addSample(float value) { + if (sampleCount < MAX_SAMPLES) { + samples[sampleCount++] = value; + } else { + Serial.println(F("Buffer full")); + } + } + + float getAverage() const { + if (sampleCount == 0) return 0.0; + float sum = 0.0; + for (uint16_t i = 0; i < sampleCount; i++) { + sum += samples[i]; + } + return sum / sampleCount; + } + + uint16_t getMaxCapacity() const { return MAX_SAMPLES; } + uint16_t getCurrentCount() const { return sampleCount; } +}; +``` + +## Feature Detection + +```cpp +class FeatureDetection { +public: + static bool hasSPIFFS() { + #ifdef ESP32 + return true; + #else + return false; + #endif + } + + static bool hasEEPROM() { + #if defined(ARDUINO_AVR_UNO) || defined(ESP32) + return true; + #else + return false; + #endif + } + + static bool hasAnalogRead() { + // All Arduino boards have ADC + return true; + } + + static bool hasDAC() { + #ifdef ESP32 + return true; // ESP32 has 2 DAC channels + #else + return false; + #endif + } + + static uint8_t getAnalogResolution() { + #ifdef ESP32 + return 12; // 12-bit ADC + #elif defined(ARDUINO_ARCH_RP2040) + return 12; // 12-bit ADC + #else + return 10; // 10-bit ADC (UNO, Nano) + #endif + } +}; + +void setup() { + Serial.begin(SERIAL_BAUD); + + Serial.println(F("=== Feature Detection ===")); + Serial.print(F("SPIFFS: ")); + Serial.println(FeatureDetection::hasSPIFFS() ? F("Yes") : F("No")); + Serial.print(F("EEPROM: ")); + Serial.println(FeatureDetection::hasEEPROM() ? F("Yes") : F("No")); + Serial.print(F("DAC: ")); + Serial.println(FeatureDetection::hasDAC() ? F("Yes") : F("No")); + Serial.print(F("ADC Resolution: ")); + Serial.print(FeatureDetection::getAnalogResolution()); + Serial.println(F(" bits")); +} +``` + +## Verification +- Compile for UNO, ESP32, and RP2040 to confirm board paths. +- Trigger memory monitor output on AVR to confirm warnings. + +## Common Pitfalls & Tips +- Use preprocessor directives (#if defined) for compile-time detection. +- Create a BoardInfo static class for runtime queries. +- Monitor SRAM on AVR boards (UNO has only 2KB). +- Adaptive buffer sizing prevents out-of-memory crashes. +- Feature detection allows graceful degradation. +- Print board info in setup() for debugging. +- Use F() macro to store strings in flash (saves SRAM). diff --git a/.agents/skills/arduino-code-generator/references/patterns-i2c.md b/.agents/skills/arduino-code-generator/references/patterns-i2c.md new file mode 100644 index 0000000..c697b7b --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-i2c.md @@ -0,0 +1,90 @@ +# I2C Communication Patterns + +## Purpose +- Discover and communicate with I2C peripherals reliably. +- Standardize register read/write helpers. + +## When to Use +- Any project using I2C sensors, displays, or RTC modules. +- Diagnostics for wiring or address conflicts. + +## I2C Scanner + +```cpp +#include + +void scanI2C() { + Serial.println(F("\n=== I2C Scanner ===")); + uint8_t found = 0; + + for (uint8_t addr = 1; addr < 127; addr++) { + Wire.beginTransmission(addr); + uint8_t error = Wire.endTransmission(); + + if (error == 0) { + Serial.print(F("Device found at 0x")); + if (addr < 16) Serial.print(F("0")); + Serial.println(addr, HEX); + found++; + } + } + + Serial.print(F("Total devices: ")); + Serial.println(found); +} + +void setup() { + Serial.begin(115200); + Wire.begin(); + scanI2C(); +} +``` + +## Read Register Pattern + +```cpp +uint8_t readRegister(uint8_t addr, uint8_t reg) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.endTransmission(false); + Wire.requestFrom(addr, (uint8_t)1); + return Wire.read(); +} + +// Usage +uint8_t chipID = readRegister(0x76, 0xD0); // BME280 chip ID +``` + +## Write Register Pattern + +```cpp +void writeRegister(uint8_t addr, uint8_t reg, uint8_t value) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(value); + Wire.endTransmission(); +} +``` + +## Device Detection + +```cpp +bool checkI2CDevice(uint8_t addr) { + Wire.beginTransmission(addr); + return (Wire.endTransmission() == 0); +} + +// Usage +if (checkI2CDevice(0x76)) { + Serial.println(F("BME280 detected!")); +} +``` + +## Verification +- Run the scanner and confirm expected device addresses appear. +- Read a known register (e.g., BME280 ID) to validate wiring. + +## Common Pitfalls & Tips +- Ensure correct SDA/SCL pins for the target board. +- Use pull-up resistors if the module does not include them. +- I2C addresses are 7-bit (0x08–0x77 in scans). diff --git a/.agents/skills/arduino-code-generator/references/patterns-scheduler.md b/.agents/skills/arduino-code-generator/references/patterns-scheduler.md new file mode 100644 index 0000000..2502c3d --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-scheduler.md @@ -0,0 +1,222 @@ +# Non-Blocking Scheduler & Timing Patterns + +## Purpose +- Replace blocking delays with cooperative scheduling. +- Coordinate multiple time-based tasks in a single loop. + +## When to Use +- Any sketch with multiple timed actions or sensor polls. +- Systems that must remain responsive while doing periodic work. + +## EveryMs Pattern (Core Building Block) + +```cpp +class EveryMs { +private: + unsigned long interval; + unsigned long lastTrigger; + +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {} + + bool check() { + unsigned long now = millis(); + if (now - lastTrigger >= interval) { + lastTrigger = now; + return true; + } + return false; + } + + void reset() { + lastTrigger = millis(); + } +}; + +// Usage: Blink LED every 1000ms, read sensor every 500ms +EveryMs blinkTimer(1000); +EveryMs sensorTimer(500); + +void loop() { + if (blinkTimer.check()) { + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); + } + + if (sensorTimer.check()) { + int value = analogRead(A0); + Serial.println(value); + } +} +``` + +## Priority Task Scheduler + +```cpp +class Task { +public: + typedef void (*TaskFunction)(); + enum Priority { LOW, NORMAL, HIGH, CRITICAL }; + +private: + TaskFunction callback; + unsigned long interval; + unsigned long lastRun; + Priority priority; + bool enabled; + +public: + Task(TaskFunction func, unsigned long ms, Priority pri = NORMAL) + : callback(func), interval(ms), lastRun(0), priority(pri), enabled(true) {} + + bool shouldRun() const { + if (!enabled) return false; + return (millis() - lastRun >= interval); + } + + void execute() { + if (callback) callback(); + lastRun = millis(); + } + + Priority getPriority() const { return priority; } + void enable() { enabled = true; } + void disable() { enabled = false; } +}; + +class Scheduler { +private: + static const uint8_t MAX_TASKS = 10; + Task* tasks[MAX_TASKS]; + uint8_t taskCount; + +public: + Scheduler() : taskCount(0) {} + + bool addTask(Task* task) { + if (taskCount >= MAX_TASKS) return false; + tasks[taskCount++] = task; + return true; + } + + void run() { + Task* readyTask = nullptr; + Task::Priority highestPriority = Task::LOW; + + // Find highest priority task that's ready + for (uint8_t i = 0; i < taskCount; i++) { + if (tasks[i]->shouldRun()) { + if (tasks[i]->getPriority() > highestPriority || readyTask == nullptr) { + readyTask = tasks[i]; + highestPriority = tasks[i]->getPriority(); + } + } + } + + if (readyTask) { + readyTask->execute(); + } + } +}; + +// Task functions +void taskBlinkLED() { + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); +} + +void taskReadSensor() { + int value = analogRead(A0); + Serial.println(value); +} + +// Global scheduler +Scheduler scheduler; +Task ledTask(taskBlinkLED, 1000, Task::NORMAL); +Task sensorTask(taskReadSensor, 500, Task::HIGH); + +void setup() { + Serial.begin(115200); + pinMode(LED_PIN, OUTPUT); + + scheduler.addTask(&ledTask); + scheduler.addTask(&sensorTask); +} + +void loop() { + scheduler.run(); +} +``` + +## Multi-Task Environmental Monitor + +```cpp +#include + +DHT dht(2, DHT22); +EveryMs readDHTTimer(2000); // DHT22 needs 2s between reads +EveryMs readLightTimer(1000); +EveryMs displayTimer(5000); +EveryMs csvLogTimer(10000); + +struct SensorData { + float temperature; + float humidity; + int lightLevel; +} data; + +void setup() { + Serial.begin(115200); + pinMode(LED_PIN, OUTPUT); + pinMode(A0, INPUT); + dht.begin(); + + Serial.println(F("Time_ms,Temp_C,Humidity_%,Light")); +} + +void loop() { + // Task 1: Read DHT22 + if (readDHTTimer.check()) { + data.temperature = dht.readTemperature(); + data.humidity = dht.readHumidity(); + } + + // Task 2: Read light sensor + if (readLightTimer.check()) { + data.lightLevel = analogRead(A0); + } + + // Task 3: Display summary + if (displayTimer.check()) { + Serial.print(F("Temp: ")); + Serial.print(data.temperature, 1); + Serial.print(F("Β°C | Humidity: ")); + Serial.print(data.humidity, 1); + Serial.println(F("%")); + } + + // Task 4: Log CSV + if (csvLogTimer.check()) { + Serial.print(millis()); + Serial.print(F(",")); + Serial.print(data.temperature, 1); + Serial.print(F(",")); + Serial.print(data.humidity, 1); + Serial.print(F(",")); + Serial.println(data.lightLevel); + } + + // LED heartbeat + digitalWrite(LED_PIN, (millis() % 2000) < 100); +} +``` + +## Verification +- Confirm tasks fire at the expected intervals without blocking. +- Leave running past 49 days in simulation to verify rollover behavior. + +## Common Pitfalls & Tips +- NEVER use delay() for timing (blocks everything). +- Use unsigned long for millis() (handles overflow correctly). +- EveryMs pattern is the simplest non-blocking timer. +- Scheduler can run multiple tasks with priorities. +- Each task tracks its own last execution time. +- Unsigned arithmetic handles millis() overflow (every 49 days). diff --git a/.agents/skills/arduino-code-generator/references/patterns-state-machine.md b/.agents/skills/arduino-code-generator/references/patterns-state-machine.md new file mode 100644 index 0000000..8caf1a5 --- /dev/null +++ b/.agents/skills/arduino-code-generator/references/patterns-state-machine.md @@ -0,0 +1,261 @@ +# State Machine Patterns + +## Purpose +- Model complex behavior with explicit, testable states. +- Avoid nested conditionals and timing bugs. + +## When to Use +- Multi-step processes (traffic lights, robots, mode controllers). +- Systems with clear operational modes and transitions. + +## Enum-Based FSM (Recommended) + +```cpp +enum TrafficLightState { + RED, + RED_YELLOW, + GREEN, + YELLOW +}; + +TrafficLightState currentState = RED; +unsigned long stateStartTime = 0; + +void setup() { + pinMode(RED_PIN, OUTPUT); + pinMode(YELLOW_PIN, OUTPUT); + pinMode(GREEN_PIN, OUTPUT); + stateStartTime = millis(); +} + +void loop() { + unsigned long elapsed = millis() - stateStartTime; + + switch (currentState) { + case RED: + digitalWrite(RED_PIN, HIGH); + digitalWrite(YELLOW_PIN, LOW); + digitalWrite(GREEN_PIN, LOW); + + if (elapsed >= 5000) { + currentState = RED_YELLOW; + stateStartTime = millis(); + } + break; + + case RED_YELLOW: + digitalWrite(RED_PIN, HIGH); + digitalWrite(YELLOW_PIN, HIGH); + digitalWrite(GREEN_PIN, LOW); + + if (elapsed >= 2000) { + currentState = GREEN; + stateStartTime = millis(); + } + break; + + case GREEN: + digitalWrite(RED_PIN, LOW); + digitalWrite(YELLOW_PIN, LOW); + digitalWrite(GREEN_PIN, HIGH); + + if (elapsed >= 5000) { + currentState = YELLOW; + stateStartTime = millis(); + } + break; + + case YELLOW: + digitalWrite(RED_PIN, LOW); + digitalWrite(YELLOW_PIN, HIGH); + digitalWrite(GREEN_PIN, LOW); + + if (elapsed >= 2000) { + currentState = RED; + stateStartTime = millis(); + } + break; + } +} +``` + +## Robot Controller FSM + +```cpp +enum RobotState { + IDLE, + MOVING_FORWARD, + TURNING_LEFT, + TURNING_RIGHT, + OBSTACLE_DETECTED, + EMERGENCY_STOP +}; + +RobotState state = IDLE; +unsigned long stateStartTime = 0; + +struct RobotContext { + int distanceSensor; + bool buttonPressed; + int batteryLevel; +} context; + +void updateState(RobotState newState) { + state = newState; + stateStartTime = millis(); + Serial.print(F("State changed to: ")); + Serial.println(newState); +} + +void loop() { + // Update sensor context + context.distanceSensor = analogRead(A0); + context.buttonPressed = digitalRead(BUTTON_PIN) == LOW; + context.batteryLevel = analogRead(BATTERY_PIN); + + unsigned long elapsed = millis() - stateStartTime; + + // Emergency stop has highest priority + if (context.batteryLevel < 500) { + if (state != EMERGENCY_STOP) { + updateState(EMERGENCY_STOP); + } + } + + switch (state) { + case IDLE: + stopMotors(); + + if (context.buttonPressed) { + updateState(MOVING_FORWARD); + } + break; + + case MOVING_FORWARD: + setMotors(255, 255); + + if (context.distanceSensor < 200) { + updateState(OBSTACLE_DETECTED); + } else if (context.buttonPressed) { + updateState(IDLE); + } + break; + + case OBSTACLE_DETECTED: + stopMotors(); + + if (elapsed >= 500) { + // Decide turn direction based on obstacle position + updateState(TURNING_RIGHT); + } + break; + + case TURNING_RIGHT: + setMotors(255, -255); + + if (elapsed >= 1000) { + updateState(MOVING_FORWARD); + } + break; + + case EMERGENCY_STOP: + stopMotors(); + digitalWrite(LED_PIN, (millis() % 500) < 250); // Blink LED + + if (context.batteryLevel > 550) { + updateState(IDLE); + } + break; + } +} + +void setMotors(int left, int right) { + // Motor driver code here +} + +void stopMotors() { + setMotors(0, 0); +} +``` + +## Button-Triggered FSM + +```cpp +enum SystemMode { + OFF, + HEATING, + COOLING, + AUTO +}; + +SystemMode mode = OFF; +DebouncedButton modeButton(2); + +void setup() { + Serial.begin(115200); + pinMode(HEATER_PIN, OUTPUT); + pinMode(COOLER_PIN, OUTPUT); +} + +void loop() { + modeButton.update(); + + // Button cycles through modes + if (modeButton.pressed()) { + switch (mode) { + case OFF: mode = HEATING; break; + case HEATING: mode = COOLING; break; + case COOLING: mode = AUTO; break; + case AUTO: mode = OFF; break; + } + Serial.print(F("Mode: ")); + Serial.println(mode); + } + + // Execute mode behavior + float temperature = readTemperature(); + + switch (mode) { + case OFF: + digitalWrite(HEATER_PIN, LOW); + digitalWrite(COOLER_PIN, LOW); + break; + + case HEATING: + digitalWrite(HEATER_PIN, HIGH); + digitalWrite(COOLER_PIN, LOW); + break; + + case COOLING: + digitalWrite(HEATER_PIN, LOW); + digitalWrite(COOLER_PIN, HIGH); + break; + + case AUTO: + if (temperature < 20.0) { + digitalWrite(HEATER_PIN, HIGH); + digitalWrite(COOLER_PIN, LOW); + } else if (temperature > 25.0) { + digitalWrite(HEATER_PIN, LOW); + digitalWrite(COOLER_PIN, HIGH); + } else { + digitalWrite(HEATER_PIN, LOW); + digitalWrite(COOLER_PIN, LOW); + } + break; + } +} +``` + +## Verification +- Print current state changes and verify transitions follow the diagram. +- Simulate inputs and confirm no invalid transitions occur. + +## Common Pitfalls & Tips +- Use enum for state definitions (readable, type-safe). +- Track stateStartTime for elapsed time checks. +- Each state handles its own transitions. +- Emergency states should be checked BEFORE normal state logic. +- Use updateState() helper to centralize state changes. +- Combine with EveryMs timers for periodic state updates. +- States should be mutually exclusive (only one active at a time). diff --git a/.agents/skills/arduino-code-generator/rules/board-optimization.md b/.agents/skills/arduino-code-generator/rules/board-optimization.md new file mode 100644 index 0000000..bd3596e --- /dev/null +++ b/.agents/skills/arduino-code-generator/rules/board-optimization.md @@ -0,0 +1,182 @@ +# Board-Specific Optimization + +Optimize Arduino code for specific board capabilities and constraints to maximize performance and reliability. + +## Arduino UNO (ATmega328P) +**Memory:** 2KB SRAM, 32KB Flash, 1KB EEPROM +**Architecture:** 8-bit AVR, 16MHz clock +**Features:** Basic I/O, limited peripherals + +### Memory Optimization +- Use `F()` macro for **all string literals** to store in flash memory +- Minimize global variables and buffers +- Prefer `char[]` arrays over `String` objects +- Use `PROGMEM` for read-only data tables + +```cpp +// UNO-optimized string handling +Serial.println(F("Initializing sensor...")); +const char message[] PROGMEM = "Error: Sensor not found"; + +// Avoid String class +char buffer[32]; +strncpy(buffer, "Sensor data", sizeof(buffer)); +``` + +### Timing Considerations +- `millis()` resolution: ~16ms (due to 16MHz/1024 prescaler) +- Avoid microsecond-precision timing +- Use `delayMicroseconds()` sparingly for short delays + +### Peripheral Limitations +- Limited interrupt pins (2 external interrupts) +- No hardware I2C/SPI buffering +- Basic ADC (10-bit, ~100ΞΌs conversion time) +- No native USB (uses serial-to-USB converter) + +## ESP32 (ESP32-WROOM-32) +**Memory:** 520KB SRAM, 4MB+ Flash, 4KB EEPROM emulation +**Architecture:** 32-bit Xtensa LX6, dual-core, 240MHz +**Features:** WiFi, Bluetooth, extensive peripherals + +### Advanced Features +- **FreeRTOS multitasking** - use tasks for concurrent operations +- **WiFi connectivity** - implement IoT patterns +- **Bluetooth/BLE** - wireless communication +- **Dual-core processing** - distribute workloads + +```cpp +// ESP32-specific patterns +#include +#include + +// Use FreeRTOS tasks +TaskHandle_t sensorTask; +void sensorTaskFunction(void *parameter) { + for (;;) { + readSensors(); + vTaskDelay(pdMS_TO_TICKS(1000)); + } +} +``` + +### Memory Management +- Larger buffers acceptable (up to KB range) +- Use PSRAM if available for large data structures +- Implement proper task stack sizing +- Monitor heap usage with `ESP.getFreeHeap()` + +### Power Management +- Deep sleep modes for battery-powered applications +- Dynamic frequency scaling +- Peripheral power gating + +## RP2040 (Raspberry Pi Pico) +**Memory:** 264KB SRAM, 2MB Flash +**Architecture:** Dual-core ARM Cortex-M0+, 133MHz +**Features:** PIO, USB host, extensive I/O + +### PIO (Programmable I/O) +- **Custom protocols** for timing-critical applications +- **Precise timing** without CPU intervention +- **Parallel interfaces** for displays and sensors + +```cpp +// RP2040 PIO example (conceptual) +#include + +// PIO for precise timing +PIO pio = pio0; +uint offset = pio_add_program(pio, &timing_program); +pio_sm_config config = timing_program_get_default_config(offset); +``` + +### Multicore Features +- **Dual-core processing** with `Core1` for dedicated tasks +- **Inter-core communication** via FIFO or shared memory +- **Load balancing** for compute-intensive operations + +```cpp +// RP2040 multicore +#include + +void core1_entry() { + while (true) { + // Dedicated processing on core 1 + processData(); + } +} + +void setup() { + multicore_launch_core1(core1_entry); +} +``` + +### USB Capabilities +- **USB host mode** for connecting peripherals +- **High-speed data transfer** +- **Device emulation** for custom interfaces + +### Performance Optimization +- **133MHz clock speed** for faster processing +- **Large SRAM** allows more complex algorithms +- **Hardware floating point** for mathematical operations + +## Cross-Board Compatibility + +### Conditional Compilation +Use preprocessor directives for board-specific code: + +```cpp +#if defined(ARDUINO_AVR_UNO) + // UNO-specific code + Serial.println(F("UNO detected")); +#elif defined(ESP32) + // ESP32-specific code + WiFi.begin(ssid, password); +#elif defined(ARDUINO_ARCH_RP2040) + // RP2040-specific code + multicore_launch_core1(taskFunction); +#endif +``` + +### Runtime Detection +Implement runtime board detection for adaptive behavior: + +```cpp +// Board detection patterns +bool isESP32() { + #ifdef ESP32 + return true; + #else + return false; + #endif +} + +void adaptiveConfiguration() { + if (isESP32()) { + // ESP32 configuration + enableWiFi(); + } else { + // UNO/RP2040 configuration + useSerialOnly(); + } +} +``` + +## Performance Benchmarks + +### Memory Usage Guidelines +- **UNO:** Keep RAM usage under 1.5KB for stability +- **ESP32:** Up to 300KB RAM acceptable for most applications +- **RP2040:** Up to 200KB RAM available for user applications + +### Timing Accuracy +- **UNO:** Β±16ms for millis() timing +- **ESP32:** Β±1ms with hardware timers +- **RP2040:** Β±1ΞΌs with PIO for precise timing + +### Power Consumption +- **UNO:** ~50mA active, ~20mA sleep +- **ESP32:** 80-240mA active, <1mA deep sleep +- **RP2040:** ~25mA active, ~1mA sleep \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/rules/common-pitfalls.md b/.agents/skills/arduino-code-generator/rules/common-pitfalls.md new file mode 100644 index 0000000..5858bd0 --- /dev/null +++ b/.agents/skills/arduino-code-generator/rules/common-pitfalls.md @@ -0,0 +1,269 @@ +# Common Pitfalls to Avoid + +Critical mistakes that can cause Arduino projects to fail, crash, or behave unexpectedly. + +## ❌ Timing Pitfalls + +### Never use `delay()` for timing +**Problem:** `delay()` blocks all execution, preventing other tasks from running. + +```cpp +// WRONG - blocks everything +void loop() { + digitalWrite(LED_PIN, HIGH); + delay(1000); // Nothing else can happen here + digitalWrite(LED_PIN, LOW); + delay(1000); +} +``` + +```cpp +// CORRECT - non-blocking timing +unsigned long lastBlink = 0; +const unsigned long BLINK_INTERVAL = 1000; + +void loop() { + unsigned long currentTime = millis(); + if (currentTime - lastBlink >= BLINK_INTERVAL) { + digitalToggle(LED_PIN); + lastBlink = currentTime; + } + // Other code can run here +} +``` + +### Never use signed types for millis() comparisons +**Problem:** `int` overflows at 32,767ms (~32 seconds), causing timing failures. + +```cpp +// WRONG - will fail after 32 seconds +int startTime = millis(); +if (millis() - startTime > 5000) { ... } + +// CORRECT - use unsigned long +unsigned long startTime = millis(); +if (millis() - startTime > 5000) { ... } +``` + +### Never ignore millis() overflow +**Problem:** `millis()` wraps to 0 every ~49 days, breaking timing calculations. + +```cpp +// WRONG - fails when millis() overflows +if (millis() > previousTime + INTERVAL) { ... } + +// CORRECT - handles overflow properly +if (millis() - previousTime >= INTERVAL) { ... } +``` + +## ❌ Hardware Initialization Pitfalls + +### Never forget to call `begin()` on peripherals +**Problem:** Sensors and communication modules won't work without initialization. + +```cpp +// WRONG - sensor won't respond +DHT dht(DHT_PIN, DHT_TYPE); +// Missing: dht.begin(); + +// CORRECT +DHT dht(DHT_PIN, DHT_TYPE); +dht.begin(); // Required initialization +``` + +### Never assume hardware is present without checking +**Problem:** Code crashes or hangs when hardware is missing. + +```cpp +// WRONG - assumes sensor is connected +float temp = dht.readTemperature(); +if (isnan(temp)) { + // Handle error +} + +// BETTER - check hardware availability +if (dht.begin()) { + float temp = dht.readTemperature(); + // Use temperature +} else { + Serial.println(F("DHT sensor not found")); +} +``` + +## ❌ Memory Management Pitfalls + +### Never use String class on memory-constrained boards +**Problem:** String concatenation causes heap fragmentation on UNO. + +```cpp +// WRONG on UNO - causes memory issues +String message = "Temperature: "; +message += String(temp); +Serial.println(message); + +// CORRECT - use char arrays +char buffer[32]; +snprintf(buffer, sizeof(buffer), "Temperature: %.1f", temp); +Serial.println(buffer); +``` + +### Never forget F() macro for strings on UNO +**Problem:** String literals consume precious RAM instead of flash. + +```cpp +// WRONG - uses 20 bytes of RAM +Serial.println("Initializing sensor..."); + +// CORRECT - stores in flash memory +Serial.println(F("Initializing sensor...")); +``` + +## ❌ Input Handling Pitfalls + +### Never mix polling and interrupt-based input +**Problem:** Inconsistent behavior and missed events. + +```cpp +// WRONG - mixing approaches +volatile bool buttonPressed = false; + +void buttonISR() { + buttonPressed = true; +} + +void loop() { + if (digitalRead(BUTTON_PIN) == LOW) { // Polling + // Handle press + } + if (buttonPressed) { // Interrupt flag + // Handle press again? + } +} + +// CORRECT - choose one approach +// Either pure polling with debouncing, or pure interrupt-driven +``` + +### Never debounce buttons incorrectly +**Problem:** False triggers from contact bounce. + +```cpp +// WRONG - no debouncing +if (digitalRead(BUTTON_PIN) == LOW) { + // Button press detected (but might be bounce) +} + +// CORRECT - software debouncing +unsigned long lastDebounceTime = 0; +const unsigned long DEBOUNCE_DELAY = 50; +bool lastButtonState = HIGH; +bool buttonState; + +void loop() { + bool reading = digitalRead(BUTTON_PIN); + if (reading != lastButtonState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) { + if (reading != buttonState) { + buttonState = reading; + if (buttonState == LOW) { + // Valid button press + } + } + } + lastButtonState = reading; +} +``` + +## ❌ Communication Pitfalls + +### Never ignore I2C/SPI initialization failures +**Problem:** Silent failures lead to debugging nightmares. + +```cpp +// WRONG - no error checking +Wire.begin(); +Wire.beginTransmission(0x68); +Wire.write(0x00); +Wire.endTransmission(); + +// CORRECT - check for errors +Wire.begin(); +Wire.beginTransmission(0x68); +byte error = Wire.endTransmission(); +if (error == 0) { + // Success - device found +} else { + Serial.print(F("I2C error: ")); + Serial.println(error); +} +``` + +### Never use blocking Serial operations in time-critical code +**Problem:** `Serial.print()` can block for milliseconds. + +```cpp +// WRONG in timing-critical sections +void loop() { + unsigned long start = micros(); + // Time-critical operation + Serial.println(micros() - start); // Blocks execution +} + +// CORRECT - buffer or use non-blocking approaches +char buffer[32]; +snprintf(buffer, sizeof(buffer), "Time: %lu", micros() - start); +Serial.println(buffer); +``` + +## ❌ Data Handling Pitfalls + +### Never access arrays without bounds checking +**Problem:** Buffer overflows corrupt memory. + +```cpp +// WRONG - potential buffer overflow +char buffer[10]; +for (int i = 0; i < 20; i++) { + buffer[i] = data[i]; // Overflows buffer +} + +// CORRECT - bounds checking +char buffer[10]; +int copyLength = min(sizeof(buffer) - 1, dataLength); +memcpy(buffer, data, copyLength); +buffer[copyLength] = '\0'; +``` + +### Never use floating point in interrupt service routines +**Problem:** Floating point operations are not reentrant and slow. + +```cpp +// WRONG in ISR +volatile float temperature; +void sensorISR() { + temperature = readSensor(); // Floating point in ISR +} + +// CORRECT - use integer math in ISRs +volatile int32_t temperatureRaw; +void sensorISR() { + temperatureRaw = analogRead(SENSOR_PIN); +} +// Convert to float in main code +``` + +## βœ… Best Practices Summary + +- **Always use millis()** for non-blocking timing +- **Always check hardware initialization** return values +- **Always use F() macro** for strings on UNO +- **Always debounce buttons** properly +- **Always check array bounds** before access +- **Always handle millis() overflow** in timing calculations +- **Never use delay()** in production code +- **Never mix input handling strategies** +- **Never ignore error conditions** +- **Never use String class** on memory-constrained boards \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/rules/quality-standards.md b/.agents/skills/arduino-code-generator/rules/quality-standards.md new file mode 100644 index 0000000..6e0b277 --- /dev/null +++ b/.agents/skills/arduino-code-generator/rules/quality-standards.md @@ -0,0 +1,96 @@ +# Quality Standards + +All generated Arduino code must adhere to these quality standards for production readiness. + +## Compilation Requirements +- βœ… **Compile without warnings** on Arduino IDE 1.8.x and 2.x +- βœ… **No deprecated function usage** (avoid old Arduino APIs) +- βœ… **Proper include guards** for custom headers +- βœ… **Correct preprocessor directives** (#ifdef, #define, etc.) + +## Timing and Concurrency +- βœ… **Use unsigned long for millis() timing** - never signed types +- βœ… **Handle overflow conditions** - millis() wraps every ~49 days +- βœ… **Never use delay()** for timing-critical operations +- βœ… **Implement proper timing comparisons** with overflow protection + +```cpp +// Correct timing implementation +unsigned long previousTime = 0; +const unsigned long INTERVAL = 1000; + +void loop() { + unsigned long currentTime = millis(); + if (currentTime - previousTime >= INTERVAL) { + // Execute timed action + previousTime = currentTime; + } +} +``` + +## Memory Safety +- βœ… **Include bounds checking** for all array operations +- βœ… **Use const for read-only data** to prevent accidental modification +- βœ… **Define magic numbers as named constants** +- βœ… **Avoid buffer overflows** through proper sizing +- βœ… **Use F() macro** for strings on memory-constrained boards + +```cpp +// Memory-safe implementations +#define BUFFER_SIZE 64 +char buffer[BUFFER_SIZE]; + +const char* SENSOR_NAME = "DHT22"; +const uint8_t PIN_LED = 13; + +// Use F() macro on UNO +Serial.println(F("Sensor initialized")); +``` + +## Error Handling +- βœ… **Check return values** from peripheral initialization functions +- βœ… **Provide meaningful error messages** via Serial output +- βœ… **Implement fallback strategies** for missing hardware +- βœ… **Handle edge cases** gracefully (null pointers, invalid data) + +```cpp +// Proper error handling +bool initializeSensor() { + if (!sensor.begin()) { + Serial.println(F("ERROR: Sensor initialization failed")); + return false; + } + Serial.println(F("Sensor initialized successfully")); + return true; +} +``` + +## Code Structure +- βœ… **Clear, descriptive variable names** following Arduino conventions +- βœ… **Consistent indentation** (2 or 4 spaces, or Arduino IDE default) +- βœ… **Logical function organization** with single responsibilities +- βœ… **Proper comment placement** explaining "why" not "what" + +## Board Compatibility +- βœ… **Test compilation** on target boards (UNO, ESP32, RP2040) +- βœ… **Account for board-specific limitations** (memory, pins, features) +- βœ… **Use conditional compilation** for board-specific code +- βœ… **Document board requirements** and compatibility + +## Performance Standards +- βœ… **Minimize blocking operations** in loop() +- βœ… **Optimize for power consumption** when applicable +- βœ… **Use appropriate data types** for the task +- βœ… **Avoid unnecessary computations** in tight loops + +## Testing Requirements +- βœ… **Provide expected output** for verification +- βœ… **Include debugging Serial output** for troubleshooting +- βœ… **Document test procedures** and expected behavior +- βœ… **Handle common failure modes** gracefully + +## Documentation Standards +- βœ… **Include setup instructions** with wiring diagrams +- βœ… **Document configuration options** and parameters +- βœ… **Explain integration points** with other patterns +- βœ… **Provide troubleshooting guidance** for common issues \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/scripts/generate_snippet.py b/.agents/skills/arduino-code-generator/scripts/generate_snippet.py new file mode 100644 index 0000000..b21c616 --- /dev/null +++ b/.agents/skills/arduino-code-generator/scripts/generate_snippet.py @@ -0,0 +1,1153 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.10" +# dependencies = [] +# /// +""" +Arduino Code Snippet Generator + +Generate production-ready Arduino code snippets from pattern templates. +Supports UNO, ESP32, and RP2040 boards with proper pin configurations. + +Usage: + uv run --no-project scripts/generate_snippet.py --pattern config --board esp32 + uv run --no-project scripts/generate_snippet.py --pattern buttons --board uno --pin 3 + uv run --no-project scripts/generate_snippet.py --pattern i2c --board rp2040 --output scanner.ino + uv run --no-project scripts/generate_snippet.py --interactive + uv run --no-project scripts/generate_snippet.py --list +""" + +import argparse +import sys +from dataclasses import dataclass +from typing import Optional + +# ============================================================================= +# Board Configurations +# ============================================================================= + +BOARD_CONFIGS = { + "uno": { + "name": "Arduino UNO", + "define": "ARDUINO_AVR_UNO", + "led": 13, + "button": 2, + "sda": "A4", + "scl": "A5", + "sram": 2048, + "baud": 9600, + "adc_bits": 10, + "has_wifi": False, + }, + "esp32": { + "name": "ESP32", + "define": "ESP32", + "led": 2, + "button": 4, + "sda": 21, + "scl": 22, + "sram": 520000, + "baud": 115200, + "adc_bits": 12, + "has_wifi": True, + }, + "rp2040": { + "name": "RP2040", + "define": "ARDUINO_ARCH_RP2040", + "led": 25, + "button": 14, + "sda": 4, + "scl": 5, + "sram": 264000, + "baud": 115200, + "adc_bits": 12, + "has_wifi": False, + }, +} + +# ============================================================================= +# Pattern Templates +# ============================================================================= + +PATTERNS = { + "config": { + "name": "Config.h Hardware Abstraction", + "description": "Multi-board configuration with conditional compilation", + "template": '''// config.h - Hardware abstraction layer +#ifndef CONFIG_H +#define CONFIG_H + +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO) + #define BOARD_NAME "Arduino UNO" + #define LED_PIN 13 + #define BUTTON_PIN 2 + #define I2C_SDA A4 + #define I2C_SCL A5 + #define SRAM_SIZE 2048 + #define SERIAL_BAUD 9600 + +#elif defined(ESP32) + #define BOARD_NAME "ESP32" + #define LED_PIN 2 + #define BUTTON_PIN 4 + #define I2C_SDA 21 + #define I2C_SCL 22 + #define SRAM_SIZE 520000 + #define SERIAL_BAUD 115200 + #define HAS_WIFI 1 + #define HAS_BLE 1 + +#elif defined(ARDUINO_ARCH_RP2040) + #define BOARD_NAME "RP2040" + #define LED_PIN 25 + #define BUTTON_PIN 14 + #define I2C_SDA 4 + #define I2C_SCL 5 + #define SRAM_SIZE 264000 + #define SERIAL_BAUD 115200 + +#else + #error "Unsupported board! Add configuration for your board." +#endif + +// === Common Settings === +#define DEBOUNCE_MS 50 +#define SENSOR_INTERVAL_MS 1000 +#define LOG_INTERVAL_MS 60000 + +#endif // CONFIG_H +''', + }, + "buttons": { + "name": "Button Debouncing", + "description": "Debounced button with press/release/long-press detection", + "template": '''// DebouncedButton - Software debouncing with event detection +// Pin: {button_pin} (configured for INPUT_PULLUP) + +class DebouncedButton {{ +public: + enum Event {{ NONE, PRESSED, RELEASED, LONG_PRESS }}; + +private: + uint8_t pin; + bool lastStableState; + bool lastReading; + unsigned long lastDebounceTime; + unsigned long pressStartTime; + bool longPressTriggered; + + static const unsigned long DEBOUNCE_MS = 50; + static const unsigned long LONG_PRESS_MS = 1000; + +public: + DebouncedButton(uint8_t p) : pin(p), lastStableState(HIGH), lastReading(HIGH), + lastDebounceTime(0), pressStartTime(0), longPressTriggered(false) {{}} + + void begin() {{ + pinMode(pin, INPUT_PULLUP); + }} + + Event update() {{ + bool reading = digitalRead(pin); + unsigned long now = millis(); + + // Reset debounce timer on state change + if (reading != lastReading) {{ + lastDebounceTime = now; + }} + lastReading = reading; + + // Wait for stable state + if ((now - lastDebounceTime) < DEBOUNCE_MS) {{ + return NONE; + }} + + // State changed + if (reading != lastStableState) {{ + lastStableState = reading; + + if (reading == LOW) {{ + // Button pressed + pressStartTime = now; + longPressTriggered = false; + return PRESSED; + }} else {{ + // Button released + return longPressTriggered ? NONE : RELEASED; + }} + }} + + // Check for long press (while held) + if (lastStableState == LOW && !longPressTriggered) {{ + if ((now - pressStartTime) >= LONG_PRESS_MS) {{ + longPressTriggered = true; + return LONG_PRESS; + }} + }} + + return NONE; + }} + + bool isPressed() const {{ return lastStableState == LOW; }} +}}; + +// === Usage Example === +DebouncedButton button({button_pin}); + +void setup() {{ + Serial.begin({baud}); + button.begin(); + Serial.println(F("Button ready - press, release, or hold for 1s")); +}} + +void loop() {{ + DebouncedButton::Event event = button.update(); + + switch (event) {{ + case DebouncedButton::PRESSED: + Serial.println(F("PRESSED")); + break; + case DebouncedButton::RELEASED: + Serial.println(F("RELEASED (short press)")); + break; + case DebouncedButton::LONG_PRESS: + Serial.println(F("LONG PRESS detected!")); + break; + default: + break; + }} +}} +''', + }, + "i2c": { + "name": "I2C Scanner & Diagnostics", + "description": "Scan I2C bus and identify connected devices", + "template": '''// I2C Scanner - Detect devices on the I2C bus +// SDA: Pin {sda}, SCL: Pin {scl} + +#include + +// Known I2C device addresses +struct I2CDevice {{ + uint8_t address; + const char* name; +}}; + +const I2CDevice KNOWN_DEVICES[] = {{ + {{0x3C, "SSD1306 OLED"}}, + {{0x3D, "SSD1306 OLED (alt)"}}, + {{0x27, "LCD I2C (PCF8574)"}}, + {{0x3F, "LCD I2C (PCF8574A)"}}, + {{0x76, "BME280/BMP280"}}, + {{0x77, "BME280/BMP280 (alt)"}}, + {{0x68, "MPU6050/DS3231 RTC"}}, + {{0x57, "AT24C32 EEPROM"}}, + {{0x50, "AT24C256 EEPROM"}}, + {{0x48, "ADS1115 ADC"}}, + {{0x40, "INA219 Current Sensor"}}, + {{0x29, "VL53L0X ToF Sensor"}}, + {{0x39, "APDS9960 Gesture"}}, + {{0x5A, "MLX90614 IR Temp"}}, +}}; +const uint8_t KNOWN_COUNT = sizeof(KNOWN_DEVICES) / sizeof(KNOWN_DEVICES[0]); + +const char* identifyDevice(uint8_t address) {{ + for (uint8_t i = 0; i < KNOWN_COUNT; i++) {{ + if (KNOWN_DEVICES[i].address == address) {{ + return KNOWN_DEVICES[i].name; + }} + }} + return "Unknown"; +}} + +void scanI2C() {{ + Serial.println(F("\\n=== I2C Scanner ===")); + Serial.println(F("Scanning...\\n")); + + uint8_t found = 0; + + for (uint8_t addr = 1; addr < 127; addr++) {{ + Wire.beginTransmission(addr); + uint8_t error = Wire.endTransmission(); + + if (error == 0) {{ + found++; + Serial.print(F("Found: 0x")); + if (addr < 16) Serial.print('0'); + Serial.print(addr, HEX); + Serial.print(F(" - ")); + Serial.println(identifyDevice(addr)); + }} else if (error == 4) {{ + Serial.print(F("Error at 0x")); + if (addr < 16) Serial.print('0'); + Serial.println(addr, HEX); + }} + }} + + Serial.print(F("\\nDevices found: ")); + Serial.println(found); + + if (found == 0) {{ + Serial.println(F("\\nTroubleshooting:")); + Serial.println(F("- Check wiring (SDA/SCL swapped?)")); + Serial.println(F("- Verify power to I2C devices")); + Serial.println(F("- Add 4.7kΞ© pull-up resistors")); + Serial.println(F("- Check I2C address on device")); + }} +}} + +void setup() {{ + Serial.begin({baud}); + while (!Serial) {{ ; }} // Wait for Serial (Leonardo/Pro Micro) + + Wire.begin({sda}, {scl}); + Serial.println(F("I2C Scanner Ready")); + Serial.print(F("SDA: Pin {sda}, SCL: Pin {scl}")); + + scanI2C(); +}} + +void loop() {{ + // Rescan every 5 seconds + delay(5000); + scanI2C(); +}} +''', + }, + "scheduler": { + "name": "Non-blocking Scheduler", + "description": "millis()-based timer for multi-tasking without delay()", + "template": '''// EveryMs - Non-blocking timer class +// Replaces delay() with proper millis() timing + +class EveryMs {{ +private: + unsigned long interval; + unsigned long lastTrigger; + +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {{}} + + bool check() {{ + unsigned long now = millis(); + // Handles millis() overflow correctly (unsigned subtraction) + if (now - lastTrigger >= interval) {{ + lastTrigger = now; + return true; + }} + return false; + }} + + void reset() {{ + lastTrigger = millis(); + }} + + void setInterval(unsigned long ms) {{ + interval = ms; + }} + + unsigned long elapsed() const {{ + return millis() - lastTrigger; + }} +}}; + +// === Multi-Task Example === +EveryMs ledTimer(500); // Blink LED every 500ms +EveryMs sensorTimer(2000); // Read sensor every 2s +EveryMs logTimer(10000); // Log data every 10s + +bool ledState = false; + +void setup() {{ + Serial.begin({baud}); + pinMode({led_pin}, OUTPUT); + Serial.println(F("Non-blocking scheduler ready")); +}} + +void loop() {{ + // Task 1: Blink LED (500ms) + if (ledTimer.check()) {{ + ledState = !ledState; + digitalWrite({led_pin}, ledState); + }} + + // Task 2: Read sensor (2s) + if (sensorTimer.check()) {{ + int value = analogRead(A0); + Serial.print(F("Sensor: ")); + Serial.println(value); + }} + + // Task 3: Log data (10s) + if (logTimer.check()) {{ + Serial.print(F("Uptime: ")); + Serial.print(millis() / 1000); + Serial.println(F(" seconds")); + }} + + // Other non-blocking code can run here +}} +''', + }, + "csv": { + "name": "CSV Data Logger", + "description": "Structured CSV output for data logging and analysis", + "template": '''// CSV Logger - Structured data output for Serial/SD +// Format: timestamp,sensor1,sensor2,... + +class CSVLogger {{ +private: + bool headerPrinted; + unsigned long startTime; + +public: + CSVLogger() : headerPrinted(false), startTime(0) {{}} + + void begin() {{ + startTime = millis(); + }} + + void printHeader(const char* fields[], uint8_t count) {{ + if (!headerPrinted) {{ + Serial.print(F("time_ms")); + for (uint8_t i = 0; i < count; i++) {{ + Serial.print(','); + Serial.print(fields[i]); + }} + Serial.println(); + headerPrinted = true; + }} + }} + + void startRow() {{ + Serial.print(millis() - startTime); + }} + + void addInt(int value) {{ + Serial.print(','); + Serial.print(value); + }} + + void addFloat(float value, uint8_t decimals = 2) {{ + Serial.print(','); + Serial.print(value, decimals); + }} + + void addString(const char* value) {{ + Serial.print(','); + Serial.print(value); + }} + + void endRow() {{ + Serial.println(); + }} +}}; + +// === Usage Example === +CSVLogger logger; +const char* FIELDS[] = {{"temp_c", "humidity", "light"}}; +const uint8_t FIELD_COUNT = 3; + +EveryMs logTimer(5000); // Log every 5 seconds + +void setup() {{ + Serial.begin({baud}); + logger.begin(); + logger.printHeader(FIELDS, FIELD_COUNT); +}} + +void loop() {{ + if (logTimer.check()) {{ + // Simulate sensor readings + float temp = 22.5 + (random(-20, 20) / 10.0); + float humidity = 45.0 + (random(-50, 50) / 10.0); + int light = analogRead(A0); + + logger.startRow(); + logger.addFloat(temp); + logger.addFloat(humidity); + logger.addInt(light); + logger.endRow(); + }} +}} + +// Output format (copy to Excel/Python): +// time_ms,temp_c,humidity,light +// 5000,22.7,44.2,512 +// 10000,22.3,45.8,498 +// 15000,23.1,44.9,521 +''', + }, + "filtering": { + "name": "ADC Filtering", + "description": "Moving average and median filters for noisy sensors", + "template": '''// Sensor Filters - Reduce noise from analog readings + +// === Moving Average Filter === +template +class MovingAverageFilter {{ +private: + int values[SIZE]; + uint8_t index; + uint8_t count; + long sum; + +public: + MovingAverageFilter() : index(0), count(0), sum(0) {{ + for (uint8_t i = 0; i < SIZE; i++) values[i] = 0; + }} + + int filter(int newValue) {{ + sum -= values[index]; + values[index] = newValue; + sum += newValue; + index = (index + 1) % SIZE; + if (count < SIZE) count++; + return sum / count; + }} + + void reset() {{ + index = 0; + count = 0; + sum = 0; + for (uint8_t i = 0; i < SIZE; i++) values[i] = 0; + }} +}}; + +// === Median Filter (3 samples) === +class MedianFilter3 {{ +private: + int v0, v1, v2; + +public: + MedianFilter3() : v0(0), v1(0), v2(0) {{}} + + int filter(int newValue) {{ + v0 = v1; + v1 = v2; + v2 = newValue; + + // Sort and return middle value + if (v0 <= v1) {{ + if (v1 <= v2) return v1; // v0 <= v1 <= v2 + if (v0 <= v2) return v2; // v0 <= v2 < v1 + return v0; // v2 < v0 <= v1 + }} else {{ + if (v0 <= v2) return v0; // v1 < v0 <= v2 + if (v1 <= v2) return v2; // v1 <= v2 < v0 + return v1; // v2 < v1 < v0 + }} + }} +}}; + +// === Usage Example === +MovingAverageFilter<10> avgFilter; // 10-sample average +MedianFilter3 medFilter; // 3-sample median + +EveryMs readTimer(100); // Read every 100ms +EveryMs printTimer(1000); // Print every 1s + +int rawValue, avgValue, medValue; + +void setup() {{ + Serial.begin({baud}); + Serial.println(F("Raw,Average,Median")); +}} + +void loop() {{ + if (readTimer.check()) {{ + rawValue = analogRead(A0); + avgValue = avgFilter.filter(rawValue); + medValue = medFilter.filter(rawValue); + }} + + if (printTimer.check()) {{ + Serial.print(rawValue); + Serial.print(','); + Serial.print(avgValue); + Serial.print(','); + Serial.println(medValue); + }} +}} +''', + }, + "state-machine": { + "name": "State Machine", + "description": "Enum-based FSM for complex behavior control", + "template": '''// State Machine - Enum-based finite state machine +// Example: Traffic light controller + +enum class State {{ + RED, + RED_YELLOW, + GREEN, + YELLOW +}}; + +const char* stateNames[] = {{"RED", "RED_YELLOW", "GREEN", "YELLOW"}}; + +class TrafficLight {{ +private: + State currentState; + unsigned long stateStartTime; + + // State durations in milliseconds + static const unsigned long RED_DURATION = 5000; + static const unsigned long RED_YELLOW_DURATION = 1000; + static const unsigned long GREEN_DURATION = 4000; + static const unsigned long YELLOW_DURATION = 2000; + +public: + TrafficLight() : currentState(State::RED), stateStartTime(0) {{}} + + void begin() {{ + stateStartTime = millis(); + updateOutputs(); + printState(); + }} + + void update() {{ + unsigned long elapsed = millis() - stateStartTime; + State nextState = currentState; + + switch (currentState) {{ + case State::RED: + if (elapsed >= RED_DURATION) nextState = State::RED_YELLOW; + break; + case State::RED_YELLOW: + if (elapsed >= RED_YELLOW_DURATION) nextState = State::GREEN; + break; + case State::GREEN: + if (elapsed >= GREEN_DURATION) nextState = State::YELLOW; + break; + case State::YELLOW: + if (elapsed >= YELLOW_DURATION) nextState = State::RED; + break; + }} + + if (nextState != currentState) {{ + transition(nextState); + }} + }} + +private: + void transition(State newState) {{ + currentState = newState; + stateStartTime = millis(); + updateOutputs(); + printState(); + }} + + void updateOutputs() {{ + // Set LED outputs based on state + bool red = false, yellow = false, green = false; + + switch (currentState) {{ + case State::RED: red = true; break; + case State::RED_YELLOW: red = true; yellow = true; break; + case State::GREEN: green = true; break; + case State::YELLOW: yellow = true; break; + }} + + // digitalWrite(RED_LED, red); + // digitalWrite(YELLOW_LED, yellow); + // digitalWrite(GREEN_LED, green); + }} + + void printState() {{ + Serial.print(F("State: ")); + Serial.println(stateNames[static_cast(currentState)]); + }} +}}; + +// === Usage === +TrafficLight traffic; + +void setup() {{ + Serial.begin({baud}); + traffic.begin(); +}} + +void loop() {{ + traffic.update(); +}} +''', + }, + "hardware-detection": { + "name": "Hardware Detection", + "description": "Auto-detect board type and connected sensors", + "template": '''// Hardware Detection - Identify board and peripherals at runtime + +#include + +struct BoardInfo {{ + const char* name; + uint32_t sramSize; + uint32_t flashSize; + bool hasWifi; + bool hasBle; +}}; + +BoardInfo detectBoard() {{ + BoardInfo info; + + #if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO) + info.name = "Arduino UNO/Nano"; + info.sramSize = 2048; + info.flashSize = 32768; + info.hasWifi = false; + info.hasBle = false; + #elif defined(ESP32) + info.name = "ESP32"; + info.sramSize = 520000; + info.flashSize = 4194304; + info.hasWifi = true; + info.hasBle = true; + #elif defined(ARDUINO_ARCH_RP2040) + info.name = "RP2040"; + info.sramSize = 264000; + info.flashSize = 2097152; + info.hasWifi = false; // Unless Pico W + info.hasBle = false; + #else + info.name = "Unknown"; + info.sramSize = 0; + info.flashSize = 0; + info.hasWifi = false; + info.hasBle = false; + #endif + + return info; +}} + +uint32_t getFreeSram() {{ + #if defined(ESP32) + return ESP.getFreeHeap(); + #elif defined(ARDUINO_ARCH_RP2040) + return rp2040.getFreeHeap(); + #else + // AVR: estimate free RAM + extern int __heap_start, *__brkval; + int v; + return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval); + #endif +}} + +bool scanI2CDevice(uint8_t address) {{ + Wire.beginTransmission(address); + return Wire.endTransmission() == 0; +}} + +void detectSensors() {{ + Serial.println(F("\\n=== Sensor Detection ===")); + + // BME280/BMP280 + if (scanI2CDevice(0x76) || scanI2CDevice(0x77)) {{ + Serial.println(F("βœ“ BME280/BMP280 found")); + }} + + // SSD1306 OLED + if (scanI2CDevice(0x3C) || scanI2CDevice(0x3D)) {{ + Serial.println(F("βœ“ SSD1306 OLED found")); + }} + + // MPU6050 + if (scanI2CDevice(0x68)) {{ + Serial.println(F("βœ“ MPU6050 IMU found (or DS3231 RTC)")); + }} + + // LCD I2C + if (scanI2CDevice(0x27) || scanI2CDevice(0x3F)) {{ + Serial.println(F("βœ“ LCD I2C found")); + }} +}} + +void setup() {{ + Serial.begin({baud}); + while (!Serial) {{ ; }} + + BoardInfo board = detectBoard(); + + Serial.println(F("\\n=== Board Information ===")); + Serial.print(F("Board: ")); + Serial.println(board.name); + Serial.print(F("SRAM: ")); + Serial.print(board.sramSize); + Serial.println(F(" bytes")); + Serial.print(F("Flash: ")); + Serial.print(board.flashSize); + Serial.println(F(" bytes")); + Serial.print(F("WiFi: ")); + Serial.println(board.hasWifi ? "Yes" : "No"); + Serial.print(F("BLE: ")); + Serial.println(board.hasBle ? "Yes" : "No"); + + Serial.print(F("\\nFree SRAM: ")); + Serial.print(getFreeSram()); + Serial.println(F(" bytes")); + + Wire.begin(); + detectSensors(); +}} + +void loop() {{ + // Periodically check memory + static EveryMs memCheck(5000); + if (memCheck.check()) {{ + Serial.print(F("Free SRAM: ")); + Serial.println(getFreeSram()); + }} +}} +''', + }, + "data-logging": { + "name": "Data Logging (EEPROM/SD)", + "description": "Persistent storage with EEPROM and SD card", + "template": '''// Data Logging - EEPROM settings + SD card logging + +#include +#if defined(ESP32) + #include +#else + #include +#endif + +// === CRC8 for data validation === +uint8_t crc8(const uint8_t* data, size_t len) {{ + uint8_t crc = 0xFF; + while (len--) {{ + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) {{ + crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : crc << 1; + }} + }} + return crc; +}} + +// === EEPROM Settings Manager === +struct Settings {{ + uint8_t version; + uint16_t logInterval; + uint8_t sensorCount; + char deviceName[16]; + uint8_t crc; +}}; + +class SettingsManager {{ +private: + static const uint8_t CURRENT_VERSION = 1; + static const uint16_t EEPROM_ADDR = 0; + +public: + Settings settings; + + void loadDefaults() {{ + settings.version = CURRENT_VERSION; + settings.logInterval = 5000; + settings.sensorCount = 3; + strncpy(settings.deviceName, "DataLogger", 15); + settings.deviceName[15] = '\\0'; + }} + + bool load() {{ + #if defined(ESP32) + EEPROM.begin(sizeof(Settings)); + #endif + + EEPROM.get(EEPROM_ADDR, settings); + + uint8_t stored_crc = settings.crc; + settings.crc = 0; + uint8_t calc_crc = crc8((uint8_t*)&settings, sizeof(Settings) - 1); + + if (stored_crc != calc_crc || settings.version != CURRENT_VERSION) {{ + Serial.println(F("EEPROM: Invalid or old data, loading defaults")); + loadDefaults(); + return false; + }} + + settings.crc = stored_crc; + Serial.println(F("EEPROM: Settings loaded")); + return true; + }} + + void save() {{ + settings.crc = 0; + settings.crc = crc8((uint8_t*)&settings, sizeof(Settings) - 1); + + EEPROM.put(EEPROM_ADDR, settings); + + #if defined(ESP32) + EEPROM.commit(); + #endif + + Serial.println(F("EEPROM: Settings saved")); + }} + + void print() {{ + Serial.println(F("=== Current Settings ===")); + Serial.print(F("Device: ")); + Serial.println(settings.deviceName); + Serial.print(F("Log Interval: ")); + Serial.print(settings.logInterval); + Serial.println(F(" ms")); + Serial.print(F("Sensor Count: ")); + Serial.println(settings.sensorCount); + }} +}}; + +// === SD Card Logger === +class SDLogger {{ +private: + const char* filename; + bool sdReady; + +public: + SDLogger(const char* fname) : filename(fname), sdReady(false) {{}} + + bool begin(uint8_t csPin = 10) {{ + if (SD.begin(csPin)) {{ + sdReady = true; + Serial.println(F("SD card ready")); + + // Create header if new file + if (!SD.exists(filename)) {{ + File f = SD.open(filename, FILE_WRITE); + if (f) {{ + f.println(F("timestamp_ms,temp_c,humidity,light")); + f.close(); + }} + }} + return true; + }} + Serial.println(F("SD card failed")); + return false; + }} + + bool log(unsigned long timestamp, float temp, float humidity, int light) {{ + if (!sdReady) return false; + + File f = SD.open(filename, FILE_WRITE); + if (f) {{ + f.print(timestamp); + f.print(','); + f.print(temp, 2); + f.print(','); + f.print(humidity, 2); + f.print(','); + f.println(light); + f.close(); + return true; + }} + return false; + }} +}}; + +// === Usage Example === +SettingsManager settings; +SDLogger sdLog("datalog.csv"); + +void setup() {{ + Serial.begin({baud}); + + settings.load(); + settings.print(); + + sdLog.begin(); +}} + +void loop() {{ + // Log data every interval + static EveryMs logTimer(settings.settings.logInterval); + + if (logTimer.check()) {{ + // Simulate sensor data + float temp = 22.5; + float humidity = 45.0; + int light = analogRead(A0); + + sdLog.log(millis(), temp, humidity, light); + Serial.println(F("Data logged")); + }} + + // Check for settings update command + if (Serial.available()) {{ + char cmd = Serial.read(); + if (cmd == 's') {{ + settings.save(); + }} else if (cmd == 'p') {{ + settings.print(); + }} + }} +}} +''', + }, +} + +# ============================================================================= +# Code Generation Functions +# ============================================================================= + + +def generate_snippet(pattern: str, board: str, **kwargs) -> str: + """Generate code snippet for specified pattern and board.""" + if pattern not in PATTERNS: + return f"Error: Unknown pattern '{pattern}'. Use --list to see available patterns." + + if board not in BOARD_CONFIGS: + return f"Error: Unknown board '{board}'. Supported: uno, esp32, rp2040" + + template = PATTERNS[pattern]["template"] + config = BOARD_CONFIGS[board] + + # Merge board config with user overrides + params = { + "baud": config["baud"], + "led_pin": kwargs.get("led_pin", config["led"]), + "button_pin": kwargs.get("button_pin", config["button"]), + "sda": kwargs.get("sda", config["sda"]), + "scl": kwargs.get("scl", config["scl"]), + "board_name": config["name"], + } + + try: + return template.format(**params) + except KeyError as e: + return f"Error: Missing parameter {e}" + + +def list_patterns(): + """List all available patterns.""" + print("\n=== Available Patterns ===\n") + for key, info in PATTERNS.items(): + print(f" {key:20} - {info['description']}") + print("\n=== Supported Boards ===\n") + for key, info in BOARD_CONFIGS.items(): + wifi = "WiFi" if info["has_wifi"] else "" + print(f" {key:10} - {info['name']:15} ({info['sram']} bytes SRAM) {wifi}") + print() + + +def interactive_mode(): + """Run interactive wizard for code generation.""" + print("\nπŸ”§ Arduino Code Snippet Generator - Interactive Mode\n") + + # Select pattern + print("Available patterns:") + patterns_list = list(PATTERNS.keys()) + for i, p in enumerate(patterns_list, 1): + print(f" {i}. {p} - {PATTERNS[p]['description']}") + + while True: + try: + choice = input("\nSelect pattern (1-9): ").strip() + idx = int(choice) - 1 + if 0 <= idx < len(patterns_list): + pattern = patterns_list[idx] + break + except ValueError: + pass + print("Invalid choice, try again.") + + # Select board + print("\nAvailable boards:") + boards_list = list(BOARD_CONFIGS.keys()) + for i, b in enumerate(boards_list, 1): + wifi = " (WiFi)" if BOARD_CONFIGS[b]["has_wifi"] else "" + print(f" {i}. {b} - {BOARD_CONFIGS[b]['name']}{wifi}") + + while True: + try: + choice = input("\nSelect board (1-3): ").strip() + idx = int(choice) - 1 + if 0 <= idx < len(boards_list): + board = boards_list[idx] + break + except ValueError: + pass + print("Invalid choice, try again.") + + # Generate + print(f"\n{'='*60}") + print(f"Generating: {PATTERNS[pattern]['name']} for {BOARD_CONFIGS[board]['name']}") + print(f"{'='*60}\n") + + code = generate_snippet(pattern, board) + print(code) + + # Save option + save = input("\nSave to file? (y/n): ").strip().lower() + if save == "y": + filename = input("Filename (e.g., sketch.ino): ").strip() + if filename: + with open(filename, "w") as f: + f.write(code) + print(f"βœ“ Saved to {filename}") + + +# ============================================================================= +# Main +# ============================================================================= + + +def main(): + parser = argparse.ArgumentParser( + description="Generate Arduino code snippets from pattern templates", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + uv run --no-project scripts/generate_snippet.py --list + uv run --no-project scripts/generate_snippet.py --pattern config --board esp32 + uv run --no-project scripts/generate_snippet.py --pattern buttons --board uno --output button.ino + uv run --no-project scripts/generate_snippet.py --interactive + """, + ) + + parser.add_argument("--pattern", "-p", help="Pattern to generate (see --list)") + parser.add_argument( + "--board", "-b", default="uno", help="Target board: uno, esp32, rp2040" + ) + parser.add_argument("--output", "-o", help="Output file (default: stdout)") + parser.add_argument( + "--list", "-l", action="store_true", help="List available patterns" + ) + parser.add_argument( + "--interactive", "-i", action="store_true", help="Interactive wizard mode" + ) + parser.add_argument("--button-pin", type=int, help="Override button pin") + parser.add_argument("--led-pin", type=int, help="Override LED pin") + + args = parser.parse_args() + + if args.list: + list_patterns() + return + + if args.interactive: + interactive_mode() + return + + if not args.pattern: + parser.print_help() + print("\nError: --pattern is required (or use --list / --interactive)") + sys.exit(1) + + # Generate code + kwargs = {} + if args.button_pin: + kwargs["button_pin"] = args.button_pin + if args.led_pin: + kwargs["led_pin"] = args.led_pin + + code = generate_snippet(args.pattern, args.board, **kwargs) + + if args.output: + with open(args.output, "w") as f: + f.write(code) + print(f"βœ“ Generated {args.output}") + else: + print(code) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/arduino-code-generator/scripts/verify_patterns.ps1 b/.agents/skills/arduino-code-generator/scripts/verify_patterns.ps1 new file mode 100644 index 0000000..4f3af5a --- /dev/null +++ b/.agents/skills/arduino-code-generator/scripts/verify_patterns.ps1 @@ -0,0 +1,52 @@ +param( + [string[]]$FqbnList = @( + "arduino:avr:uno", + "esp32:esp32:esp32", + "rp2040:rp2040:rpipico" + ) +) + +$cli = Get-Command arduino-cli -ErrorAction SilentlyContinue +if (-not $cli) { + Write-Error "arduino-cli not found in PATH. Install it before running this script." + exit 1 +} + +$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$examplesDir = Resolve-Path (Join-Path $scriptRoot "..\examples") + +$examples = @( + "config-example.ino", + "filtering-example.ino", + "buttons-example.ino", + "i2c-example.ino", + "csv-example.ino", + "scheduler-example.ino", + "state-machine-example.ino", + "hardware-detection-example.ino", + "data-logging-example.ino" +) + +$failed = @() + +foreach ($fqbn in $FqbnList) { + Write-Host "\n=== Compiling examples for $fqbn ===" -ForegroundColor Cyan + + foreach ($example in $examples) { + $examplePath = Join-Path $examplesDir $example + Write-Host "Compiling $example" -ForegroundColor Gray + + & arduino-cli compile --fqbn $fqbn $examplePath + if ($LASTEXITCODE -ne 0) { + $failed += "$fqbn :: $example" + } + } +} + +if ($failed.Count -gt 0) { + Write-Host "\nCompilation failures:" -ForegroundColor Red + $failed | ForEach-Object { Write-Host " - $_" } + exit 1 +} + +Write-Host "\nAll examples compiled successfully." -ForegroundColor Green diff --git a/.agents/skills/arduino-code-generator/scripts/verify_patterns.sh b/.agents/skills/arduino-code-generator/scripts/verify_patterns.sh new file mode 100644 index 0000000..af24f5e --- /dev/null +++ b/.agents/skills/arduino-code-generator/scripts/verify_patterns.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +FQBNS=( + "arduino:avr:uno" + "esp32:esp32:esp32" + "rp2040:rp2040:rpipico" +) + +if ! command -v arduino-cli >/dev/null 2>&1; then + echo "arduino-cli not found in PATH. Install it before running this script." >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EXAMPLES_DIR="${SCRIPT_DIR}/../examples" + +EXAMPLES=( + "config-example.ino" + "filtering-example.ino" + "buttons-example.ino" + "i2c-example.ino" + "csv-example.ino" + "scheduler-example.ino" + "state-machine-example.ino" + "hardware-detection-example.ino" + "data-logging-example.ino" +) + +FAILED=() + +for fqbn in "${FQBNS[@]}"; do + echo "" + echo "=== Compiling examples for ${fqbn} ===" + + for example in "${EXAMPLES[@]}"; do + echo "Compiling ${example}" + if ! arduino-cli compile --fqbn "${fqbn}" "${EXAMPLES_DIR}/${example}"; then + FAILED+=("${fqbn} :: ${example}") + fi + done +done + +if [ "${#FAILED[@]}" -gt 0 ]; then + echo "" + echo "Compilation failures:" + for item in "${FAILED[@]}"; do + echo " - ${item}" + done + exit 1 +fi + +echo "" +echo "All examples compiled successfully." diff --git a/.agents/skills/arduino-code-generator/templates/code-output-template.md b/.agents/skills/arduino-code-generator/templates/code-output-template.md new file mode 100644 index 0000000..d6e243f --- /dev/null +++ b/.agents/skills/arduino-code-generator/templates/code-output-template.md @@ -0,0 +1,254 @@ +# Code Output Template + +Standardized template for generating Arduino code snippets with consistent structure and documentation. + +## Template Structure + +```cpp +/* + * [Pattern Name] - [Brief Description] + * Generated for [Board Type] using Arduino Code Generator + * + * Features: + * - [List key features] + * - [List capabilities] + * + * Hardware Requirements: + * - [Board type] + * - [List required components] + * + * Connections: + * [ASCII wiring diagram] + * + * Usage: + * 1. Upload this sketch to your Arduino + * 2. Open Serial Monitor at 9600 baud + * 3. [Expected behavior description] + * + * Expected Output: + * [Sample Serial output] + */ + +#include <[Required Libraries]> + +// Configuration +#define [PIN_NAME] [PIN_NUMBER] +#define [CONSTANT_NAME] [VALUE] + +// Timing constants +const unsigned long [INTERVAL_NAME] = [VALUE_MS]; + +// Global variables +unsigned long [timing_variable] = 0; +[Other global variables] + +// Function declarations +void [function_name](); +[Other function declarations] + +void setup() { + // Initialize serial communication + Serial.begin(9600); + #if defined(ARDUINO_ARCH_AVR) + while (!Serial); // Wait for serial on Leonardo/UNO + #endif + + Serial.println(F("[Initialization message]")); + + // Hardware initialization + [hardware_init_code] + + // Verify initialization + if ([init_check_condition]) { + Serial.println(F("[Success message]")); + } else { + Serial.println(F("[Error message]")); + while (1); // Halt on critical error + } +} + +void loop() { + // Non-blocking timing + unsigned long currentTime = millis(); + + // Handle millis() overflow + if (currentTime - [timing_variable] >= [INTERVAL_NAME]) { + [timed_action_code] + [timing_variable] = currentTime; + } + + // Main logic + [main_logic_code] + + // Error handling + [error_handling_code] +} + +// Implementation functions +void [function_name]() { + [function_implementation] +} + +[Additional helper functions] +``` + +## Template Customization Guide + +### Header Comments +- **Pattern Name**: Descriptive name (e.g., "DHT22 Temperature Sensor") +- **Board Type**: Target board (UNO, ESP32, RP2040) +- **Features List**: 3-5 bullet points of capabilities +- **Hardware Requirements**: Specific components needed +- **Connections**: ASCII art wiring diagram +- **Usage Steps**: Numbered steps for user +- **Expected Output**: Sample Serial monitor output + +### Include Section +- Group standard libraries first (``, ``) +- Then third-party libraries (``) +- Finally custom headers (`"config.h"`) + +### Configuration Section +- Use `#define` for pin numbers and constants +- Group related constants together +- Use descriptive names in ALL_CAPS + +### Timing Section +- Define timing intervals as `const unsigned long` +- Use milliseconds for all time values +- Name intervals descriptively (e.g., `READ_INTERVAL_MS`) + +### Global Variables +- Initialize timing variables to 0 +- Use descriptive names +- Minimize global scope + +### Setup Function +- Always initialize Serial first +- Use F() macro for strings on UNO +- Include hardware initialization checks +- Provide clear success/error messages + +### Loop Function +- Implement proper millis() timing with overflow protection +- Keep loop() responsive (no blocking operations) +- Include error checking and recovery + +### Function Organization +- Declare all functions at top (Arduino requirement) +- Implement functions after loop() +- Use clear, descriptive names +- Single responsibility per function + +## Board-Specific Templates + +### Arduino UNO Template +```cpp +// Memory-optimized for 2KB SRAM +#define BUFFER_SIZE 64 +char buffer[BUFFER_SIZE]; + +// Use F() macro for all strings +Serial.println(F("UNO-optimized output")); +``` + +### ESP32 Template +```cpp +// WiFi-capable with FreeRTOS +#include + +// Task for concurrent operations +TaskHandle_t sensorTask; +``` + +### RP2040 Template +```cpp +// Dual-core capable +#include + +// PIO for precise timing if needed +``` + +## Error Handling Template + +```cpp +// Standardized error handling +bool initializeHardware() { + if (![init_success_condition]) { + Serial.println(F("ERROR: Hardware initialization failed")); + return false; + } + return true; +} + +void handleErrors() { + static unsigned long lastErrorCheck = 0; + const unsigned long ERROR_CHECK_INTERVAL = 5000; + + if (millis() - lastErrorCheck >= ERROR_CHECK_INTERVAL) { + if ([error_condition]) { + Serial.println(F("WARNING: [specific error]")); + [recovery_action] + } + lastErrorCheck = millis(); + } +} +``` + +## Integration Template + +When combining patterns, use this structure: + +```cpp +// Primary pattern includes +#include <[primary_pattern_libs]> + +// Secondary pattern includes +#include <[secondary_pattern_libs]> + +// Shared configuration +#define SHARED_PIN [pin_number] + +// Primary pattern variables +[primary_variables] + +// Secondary pattern variables +[secondary_variables] + +// Initialize both patterns +void setup() { + initPrimaryPattern(); + initSecondaryPattern(); +} + +void loop() { + updatePrimaryPattern(); + updateSecondaryPattern(); +} +``` + +## Testing Template + +Include this testing section for validation: + +```cpp +// Testing and debugging +#define DEBUG_MODE true + +void debugOutput() { + #if DEBUG_MODE + Serial.print(F("Debug: ")); + Serial.println([debug_value]); + #endif +} + +// Call debugOutput() at key points +``` + +## Documentation Standards + +- **Comments**: Explain "why" decisions, not "what" code does +- **Variable Names**: descriptive and consistent +- **Function Names**: verb-based (readSensor, updateDisplay) +- **Constants**: ALL_CAPS_WITH_UNDERSCORES +- **Indentation**: 2 spaces (Arduino IDE default) \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/workflow/step1-identify-pattern.md b/.agents/skills/arduino-code-generator/workflow/step1-identify-pattern.md new file mode 100644 index 0000000..8ec55f8 --- /dev/null +++ b/.agents/skills/arduino-code-generator/workflow/step1-identify-pattern.md @@ -0,0 +1,82 @@ +# Step 1: Identify Pattern Type + +When a user requests Arduino code, first identify the core pattern category from their request: + +## Pattern Categories + +### Hardware Abstraction +- Multi-board configuration +- Pin definitions +- Memory management +- Board detection + +**Keywords:** config, pins, setup, board, memory + +### Sensor Reading & Filtering +- ADC noise reduction +- Sensor data processing +- Calibration and validation +- Environmental sensors (DHT22, BME280) + +**Keywords:** sensor, read, temperature, humidity, filter, noise, adc + +### Input Handling +- Button debouncing +- Edge detection +- Multi-button management +- User input processing + +**Keywords:** button, press, input, debounce, switch + +### Communication +- I2C device management +- SPI configuration +- UART/Serial protocols +- Data output (CSV) + +**Keywords:** i2c, spi, serial, uart, communication, csv, data + +### Timing & Concurrency +- Non-blocking timing +- Task scheduling +- State machines +- Event-driven programming + +**Keywords:** timer, schedule, state machine, non-blocking, millis + +### Hardware Detection +- Auto-detection +- Fallback strategies +- Adaptive configuration +- Resource monitoring + +**Keywords:** detect, auto, fallback, adaptive + +### Data Persistence +- EEPROM storage +- SD card logging +- Data validation +- Wear leveling + +**Keywords:** eeprom, sd card, log, save, store, data + +## Identification Process + +1. **Scan request for keywords** from the categories above +2. **Determine primary pattern** (most relevant category) +3. **Note secondary patterns** that might be needed for integration +4. **Consider board constraints** (UNO vs ESP32 vs RP2040) + +## Examples + +**"Generate code to read a DHT22 sensor without blocking"** +β†’ Primary: Sensor Reading & Filtering + Timing & Concurrency + +**"Create a button handler with long press detection"** +β†’ Primary: Input Handling + +**"Make an I2C scanner for my Arduino"** +β†’ Primary: Communication (I2C) + +**"Log data to SD card every 10 seconds"** +β†’ Primary: Data Persistence + Timing & Concurrency \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/workflow/step2-read-reference.md b/.agents/skills/arduino-code-generator/workflow/step2-read-reference.md new file mode 100644 index 0000000..3dda17f --- /dev/null +++ b/.agents/skills/arduino-code-generator/workflow/step2-read-reference.md @@ -0,0 +1,56 @@ +# Step 2: Read Relevant Reference File + +After identifying the pattern type, read the corresponding reference documentation to understand implementation details. + +## Reference File Mapping + +| Pattern Category | Reference File | Location | +|------------------|----------------|----------| +| Hardware Abstraction | patterns-config.md | references/ | +| Sensor Reading & Filtering | patterns-filtering.md | references/ | +| Input Handling | patterns-buttons.md | references/ | +| Communication (I2C) | patterns-i2c.md | references/ | +| Communication (SPI) | patterns-spi.md | references/ | +| Communication (CSV) | patterns-csv.md | references/ | +| Timing & Concurrency (Scheduler) | patterns-scheduler.md | references/ | +| Timing & Concurrency (State Machine) | patterns-state-machine.md | references/ | +| Hardware Detection | patterns-hardware-detection.md | references/ | +| Data Persistence | patterns-data-logging.md | references/ | + +## Reading Process + +1. **Locate the reference file** based on pattern mapping +2. **Read the complete file** to understand: + - Required libraries and includes + - Pin configurations + - Function signatures + - Error handling patterns + - Board-specific considerations + - Example implementations + +3. **Extract key implementation details**: + - Data structures used + - Timing constraints + - Memory requirements + - Initialization sequences + - Common pitfalls + +4. **Note integration points** with other patterns + +## Reference File Structure + +Each reference file contains: +- **Overview**: Pattern description and use cases +- **API Reference**: Function signatures and parameters +- **Implementation Guide**: Step-by-step coding instructions +- **Board-Specific Notes**: UNO/ESP32/RP2040 differences +- **Examples**: Code snippets and usage patterns +- **Integration**: How to combine with other patterns + +## Fallback Strategy + +If a specific reference file doesn't exist: +1. Check for similar patterns in existing files +2. Use general Arduino best practices +3. Consult the quality standards and common pitfalls +4. Generate code following established patterns \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/workflow/step3-generate-code.md b/.agents/skills/arduino-code-generator/workflow/step3-generate-code.md new file mode 100644 index 0000000..56717f4 --- /dev/null +++ b/.agents/skills/arduino-code-generator/workflow/step3-generate-code.md @@ -0,0 +1,150 @@ +# Step 3: Generate Code + +Generate production-ready Arduino code following these comprehensive rules and best practices. + +## Code Generation Rules + +### 1. Include Statements +- Include all necessary `#include` statements at the top +- Use angle brackets for standard libraries: `#include ` +- Use quotes for custom headers: `#include "config.h"` +- Group includes logically (standard, then third-party, then custom) + +### 2. Configuration Management +- Define pins in config.h style with conditional compilation +- Use `#ifdef` for board-specific configurations +- Define constants for magic numbers +- Group related constants together + +### 3. Timing Patterns +- **Never use `delay()`** - always use `millis()` for non-blocking timing +- Use `unsigned long` for all time variables +- Handle millis() overflow (wraps every ~49 days) +- Implement proper timing comparisons + +### 4. Memory Management +- Use `F()` macro for string literals on memory-constrained boards +- Avoid `String` class on UNO - use char arrays +- Implement bounds checking for all arrays +- Use `const` for read-only data +- Minimize global variables + +### 5. Error Handling +- Check return values from peripheral initialization +- Provide meaningful error messages via Serial +- Implement fallback strategies for missing hardware +- Use appropriate error codes or states + +### 6. Debugging Support +- Add Serial output for debugging at key points +- Include status messages during initialization +- Log errors and unusual conditions +- Comment "why" decisions were made, not "what" the code does + +### 7. Code Structure +- Use clear, descriptive variable names +- Implement proper indentation and spacing +- Add comments for complex logic +- Separate concerns into functions +- Follow Arduino naming conventions + +## Board-Specific Considerations + +### Arduino UNO (ATmega328P, 2KB SRAM) +```cpp +// Use F() macro for all strings +Serial.println(F("Initializing sensor...")); + +// Prefer char arrays over String +char buffer[32]; + +// Minimize buffer sizes +#define BUFFER_SIZE 64 +``` + +### ESP32 (520KB SRAM, WiFi/BLE capable) +```cpp +// Can use larger buffers +#define BUFFER_SIZE 1024 + +// Leverage FreeRTOS +TaskHandle_t sensorTask; + +// Enable WiFi patterns +#include +``` + +### RP2040 (264KB SRAM, dual-core) +```cpp +// Use PIO for timing-critical tasks +// Support USB host mode +// Multicore patterns available +``` + +## Code Template Structure + +```cpp +// 1. Includes +#include +#include +#include "config.h" + +// 2. Constants and Configuration +#define PIN_LED 13 +#define INTERVAL_MS 1000 +const char* DEVICE_NAME = "SensorNode"; + +// 3. Global Variables +unsigned long lastUpdate = 0; +bool sensorActive = false; + +// 4. Function Declarations +void initializeHardware(); +void updateSensor(); +void handleErrors(); + +// 5. Setup Function +void setup() { + Serial.begin(9600); + while (!Serial); // Wait for serial on Leonardo/UNO + + Serial.println(F("Initializing...")); + initializeHardware(); + Serial.println(F("Ready")); +} + +// 6. Main Loop +void loop() { + unsigned long currentTime = millis(); + + // Handle millis() overflow + if (currentTime - lastUpdate >= INTERVAL_MS) { + updateSensor(); + lastUpdate = currentTime; + } + + handleErrors(); +} + +// 7. Implementation Functions +void initializeHardware() { + // Hardware initialization with error checking +} + +void updateSensor() { + // Sensor reading and processing +} + +void handleErrors() { + // Error detection and recovery +} +``` + +## Quality Assurance + +Before finalizing code: +- βœ… Verify compilation on target board +- βœ… Check memory usage estimates +- βœ… Validate timing calculations +- βœ… Test error conditions +- βœ… Review against common pitfalls \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/workflow/step4-provide-instructions.md b/.agents/skills/arduino-code-generator/workflow/step4-provide-instructions.md new file mode 100644 index 0000000..aa78679 --- /dev/null +++ b/.agents/skills/arduino-code-generator/workflow/step4-provide-instructions.md @@ -0,0 +1,97 @@ +# Step 4: Provide Usage Instructions + +After generating code, provide comprehensive usage instructions including wiring diagrams, configuration steps, and expected behavior. + +## Instruction Components + +### 1. Hardware Requirements +- **Components List**: Specific sensors, actuators, or modules needed +- **Board Compatibility**: Which Arduino boards are supported +- **Power Requirements**: Voltage and current specifications +- **Additional Hardware**: Cables, resistors, breadboards, etc. + +### 2. Wiring Instructions +- **Pin Connections**: Clear mapping between Arduino pins and component pins +- **Circuit Diagrams**: ASCII art or reference to visual diagrams +- **Pull-up/Pull-down Resistors**: When and where to use them +- **Power Connections**: VCC, GND, and signal pins + +### 3. Software Setup +- **Library Installation**: Which Arduino libraries to install via Library Manager +- **Board Selection**: How to select the correct board in Arduino IDE +- **Configuration**: Any settings or parameters to modify +- **Dependencies**: Other code or configurations required + +### 4. Usage Instructions +- **Initialization**: What happens during setup() +- **Operation**: How the code behaves during normal operation +- **User Interaction**: Buttons to press, sensors to trigger, etc. +- **Output**: What to expect on Serial monitor, LEDs, displays + +### 5. Testing and Validation +- **Expected Output**: Sample Serial output or LED patterns +- **Error Conditions**: What to look for if something goes wrong +- **Debugging Tips**: How to troubleshoot common issues +- **Performance Metrics**: Expected timing or resource usage + +## Wiring Diagram Format + +Use clear ASCII art for wiring diagrams: + +``` +Arduino UNO DHT22 Sensor +----------- ------------ + 5V ---------> VCC + GND ---------> GND + D2 ---------> DATA + (with 10K pull-up resistor) +``` + +Or reference existing diagrams: +- See examples/README.md for detailed wiring diagrams +- Check Fritzing diagrams in assets/ folder + +## Example Instructions Template + +### Hardware Required +- Arduino UNO, ESP32, or RP2040 board +- DHT22 temperature/humidity sensor +- 10KΞ© pull-up resistor +- Jumper wires + +### Wiring +``` +Arduino Pin | DHT22 Pin | Notes +-------------|-----------|-------- +5V | VCC | Power supply +GND | GND | Ground +D2 | DATA | Data signal (with 10K pull-up to 5V) +``` + +### Setup Steps +1. Install DHT sensor library: `Sketch > Include Library > Manage Libraries > "DHT sensor library"` +2. Select your board: `Tools > Board > Arduino UNO` +3. Upload the sketch +4. Open Serial Monitor: `Tools > Serial Monitor` + +### Expected Output +``` +Initializing DHT22 sensor... +DHT22 initialized successfully +Temperature: 23.50Β°C, Humidity: 45.20% +Temperature: 23.52Β°C, Humidity: 45.15% +... +``` + +### Troubleshooting +- **No readings**: Check wiring and power connections +- **Invalid readings**: Ensure proper pull-up resistor on data pin +- **Slow response**: DHT22 sensors can take up to 2 seconds between readings + +## Integration Notes + +When combining with other patterns: +- Mention how wiring integrates with existing circuits +- Note any pin conflicts or shared resources +- Explain how multiple components work together +- Provide combined wiring diagrams for complex projects \ No newline at end of file diff --git a/.agents/skills/arduino-code-generator/workflow/step5-mention-integration.md b/.agents/skills/arduino-code-generator/workflow/step5-mention-integration.md new file mode 100644 index 0000000..983ad20 --- /dev/null +++ b/.agents/skills/arduino-code-generator/workflow/step5-mention-integration.md @@ -0,0 +1,108 @@ +# Step 5: Mention Integration + +When relevant, suggest how the generated code can integrate with other patterns for more complex projects. + +## Integration Patterns + +### Environmental Monitor +**Combines:** Filtering + Scheduler + CSV + Data Logging + +**Use Case:** Collect sensor data over time and log to SD card +**Components:** DHT22/BME280 sensors + SD card module + real-time clock + +**Integration Points:** +- Use Scheduler for periodic readings +- Apply Filtering for noise reduction +- Format data as CSV for logging +- Use Data Logging for persistent storage + +### Button-Controlled Robot +**Combines:** Buttons + State Machine + Scheduler + +**Use Case:** Robot with button controls and different operational modes +**Components:** Multiple buttons + motor drivers + state indicators + +**Integration Points:** +- Buttons trigger state transitions +- State Machine manages robot behavior +- Scheduler handles timing-critical motor control + +### IoT Data Logger +**Combines:** Hardware Detection + WiFi + Data Logging + CSV + +**Use Case:** Remote environmental monitoring with cloud connectivity +**Components:** ESP32 board + sensors + WiFi module + SD card + +**Integration Points:** +- Hardware Detection for sensor availability +- WiFi for data transmission +- Data Logging as backup when offline +- CSV for structured data format + +### Sensor Hub +**Combines:** I2C + Filtering + Scheduler + Hardware Detection + +**Use Case:** Multiple I2C sensors with coordinated readings +**Components:** Multiple I2C sensors + microcontroller with I2C bus + +**Integration Points:** +- I2C for sensor communication +- Filtering for each sensor type +- Scheduler for coordinated sampling +- Hardware Detection for plug-and-play sensors + +## Integration Guidelines + +### When to Suggest Integration +- **User requests complex functionality** requiring multiple patterns +- **Hardware setup** naturally combines different components +- **Project scope** involves data flow between subsystems +- **Performance requirements** need coordinated timing + +### How to Present Integration +1. **Identify the primary pattern** from the user's request +2. **Suggest complementary patterns** that enhance functionality +3. **Explain the benefits** of combining patterns +4. **Provide integration examples** from the examples/ folder +5. **Note any constraints** or considerations + +### Integration Examples + +**"I want to log temperature data"** +β†’ Primary: Data Logging +β†’ Integration: Add Filtering for noise reduction, Scheduler for timing + +**"Build a smart button interface"** +β†’ Primary: Buttons +β†’ Integration: Add State Machine for complex interactions, Scheduler for timeouts + +**"Create a wireless sensor network"** +β†’ Primary: Communication (WiFi/I2C) +β†’ Integration: Add Hardware Detection for robustness, Data Logging for offline operation + +## Cross-Pattern Considerations + +### Shared Resources +- **Pins:** Ensure no conflicts between integrated patterns +- **Memory:** Account for combined memory usage +- **Timing:** Coordinate timing requirements between patterns +- **Power:** Consider power consumption of combined components + +### Code Organization +- **Modular Functions:** Keep patterns in separate functions +- **Shared Constants:** Define common pins/constants once +- **Error Handling:** Integrate error handling across patterns +- **Initialization Order:** Ensure proper startup sequence + +### Testing Strategy +- **Individual Testing:** Test each pattern separately first +- **Integration Testing:** Verify combined functionality +- **Edge Cases:** Test interactions between patterns +- **Resource Limits:** Verify memory and timing constraints + +## Documentation Links + +For detailed integration examples, see: +- [examples/README.md](examples/README.md) - Complete project examples +- Individual pattern references in [references/](references/) folder +- [assets/workflow.mmd](assets/workflow.mmd) - Integration workflow diagram \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/SKILL.md b/.agents/skills/arduino-project-builder/SKILL.md new file mode 100644 index 0000000..18df958 --- /dev/null +++ b/.agents/skills/arduino-project-builder/SKILL.md @@ -0,0 +1,82 @@ +--- +name: arduino-project-builder +description: Build complete, production-ready Arduino projects (environmental monitors, robot controllers, IoT devices, automation systems). Assembles multi-component systems combining sensors, actuators, communication protocols, state machines, data logging, and power management. Supports Arduino UNO, ESP32, and Raspberry Pi Pico with board-specific optimizations. Use this skill when users request complete Arduino applications, not just code snippets. +--- + +# Arduino Project Builder + +Assemble complete, working Arduino projects from requirements. This skill combines multiple patterns (sensors, actuators, state machines, logging, communication) into cohesive systems. + +## Quick Start + +**List available project types:** +```bash +uv run --no-project scripts/scaffold_project.py --list +``` + +**Create a complete project:** +```bash +uv run --no-project scripts/scaffold_project.py --type environmental --board esp32 --name "WeatherStation" +uv run --no-project scripts/scaffold_project.py --type robot --board uno --output ./my-robot +``` + +**Interactive mode:** +```bash +uv run --no-project scripts/scaffold_project.py --interactive +``` + +## Resources + +- **examples/** - Complete project examples (environmental monitor, robot controller, IoT device) +- **scripts/scaffold_project.py** - CLI tool for project scaffolding (config.h, main.ino, platformio.ini, README) +- **assets/workflow.mmd** - Mermaid diagram of project assembly workflow + +## Supported Project Types + +### Environmental Monitors +Multi-sensor data loggers (temperature, humidity, light, air quality) + +See [Environmental Monitor Example](examples/project-environmental-monitor.md) + +### Robot Controllers +Motor control, sensor fusion, obstacle avoidance, state machines + +See [Robot Controller Example](examples/project-robot-controller.md) + +### IoT Devices +WiFi/MQTT data transmission, cloud integration, remote monitoring + +See [IoT Device Example](examples/project-iot-device.md) + +### Home Automation +Relay control, scheduled tasks, sensor-triggered actions + +### Data Acquisition Systems +High-frequency sampling, SD card logging, real-time visualization + +## Project Assembly Workflow + +- [ ] **[Requirements Gathering](workflow/step1-requirements-gathering.md)** - Analyze user request and gather project specifications +- [ ] **[Architecture Design](workflow/step2-architecture-design.md)** - Design component connections, data flow, and state machines +- [ ] **[Code Assembly](workflow/step3-code-assembly.md)** - Combine patterns and customize for user hardware +- [ ] **[Testing & Validation](workflow/step4-testing-validation.md)** - Verify compilation, memory usage, and functionality +- [ ] **[Documentation](workflow/step5-documentation.md)** - Create wiring diagrams, usage instructions, and troubleshooting guides + +## Quality Standards & Rules + +- [ ] **[Quality Standards](rules/quality-standards.md)** - Hardware abstraction, non-blocking code, error handling, and memory safety requirements +- [ ] **[Integration Checklist](rules/integration-checklist.md)** - Pre-delivery verification for sensor validation, timing, and reliability +- [ ] **[Board Considerations](rules/board-considerations.md)** - UNO, ESP32, and RP2040 specific optimizations and constraints + +## Project Output Template + +- [ ] **[Output Template](templates/project-output-template.md)** - Standardized format for delivering complete Arduino projects + +## Resources + +- **examples/** - Complete project examples with full implementations +- **scripts/scaffold_project.py** - CLI tool for project scaffolding with config.h, main.ino, platformio.ini, README +- **assets/workflow.mmd** - Mermaid diagram of project assembly workflow +- **workflow/** - Step-by-step project assembly process +- **rules/** - Quality standards and board-specific optimizations +- **templates/** - Project output templates and documentation standards diff --git a/.agents/skills/arduino-project-builder/assets/workflow.mmd b/.agents/skills/arduino-project-builder/assets/workflow.mmd new file mode 100644 index 0000000..d36d110 --- /dev/null +++ b/.agents/skills/arduino-project-builder/assets/workflow.mmd @@ -0,0 +1,28 @@ +```mermaid +flowchart TD + A["πŸ“‹ Requirements"] --> B["πŸ”§ Hardware Inventory"] + B --> C["🎯 Board Selection"] + C --> D{"Project Type?"} + + D -->|Environmental| E1["Sensors:\nDHT22, Light\nFeatures:\nLogging, SD Card"] + D -->|Robot| E2["Sensors:\nUltrasonic\nActuators:\nMotors, Servo"] + D -->|IoT| E3["Sensors:\nBME280\nFeatures:\nWiFi, MQTT"] + + E1 & E2 & E3 --> F["πŸ“¦ Pattern Assembly\nscaffold_project.py"] + + F --> G["Code Generation"] + G --> H["πŸ“ Project Structure"] + + H --> I1["src/config.h"] + H --> I2["src/main.ino"] + H --> I3["platformio.ini"] + H --> I4["README.md"] + + I1 & I2 & I3 & I4 --> J["πŸ§ͺ Testing"] + J --> K["πŸ“ Documentation"] + K --> L["βœ… Complete Project"] + + style A fill:#e3f2fd + style L fill:#c8e6c9 + style F fill:#fff3e0 +``` diff --git a/.agents/skills/arduino-project-builder/examples/project-environmental-monitor.md b/.agents/skills/arduino-project-builder/examples/project-environmental-monitor.md new file mode 100644 index 0000000..addf8de --- /dev/null +++ b/.agents/skills/arduino-project-builder/examples/project-environmental-monitor.md @@ -0,0 +1,290 @@ +# Environmental Monitor Project + +**Description:** Multi-sensor data logger for temperature, humidity, and light levels with SD card storage and real-time Serial output. Perfect for greenhouse monitoring, weather stations, or indoor climate tracking. + +**Hardware Requirements:** +- Arduino UNO or ESP32 +- DHT22 temperature/humidity sensor +- Photoresistor (light sensor) + 10kΞ© resistor +- SD card module (optional) +- Pushbutton + 10kΞ© pulldown resistor +- LED (status indicator) + +**Wiring Diagram:** +``` +DHT22: + VCC β†’ 5V (or 3.3V for ESP32) + DATA β†’ Pin 2 + GND β†’ GND + +Photoresistor: + One leg β†’ 5V + Other leg β†’ A0 and 10kΞ© resistor to GND + +Button: + One leg β†’ Pin 3 + Other leg β†’ GND (use INPUT_PULLUP) + +LED: + Anode (+) β†’ Pin 13 β†’ 220Ξ© resistor + Cathode (-) β†’ GND + +SD Card Module (optional): + CS β†’ Pin 10 + MOSI β†’ Pin 11 + MISO β†’ Pin 12 + SCK β†’ Pin 13 + VCC β†’ 5V + GND β†’ GND +``` + +**Features:** +- Non-blocking sensor reads (DHT22 every 2s, light every 1s) +- Moving average filter for light sensor (reduces noise) +- CSV logging to Serial and SD card +- Button toggles logging on/off +- LED heartbeat (system alive indicator) +- Memory-safe on Arduino UNO (2KB SRAM) + +**Complete Code:** + +```cpp +// config.h +#if defined(ARDUINO_AVR_UNO) + #define BOARD_TYPE "Arduino UNO" + #define SERIAL_BAUD 9600 + #define USE_SD_CARD false // Limited SRAM on UNO +#elif defined(ESP32) + #define BOARD_TYPE "ESP32" + #define SERIAL_BAUD 115200 + #define USE_SD_CARD true // ESP32 has plenty of RAM +#endif + +#define DHT_PIN 2 +#define LIGHT_PIN A0 +#define BUTTON_PIN 3 +#define LED_PIN 13 + +// main.ino +#include +#if USE_SD_CARD +#include +#endif + +DHT dht(DHT_PIN, DHT22); + +// EveryMs timer class +class EveryMs { +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {} + bool check() { + unsigned long now = millis(); + if (now - lastTrigger >= interval) { + lastTrigger = now; + return true; + } + return false; + } +}; + +// Moving average filter for light sensor +class MovingAverageFilter { +private: + static const uint8_t SIZE = 10; + int values[SIZE]; + uint8_t index; + uint8_t count; +public: + MovingAverageFilter() : index(0), count(0) { + for (uint8_t i = 0; i < SIZE; i++) values[i] = 0; + } + + int filter(int newValue) { + values[index] = newValue; + index = (index + 1) % SIZE; + if (count < SIZE) count++; + + long sum = 0; + for (uint8_t i = 0; i < count; i++) { + sum += values[i]; + } + return sum / count; + } +}; + +// Debounced button +class DebouncedButton { +private: + uint8_t pin; + bool lastState; + unsigned long lastDebounceTime; + static const unsigned long DEBOUNCE_DELAY = 50; +public: + DebouncedButton(uint8_t p) : pin(p), lastState(HIGH), lastDebounceTime(0) {} + + void begin() { + pinMode(pin, INPUT_PULLUP); + } + + bool pressed() { + bool currentState = digitalRead(pin); + if (currentState != lastState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) { + if (currentState == LOW && lastState == HIGH) { + lastState = currentState; + return true; + } + } + lastState = currentState; + return false; + } +}; + +// Global objects +MovingAverageFilter lightFilter; +DebouncedButton button(BUTTON_PIN); +EveryMs dhtTimer(2000); +EveryMs lightTimer(1000); +EveryMs displayTimer(5000); +EveryMs csvLogTimer(60000); +EveryMs heartbeatTimer(2000); + +struct SensorData { + float temperature; + float humidity; + int lightLevel; + bool valid; +} data; + +bool loggingEnabled = true; + +#if USE_SD_CARD +File dataFile; +const char* LOG_FILE = "envlog.csv"; +#endif + +void setup() { + Serial.begin(SERIAL_BAUD); + pinMode(LED_PIN, OUTPUT); + button.begin(); + dht.begin(); + + Serial.println(F("=== Environmental Monitor ===")); + Serial.print(F("Board: ")); + Serial.println(F(BOARD_TYPE)); + + #if USE_SD_CARD + if (SD.begin(10)) { + Serial.println(F("SD card ready")); + if (!SD.exists(LOG_FILE)) { + dataFile = SD.open(LOG_FILE, FILE_WRITE); + if (dataFile) { + dataFile.println(F("Time_ms,Temp_C,Humidity_%,Light")); + dataFile.close(); + } + } + } else { + Serial.println(F("SD card init failed")); + } + #endif + + Serial.println(F("Time_ms,Temp_C,Humidity_%,Light")); + data.valid = false; +} + +void loop() { + // Task 1: Read DHT22 + if (dhtTimer.check()) { + data.temperature = dht.readTemperature(); + data.humidity = dht.readHumidity(); + data.valid = !isnan(data.temperature) && !isnan(data.humidity); + + if (!data.valid) { + Serial.println(F("DHT22 read error")); + } + } + + // Task 2: Read light sensor + if (lightTimer.check()) { + int rawLight = analogRead(LIGHT_PIN); + data.lightLevel = lightFilter.filter(rawLight); + } + + // Task 3: Display summary + if (displayTimer.check() && data.valid) { + Serial.print(F("Temp: ")); + Serial.print(data.temperature, 1); + Serial.print(F("Β°C | Humidity: ")); + Serial.print(data.humidity, 1); + Serial.print(F("% | Light: ")); + Serial.println(data.lightLevel); + } + + // Task 4: Log CSV + if (csvLogTimer.check() && loggingEnabled && data.valid) { + String csvLine = String(millis()) + "," + + String(data.temperature, 1) + "," + + String(data.humidity, 1) + "," + + String(data.lightLevel); + + Serial.println(csvLine); + + #if USE_SD_CARD + dataFile = SD.open(LOG_FILE, FILE_WRITE); + if (dataFile) { + dataFile.println(csvLine); + dataFile.close(); + } + #endif + } + + // Task 5: Button toggle + if (button.pressed()) { + loggingEnabled = !loggingEnabled; + Serial.print(F("Logging: ")); + Serial.println(loggingEnabled ? F("ON") : F("OFF")); + } + + // Task 6: Heartbeat LED + if (heartbeatTimer.check()) { + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); + } +} +``` + +**Upload Instructions:** +1. Install DHT sensor library: Sketch β†’ Include Library β†’ Manage Libraries β†’ Search "DHT sensor library" β†’ Install +2. Select board: Tools β†’ Board β†’ Arduino UNO (or ESP32 Dev Module) +3. Select port: Tools β†’ Port β†’ (your Arduino's port) +4. Upload sketch + +**Usage:** +- System starts logging immediately +- Press button to toggle logging on/off +- Send 'd' via Serial Monitor to dump all data (if SD card enabled) +- LED blinks every 2 seconds (heartbeat) + +**Expected Output:** +``` +=== Environmental Monitor === +Board: Arduino UNO +Time_ms,Temp_C,Humidity_%,Light +Temp: 23.5Β°C | Humidity: 45.2% | Light: 512 +60000,23.5,45.2,512 +120000,23.6,45.0,510 +Logging: OFF +Logging: ON +180000,23.4,45.3,515 +``` + +**Troubleshooting:** +- **DHT22 read error:** Check wiring, ensure VCC is 5V (3.3V for ESP32) +- **Light sensor reads 0 or 1023:** Check photoresistor orientation, verify 10kΞ© resistor +- **SD card init failed:** Check wiring, try different SD card (FAT32 formatted) +- **Button not responding:** Verify INPUT_PULLUP mode, check button wiring diff --git a/.agents/skills/arduino-project-builder/examples/project-iot-device.md b/.agents/skills/arduino-project-builder/examples/project-iot-device.md new file mode 100644 index 0000000..f930793 --- /dev/null +++ b/.agents/skills/arduino-project-builder/examples/project-iot-device.md @@ -0,0 +1,350 @@ +# IoT Temperature & Humidity Logger (ESP32) + +**Description:** ESP32-based WiFi data logger that publishes temperature and humidity readings to MQTT broker. Ideal for remote environmental monitoring, smart home integration, or IoT sensor networks. + +**Hardware Requirements:** +- ESP32 development board (DevKit, WROOM, or similar) +- DHT22 temperature/humidity sensor +- LED (status indicator) +- Pushbutton (WiFi reconnect trigger) +- USB cable (programming and power) + +**Wiring Diagram:** +``` +DHT22: + VCC β†’ 3.3V (ESP32 uses 3.3V logic) + DATA β†’ GPIO 4 + GND β†’ GND + +Button: + One leg β†’ GPIO 0 (BOOT button can also be used) + Other leg β†’ GND (INPUT_PULLUP) + +LED: + GPIO 2 β†’ 220Ξ© resistor β†’ LED anode + LED cathode β†’ GND +``` + +**Features:** +- WiFi connection with auto-reconnect +- MQTT publish every 60 seconds +- JSON payload format +- OTA (Over-The-Air) updates support +- NTP time synchronization +- Non-blocking sensor reads +- Button triggers manual WiFi reconnect +- Built-in LED status indicators + +**Complete Code:** + +```cpp +// config.h +#define WIFI_SSID "YourWiFiSSID" +#define WIFI_PASSWORD "YourWiFiPassword" +#define MQTT_BROKER "192.168.1.100" // Your MQTT broker IP +#define MQTT_PORT 1883 +#define MQTT_TOPIC "home/esp32/sensor" +#define MQTT_CLIENT_ID "ESP32_TempHumidity" + +#define DHT_PIN 4 +#define BUTTON_PIN 0 +#define LED_PIN 2 + +// main.ino +#include +#include +#include +#include + +DHT dht(DHT_PIN, DHT22); +WiFiClient wifiClient; +PubSubClient mqttClient(wifiClient); + +// EveryMs timer +class EveryMs { +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {} + bool check() { + unsigned long now = millis(); + if (now - lastTrigger >= interval) { + lastTrigger = now; + return true; + } + return false; + } + void reset() { lastTrigger = millis(); } +}; + +// Debounced button +class DebouncedButton { +private: + uint8_t pin; + bool lastState; + unsigned long lastDebounceTime; +public: + DebouncedButton(uint8_t p) : pin(p), lastState(HIGH), lastDebounceTime(0) {} + + void begin() { + pinMode(pin, INPUT_PULLUP); + } + + bool pressed() { + bool currentState = digitalRead(pin); + if (currentState != lastState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > 50) { + if (currentState == LOW && lastState == HIGH) { + lastState = currentState; + return true; + } + } + lastState = currentState; + return false; + } +}; + +// Global objects +DebouncedButton button(BUTTON_PIN); +EveryMs dhtTimer(2000); +EveryMs publishTimer(60000); +EveryMs wifiCheckTimer(10000); + +struct SensorData { + float temperature; + float humidity; + bool valid; +} data; + +void setup() { + Serial.begin(115200); + pinMode(LED_PIN, OUTPUT); + button.begin(); + dht.begin(); + + Serial.println(F("\n=== ESP32 IoT Logger ===")); + + connectWiFi(); + + mqttClient.setServer(MQTT_BROKER, MQTT_PORT); + mqttClient.setCallback(mqttCallback); + + connectMQTT(); + + data.valid = false; +} + +void connectWiFi() { + Serial.print(F("Connecting to WiFi")); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 20) { + delay(500); + Serial.print("."); + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Blink during connection + attempts++; + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.println(F("\nWiFi connected!")); + Serial.print(F("IP: ")); + Serial.println(WiFi.localIP()); + digitalWrite(LED_PIN, HIGH); // Solid LED when connected + } else { + Serial.println(F("\nWiFi connection failed!")); + digitalWrite(LED_PIN, LOW); + } +} + +void connectMQTT() { + if (WiFi.status() != WL_CONNECTED) return; + + Serial.print(F("Connecting to MQTT broker...")); + + int attempts = 0; + while (!mqttClient.connected() && attempts < 3) { + if (mqttClient.connect(MQTT_CLIENT_ID)) { + Serial.println(F("connected!")); + mqttClient.subscribe(MQTT_TOPIC "/command"); + return; + } + Serial.print("."); + delay(1000); + attempts++; + } + + if (!mqttClient.connected()) { + Serial.println(F("\nMQTT connection failed!")); + } +} + +void mqttCallback(char* topic, byte* payload, unsigned int length) { + Serial.print(F("MQTT message: ")); + for (unsigned int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + } + Serial.println(); +} + +void publishSensorData() { + if (!data.valid || !mqttClient.connected()) return; + + // Create JSON payload + StaticJsonDocument<200> doc; + doc["temperature"] = data.temperature; + doc["humidity"] = data.humidity; + doc["uptime"] = millis() / 1000; + doc["rssi"] = WiFi.RSSI(); + + char jsonBuffer[200]; + serializeJson(doc, jsonBuffer); + + if (mqttClient.publish(MQTT_TOPIC, jsonBuffer)) { + Serial.print(F("Published: ")); + Serial.println(jsonBuffer); + } else { + Serial.println(F("Publish failed!")); + } +} + +void loop() { + // Task 1: Read DHT22 + if (dhtTimer.check()) { + data.temperature = dht.readTemperature(); + data.humidity = dht.readHumidity(); + data.valid = !isnan(data.temperature) && !isnan(data.humidity); + + if (data.valid) { + Serial.print(F("Temp: ")); + Serial.print(data.temperature, 1); + Serial.print(F("Β°C | Humidity: ")); + Serial.print(data.humidity, 1); + Serial.println(F("%")); + } else { + Serial.println(F("DHT22 read error")); + } + } + + // Task 2: Publish to MQTT + if (publishTimer.check()) { + publishSensorData(); + } + + // Task 3: WiFi reconnect check + if (wifiCheckTimer.check()) { + if (WiFi.status() != WL_CONNECTED) { + Serial.println(F("WiFi disconnected, reconnecting...")); + connectWiFi(); + } + + if (!mqttClient.connected()) { + Serial.println(F("MQTT disconnected, reconnecting...")); + connectMQTT(); + } + } + + // Task 4: Button manual reconnect + if (button.pressed()) { + Serial.println(F("Manual reconnect triggered")); + connectWiFi(); + connectMQTT(); + } + + // Task 5: MQTT loop (handle incoming messages) + mqttClient.loop(); + + // Task 6: LED heartbeat (blink every 2s when connected) + if (WiFi.status() == WL_CONNECTED) { + digitalWrite(LED_PIN, (millis() % 2000) < 100); + } else { + digitalWrite(LED_PIN, LOW); + } +} +``` + +**Upload Instructions:** +1. Install libraries: + - PubSubClient (by Nick O'Leary) + - DHT sensor library (by Adafruit) + - ArduinoJson (by Benoit Blanchon) +2. Edit config.h: Set your WiFi SSID, password, and MQTT broker IP +3. Select board: Tools β†’ Board β†’ ESP32 Dev Module +4. Select port: Tools β†’ Port β†’ (your ESP32's port) +5. Upload sketch + +**MQTT Broker Setup:** +Install Mosquitto MQTT broker on your server: +```bash +# Ubuntu/Debian +sudo apt-get install mosquitto mosquitto-clients + +# Start broker +sudo systemctl start mosquitto + +# Test subscription +mosquitto_sub -h localhost -t "home/esp32/sensor" +``` + +**Usage:** +- System connects to WiFi on boot +- DHT22 readings every 2 seconds (local) +- MQTT publish every 60 seconds +- Press button to force WiFi/MQTT reconnect +- LED heartbeat when connected, off when disconnected + +**JSON Payload Format:** +```json +{ + "temperature": 23.5, + "humidity": 45.2, + "uptime": 3600, + "rssi": -65 +} +``` + +**Expected Serial Output:** +``` +=== ESP32 IoT Logger === +Connecting to WiFi......... +WiFi connected! +IP: 192.168.1.42 +Connecting to MQTT broker...connected! +Temp: 23.5Β°C | Humidity: 45.2% +Published: {"temperature":23.5,"humidity":45.2,"uptime":60,"rssi":-65} +Temp: 23.6Β°C | Humidity: 45.0% +Published: {"temperature":23.6,"humidity":45.0,"uptime":120,"rssi":-67} +``` + +**Home Assistant Integration:** +Add to `configuration.yaml`: +```yaml +sensor: + - platform: mqtt + name: "ESP32 Temperature" + state_topic: "home/esp32/sensor" + unit_of_measurement: "Β°C" + value_template: "{{ value_json.temperature }}" + + - platform: mqtt + name: "ESP32 Humidity" + state_topic: "home/esp32/sensor" + unit_of_measurement: "%" + value_template: "{{ value_json.humidity }}" +``` + +**Troubleshooting:** +- **WiFi won't connect:** Check SSID/password, ensure 2.4GHz network (ESP32 doesn't support 5GHz) +- **MQTT connection failed:** Verify broker IP, ensure port 1883 is open, check firewall +- **DHT22 read error:** Use 3.3V (not 5V), check DATA pin connection +- **JSON publish failed:** Increase document size in StaticJsonDocument<200> +- **Memory issues:** ESP32 has 327KB SRAM, no worries about overflow + +**Power Consumption:** +- Active (WiFi on): ~160mA +- Light sleep: ~20mA +- Deep sleep: ~10ΞΌA (add sleep mode for battery operation) diff --git a/.agents/skills/arduino-project-builder/examples/project-robot-controller.md b/.agents/skills/arduino-project-builder/examples/project-robot-controller.md new file mode 100644 index 0000000..c3c4228 --- /dev/null +++ b/.agents/skills/arduino-project-builder/examples/project-robot-controller.md @@ -0,0 +1,339 @@ +# Robot Controller Project + +**Description:** Button-controlled robot with obstacle avoidance using ultrasonic sensor and state machine architecture. Supports forward, turn left, turn right, and emergency stop modes. + +**Hardware Requirements:** +- Arduino UNO or ESP32 +- L298N motor driver module +- 2x DC motors with wheels +- HC-SR04 ultrasonic distance sensor +- 3x pushbuttons (forward, left, right) +- Battery pack (7.4V LiPo recommended) +- LED (status indicator) + +**Wiring Diagram:** +``` +L298N Motor Driver: + ENA β†’ Pin 9 (PWM for left motor speed) + IN1 β†’ Pin 8 (left motor direction 1) + IN2 β†’ Pin 7 (left motor direction 2) + ENB β†’ Pin 6 (PWM for right motor speed) + IN3 β†’ Pin 5 (right motor direction 1) + IN4 β†’ Pin 4 (right motor direction 2) + VCC β†’ Battery + (7.4V) + GND β†’ Arduino GND and Battery - + 5V β†’ Arduino VIN (regulated 5V output from L298N) + +HC-SR04 Ultrasonic: + VCC β†’ 5V + TRIG β†’ Pin 11 + ECHO β†’ Pin 10 + GND β†’ GND + +Buttons: + Forward button β†’ Pin 2 (INPUT_PULLUP) + Left button β†’ Pin 3 (INPUT_PULLUP) + Right button β†’ Pin 12 (INPUT_PULLUP) + +LED: + Pin 13 β†’ 220Ξ© resistor β†’ LED anode + LED cathode β†’ GND +``` + +**Features:** +- State machine with 5 states (IDLE, FORWARD, TURN_LEFT, TURN_RIGHT, OBSTACLE_DETECTED) +- Autonomous obstacle avoidance (stops at 20cm, turns right) +- Button override (manual control) +- PWM motor speed control +- Non-blocking ultrasonic sensor reads +- Emergency stop on low battery (if voltage sensor added) + +**Complete Code:** + +```cpp +// config.h +#define LEFT_MOTOR_ENA 9 +#define LEFT_MOTOR_IN1 8 +#define LEFT_MOTOR_IN2 7 +#define RIGHT_MOTOR_ENB 6 +#define RIGHT_MOTOR_IN3 5 +#define RIGHT_MOTOR_IN4 4 + +#define ULTRASONIC_TRIG 11 +#define ULTRASONIC_ECHO 10 + +#define BUTTON_FORWARD 2 +#define BUTTON_LEFT 3 +#define BUTTON_RIGHT 12 + +#define LED_PIN 13 + +#define OBSTACLE_THRESHOLD_CM 20 +#define MOTOR_SPEED 200 // 0-255 (PWM) +#define TURN_DURATION_MS 800 + +// main.ino + +// Robot state machine +enum RobotState { + IDLE, + MOVING_FORWARD, + TURNING_LEFT, + TURNING_RIGHT, + OBSTACLE_DETECTED +}; + +RobotState state = IDLE; +unsigned long stateStartTime = 0; + +// EveryMs timer +class EveryMs { +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {} + bool check() { + unsigned long now = millis(); + if (now - lastTrigger >= interval) { + lastTrigger = now; + return true; + } + return false; + } +}; + +// Debounced button +class DebouncedButton { +private: + uint8_t pin; + bool lastState; + unsigned long lastDebounceTime; +public: + DebouncedButton(uint8_t p) : pin(p), lastState(HIGH), lastDebounceTime(0) {} + + void begin() { + pinMode(pin, INPUT_PULLUP); + } + + bool pressed() { + bool currentState = digitalRead(pin); + if (currentState != lastState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > 50) { + if (currentState == LOW && lastState == HIGH) { + lastState = currentState; + return true; + } + } + lastState = currentState; + return false; + } +}; + +// Global objects +DebouncedButton btnForward(BUTTON_FORWARD); +DebouncedButton btnLeft(BUTTON_LEFT); +DebouncedButton btnRight(BUTTON_RIGHT); +EveryMs distanceTimer(100); // Check distance every 100ms + +int distanceCm = 999; + +void setup() { + Serial.begin(9600); + + // Motor pins + pinMode(LEFT_MOTOR_ENA, OUTPUT); + pinMode(LEFT_MOTOR_IN1, OUTPUT); + pinMode(LEFT_MOTOR_IN2, OUTPUT); + pinMode(RIGHT_MOTOR_ENB, OUTPUT); + pinMode(RIGHT_MOTOR_IN3, OUTPUT); + pinMode(RIGHT_MOTOR_IN4, OUTPUT); + + // Ultrasonic pins + pinMode(ULTRASONIC_TRIG, OUTPUT); + pinMode(ULTRASONIC_ECHO, INPUT); + + // LED + pinMode(LED_PIN, OUTPUT); + + // Buttons + btnForward.begin(); + btnLeft.begin(); + btnRight.begin(); + + stopMotors(); + + Serial.println(F("=== Robot Controller ===")); + Serial.println(F("States: IDLE, FORWARD, LEFT, RIGHT, OBSTACLE")); +} + +void updateState(RobotState newState) { + state = newState; + stateStartTime = millis(); + Serial.print(F("State: ")); + Serial.println(newState); +} + +void setMotors(int leftSpeed, int rightSpeed) { + // Left motor + if (leftSpeed > 0) { + digitalWrite(LEFT_MOTOR_IN1, HIGH); + digitalWrite(LEFT_MOTOR_IN2, LOW); + analogWrite(LEFT_MOTOR_ENA, abs(leftSpeed)); + } else if (leftSpeed < 0) { + digitalWrite(LEFT_MOTOR_IN1, LOW); + digitalWrite(LEFT_MOTOR_IN2, HIGH); + analogWrite(LEFT_MOTOR_ENA, abs(leftSpeed)); + } else { + digitalWrite(LEFT_MOTOR_IN1, LOW); + digitalWrite(LEFT_MOTOR_IN2, LOW); + analogWrite(LEFT_MOTOR_ENA, 0); + } + + // Right motor + if (rightSpeed > 0) { + digitalWrite(RIGHT_MOTOR_IN3, HIGH); + digitalWrite(RIGHT_MOTOR_IN4, LOW); + analogWrite(RIGHT_MOTOR_ENB, abs(rightSpeed)); + } else if (rightSpeed < 0) { + digitalWrite(RIGHT_MOTOR_IN3, LOW); + digitalWrite(RIGHT_MOTOR_IN4, HIGH); + analogWrite(RIGHT_MOTOR_ENB, abs(rightSpeed)); + } else { + digitalWrite(RIGHT_MOTOR_IN3, LOW); + digitalWrite(RIGHT_MOTOR_IN4, LOW); + analogWrite(RIGHT_MOTOR_ENB, 0); + } +} + +void stopMotors() { + setMotors(0, 0); +} + +int readUltrasonicCm() { + digitalWrite(ULTRASONIC_TRIG, LOW); + delayMicroseconds(2); + digitalWrite(ULTRASONIC_TRIG, HIGH); + delayMicroseconds(10); + digitalWrite(ULTRASONIC_TRIG, LOW); + + long duration = pulseIn(ULTRASONIC_ECHO, HIGH, 30000); // 30ms timeout + if (duration == 0) return 999; // No echo + + return duration * 0.034 / 2; // Convert to cm +} + +void loop() { + // Task 1: Read distance sensor + if (distanceTimer.check()) { + distanceCm = readUltrasonicCm(); + } + + // Task 2: Check buttons + if (btnForward.pressed()) { + updateState(MOVING_FORWARD); + } + if (btnLeft.pressed()) { + updateState(TURNING_LEFT); + } + if (btnRight.pressed()) { + updateState(TURNING_RIGHT); + } + + // Task 3: State machine + unsigned long elapsed = millis() - stateStartTime; + + switch (state) { + case IDLE: + stopMotors(); + digitalWrite(LED_PIN, LOW); + break; + + case MOVING_FORWARD: + setMotors(MOTOR_SPEED, MOTOR_SPEED); + digitalWrite(LED_PIN, HIGH); + + // Check for obstacle + if (distanceCm < OBSTACLE_THRESHOLD_CM) { + updateState(OBSTACLE_DETECTED); + } + break; + + case OBSTACLE_DETECTED: + stopMotors(); + digitalWrite(LED_PIN, (millis() % 200) < 100); // Blink fast + + if (elapsed >= 500) { + // Turn right to avoid obstacle + updateState(TURNING_RIGHT); + } + break; + + case TURNING_LEFT: + setMotors(-MOTOR_SPEED, MOTOR_SPEED); // Left backward, right forward + digitalWrite(LED_PIN, (millis() % 500) < 250); // Blink + + if (elapsed >= TURN_DURATION_MS) { + updateState(IDLE); + } + break; + + case TURNING_RIGHT: + setMotors(MOTOR_SPEED, -MOTOR_SPEED); // Left forward, right backward + digitalWrite(LED_PIN, (millis() % 500) < 250); // Blink + + if (elapsed >= TURN_DURATION_MS) { + updateState(IDLE); + } + break; + } + + // Debug output + if (Serial.available() && Serial.read() == 'd') { + Serial.print(F("Distance: ")); + Serial.print(distanceCm); + Serial.print(F("cm | State: ")); + Serial.println(state); + } +} +``` + +**Upload Instructions:** +1. Select board: Tools β†’ Board β†’ Arduino UNO +2. Select port: Tools β†’ Port β†’ (your Arduino's port) +3. Upload sketch +4. Power robot with battery (NOT USB) + +**Usage:** +- Press **Forward button** to start moving forward +- Press **Left button** to turn left for 800ms, then stop +- Press **Right button** to turn right for 800ms, then stop +- Robot automatically stops and turns right when obstacle detected (<20cm) +- LED blinks during turns, solid during forward movement + +**Safety:** +- ALWAYS test with wheels off the ground first +- Ensure battery voltage is appropriate (7.4V recommended) +- Emergency stop: remove battery or press Reset button +- Add battery voltage monitoring for low-battery warning + +**Expected Serial Output:** +``` +=== Robot Controller === +States: IDLE, FORWARD, LEFT, RIGHT, OBSTACLE +State: 1 +State: 4 +Distance: 15cm | State: 4 +State: 3 +State: 0 +``` + +**Troubleshooting:** +- **Motors not spinning:** Check L298N wiring, ensure battery connected +- **Motors spin slowly:** Battery voltage too low (need 7.4V minimum) +- **Ultrasonic returns 999:** Check TRIG/ECHO wiring, ensure 5V power +- **Robot turns wrong direction:** Swap IN1/IN2 or IN3/IN4 connections +- **Buttons don't work:** Verify INPUT_PULLUP, check button wiring diff --git a/.agents/skills/arduino-project-builder/references/project-environmental-monitor.md b/.agents/skills/arduino-project-builder/references/project-environmental-monitor.md new file mode 100644 index 0000000..addf8de --- /dev/null +++ b/.agents/skills/arduino-project-builder/references/project-environmental-monitor.md @@ -0,0 +1,290 @@ +# Environmental Monitor Project + +**Description:** Multi-sensor data logger for temperature, humidity, and light levels with SD card storage and real-time Serial output. Perfect for greenhouse monitoring, weather stations, or indoor climate tracking. + +**Hardware Requirements:** +- Arduino UNO or ESP32 +- DHT22 temperature/humidity sensor +- Photoresistor (light sensor) + 10kΞ© resistor +- SD card module (optional) +- Pushbutton + 10kΞ© pulldown resistor +- LED (status indicator) + +**Wiring Diagram:** +``` +DHT22: + VCC β†’ 5V (or 3.3V for ESP32) + DATA β†’ Pin 2 + GND β†’ GND + +Photoresistor: + One leg β†’ 5V + Other leg β†’ A0 and 10kΞ© resistor to GND + +Button: + One leg β†’ Pin 3 + Other leg β†’ GND (use INPUT_PULLUP) + +LED: + Anode (+) β†’ Pin 13 β†’ 220Ξ© resistor + Cathode (-) β†’ GND + +SD Card Module (optional): + CS β†’ Pin 10 + MOSI β†’ Pin 11 + MISO β†’ Pin 12 + SCK β†’ Pin 13 + VCC β†’ 5V + GND β†’ GND +``` + +**Features:** +- Non-blocking sensor reads (DHT22 every 2s, light every 1s) +- Moving average filter for light sensor (reduces noise) +- CSV logging to Serial and SD card +- Button toggles logging on/off +- LED heartbeat (system alive indicator) +- Memory-safe on Arduino UNO (2KB SRAM) + +**Complete Code:** + +```cpp +// config.h +#if defined(ARDUINO_AVR_UNO) + #define BOARD_TYPE "Arduino UNO" + #define SERIAL_BAUD 9600 + #define USE_SD_CARD false // Limited SRAM on UNO +#elif defined(ESP32) + #define BOARD_TYPE "ESP32" + #define SERIAL_BAUD 115200 + #define USE_SD_CARD true // ESP32 has plenty of RAM +#endif + +#define DHT_PIN 2 +#define LIGHT_PIN A0 +#define BUTTON_PIN 3 +#define LED_PIN 13 + +// main.ino +#include +#if USE_SD_CARD +#include +#endif + +DHT dht(DHT_PIN, DHT22); + +// EveryMs timer class +class EveryMs { +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {} + bool check() { + unsigned long now = millis(); + if (now - lastTrigger >= interval) { + lastTrigger = now; + return true; + } + return false; + } +}; + +// Moving average filter for light sensor +class MovingAverageFilter { +private: + static const uint8_t SIZE = 10; + int values[SIZE]; + uint8_t index; + uint8_t count; +public: + MovingAverageFilter() : index(0), count(0) { + for (uint8_t i = 0; i < SIZE; i++) values[i] = 0; + } + + int filter(int newValue) { + values[index] = newValue; + index = (index + 1) % SIZE; + if (count < SIZE) count++; + + long sum = 0; + for (uint8_t i = 0; i < count; i++) { + sum += values[i]; + } + return sum / count; + } +}; + +// Debounced button +class DebouncedButton { +private: + uint8_t pin; + bool lastState; + unsigned long lastDebounceTime; + static const unsigned long DEBOUNCE_DELAY = 50; +public: + DebouncedButton(uint8_t p) : pin(p), lastState(HIGH), lastDebounceTime(0) {} + + void begin() { + pinMode(pin, INPUT_PULLUP); + } + + bool pressed() { + bool currentState = digitalRead(pin); + if (currentState != lastState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) { + if (currentState == LOW && lastState == HIGH) { + lastState = currentState; + return true; + } + } + lastState = currentState; + return false; + } +}; + +// Global objects +MovingAverageFilter lightFilter; +DebouncedButton button(BUTTON_PIN); +EveryMs dhtTimer(2000); +EveryMs lightTimer(1000); +EveryMs displayTimer(5000); +EveryMs csvLogTimer(60000); +EveryMs heartbeatTimer(2000); + +struct SensorData { + float temperature; + float humidity; + int lightLevel; + bool valid; +} data; + +bool loggingEnabled = true; + +#if USE_SD_CARD +File dataFile; +const char* LOG_FILE = "envlog.csv"; +#endif + +void setup() { + Serial.begin(SERIAL_BAUD); + pinMode(LED_PIN, OUTPUT); + button.begin(); + dht.begin(); + + Serial.println(F("=== Environmental Monitor ===")); + Serial.print(F("Board: ")); + Serial.println(F(BOARD_TYPE)); + + #if USE_SD_CARD + if (SD.begin(10)) { + Serial.println(F("SD card ready")); + if (!SD.exists(LOG_FILE)) { + dataFile = SD.open(LOG_FILE, FILE_WRITE); + if (dataFile) { + dataFile.println(F("Time_ms,Temp_C,Humidity_%,Light")); + dataFile.close(); + } + } + } else { + Serial.println(F("SD card init failed")); + } + #endif + + Serial.println(F("Time_ms,Temp_C,Humidity_%,Light")); + data.valid = false; +} + +void loop() { + // Task 1: Read DHT22 + if (dhtTimer.check()) { + data.temperature = dht.readTemperature(); + data.humidity = dht.readHumidity(); + data.valid = !isnan(data.temperature) && !isnan(data.humidity); + + if (!data.valid) { + Serial.println(F("DHT22 read error")); + } + } + + // Task 2: Read light sensor + if (lightTimer.check()) { + int rawLight = analogRead(LIGHT_PIN); + data.lightLevel = lightFilter.filter(rawLight); + } + + // Task 3: Display summary + if (displayTimer.check() && data.valid) { + Serial.print(F("Temp: ")); + Serial.print(data.temperature, 1); + Serial.print(F("Β°C | Humidity: ")); + Serial.print(data.humidity, 1); + Serial.print(F("% | Light: ")); + Serial.println(data.lightLevel); + } + + // Task 4: Log CSV + if (csvLogTimer.check() && loggingEnabled && data.valid) { + String csvLine = String(millis()) + "," + + String(data.temperature, 1) + "," + + String(data.humidity, 1) + "," + + String(data.lightLevel); + + Serial.println(csvLine); + + #if USE_SD_CARD + dataFile = SD.open(LOG_FILE, FILE_WRITE); + if (dataFile) { + dataFile.println(csvLine); + dataFile.close(); + } + #endif + } + + // Task 5: Button toggle + if (button.pressed()) { + loggingEnabled = !loggingEnabled; + Serial.print(F("Logging: ")); + Serial.println(loggingEnabled ? F("ON") : F("OFF")); + } + + // Task 6: Heartbeat LED + if (heartbeatTimer.check()) { + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); + } +} +``` + +**Upload Instructions:** +1. Install DHT sensor library: Sketch β†’ Include Library β†’ Manage Libraries β†’ Search "DHT sensor library" β†’ Install +2. Select board: Tools β†’ Board β†’ Arduino UNO (or ESP32 Dev Module) +3. Select port: Tools β†’ Port β†’ (your Arduino's port) +4. Upload sketch + +**Usage:** +- System starts logging immediately +- Press button to toggle logging on/off +- Send 'd' via Serial Monitor to dump all data (if SD card enabled) +- LED blinks every 2 seconds (heartbeat) + +**Expected Output:** +``` +=== Environmental Monitor === +Board: Arduino UNO +Time_ms,Temp_C,Humidity_%,Light +Temp: 23.5Β°C | Humidity: 45.2% | Light: 512 +60000,23.5,45.2,512 +120000,23.6,45.0,510 +Logging: OFF +Logging: ON +180000,23.4,45.3,515 +``` + +**Troubleshooting:** +- **DHT22 read error:** Check wiring, ensure VCC is 5V (3.3V for ESP32) +- **Light sensor reads 0 or 1023:** Check photoresistor orientation, verify 10kΞ© resistor +- **SD card init failed:** Check wiring, try different SD card (FAT32 formatted) +- **Button not responding:** Verify INPUT_PULLUP mode, check button wiring diff --git a/.agents/skills/arduino-project-builder/references/project-iot-device.md b/.agents/skills/arduino-project-builder/references/project-iot-device.md new file mode 100644 index 0000000..f930793 --- /dev/null +++ b/.agents/skills/arduino-project-builder/references/project-iot-device.md @@ -0,0 +1,350 @@ +# IoT Temperature & Humidity Logger (ESP32) + +**Description:** ESP32-based WiFi data logger that publishes temperature and humidity readings to MQTT broker. Ideal for remote environmental monitoring, smart home integration, or IoT sensor networks. + +**Hardware Requirements:** +- ESP32 development board (DevKit, WROOM, or similar) +- DHT22 temperature/humidity sensor +- LED (status indicator) +- Pushbutton (WiFi reconnect trigger) +- USB cable (programming and power) + +**Wiring Diagram:** +``` +DHT22: + VCC β†’ 3.3V (ESP32 uses 3.3V logic) + DATA β†’ GPIO 4 + GND β†’ GND + +Button: + One leg β†’ GPIO 0 (BOOT button can also be used) + Other leg β†’ GND (INPUT_PULLUP) + +LED: + GPIO 2 β†’ 220Ξ© resistor β†’ LED anode + LED cathode β†’ GND +``` + +**Features:** +- WiFi connection with auto-reconnect +- MQTT publish every 60 seconds +- JSON payload format +- OTA (Over-The-Air) updates support +- NTP time synchronization +- Non-blocking sensor reads +- Button triggers manual WiFi reconnect +- Built-in LED status indicators + +**Complete Code:** + +```cpp +// config.h +#define WIFI_SSID "YourWiFiSSID" +#define WIFI_PASSWORD "YourWiFiPassword" +#define MQTT_BROKER "192.168.1.100" // Your MQTT broker IP +#define MQTT_PORT 1883 +#define MQTT_TOPIC "home/esp32/sensor" +#define MQTT_CLIENT_ID "ESP32_TempHumidity" + +#define DHT_PIN 4 +#define BUTTON_PIN 0 +#define LED_PIN 2 + +// main.ino +#include +#include +#include +#include + +DHT dht(DHT_PIN, DHT22); +WiFiClient wifiClient; +PubSubClient mqttClient(wifiClient); + +// EveryMs timer +class EveryMs { +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {} + bool check() { + unsigned long now = millis(); + if (now - lastTrigger >= interval) { + lastTrigger = now; + return true; + } + return false; + } + void reset() { lastTrigger = millis(); } +}; + +// Debounced button +class DebouncedButton { +private: + uint8_t pin; + bool lastState; + unsigned long lastDebounceTime; +public: + DebouncedButton(uint8_t p) : pin(p), lastState(HIGH), lastDebounceTime(0) {} + + void begin() { + pinMode(pin, INPUT_PULLUP); + } + + bool pressed() { + bool currentState = digitalRead(pin); + if (currentState != lastState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > 50) { + if (currentState == LOW && lastState == HIGH) { + lastState = currentState; + return true; + } + } + lastState = currentState; + return false; + } +}; + +// Global objects +DebouncedButton button(BUTTON_PIN); +EveryMs dhtTimer(2000); +EveryMs publishTimer(60000); +EveryMs wifiCheckTimer(10000); + +struct SensorData { + float temperature; + float humidity; + bool valid; +} data; + +void setup() { + Serial.begin(115200); + pinMode(LED_PIN, OUTPUT); + button.begin(); + dht.begin(); + + Serial.println(F("\n=== ESP32 IoT Logger ===")); + + connectWiFi(); + + mqttClient.setServer(MQTT_BROKER, MQTT_PORT); + mqttClient.setCallback(mqttCallback); + + connectMQTT(); + + data.valid = false; +} + +void connectWiFi() { + Serial.print(F("Connecting to WiFi")); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 20) { + delay(500); + Serial.print("."); + digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Blink during connection + attempts++; + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.println(F("\nWiFi connected!")); + Serial.print(F("IP: ")); + Serial.println(WiFi.localIP()); + digitalWrite(LED_PIN, HIGH); // Solid LED when connected + } else { + Serial.println(F("\nWiFi connection failed!")); + digitalWrite(LED_PIN, LOW); + } +} + +void connectMQTT() { + if (WiFi.status() != WL_CONNECTED) return; + + Serial.print(F("Connecting to MQTT broker...")); + + int attempts = 0; + while (!mqttClient.connected() && attempts < 3) { + if (mqttClient.connect(MQTT_CLIENT_ID)) { + Serial.println(F("connected!")); + mqttClient.subscribe(MQTT_TOPIC "/command"); + return; + } + Serial.print("."); + delay(1000); + attempts++; + } + + if (!mqttClient.connected()) { + Serial.println(F("\nMQTT connection failed!")); + } +} + +void mqttCallback(char* topic, byte* payload, unsigned int length) { + Serial.print(F("MQTT message: ")); + for (unsigned int i = 0; i < length; i++) { + Serial.print((char)payload[i]); + } + Serial.println(); +} + +void publishSensorData() { + if (!data.valid || !mqttClient.connected()) return; + + // Create JSON payload + StaticJsonDocument<200> doc; + doc["temperature"] = data.temperature; + doc["humidity"] = data.humidity; + doc["uptime"] = millis() / 1000; + doc["rssi"] = WiFi.RSSI(); + + char jsonBuffer[200]; + serializeJson(doc, jsonBuffer); + + if (mqttClient.publish(MQTT_TOPIC, jsonBuffer)) { + Serial.print(F("Published: ")); + Serial.println(jsonBuffer); + } else { + Serial.println(F("Publish failed!")); + } +} + +void loop() { + // Task 1: Read DHT22 + if (dhtTimer.check()) { + data.temperature = dht.readTemperature(); + data.humidity = dht.readHumidity(); + data.valid = !isnan(data.temperature) && !isnan(data.humidity); + + if (data.valid) { + Serial.print(F("Temp: ")); + Serial.print(data.temperature, 1); + Serial.print(F("Β°C | Humidity: ")); + Serial.print(data.humidity, 1); + Serial.println(F("%")); + } else { + Serial.println(F("DHT22 read error")); + } + } + + // Task 2: Publish to MQTT + if (publishTimer.check()) { + publishSensorData(); + } + + // Task 3: WiFi reconnect check + if (wifiCheckTimer.check()) { + if (WiFi.status() != WL_CONNECTED) { + Serial.println(F("WiFi disconnected, reconnecting...")); + connectWiFi(); + } + + if (!mqttClient.connected()) { + Serial.println(F("MQTT disconnected, reconnecting...")); + connectMQTT(); + } + } + + // Task 4: Button manual reconnect + if (button.pressed()) { + Serial.println(F("Manual reconnect triggered")); + connectWiFi(); + connectMQTT(); + } + + // Task 5: MQTT loop (handle incoming messages) + mqttClient.loop(); + + // Task 6: LED heartbeat (blink every 2s when connected) + if (WiFi.status() == WL_CONNECTED) { + digitalWrite(LED_PIN, (millis() % 2000) < 100); + } else { + digitalWrite(LED_PIN, LOW); + } +} +``` + +**Upload Instructions:** +1. Install libraries: + - PubSubClient (by Nick O'Leary) + - DHT sensor library (by Adafruit) + - ArduinoJson (by Benoit Blanchon) +2. Edit config.h: Set your WiFi SSID, password, and MQTT broker IP +3. Select board: Tools β†’ Board β†’ ESP32 Dev Module +4. Select port: Tools β†’ Port β†’ (your ESP32's port) +5. Upload sketch + +**MQTT Broker Setup:** +Install Mosquitto MQTT broker on your server: +```bash +# Ubuntu/Debian +sudo apt-get install mosquitto mosquitto-clients + +# Start broker +sudo systemctl start mosquitto + +# Test subscription +mosquitto_sub -h localhost -t "home/esp32/sensor" +``` + +**Usage:** +- System connects to WiFi on boot +- DHT22 readings every 2 seconds (local) +- MQTT publish every 60 seconds +- Press button to force WiFi/MQTT reconnect +- LED heartbeat when connected, off when disconnected + +**JSON Payload Format:** +```json +{ + "temperature": 23.5, + "humidity": 45.2, + "uptime": 3600, + "rssi": -65 +} +``` + +**Expected Serial Output:** +``` +=== ESP32 IoT Logger === +Connecting to WiFi......... +WiFi connected! +IP: 192.168.1.42 +Connecting to MQTT broker...connected! +Temp: 23.5Β°C | Humidity: 45.2% +Published: {"temperature":23.5,"humidity":45.2,"uptime":60,"rssi":-65} +Temp: 23.6Β°C | Humidity: 45.0% +Published: {"temperature":23.6,"humidity":45.0,"uptime":120,"rssi":-67} +``` + +**Home Assistant Integration:** +Add to `configuration.yaml`: +```yaml +sensor: + - platform: mqtt + name: "ESP32 Temperature" + state_topic: "home/esp32/sensor" + unit_of_measurement: "Β°C" + value_template: "{{ value_json.temperature }}" + + - platform: mqtt + name: "ESP32 Humidity" + state_topic: "home/esp32/sensor" + unit_of_measurement: "%" + value_template: "{{ value_json.humidity }}" +``` + +**Troubleshooting:** +- **WiFi won't connect:** Check SSID/password, ensure 2.4GHz network (ESP32 doesn't support 5GHz) +- **MQTT connection failed:** Verify broker IP, ensure port 1883 is open, check firewall +- **DHT22 read error:** Use 3.3V (not 5V), check DATA pin connection +- **JSON publish failed:** Increase document size in StaticJsonDocument<200> +- **Memory issues:** ESP32 has 327KB SRAM, no worries about overflow + +**Power Consumption:** +- Active (WiFi on): ~160mA +- Light sleep: ~20mA +- Deep sleep: ~10ΞΌA (add sleep mode for battery operation) diff --git a/.agents/skills/arduino-project-builder/references/project-robot-controller.md b/.agents/skills/arduino-project-builder/references/project-robot-controller.md new file mode 100644 index 0000000..c3c4228 --- /dev/null +++ b/.agents/skills/arduino-project-builder/references/project-robot-controller.md @@ -0,0 +1,339 @@ +# Robot Controller Project + +**Description:** Button-controlled robot with obstacle avoidance using ultrasonic sensor and state machine architecture. Supports forward, turn left, turn right, and emergency stop modes. + +**Hardware Requirements:** +- Arduino UNO or ESP32 +- L298N motor driver module +- 2x DC motors with wheels +- HC-SR04 ultrasonic distance sensor +- 3x pushbuttons (forward, left, right) +- Battery pack (7.4V LiPo recommended) +- LED (status indicator) + +**Wiring Diagram:** +``` +L298N Motor Driver: + ENA β†’ Pin 9 (PWM for left motor speed) + IN1 β†’ Pin 8 (left motor direction 1) + IN2 β†’ Pin 7 (left motor direction 2) + ENB β†’ Pin 6 (PWM for right motor speed) + IN3 β†’ Pin 5 (right motor direction 1) + IN4 β†’ Pin 4 (right motor direction 2) + VCC β†’ Battery + (7.4V) + GND β†’ Arduino GND and Battery - + 5V β†’ Arduino VIN (regulated 5V output from L298N) + +HC-SR04 Ultrasonic: + VCC β†’ 5V + TRIG β†’ Pin 11 + ECHO β†’ Pin 10 + GND β†’ GND + +Buttons: + Forward button β†’ Pin 2 (INPUT_PULLUP) + Left button β†’ Pin 3 (INPUT_PULLUP) + Right button β†’ Pin 12 (INPUT_PULLUP) + +LED: + Pin 13 β†’ 220Ξ© resistor β†’ LED anode + LED cathode β†’ GND +``` + +**Features:** +- State machine with 5 states (IDLE, FORWARD, TURN_LEFT, TURN_RIGHT, OBSTACLE_DETECTED) +- Autonomous obstacle avoidance (stops at 20cm, turns right) +- Button override (manual control) +- PWM motor speed control +- Non-blocking ultrasonic sensor reads +- Emergency stop on low battery (if voltage sensor added) + +**Complete Code:** + +```cpp +// config.h +#define LEFT_MOTOR_ENA 9 +#define LEFT_MOTOR_IN1 8 +#define LEFT_MOTOR_IN2 7 +#define RIGHT_MOTOR_ENB 6 +#define RIGHT_MOTOR_IN3 5 +#define RIGHT_MOTOR_IN4 4 + +#define ULTRASONIC_TRIG 11 +#define ULTRASONIC_ECHO 10 + +#define BUTTON_FORWARD 2 +#define BUTTON_LEFT 3 +#define BUTTON_RIGHT 12 + +#define LED_PIN 13 + +#define OBSTACLE_THRESHOLD_CM 20 +#define MOTOR_SPEED 200 // 0-255 (PWM) +#define TURN_DURATION_MS 800 + +// main.ino + +// Robot state machine +enum RobotState { + IDLE, + MOVING_FORWARD, + TURNING_LEFT, + TURNING_RIGHT, + OBSTACLE_DETECTED +}; + +RobotState state = IDLE; +unsigned long stateStartTime = 0; + +// EveryMs timer +class EveryMs { +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {} + bool check() { + unsigned long now = millis(); + if (now - lastTrigger >= interval) { + lastTrigger = now; + return true; + } + return false; + } +}; + +// Debounced button +class DebouncedButton { +private: + uint8_t pin; + bool lastState; + unsigned long lastDebounceTime; +public: + DebouncedButton(uint8_t p) : pin(p), lastState(HIGH), lastDebounceTime(0) {} + + void begin() { + pinMode(pin, INPUT_PULLUP); + } + + bool pressed() { + bool currentState = digitalRead(pin); + if (currentState != lastState) { + lastDebounceTime = millis(); + } + + if ((millis() - lastDebounceTime) > 50) { + if (currentState == LOW && lastState == HIGH) { + lastState = currentState; + return true; + } + } + lastState = currentState; + return false; + } +}; + +// Global objects +DebouncedButton btnForward(BUTTON_FORWARD); +DebouncedButton btnLeft(BUTTON_LEFT); +DebouncedButton btnRight(BUTTON_RIGHT); +EveryMs distanceTimer(100); // Check distance every 100ms + +int distanceCm = 999; + +void setup() { + Serial.begin(9600); + + // Motor pins + pinMode(LEFT_MOTOR_ENA, OUTPUT); + pinMode(LEFT_MOTOR_IN1, OUTPUT); + pinMode(LEFT_MOTOR_IN2, OUTPUT); + pinMode(RIGHT_MOTOR_ENB, OUTPUT); + pinMode(RIGHT_MOTOR_IN3, OUTPUT); + pinMode(RIGHT_MOTOR_IN4, OUTPUT); + + // Ultrasonic pins + pinMode(ULTRASONIC_TRIG, OUTPUT); + pinMode(ULTRASONIC_ECHO, INPUT); + + // LED + pinMode(LED_PIN, OUTPUT); + + // Buttons + btnForward.begin(); + btnLeft.begin(); + btnRight.begin(); + + stopMotors(); + + Serial.println(F("=== Robot Controller ===")); + Serial.println(F("States: IDLE, FORWARD, LEFT, RIGHT, OBSTACLE")); +} + +void updateState(RobotState newState) { + state = newState; + stateStartTime = millis(); + Serial.print(F("State: ")); + Serial.println(newState); +} + +void setMotors(int leftSpeed, int rightSpeed) { + // Left motor + if (leftSpeed > 0) { + digitalWrite(LEFT_MOTOR_IN1, HIGH); + digitalWrite(LEFT_MOTOR_IN2, LOW); + analogWrite(LEFT_MOTOR_ENA, abs(leftSpeed)); + } else if (leftSpeed < 0) { + digitalWrite(LEFT_MOTOR_IN1, LOW); + digitalWrite(LEFT_MOTOR_IN2, HIGH); + analogWrite(LEFT_MOTOR_ENA, abs(leftSpeed)); + } else { + digitalWrite(LEFT_MOTOR_IN1, LOW); + digitalWrite(LEFT_MOTOR_IN2, LOW); + analogWrite(LEFT_MOTOR_ENA, 0); + } + + // Right motor + if (rightSpeed > 0) { + digitalWrite(RIGHT_MOTOR_IN3, HIGH); + digitalWrite(RIGHT_MOTOR_IN4, LOW); + analogWrite(RIGHT_MOTOR_ENB, abs(rightSpeed)); + } else if (rightSpeed < 0) { + digitalWrite(RIGHT_MOTOR_IN3, LOW); + digitalWrite(RIGHT_MOTOR_IN4, HIGH); + analogWrite(RIGHT_MOTOR_ENB, abs(rightSpeed)); + } else { + digitalWrite(RIGHT_MOTOR_IN3, LOW); + digitalWrite(RIGHT_MOTOR_IN4, LOW); + analogWrite(RIGHT_MOTOR_ENB, 0); + } +} + +void stopMotors() { + setMotors(0, 0); +} + +int readUltrasonicCm() { + digitalWrite(ULTRASONIC_TRIG, LOW); + delayMicroseconds(2); + digitalWrite(ULTRASONIC_TRIG, HIGH); + delayMicroseconds(10); + digitalWrite(ULTRASONIC_TRIG, LOW); + + long duration = pulseIn(ULTRASONIC_ECHO, HIGH, 30000); // 30ms timeout + if (duration == 0) return 999; // No echo + + return duration * 0.034 / 2; // Convert to cm +} + +void loop() { + // Task 1: Read distance sensor + if (distanceTimer.check()) { + distanceCm = readUltrasonicCm(); + } + + // Task 2: Check buttons + if (btnForward.pressed()) { + updateState(MOVING_FORWARD); + } + if (btnLeft.pressed()) { + updateState(TURNING_LEFT); + } + if (btnRight.pressed()) { + updateState(TURNING_RIGHT); + } + + // Task 3: State machine + unsigned long elapsed = millis() - stateStartTime; + + switch (state) { + case IDLE: + stopMotors(); + digitalWrite(LED_PIN, LOW); + break; + + case MOVING_FORWARD: + setMotors(MOTOR_SPEED, MOTOR_SPEED); + digitalWrite(LED_PIN, HIGH); + + // Check for obstacle + if (distanceCm < OBSTACLE_THRESHOLD_CM) { + updateState(OBSTACLE_DETECTED); + } + break; + + case OBSTACLE_DETECTED: + stopMotors(); + digitalWrite(LED_PIN, (millis() % 200) < 100); // Blink fast + + if (elapsed >= 500) { + // Turn right to avoid obstacle + updateState(TURNING_RIGHT); + } + break; + + case TURNING_LEFT: + setMotors(-MOTOR_SPEED, MOTOR_SPEED); // Left backward, right forward + digitalWrite(LED_PIN, (millis() % 500) < 250); // Blink + + if (elapsed >= TURN_DURATION_MS) { + updateState(IDLE); + } + break; + + case TURNING_RIGHT: + setMotors(MOTOR_SPEED, -MOTOR_SPEED); // Left forward, right backward + digitalWrite(LED_PIN, (millis() % 500) < 250); // Blink + + if (elapsed >= TURN_DURATION_MS) { + updateState(IDLE); + } + break; + } + + // Debug output + if (Serial.available() && Serial.read() == 'd') { + Serial.print(F("Distance: ")); + Serial.print(distanceCm); + Serial.print(F("cm | State: ")); + Serial.println(state); + } +} +``` + +**Upload Instructions:** +1. Select board: Tools β†’ Board β†’ Arduino UNO +2. Select port: Tools β†’ Port β†’ (your Arduino's port) +3. Upload sketch +4. Power robot with battery (NOT USB) + +**Usage:** +- Press **Forward button** to start moving forward +- Press **Left button** to turn left for 800ms, then stop +- Press **Right button** to turn right for 800ms, then stop +- Robot automatically stops and turns right when obstacle detected (<20cm) +- LED blinks during turns, solid during forward movement + +**Safety:** +- ALWAYS test with wheels off the ground first +- Ensure battery voltage is appropriate (7.4V recommended) +- Emergency stop: remove battery or press Reset button +- Add battery voltage monitoring for low-battery warning + +**Expected Serial Output:** +``` +=== Robot Controller === +States: IDLE, FORWARD, LEFT, RIGHT, OBSTACLE +State: 1 +State: 4 +Distance: 15cm | State: 4 +State: 3 +State: 0 +``` + +**Troubleshooting:** +- **Motors not spinning:** Check L298N wiring, ensure battery connected +- **Motors spin slowly:** Battery voltage too low (need 7.4V minimum) +- **Ultrasonic returns 999:** Check TRIG/ECHO wiring, ensure 5V power +- **Robot turns wrong direction:** Swap IN1/IN2 or IN3/IN4 connections +- **Buttons don't work:** Verify INPUT_PULLUP, check button wiring diff --git a/.agents/skills/arduino-project-builder/rules/board-considerations.md b/.agents/skills/arduino-project-builder/rules/board-considerations.md new file mode 100644 index 0000000..6de01a7 --- /dev/null +++ b/.agents/skills/arduino-project-builder/rules/board-considerations.md @@ -0,0 +1,105 @@ +# Board-Specific Considerations + +Hardware and software optimizations for different Arduino-compatible boards. + +## Arduino UNO/Nano (ATmega328P) + +### Memory Constraints +- **SRAM:** 2KB total - Keep arrays small, monitor usage constantly +- **Flash:** 32KB - Code size limited, optimize for space +- **EEPROM:** 1KB - Use sparingly, implement wear leveling + +### Hardware Features +- **ADC:** 10-bit resolution (0-1023 range) +- **PWM:** 6 pins (3, 5, 6, 9, 10, 11) +- **Interrupts:** Only 2 external (pins 2, 3) +- **I2C:** Software implementation, slower than hardware + +### Optimization Strategies +- Use F() macro for all string literals to save RAM +- Avoid large arrays and complex data structures +- Implement simple filtering algorithms +- Use interrupt-driven inputs sparingly + +### Communication +- No built-in WiFi/Bluetooth +- Serial communication at 9600 baud recommended +- External modules required for wireless connectivity + +## ESP32 (ESP32-WROOM-32) + +### Memory Resources +- **SRAM:** 327KB+ - Can use large buffers and complex structures +- **Flash:** 4MB+ - Ample space for code and data +- **PSRAM:** Optional external RAM for large datasets + +### Hardware Features +- **ADC:** 12-bit resolution (0-4095 range), 18 channels +- **PWM:** 16 channels with adjustable frequency +- **Interrupts:** Multiple GPIO pins support interrupts +- **I2C/SPI:** Hardware accelerated, multiple buses + +### Advanced Capabilities +- **Dual-core:** Run tasks in parallel on CPU0/CPU1 +- **WiFi/Bluetooth:** Built-in 802.11 b/g/n WiFi, Bluetooth 4.2 +- **Deep sleep:** Ultra-low power consumption modes +- **RTC:** Real-time clock with battery backup + +### IoT Optimization +- Implement WiFi reconnection with exponential backoff +- Use FreeRTOS tasks for concurrent operations +- Leverage deep sleep for battery-powered applications +- Implement OTA (Over-The-Air) updates + +## Raspberry Pi Pico (RP2040) + +### Memory Resources +- **SRAM:** 262KB - Good balance between UNO and ESP32 +- **Flash:** 2MB - Sufficient for most applications +- **No EEPROM:** Use flash for persistent storage + +### Hardware Features +- **ADC:** 12-bit resolution (0-4095 range), 5 channels +- **PWM:** 16 channels, 8 slices +- **PIO:** Programmable I/O for custom protocols +- **Interrupts:** All GPIO pins support interrupts + +### Advanced Features +- **Dual-core:** Cortex-M0+ cores for parallel processing +- **PIO State Machines:** Hardware-accelerated custom protocols +- **USB:** Full-speed USB 1.1 with device/host support +- **High-speed interfaces:** SPI, I2C, UART with DMA + +### Performance Optimization +- Utilize PIO for timing-critical operations +- Implement DMA for high-speed data transfer +- Use both cores for computationally intensive tasks +- Leverage USB for high-bandwidth communication + +## Cross-Board Compatibility + +### Conditional Compilation +```cpp +#ifdef ARDUINO_AVR_UNO + // UNO-specific code +#elif defined(ESP32) + // ESP32-specific code +#elif defined(ARDUINO_ARCH_RP2040) + // Pico-specific code +#endif +``` + +### Hardware Detection +- Implement runtime board detection +- Use conditional compilation for optimal performance +- Provide fallback implementations for missing features + +### Memory Management +- Monitor memory usage across all platforms +- Implement different strategies for different memory sizes +- Use heap allocation carefully on constrained boards + +### Communication Abstraction +- Abstract communication interfaces (WiFi, Serial) +- Provide board-specific implementations +- Gracefully degrade when features unavailable \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/rules/integration-checklist.md b/.agents/skills/arduino-project-builder/rules/integration-checklist.md new file mode 100644 index 0000000..f9ce918 --- /dev/null +++ b/.agents/skills/arduino-project-builder/rules/integration-checklist.md @@ -0,0 +1,74 @@ +# Integration Checklist + +Comprehensive checklist to verify before delivering any Arduino project. + +## Sensor Integration +- [ ] All sensor readings validated (NaN checks, range checks) +- [ ] Sensor initialization successful (I2C scan, SPI connection) +- [ ] Calibration values applied correctly +- [ ] Sensor error handling implemented (timeout, retry logic) +- [ ] Multiple sensors don't interfere with each other + +## Input Handling +- [ ] Button inputs debounced (minimum 50ms debounce time) +- [ ] Interrupt pins used appropriately (UNO limited to pins 2,3) +- [ ] Analog inputs filtered for noise reduction +- [ ] Input validation prevents invalid states + +## Communication Systems +- [ ] I2C devices scanned and detected at startup +- [ ] SPI communication verified (clock polarity, phase) +- [ ] UART baud rates match between devices +- [ ] WiFi reconnection logic implemented (ESP32 projects) +- [ ] MQTT connection handling with retry logic + +## Data Management +- [ ] CSV logging includes proper headers +- [ ] Data formatting consistent (timestamps, units) +- [ ] Buffer sizes adequate for data rates +- [ ] EEPROM writes include CRC validation +- [ ] SD card file operations error-checked + +## State Machine Logic +- [ ] All states defined with clear transitions +- [ ] Default/error states implemented +- [ ] State transitions validated +- [ ] No infinite loops or deadlocks +- [ ] State persistence if required + +## Timing & Performance +- [ ] All timers use non-blocking millis() patterns +- [ ] Loop execution time within requirements +- [ ] Interrupt service routines are short +- [ ] Watchdog timer implemented for critical systems +- [ ] Power management for battery-operated devices + +## User Interface +- [ ] LED indicators for system status (heartbeat, error, active) +- [ ] Serial commands documented and implemented +- [ ] Serial baud rate matches board (9600 for UNO, 115200 for ESP32) +- [ ] User feedback for all interactive elements + +## Memory Management +- [ ] SRAM usage monitored and within limits +- [ ] String literals use F() macro to save RAM +- [ ] Dynamic memory allocation minimized +- [ ] Stack overflow protection implemented + +## Power Systems +- [ ] Power supply adequate for all components +- [ ] Brown-out detection implemented +- [ ] Sleep modes utilized for low-power applications +- [ ] Power-on reset sequence proper + +## Safety & Reliability +- [ ] Fail-safe modes for critical failures +- [ ] Watchdog timer prevents hangs +- [ ] Input validation prevents buffer overflows +- [ ] Critical operations have timeout protection + +## Documentation +- [ ] Wiring diagram complete and accurate +- [ ] Code comments explain complex logic +- [ ] Usage instructions clear and complete +- [ ] Troubleshooting guide addresses common issues \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/rules/quality-standards.md b/.agents/skills/arduino-project-builder/rules/quality-standards.md new file mode 100644 index 0000000..3fe3d30 --- /dev/null +++ b/.agents/skills/arduino-project-builder/rules/quality-standards.md @@ -0,0 +1,54 @@ +# Quality Standards + +All generated Arduino projects must adhere to these quality standards for production readiness. + +## Core Requirements + +### 1. Hardware Abstraction +- **config.h file:** Required for all projects with board detection and pin definitions +- **Conditional compilation:** Use #ifdef/#endif for board-specific code +- **Pin constants:** Define all pins as named constants, not magic numbers + +### 2. Non-blocking Design +- **No delay() calls:** Use millis() timers for all timing requirements +- **EveryMs pattern:** Implement task scheduling without blocking +- **Responsive loop:** Main loop must complete within timing constraints + +### 3. Error Handling +- **Sensor validation:** Check for NaN, out-of-range, and failure conditions +- **Graceful degradation:** Continue operation when non-critical components fail +- **Error states:** State machines must include error/default states + +### 4. Diagnostics & Monitoring +- **Serial output:** Print status messages and sensor readings +- **Heartbeat indicators:** LED blinking or serial status updates +- **Debug levels:** Different verbosity levels for troubleshooting + +### 5. Memory Safety +- **Bounds checking:** Validate array indices before access +- **CRC validation:** Use CRC for EEPROM and critical data storage +- **Memory monitoring:** Track SRAM usage on constrained boards + +### 6. Documentation Standards +- **Wiring tables:** Clear text-based pin assignment documentation +- **Code comments:** Explain complex logic and hardware interactions +- **Usage instructions:** Include setup and operation procedures + +### 7. Compilation Verification +- **Board compatibility:** Verify compilation for target board +- **Library dependencies:** Ensure all required libraries are available +- **Optimization flags:** Use appropriate compiler optimizations + +## Code Quality Metrics + +- **Cyclomatic complexity:** Keep functions under 10 complexity points +- **Function length:** Limit functions to 50 lines maximum +- **Global variables:** Minimize use, prefer local scope +- **Magic numbers:** Replace with named constants + +## Testing Requirements + +- **Unit testing:** Test individual components in isolation +- **Integration testing:** Verify component interactions +- **Performance testing:** Ensure timing requirements are met +- **Memory testing:** Validate memory usage under load \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/scripts/scaffold_project.py b/.agents/skills/arduino-project-builder/scripts/scaffold_project.py new file mode 100644 index 0000000..c9c9d39 --- /dev/null +++ b/.agents/skills/arduino-project-builder/scripts/scaffold_project.py @@ -0,0 +1,1054 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.10" +# dependencies = [] +# /// +""" +Arduino Project Scaffolding Tool + +Generate complete Arduino project structures with config.h, main.ino, +platformio.ini, and README.md from project templates. + +Usage: + uv run --no-project scripts/scaffold_project.py --type environmental --board esp32 --name "WeatherStation" + uv run --no-project scripts/scaffold_project.py --type robot --board uno --output ./my-robot + uv run --no-project scripts/scaffold_project.py --interactive + uv run --no-project scripts/scaffold_project.py --list +""" + +import argparse +import os +import sys +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Dict, List, Optional + +# ============================================================================= +# Project Templates +# ============================================================================= + +PROJECT_TYPES = { + "environmental": { + "name": "Environmental Monitor", + "description": "Multi-sensor data logger (temperature, humidity, light)", + "sensors": ["DHT22", "Photoresistor"], + "features": ["CSV logging", "SD card (optional)", "Button control", "LED status"], + "patterns": ["config", "filtering", "scheduler", "csv", "data-logging"], + "libraries": ["DHT sensor library"], + }, + "robot": { + "name": "Robot Controller", + "description": "Motor control with obstacle avoidance and state machine", + "sensors": ["Ultrasonic HC-SR04", "Line sensor (optional)"], + "actuators": ["DC motors (L298N)", "Servo"], + "features": ["State machine", "Button control", "Obstacle avoidance"], + "patterns": ["config", "buttons", "state-machine", "scheduler"], + "libraries": ["Servo"], + }, + "iot": { + "name": "IoT Data Logger", + "description": "WiFi-connected sensor with MQTT/HTTP data transmission", + "sensors": ["BME280", "DHT22 (alternative)"], + "features": ["WiFi connectivity", "MQTT publishing", "JSON formatting", "Deep sleep"], + "patterns": ["config", "hardware-detection", "scheduler", "filtering"], + "libraries": ["WiFi", "PubSubClient", "ArduinoJson"], + "board_requirement": "esp32", + }, +} + +BOARD_CONFIGS = { + "uno": { + "name": "Arduino UNO", + "platform": "atmelavr", + "board": "uno", + "framework": "arduino", + "f_cpu": "16000000L", + "baud": 9600, + "sram": 2048, + "defines": ["ARDUINO_AVR_UNO"], + }, + "esp32": { + "name": "ESP32 DevKit", + "platform": "espressif32", + "board": "esp32dev", + "framework": "arduino", + "f_cpu": "240000000L", + "baud": 115200, + "sram": 520000, + "defines": ["ESP32"], + }, + "rp2040": { + "name": "Raspberry Pi Pico", + "platform": "raspberrypi", + "board": "pico", + "framework": "arduino", + "f_cpu": "133000000L", + "baud": 115200, + "sram": 264000, + "defines": ["ARDUINO_ARCH_RP2040"], + }, +} + +# ============================================================================= +# Template Generators +# ============================================================================= + + +def generate_config_h(project_type: str, board: str, project_name: str) -> str: + """Generate config.h with board-specific settings.""" + proj = PROJECT_TYPES[project_type] + + config = f'''// config.h - Hardware configuration for {project_name} +// Project: {proj["name"]} +// Generated: {datetime.now().strftime("%Y-%m-%d")} + +#ifndef CONFIG_H +#define CONFIG_H + +// === Board Detection === +#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO) + #define BOARD_NAME "Arduino UNO" + #define LED_PIN 13 + #define BUTTON_PIN 2 + #define I2C_SDA A4 + #define I2C_SCL A5 + #define SRAM_SIZE 2048 + #define SERIAL_BAUD 9600 + #define USE_F_MACRO 1 // Use F() for string constants + +#elif defined(ESP32) + #define BOARD_NAME "ESP32" + #define LED_PIN 2 + #define BUTTON_PIN 4 + #define I2C_SDA 21 + #define I2C_SCL 22 + #define SRAM_SIZE 520000 + #define SERIAL_BAUD 115200 + #define HAS_WIFI 1 + #define HAS_BLE 1 + +#elif defined(ARDUINO_ARCH_RP2040) + #define BOARD_NAME "RP2040" + #define LED_PIN 25 + #define BUTTON_PIN 14 + #define I2C_SDA 4 + #define I2C_SCL 5 + #define SRAM_SIZE 264000 + #define SERIAL_BAUD 115200 + +#else + #error "Unsupported board! Add configuration." +#endif + +''' + + # Project-specific pins + if project_type == "environmental": + config += '''// === Environmental Monitor Pins === +#define DHT_PIN 2 +#define LIGHT_PIN A0 +#define SD_CS_PIN 10 + +// Timing intervals (ms) +#define DHT_INTERVAL 2000 // DHT22 needs 2s between reads +#define LIGHT_INTERVAL 1000 +#define LOG_INTERVAL 60000 +#define HEARTBEAT_INTERVAL 2000 + +''' + elif project_type == "robot": + config += '''// === Robot Controller Pins === +#define MOTOR_L_EN 5 +#define MOTOR_L_IN1 6 +#define MOTOR_L_IN2 7 +#define MOTOR_R_EN 9 +#define MOTOR_R_IN1 10 +#define MOTOR_R_IN2 11 + +#define ULTRASONIC_TRIG 12 +#define ULTRASONIC_ECHO 3 +#define SERVO_PIN 8 + +// Robot parameters +#define OBSTACLE_DISTANCE_CM 20 +#define MOTOR_SPEED_DEFAULT 200 +#define TURN_DURATION_MS 500 + +''' + elif project_type == "iot": + config += '''// === IoT Device Configuration === +#define DHT_PIN 4 +#define STATUS_LED 2 + +// WiFi credentials (change these!) +#define WIFI_SSID "your-wifi-ssid" +#define WIFI_PASSWORD "your-wifi-password" + +// MQTT settings +#define MQTT_SERVER "mqtt.example.com" +#define MQTT_PORT 1883 +#define MQTT_TOPIC "sensors/environmental" + +// Timing intervals (ms) +#define SENSOR_INTERVAL 30000 // Read sensors every 30s +#define PUBLISH_INTERVAL 60000 // Publish every 60s +#define WIFI_TIMEOUT 30000 // WiFi connection timeout + +''' + + config += '''// === Common Settings === +#define DEBOUNCE_MS 50 +#define FILTER_SIZE 10 + +#endif // CONFIG_H +''' + return config + + +def generate_main_ino(project_type: str, board: str, project_name: str) -> str: + """Generate main.ino for the project type.""" + proj = PROJECT_TYPES[project_type] + + if project_type == "environmental": + return f'''// {project_name} - Environmental Monitor +// {proj["description"]} +// Generated: {datetime.now().strftime("%Y-%m-%d")} + +#include "config.h" +#include + +// === Timer Class === +class EveryMs {{ +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {{}} + bool check() {{ + unsigned long now = millis(); + if (now - lastTrigger >= interval) {{ + lastTrigger = now; + return true; + }} + return false; + }} +}}; + +// === Moving Average Filter === +class MovingAverageFilter {{ +private: + static const uint8_t SIZE = FILTER_SIZE; + int values[SIZE]; + uint8_t index = 0; + uint8_t count = 0; +public: + int filter(int newValue) {{ + values[index] = newValue; + index = (index + 1) % SIZE; + if (count < SIZE) count++; + long sum = 0; + for (uint8_t i = 0; i < count; i++) sum += values[i]; + return sum / count; + }} +}}; + +// === Debounced Button === +class DebouncedButton {{ +private: + uint8_t pin; + bool lastState = HIGH; + unsigned long lastDebounce = 0; +public: + DebouncedButton(uint8_t p) : pin(p) {{}} + void begin() {{ pinMode(pin, INPUT_PULLUP); }} + bool pressed() {{ + bool state = digitalRead(pin); + if (state != lastState && (millis() - lastDebounce) > DEBOUNCE_MS) {{ + lastDebounce = millis(); + lastState = state; + return state == LOW; + }} + return false; + }} +}}; + +// === Global Objects === +DHT dht(DHT_PIN, DHT22); +MovingAverageFilter lightFilter; +DebouncedButton button(BUTTON_PIN); + +EveryMs dhtTimer(DHT_INTERVAL); +EveryMs lightTimer(LIGHT_INTERVAL); +EveryMs logTimer(LOG_INTERVAL); +EveryMs heartbeatTimer(HEARTBEAT_INTERVAL); + +struct SensorData {{ + float temperature = NAN; + float humidity = NAN; + int lightLevel = 0; +}} data; + +bool loggingEnabled = true; +bool ledState = false; + +void setup() {{ + Serial.begin(SERIAL_BAUD); + pinMode(LED_PIN, OUTPUT); + button.begin(); + dht.begin(); + + Serial.println(F("=== {project_name} ===")); + Serial.print(F("Board: ")); + Serial.println(F(BOARD_NAME)); + Serial.println(F("time_ms,temp_c,humidity_%,light")); +}} + +void loop() {{ + // Heartbeat LED + if (heartbeatTimer.check()) {{ + ledState = !ledState; + digitalWrite(LED_PIN, ledState); + }} + + // Toggle logging with button + if (button.pressed()) {{ + loggingEnabled = !loggingEnabled; + Serial.print(F("Logging: ")); + Serial.println(loggingEnabled ? F("ON") : F("OFF")); + }} + + // Read DHT22 + if (dhtTimer.check()) {{ + float t = dht.readTemperature(); + float h = dht.readHumidity(); + if (!isnan(t)) data.temperature = t; + if (!isnan(h)) data.humidity = h; + }} + + // Read light sensor + if (lightTimer.check()) {{ + data.lightLevel = lightFilter.filter(analogRead(LIGHT_PIN)); + }} + + // Log data + if (loggingEnabled && logTimer.check()) {{ + Serial.print(millis()); + Serial.print(','); + Serial.print(data.temperature, 1); + Serial.print(','); + Serial.print(data.humidity, 1); + Serial.print(','); + Serial.println(data.lightLevel); + }} +}} +''' + + elif project_type == "robot": + return f'''// {project_name} - Robot Controller +// {proj["description"]} +// Generated: {datetime.now().strftime("%Y-%m-%d")} + +#include "config.h" +#include + +// === State Machine === +enum class RobotState {{ + IDLE, + FORWARD, + TURNING_LEFT, + TURNING_RIGHT, + REVERSE, + SCANNING +}}; + +const char* stateNames[] = {{"IDLE", "FORWARD", "LEFT", "RIGHT", "REVERSE", "SCAN"}}; + +// === Timer Class === +class EveryMs {{ +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {{}} + bool check() {{ + unsigned long now = millis(); + if (now - lastTrigger >= interval) {{ + lastTrigger = now; + return true; + }} + return false; + }} + void reset() {{ lastTrigger = millis(); }} +}}; + +// === Debounced Button === +class DebouncedButton {{ +private: + uint8_t pin; + bool lastState = HIGH; + unsigned long lastDebounce = 0; +public: + DebouncedButton(uint8_t p) : pin(p) {{}} + void begin() {{ pinMode(pin, INPUT_PULLUP); }} + bool pressed() {{ + bool state = digitalRead(pin); + if (state != lastState && (millis() - lastDebounce) > DEBOUNCE_MS) {{ + lastDebounce = millis(); + lastState = state; + return state == LOW; + }} + return false; + }} +}}; + +// === Global Objects === +Servo servo; +DebouncedButton startButton(BUTTON_PIN); + +RobotState currentState = RobotState::IDLE; +unsigned long stateStartTime = 0; + +EveryMs sensorTimer(50); // Check sensors every 50ms +EveryMs statusTimer(1000); // Print status every 1s + +// === Motor Control === +void setMotors(int leftSpeed, int rightSpeed) {{ + // Left motor + if (leftSpeed >= 0) {{ + digitalWrite(MOTOR_L_IN1, HIGH); + digitalWrite(MOTOR_L_IN2, LOW); + }} else {{ + digitalWrite(MOTOR_L_IN1, LOW); + digitalWrite(MOTOR_L_IN2, HIGH); + leftSpeed = -leftSpeed; + }} + analogWrite(MOTOR_L_EN, constrain(leftSpeed, 0, 255)); + + // Right motor + if (rightSpeed >= 0) {{ + digitalWrite(MOTOR_R_IN1, HIGH); + digitalWrite(MOTOR_R_IN2, LOW); + }} else {{ + digitalWrite(MOTOR_R_IN1, LOW); + digitalWrite(MOTOR_R_IN2, HIGH); + rightSpeed = -rightSpeed; + }} + analogWrite(MOTOR_R_EN, constrain(rightSpeed, 0, 255)); +}} + +void stopMotors() {{ + setMotors(0, 0); +}} + +// === Ultrasonic Sensor === +long readDistanceCm() {{ + digitalWrite(ULTRASONIC_TRIG, LOW); + delayMicroseconds(2); + digitalWrite(ULTRASONIC_TRIG, HIGH); + delayMicroseconds(10); + digitalWrite(ULTRASONIC_TRIG, LOW); + + long duration = pulseIn(ULTRASONIC_ECHO, HIGH, 30000); + return duration * 0.034 / 2; // Convert to cm +}} + +// === State Machine === +void transitionTo(RobotState newState) {{ + if (newState != currentState) {{ + currentState = newState; + stateStartTime = millis(); + Serial.print(F("State: ")); + Serial.println(stateNames[static_cast(currentState)]); + }} +}} + +void updateStateMachine() {{ + long distance = readDistanceCm(); + unsigned long elapsed = millis() - stateStartTime; + + switch (currentState) {{ + case RobotState::IDLE: + stopMotors(); + break; + + case RobotState::FORWARD: + setMotors(MOTOR_SPEED_DEFAULT, MOTOR_SPEED_DEFAULT); + if (distance > 0 && distance < OBSTACLE_DISTANCE_CM) {{ + transitionTo(RobotState::REVERSE); + }} + break; + + case RobotState::REVERSE: + setMotors(-MOTOR_SPEED_DEFAULT/2, -MOTOR_SPEED_DEFAULT/2); + if (elapsed > 500) {{ + transitionTo(RobotState::SCANNING); + }} + break; + + case RobotState::SCANNING: + stopMotors(); + // Scan left and right, pick clearest direction + servo.write(45); + delay(200); + long leftDist = readDistanceCm(); + servo.write(135); + delay(200); + long rightDist = readDistanceCm(); + servo.write(90); + + if (leftDist > rightDist) {{ + transitionTo(RobotState::TURNING_LEFT); + }} else {{ + transitionTo(RobotState::TURNING_RIGHT); + }} + break; + + case RobotState::TURNING_LEFT: + setMotors(-MOTOR_SPEED_DEFAULT, MOTOR_SPEED_DEFAULT); + if (elapsed > TURN_DURATION_MS) {{ + transitionTo(RobotState::FORWARD); + }} + break; + + case RobotState::TURNING_RIGHT: + setMotors(MOTOR_SPEED_DEFAULT, -MOTOR_SPEED_DEFAULT); + if (elapsed > TURN_DURATION_MS) {{ + transitionTo(RobotState::FORWARD); + }} + break; + }} +}} + +void setup() {{ + Serial.begin(SERIAL_BAUD); + + // Motor pins + pinMode(MOTOR_L_EN, OUTPUT); + pinMode(MOTOR_L_IN1, OUTPUT); + pinMode(MOTOR_L_IN2, OUTPUT); + pinMode(MOTOR_R_EN, OUTPUT); + pinMode(MOTOR_R_IN1, OUTPUT); + pinMode(MOTOR_R_IN2, OUTPUT); + + // Ultrasonic pins + pinMode(ULTRASONIC_TRIG, OUTPUT); + pinMode(ULTRASONIC_ECHO, INPUT); + + // Servo + servo.attach(SERVO_PIN); + servo.write(90); + + // Button + startButton.begin(); + pinMode(LED_PIN, OUTPUT); + + Serial.println(F("=== {project_name} ===")); + Serial.println(F("Press button to start/stop")); +}} + +void loop() {{ + // Start/stop with button + if (startButton.pressed()) {{ + if (currentState == RobotState::IDLE) {{ + transitionTo(RobotState::FORWARD); + }} else {{ + transitionTo(RobotState::IDLE); + }} + }} + + // Update state machine + if (sensorTimer.check()) {{ + updateStateMachine(); + }} + + // Status LED + digitalWrite(LED_PIN, currentState != RobotState::IDLE); + + // Print status + if (statusTimer.check()) {{ + Serial.print(F("Distance: ")); + Serial.print(readDistanceCm()); + Serial.println(F(" cm")); + }} +}} +''' + + elif project_type == "iot": + return f'''// {project_name} - IoT Data Logger +// {proj["description"]} +// Generated: {datetime.now().strftime("%Y-%m-%d")} +// Requires: ESP32 + +#include "config.h" + +#ifdef HAS_WIFI +#include +#include +#include + +// === Timer Class === +class EveryMs {{ +private: + unsigned long interval; + unsigned long lastTrigger; +public: + EveryMs(unsigned long ms) : interval(ms), lastTrigger(0) {{}} + bool check() {{ + unsigned long now = millis(); + if (now - lastTrigger >= interval) {{ + lastTrigger = now; + return true; + }} + return false; + }} +}}; + +// === Global Objects === +WiFiClient wifiClient; +PubSubClient mqtt(wifiClient); +DHT dht(DHT_PIN, DHT22); + +EveryMs sensorTimer(SENSOR_INTERVAL); +EveryMs publishTimer(PUBLISH_INTERVAL); +EveryMs statusTimer(5000); + +struct SensorData {{ + float temperature = NAN; + float humidity = NAN; +}} data; + +// === WiFi Management === +bool connectWiFi() {{ + if (WiFi.status() == WL_CONNECTED) return true; + + Serial.print(F("Connecting to WiFi")); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + + unsigned long start = millis(); + while (WiFi.status() != WL_CONNECTED) {{ + if (millis() - start > WIFI_TIMEOUT) {{ + Serial.println(F(" FAILED")); + return false; + }} + delay(500); + Serial.print('.'); + }} + + Serial.println(F(" OK")); + Serial.print(F("IP: ")); + Serial.println(WiFi.localIP()); + return true; +}} + +// === MQTT Management === +bool connectMQTT() {{ + if (mqtt.connected()) return true; + if (!connectWiFi()) return false; + + Serial.print(F("Connecting to MQTT...")); + + String clientId = "ESP32-"; + clientId += String(random(0xffff), HEX); + + if (mqtt.connect(clientId.c_str())) {{ + Serial.println(F(" OK")); + return true; + }} + + Serial.print(F(" FAILED, rc=")); + Serial.println(mqtt.state()); + return false; +}} + +// === JSON Publishing === +void publishData() {{ + if (!connectMQTT()) return; + + char json[128]; + snprintf(json, sizeof(json), + "{{\\"temp\\":%.1f,\\"humidity\\":%.1f,\\"uptime\\":%lu}}", + data.temperature, data.humidity, millis() / 1000); + + if (mqtt.publish(MQTT_TOPIC, json)) {{ + Serial.print(F("Published: ")); + Serial.println(json); + }} else {{ + Serial.println(F("Publish failed")); + }} +}} + +void setup() {{ + Serial.begin(SERIAL_BAUD); + pinMode(STATUS_LED, OUTPUT); + dht.begin(); + + mqtt.setServer(MQTT_SERVER, MQTT_PORT); + + Serial.println(F("=== {project_name} ===")); + Serial.println(F("IoT Environmental Logger")); + + connectWiFi(); +}} + +void loop() {{ + mqtt.loop(); + + // Status LED (blink when connected) + static bool ledState = false; + if (WiFi.status() == WL_CONNECTED) {{ + ledState = !ledState; + digitalWrite(STATUS_LED, ledState); + }} else {{ + digitalWrite(STATUS_LED, LOW); + }} + + // Read sensors + if (sensorTimer.check()) {{ + float t = dht.readTemperature(); + float h = dht.readHumidity(); + if (!isnan(t)) data.temperature = t; + if (!isnan(h)) data.humidity = h; + }} + + // Publish data + if (publishTimer.check()) {{ + publishData(); + }} + + // Status output + if (statusTimer.check()) {{ + Serial.print(F("Temp: ")); + Serial.print(data.temperature, 1); + Serial.print(F("C, Humidity: ")); + Serial.print(data.humidity, 1); + Serial.print(F("%, WiFi: ")); + Serial.println(WiFi.status() == WL_CONNECTED ? F("OK") : F("DISCONNECTED")); + }} + + delay(100); +}} + +#else +void setup() {{ + Serial.begin(115200); + Serial.println(F("ERROR: IoT project requires ESP32 with WiFi")); + Serial.println(F("Please use --board esp32")); +}} +void loop() {{}} +#endif +''' + + return f"// {project_name} - Generated {datetime.now()}\n// TODO: Implement project type '{project_type}'" + + +def generate_platformio_ini(board: str, project_name: str, libraries: List[str]) -> str: + """Generate platformio.ini configuration.""" + cfg = BOARD_CONFIGS[board] + + lib_deps = "\n".join(f" {lib}" for lib in libraries) if libraries else " ; No external libraries" + + return f'''; PlatformIO Project Configuration +; {project_name} +; Generated: {datetime.now().strftime("%Y-%m-%d")} + +[env:{cfg["board"]}] +platform = {cfg["platform"]} +board = {cfg["board"]} +framework = {cfg["framework"]} + +; Serial monitor +monitor_speed = {cfg["baud"]} + +; Build flags +build_flags = + -D {cfg["defines"][0]} + +; Library dependencies +lib_deps = +{lib_deps} + +; Upload settings +upload_speed = 921600 +''' + + +def generate_readme(project_name: str, project_type: str, board: str) -> str: + """Generate README.md for the project.""" + proj = PROJECT_TYPES[project_type] + cfg = BOARD_CONFIGS[board] + + sensors = "\n".join(f"- {s}" for s in proj.get("sensors", [])) + features = "\n".join(f"- {f}" for f in proj["features"]) + + return f'''# {project_name} + +> {proj["description"]} + +## Features + +{features} + +## Hardware Required + +**Board:** {cfg["name"]} + +**Sensors:** +{sensors} + +## Installation + +### PlatformIO (Recommended) + +```bash +# Clone/download this project +cd {project_name.lower().replace(" ", "-")} + +# Build and upload +pio run -t upload + +# Open serial monitor +pio device monitor +``` + +### Arduino IDE + +1. Open `src/main.ino` +2. Select board: **{cfg["name"]}** +3. Install required libraries from Library Manager +4. Upload + +## Configuration + +Edit `src/config.h` to customize: +- Pin assignments +- Timing intervals +- WiFi credentials (if applicable) + +## Usage + +1. Connect hardware according to pin definitions +2. Upload firmware +3. Open Serial Monitor at {cfg["baud"]} baud +4. Press button to start/stop (if applicable) + +## Troubleshooting + +- **No serial output:** Check baud rate ({cfg["baud"]}) +- **Sensor not detected:** Verify wiring and I2C address +- **WiFi connection fails:** Check credentials in config.h + +## License + +MIT License - feel free to use and modify! + +--- +Generated by Arduino Project Builder +''' + + +# ============================================================================= +# Project Scaffolding +# ============================================================================= + + +def scaffold_project( + project_name: str, + project_type: str, + board: str, + output_dir: Optional[str] = None +) -> str: + """Create complete project directory structure.""" + + # Validate inputs + if project_type not in PROJECT_TYPES: + return f"Error: Unknown project type '{project_type}'" + if board not in BOARD_CONFIGS: + return f"Error: Unknown board '{board}'" + + # Check board requirement + proj = PROJECT_TYPES[project_type] + if "board_requirement" in proj and board != proj["board_requirement"]: + return f"Error: {proj['name']} requires {proj['board_requirement']} board" + + # Create output directory + safe_name = project_name.lower().replace(" ", "-") + if output_dir: + base_path = Path(output_dir) + else: + base_path = Path(safe_name) + + src_path = base_path / "src" + + try: + base_path.mkdir(parents=True, exist_ok=True) + src_path.mkdir(exist_ok=True) + + # Generate files + files_created = [] + + # config.h + config_content = generate_config_h(project_type, board, project_name) + (src_path / "config.h").write_text(config_content) + files_created.append("src/config.h") + + # main.ino + main_content = generate_main_ino(project_type, board, project_name) + (src_path / "main.ino").write_text(main_content) + files_created.append("src/main.ino") + + # platformio.ini + libraries = proj.get("libraries", []) + pio_content = generate_platformio_ini(board, project_name, libraries) + (base_path / "platformio.ini").write_text(pio_content) + files_created.append("platformio.ini") + + # README.md + readme_content = generate_readme(project_name, project_type, board) + (base_path / "README.md").write_text(readme_content) + files_created.append("README.md") + + # .gitignore + gitignore = '''.pio/ +.vscode/ +*.o +*.elf +*.hex +''' + (base_path / ".gitignore").write_text(gitignore) + files_created.append(".gitignore") + + result = f"βœ“ Created project: {project_name}\n" + result += f" Location: {base_path.absolute()}\n" + result += f" Type: {proj['name']}\n" + result += f" Board: {BOARD_CONFIGS[board]['name']}\n" + result += f" Files created:\n" + for f in files_created: + result += f" - {f}\n" + result += f"\nNext steps:\n" + result += f" cd {base_path}\n" + result += f" pio run -t upload\n" + + return result + + except Exception as e: + return f"Error creating project: {e}" + + +def list_project_types(): + """List available project types.""" + print("\n=== Available Project Types ===\n") + for key, proj in PROJECT_TYPES.items(): + req = f" (requires {proj['board_requirement']})" if "board_requirement" in proj else "" + print(f" {key:15} - {proj['name']}{req}") + print(f" {proj['description']}") + print() + + print("=== Supported Boards ===\n") + for key, cfg in BOARD_CONFIGS.items(): + print(f" {key:10} - {cfg['name']} ({cfg['sram']} bytes SRAM)") + print() + + +def interactive_mode(): + """Interactive project creation wizard.""" + print("\nπŸ”§ Arduino Project Builder - Interactive Mode\n") + + # Get project name + project_name = input("Project name: ").strip() + if not project_name: + project_name = "MyArduinoProject" + + # Select project type + print("\nAvailable project types:") + types_list = list(PROJECT_TYPES.keys()) + for i, t in enumerate(types_list, 1): + proj = PROJECT_TYPES[t] + print(f" {i}. {t} - {proj['name']}") + + while True: + try: + choice = input("\nSelect type (1-3): ").strip() + idx = int(choice) - 1 + if 0 <= idx < len(types_list): + project_type = types_list[idx] + break + except ValueError: + pass + print("Invalid choice, try again.") + + # Select board + print("\nAvailable boards:") + boards_list = list(BOARD_CONFIGS.keys()) + for i, b in enumerate(boards_list, 1): + cfg = BOARD_CONFIGS[b] + print(f" {i}. {b} - {cfg['name']}") + + while True: + try: + choice = input("\nSelect board (1-3): ").strip() + idx = int(choice) - 1 + if 0 <= idx < len(boards_list): + board = boards_list[idx] + break + except ValueError: + pass + print("Invalid choice, try again.") + + # Output directory + output_dir = input(f"\nOutput directory (default: ./{project_name.lower().replace(' ', '-')}): ").strip() + if not output_dir: + output_dir = None + + # Create project + print("\n" + "=" * 60) + result = scaffold_project(project_name, project_type, board, output_dir) + print(result) + + +# ============================================================================= +# Main +# ============================================================================= + + +def main(): + parser = argparse.ArgumentParser( + description="Scaffold complete Arduino projects from templates", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + uv run --no-project scripts/scaffold_project.py --list + uv run --no-project scripts/scaffold_project.py --type environmental --board esp32 --name "WeatherStation" + uv run --no-project scripts/scaffold_project.py --type robot --board uno --output ./my-robot + uv run --no-project scripts/scaffold_project.py --interactive + """, + ) + + parser.add_argument("--type", "-t", help="Project type (see --list)") + parser.add_argument("--board", "-b", default="uno", help="Target board: uno, esp32, rp2040") + parser.add_argument("--name", "-n", default="MyProject", help="Project name") + parser.add_argument("--output", "-o", help="Output directory") + parser.add_argument("--list", "-l", action="store_true", help="List project types") + parser.add_argument("--interactive", "-i", action="store_true", help="Interactive wizard") + + args = parser.parse_args() + + if args.list: + list_project_types() + return + + if args.interactive: + interactive_mode() + return + + if not args.type: + parser.print_help() + print("\nError: --type is required (or use --list / --interactive)") + sys.exit(1) + + result = scaffold_project(args.name, args.type, args.board, args.output) + print(result) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/arduino-project-builder/templates/project-output-template.md b/.agents/skills/arduino-project-builder/templates/project-output-template.md new file mode 100644 index 0000000..b352180 --- /dev/null +++ b/.agents/skills/arduino-project-builder/templates/project-output-template.md @@ -0,0 +1,159 @@ +# Project Output Template + +Standardized format for delivering complete Arduino projects. + +## Template Structure + +``` +=== [PROJECT_NAME] for [BOARD_TYPE] === + +[BRIEF_DESCRIPTION] + +## WIRING DIAGRAM +[ASCII_ART_OR_TEXT_BASED_WIRING] + +Component Connections: +- [COMPONENT]: [PIN_ASSIGNMENT] +- [COMPONENT]: [PIN_ASSIGNMENT] +- [COMPONENT]: [PIN_ASSIGNMENT] + +## UPLOAD INSTRUCTIONS +Board: [BOARD_TYPE] +Baud Rate: [BAUD_RATE] +Port: [AUTO_DETECT_OR_SPECIFY] + +## CONFIGURATION +[CONFIGURABLE_PARAMETERS] +- Parameter: [DEFAULT_VALUE] ([DESCRIPTION]) +- Parameter: [DEFAULT_VALUE] ([DESCRIPTION]) + +## USAGE +### Setup +1. [STEP_1] +2. [STEP_2] +3. [STEP_3] + +### Operation +- [COMMAND]: [DESCRIPTION] +- [COMMAND]: [DESCRIPTION] +- [INDICATOR]: [MEANING] + +### Serial Commands +- '[COMMAND]': [ACTION_DESCRIPTION] +- '[COMMAND]': [ACTION_DESCRIPTION] + +## TROUBLESHOOTING +### Common Issues +- **Issue**: [DESCRIPTION] + **Solution**: [STEP_BY_STEP_FIX] + +- **Issue**: [DESCRIPTION] + **Solution**: [STEP_BY_STEP_FIX] + +### Error Codes +- **Code**: [NUMBER] - [DESCRIPTION] ([CAUSE]) + +## CODE +[COMPLETE_INO_FILE_CONTENT] +``` + +## Required Sections + +### Project Header +- **Project Name:** Descriptive and specific +- **Board Type:** UNO, ESP32, Pico (with variant if applicable) +- **Brief Description:** One-sentence project summary + +### Hardware Documentation +- **Wiring Diagram:** Clear, text-based representation +- **Component List:** All required parts with pin connections +- **Power Requirements:** Voltage and current specifications + +### Software Documentation +- **Upload Instructions:** Board selection and configuration +- **Configuration Options:** User-modifiable parameters +- **Usage Guide:** Setup and operation procedures +- **Serial Interface:** Available commands and responses + +### Support Documentation +- **Troubleshooting Guide:** Common problems and solutions +- **Error Codes:** Diagnostic information +- **Performance Notes:** Expected behavior and limitations + +### Code Delivery +- **Complete Source:** Full .ino file content +- **Additional Files:** config.h, platformio.ini if applicable +- **Library Dependencies:** Required Arduino libraries + +## Quality Standards + +- [ ] All sections completed +- [ ] Wiring diagram accurate and clear +- [ ] Code compiles and runs on target board +- [ ] Instructions tested and verified +- [ ] Troubleshooting covers common issues +- [ ] Links to additional resources provided + +## Example Implementation + +``` +=== Environmental Monitor for Arduino UNO === + +Multi-sensor data logger with temperature, humidity, and light monitoring. + +## WIRING DIAGRAM +``` +DHT22 β†’ Pin 2 +Photoresistor β†’ A0 +Button β†’ Pin 3 (INPUT_PULLUP) +LED β†’ Pin 13 +``` + +Component Connections: +- DHT22 Temperature/Humidity Sensor: Digital Pin 2 +- Photoresistor (Light Sensor): Analog Pin A0 +- Push Button: Digital Pin 3 (with internal pull-up) +- Status LED: Digital Pin 13 + +## UPLOAD INSTRUCTIONS +Board: Arduino UNO +Baud Rate: 9600 +Port: Auto-detect + +## CONFIGURATION +- LOG_INTERVAL: 60 seconds (How often to log data) +- SENSOR_TIMEOUT: 5000ms (Sensor read timeout) +- LED_BLINK_RATE: 2000ms (Status LED blink interval) + +## USAGE +### Setup +1. Connect sensors according to wiring diagram +2. Upload code to Arduino UNO +3. Open Serial Monitor at 9600 baud +4. Press button to start/stop logging + +### Operation +- LED blinks every 2 seconds (heartbeat) +- Serial output shows sensor readings every 5 seconds +- Data logging occurs every 60 seconds when active + +### Serial Commands +- 'd': Dump all logged CSV data +- 'c': Clear logged data +- 's': Show current sensor readings + +## TROUBLESHOOTING +### Common Issues +- **No sensor readings**: Check wiring connections and power + **Solution**: Verify DHT22 is connected to pin 2, photoresistor to A0 + +- **Compilation errors**: Ensure DHT library is installed + **Solution**: Install "DHT sensor library" from Arduino IDE + +### Error Codes +- **Code: 1** - DHT sensor read failure (Check wiring and power) +- **Code: 2** - SD card initialization failed (Check card insertion) + +## CODE +[Full Arduino sketch content here] +``` \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/workflow/step1-requirements-gathering.md b/.agents/skills/arduino-project-builder/workflow/step1-requirements-gathering.md new file mode 100644 index 0000000..3352c33 --- /dev/null +++ b/.agents/skills/arduino-project-builder/workflow/step1-requirements-gathering.md @@ -0,0 +1,26 @@ +# Step 1: Requirements Gathering + +Analyze user request to understand project scope and gather all necessary information. + +## Key Activities + +- **User Intent Analysis:** Understand the project goal (what should it do?) +- **Hardware Inventory:** Identify required sensors, actuators, and communication modules +- **Board Selection:** Determine appropriate Arduino board (UNO, ESP32, or Raspberry Pi Pico) +- **Constraint Assessment:** Evaluate power source, memory limits, and real-time requirements + +## Questions to Ask + +- What is the primary function of the project? +- What sensors or actuators are needed? +- What is the target board (UNO/ESP32/Pico)? +- Are there power constraints (battery vs. wall power)? +- What data output is required (Serial, SD card, WiFi)? +- Are there size or enclosure requirements? + +## Deliverables + +- Clear project requirements document +- Hardware component list +- Board selection justification +- Constraint specifications \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/workflow/step2-architecture-design.md b/.agents/skills/arduino-project-builder/workflow/step2-architecture-design.md new file mode 100644 index 0000000..51b4882 --- /dev/null +++ b/.agents/skills/arduino-project-builder/workflow/step2-architecture-design.md @@ -0,0 +1,42 @@ +# Step 2: Architecture Design + +Design the overall system architecture including component connections, data flow, and state management. + +## Key Activities + +- **Component Diagram:** Map which sensors/actuators connect to which pins +- **Data Flow Design:** Define how data moves through the system +- **State Machine Design:** Identify distinct modes/states if project has them +- **Timing Requirements:** Specify task intervals and execution frequencies + +## Architecture Elements + +### Hardware Architecture +- Pin assignments for all components +- Power distribution design +- Communication bus layout (I2C, SPI, UART) + +### Software Architecture +- Main loop structure +- Timer-based task scheduling +- Interrupt handling strategy +- Memory allocation plan + +### Data Architecture +- Sensor data structures +- Logging format specifications +- Communication protocols + +## Design Validation + +- [ ] No pin conflicts +- [ ] Adequate power for all components +- [ ] Memory usage within board limits +- [ ] Timing requirements achievable + +## Deliverables + +- System architecture diagram +- Pin assignment table +- State machine diagram (if applicable) +- Timing specifications \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/workflow/step3-code-assembly.md b/.agents/skills/arduino-project-builder/workflow/step3-code-assembly.md new file mode 100644 index 0000000..bd1f6fc --- /dev/null +++ b/.agents/skills/arduino-project-builder/workflow/step3-code-assembly.md @@ -0,0 +1,43 @@ +# Step 3: Code Assembly + +Assemble the complete Arduino project by combining patterns and customizing for user requirements. + +## Key Activities + +- **Pattern Integration:** Pull and combine patterns from examples/ directory +- **Hardware Customization:** Adapt code for specific pin assignments and sensor types +- **State Machine Implementation:** Implement state logic if project has modes +- **Data Logging Integration:** Add appropriate logging mechanism (Serial, SD card, EEPROM) + +## Code Components + +### Core Files +- **main.ino:** Main sketch file with setup() and loop() +- **config.h:** Hardware abstraction and pin definitions +- **platformio.ini:** Build configuration (if using PlatformIO) + +### Pattern Integration +- **Sensor Reading:** Implement appropriate sensor interfaces +- **Actuator Control:** Add motor, relay, or LED control logic +- **Communication:** Integrate I2C, SPI, or WiFi as needed +- **Data Processing:** Add filtering, validation, and formatting + +### State Machine (if applicable) +- Define state enumeration +- Implement state transition logic +- Add state-specific behavior +- Include error state handling + +## Quality Checks + +- [ ] Non-blocking code (no delay() calls) +- [ ] Proper error handling +- [ ] Memory-safe operations +- [ ] Board-specific optimizations + +## Deliverables + +- Complete .ino file +- config.h file +- platformio.ini (if needed) +- Code documentation comments \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/workflow/step4-testing-validation.md b/.agents/skills/arduino-project-builder/workflow/step4-testing-validation.md new file mode 100644 index 0000000..36c2dda --- /dev/null +++ b/.agents/skills/arduino-project-builder/workflow/step4-testing-validation.md @@ -0,0 +1,53 @@ +# Step 4: Testing & Validation + +Verify the assembled project meets all requirements and functions correctly. + +## Key Activities + +- **Compilation Testing:** Ensure code compiles for target board +- **Memory Analysis:** Verify usage is within board limits +- **Pin Conflict Check:** Confirm no duplicate pin assignments +- **Timing Validation:** Ensure all tasks fit within loop execution time + +## Validation Checks + +### Compilation +- [ ] Code compiles without errors for target board +- [ ] All libraries are available and compatible +- [ ] Board-specific optimizations applied correctly + +### Memory Usage +- [ ] SRAM usage within limits (UNO: 2KB, ESP32: 327KB, Pico: 262KB) +- [ ] Program memory (Flash) within board capacity +- [ ] No dynamic memory allocation issues + +### Hardware Validation +- [ ] Pin assignments match wiring diagram +- [ ] No conflicts between digital/analog pins +- [ ] Interrupt pins used appropriately +- [ ] Power requirements met + +### Functional Testing +- [ ] Sensor readings are valid (no NaN, within expected ranges) +- [ ] Actuators respond correctly to inputs +- [ ] State machine transitions work properly +- [ ] Data logging functions as expected + +### Performance Testing +- [ ] Loop execution time meets real-time requirements +- [ ] Timer intervals are accurate +- [ ] No blocking operations in main loop + +## Testing Tools + +- Arduino IDE Serial Monitor for debugging +- PlatformIO for advanced compilation checking +- Logic analyzer for timing verification +- Multimeter for hardware validation + +## Deliverables + +- Compilation verification report +- Memory usage analysis +- Test results summary +- Bug fixes and improvements \ No newline at end of file diff --git a/.agents/skills/arduino-project-builder/workflow/step5-documentation.md b/.agents/skills/arduino-project-builder/workflow/step5-documentation.md new file mode 100644 index 0000000..852847c --- /dev/null +++ b/.agents/skills/arduino-project-builder/workflow/step5-documentation.md @@ -0,0 +1,74 @@ +# Step 5: Documentation + +Create comprehensive documentation for the completed Arduino project. + +## Key Activities + +- **Wiring Documentation:** Create clear pin connection diagrams +- **Usage Instructions:** Provide upload and configuration guidance +- **Serial Commands:** Document available commands and responses +- **Troubleshooting Guide:** Address common issues and solutions + +## Documentation Components + +### Hardware Documentation +- **Wiring Diagram:** ASCII art or text-based pin connections +- **Component List:** All required parts with specifications +- **Power Requirements:** Voltage and current specifications +- **Enclosure Notes:** Size and mounting considerations + +### Software Documentation +- **Upload Instructions:** Board selection and upload procedure +- **Configuration:** How to modify settings and parameters +- **Serial Interface:** Available commands and expected output +- **API Reference:** Function descriptions and parameters + +### Usage Guide +- **Setup Process:** Step-by-step initialization +- **Operation:** Normal usage procedures +- **Maintenance:** Calibration and upkeep procedures +- **Safety Notes:** Important safety considerations + +### Troubleshooting +- **Common Issues:** Frequently encountered problems +- **Error Messages:** What error codes mean and how to fix +- **Debugging Tips:** How to diagnose issues +- **Recovery Procedures:** How to reset or recover from errors + +## Output Format + +Standardize documentation in this format: + +``` +=== [Project Name] for [Board Type] === + +WIRING: +[Component] β†’ [Pin] +[Component] β†’ [Pin] +... + +UPLOAD: +Board: [Board Type] +Baud Rate: [Rate] + +USAGE: +- [Instruction] +- [Instruction] +... + +TROUBLESHOOTING: +- [Issue]: [Solution] +- [Issue]: [Solution] +... + +CODE: +[Full .ino file content] +``` + +## Deliverables + +- Complete project documentation +- Wiring diagrams +- Usage instructions +- Troubleshooting guide +- README file for the project \ No newline at end of file diff --git a/.agents/skills/arduino-serial-monitor/SKILL.md b/.agents/skills/arduino-serial-monitor/SKILL.md new file mode 100644 index 0000000..23e4899 --- /dev/null +++ b/.agents/skills/arduino-serial-monitor/SKILL.md @@ -0,0 +1,166 @@ +--- +name: arduino-serial-monitor +description: Tools for reading and analyzing Arduino serial monitor output for enhanced debugging. Provides real-time monitoring, data logging, filtering, and pattern matching to help troubleshoot Arduino sketches using arduino-cli or Arduino IDE. +category: arduino +--- + +# Arduino Serial Monitor + +This skill provides advanced tools for reading and analyzing serial monitor data from Arduino boards, enhancing the debugging experience beyond the basic Arduino IDE serial monitor. + +## Features + +- **Real-time Serial Monitoring**: Connect to Arduino serial ports and display data in real-time +- **Data Logging**: Save serial output to files with timestamps for later analysis +- **Filtering & Pattern Matching**: Filter output by keywords, regex patterns, or data types +- **Error Detection**: Automatically highlight common error patterns and warnings +- **Multiple Format Support**: Handle different data formats (text, JSON, CSV, binary) +- **Cross-platform**: Works with Windows, macOS, and Linux serial ports + +## Usage + +### Basic Serial Monitoring + +```bash +# Monitor serial port with default settings (9600 baud) +uv run --no-project scripts/monitor_serial.py --port COM3 + +# Specify baud rate and output file +uv run --no-project scripts/monitor_serial.py --port /dev/ttyACM0 --baud 115200 --output debug.log + +# Filter for specific patterns +uv run --no-project scripts/monitor_serial.py --port COM3 --filter "ERROR|WARNING" +``` + +### Advanced Debugging + +```bash +# Parse JSON data from serial +uv run --no-project scripts/monitor_serial.py --port COM3 --format json --pretty + +# Monitor with timestamp and color coding +uv run --no-project scripts/monitor_serial.py --port COM3 --timestamp --color + +# Detect common Arduino errors +uv run --no-project scripts/monitor_serial.py --port COM3 --detect-errors +``` + +## Script Options + +- `--port`: Serial port (e.g., COM3, /dev/ttyACM0) +- `--baud`: Baud rate (default: 9600) +- `--output`: Output file for logging +- `--filter`: Regex pattern to filter lines +- `--format`: Data format (text, json, csv, binary) +- `--timestamp`: Add timestamps to output +- `--color`: Enable color-coded output +- `--detect-errors`: Highlight common error patterns +- `--timeout`: Connection timeout in seconds + +## Common Arduino Debugging Scenarios + +### Memory Issues +``` +Filter for: "low memory|stack overflow|heap" +``` + +### Sensor Data Validation +``` +Filter for: "sensor|reading|value" +Format as: json +``` + +### Timing Analysis +``` +Enable: --timestamp +Filter for: "start|end|duration" +``` + +### Communication Errors +``` +Filter for: "timeout|failed|error" +Enable: --detect-errors +``` + +## Integration with Arduino CLI + +```bash +# Compile and upload, then monitor +arduino-cli compile --fqbn arduino:avr:uno sketch/ +arduino-cli upload -p COM3 --fqbn arduino:avr:uno sketch/ +uv run --no-project scripts/monitor_serial.py --port COM3 +``` + +## Troubleshooting + +### Port Not Found +- Check `arduino-cli board list` for available ports +- Ensure Arduino is connected and drivers are installed +- Try different port names (COM1-COM99 on Windows, /dev/ttyACM* on Linux) + +### No Data Received +- Verify baud rate matches Arduino sketch (`Serial.begin(9600)`) +- Check USB cable connection +- Reset Arduino board while monitoring + +### Permission Errors (Linux/macOS) +```bash +# Add user to dialout group +sudo usermod -a -G dialout $USER +# Logout and login again +``` + +## Dependencies + +- Python 3.8+ +- pyserial +- colorama (for colored output) +- Install via: `uv add pyserial colorama` + +## Examples + +### Basic Temperature Sensor Monitoring + +```python +// Arduino sketch +void setup() { + Serial.begin(9600); +} + +void loop() { + float temp = analogRead(A0) * 0.488; + Serial.print("Temperature: "); + Serial.print(temp); + Serial.println(" C"); + delay(1000); +} +``` + +```bash +uv run --no-project scripts/monitor_serial.py --port COM3 --filter "Temperature" --timestamp +``` + +### JSON Data Parsing + +```python +// Arduino sketch with JSON output +#include + +void setup() { + Serial.begin(115200); +} + +void loop() { + StaticJsonDocument<200> doc; + doc["temperature"] = analogRead(A0) * 0.488; + doc["humidity"] = analogRead(A1) * 0.146; + doc["timestamp"] = millis(); + serializeJson(doc, Serial); + Serial.println(); + delay(2000); +} +``` + +```bash +uv run --no-project scripts/monitor_serial.py --port COM3 --format json --pretty +``` \ No newline at end of file diff --git a/.agents/skills/arduino-serial-monitor/scripts/monitor_serial.py b/.agents/skills/arduino-serial-monitor/scripts/monitor_serial.py new file mode 100644 index 0000000..3539455 --- /dev/null +++ b/.agents/skills/arduino-serial-monitor/scripts/monitor_serial.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +Arduino Serial Monitor - Enhanced debugging tool for Arduino serial output +""" + +import serial +import serial.tools.list_ports +import argparse +import re +import json +import time +import sys +from datetime import datetime +import colorama +from colorama import Fore, Back, Style + +colorama.init() + +class ArduinoSerialMonitor: + def __init__(self, port, baud=9600, timeout=1): + self.port = port + self.baud = baud + self.timeout = timeout + self.serial_conn = None + + def connect(self): + try: + self.serial_conn = serial.Serial( + port=self.port, + baudrate=self.baud, + timeout=self.timeout, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + bytesize=serial.EIGHTBITS + ) + print(f"Connected to {self.port} at {self.baud} baud") + return True + except serial.SerialException as e: + print(f"Error connecting to {self.port}: {e}") + return False + + def disconnect(self): + if self.serial_conn and self.serial_conn.is_open: + self.serial_conn.close() + print(f"Disconnected from {self.port}") + + def read_line(self): + if not self.serial_conn or not self.serial_conn.is_open: + return None + try: + line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip() + return line if line else None + except serial.SerialException: + return None + +def detect_errors(line): + """Detect common Arduino error patterns""" + error_patterns = [ + r'error|Error|ERROR', + r'exception|Exception|EXCEPTION', + r'failed|Failed|FAILED', + r'timeout|Timeout|TIMEOUT', + r'overflow|Overflow|OVERFLOW', + r'out of memory|Out of memory', + r'stack overflow|Stack overflow', + r'panic|Panic|PANIC' + ] + + for pattern in error_patterns: + if re.search(pattern, line, re.IGNORECASE): + return True + return False + +def format_json(line, pretty=False): + """Try to parse and format JSON""" + try: + data = json.loads(line) + if pretty: + return json.dumps(data, indent=2) + else: + return json.dumps(data) + except json.JSONDecodeError: + return line + +def colorize_line(line, detect_errors_flag=False): + """Add color coding to output""" + if detect_errors_flag and detect_errors(line): + return Fore.RED + line + Style.RESET_ALL + elif 'WARNING' in line.upper(): + return Fore.YELLOW + line + Style.RESET_ALL + elif 'INFO' in line.upper(): + return Fore.BLUE + line + Style.RESET_ALL + elif 'DEBUG' in line.upper(): + return Fore.GREEN + line + Style.RESET_ALL + else: + return line + +def list_ports(): + """List available serial ports""" + ports = serial.tools.list_ports.comports() + if not ports: + print("No serial ports found") + return + + print("Available serial ports:") + for port in ports: + print(f" {port.device}: {port.description}") + +def main(): + parser = argparse.ArgumentParser(description='Arduino Serial Monitor - Enhanced debugging tool') + parser.add_argument('--port', '-p', required=True, help='Serial port (e.g., COM3, /dev/ttyACM0)') + parser.add_argument('--baud', '-b', type=int, default=9600, help='Baud rate (default: 9600)') + parser.add_argument('--output', '-o', help='Output file for logging') + parser.add_argument('--filter', '-f', help='Regex pattern to filter lines') + parser.add_argument('--format', choices=['text', 'json', 'csv'], default='text', help='Data format') + parser.add_argument('--pretty', action='store_true', help='Pretty print JSON') + parser.add_argument('--timestamp', '-t', action='store_true', help='Add timestamps') + parser.add_argument('--color', '-c', action='store_true', help='Enable color output') + parser.add_argument('--detect-errors', '-e', action='store_true', help='Highlight error patterns') + parser.add_argument('--timeout', type=int, default=1, help='Connection timeout in seconds') + parser.add_argument('--list-ports', '-l', action='store_true', help='List available serial ports') + + args = parser.parse_args() + + if args.list_ports: + list_ports() + return + + # Initialize monitor + monitor = ArduinoSerialMonitor(args.port, args.baud, args.timeout) + + # Connect + if not monitor.connect(): + sys.exit(1) + + # Open output file if specified + output_file = None + if args.output: + try: + output_file = open(args.output, 'a', encoding='utf-8') + print(f"Logging to {args.output}") + except IOError as e: + print(f"Error opening output file: {e}") + monitor.disconnect() + sys.exit(1) + + # Compile filter regex if specified + filter_regex = None + if args.filter: + try: + filter_regex = re.compile(args.filter, re.IGNORECASE) + except re.error as e: + print(f"Invalid regex pattern: {e}") + monitor.disconnect() + if output_file: + output_file.close() + sys.exit(1) + + print("Monitoring serial output... (Ctrl+C to stop)") + + try: + while True: + line = monitor.read_line() + if line is None: + continue + + # Apply filtering + if filter_regex and not filter_regex.search(line): + continue + + # Format data + if args.format == 'json': + line = format_json(line, args.pretty) + # CSV format could be added here + + # Add timestamp + if args.timestamp: + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + line = f"[{timestamp}] {line}" + + # Colorize + if args.color or args.detect_errors: + line = colorize_line(line, args.detect_errors) + + # Print to console + print(line) + + # Write to file + if output_file: + # Remove ANSI color codes for file output + clean_line = re.sub(r'\x1b\[[0-9;]*m', '', line) + output_file.write(clean_line + '\n') + output_file.flush() + + except KeyboardInterrupt: + print("\nStopping monitor...") + except Exception as e: + print(f"Error: {e}") + finally: + monitor.disconnect() + if output_file: + output_file.close() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/.agents/skills/bom-generator/SKILL.md b/.agents/skills/bom-generator/SKILL.md new file mode 100644 index 0000000..db1f157 --- /dev/null +++ b/.agents/skills/bom-generator/SKILL.md @@ -0,0 +1,291 @@ +--- +name: bom-generator +description: Generates Bill of Materials (BOM) from project descriptions for Arduino/ESP32/RP2040 projects. Use when user needs component lists, parts shopping lists, cost estimates, or asks "what parts do I need". Outputs formatted BOMs with part numbers, quantities, suppliers (DigiKey, Mouser, Amazon, AliExpress), and compatibility warnings. Run scripts/generate_bom.py for xlsx/csv export. +--- + +# BOM Generator + +Creates comprehensive Bill of Materials for maker projects with supplier links and compatibility checks. + +## Resources + +- **scripts/generate_bom.py** - Python script for xlsx/csv/markdown BOM generation (requires openpyxl) +- **references/example-bom.md** - Complete example BOM +- **assets/example-project.json** - Sample project configuration for script + +## Quick Start + +Generate BOM interactively: +```bash +uv run --no-project scripts/generate_bom.py --interactive +``` + +Generate from JSON configuration: +```bash +uv run --no-project scripts/generate_bom.py --json assets/example-project.json --output bom.xlsx +``` + +List component database: +```bash +uv run --no-project scripts/generate_bom.py --list +``` + +Export formats: xlsx (default), csv, md, json + +## When to Use +- User describes a project and needs parts list +- User asks "what components do I need for X" +- User wants to order parts for a design +- User needs cost estimates + +## Workflow + +### Step 1: Gather Project Requirements + +Ask user for: +``` +1. Project description (what does it do?) +2. Target microcontroller (Arduino UNO/Nano/Mega, ESP32, RP2040) +3. Power source (USB, batteries, wall adapter) +4. Quantity (how many units to build?) +5. Budget constraints (optional) +6. Supplier preference (optional) +``` + +### Step 2: Generate BOM + +Run `scripts/generate_bom.py --interactive` for guided generation, or use this template format: + +```markdown +# Bill of Materials: [Project Name] + +**Generated:** [Date] +**Target Board:** [MCU] +**Quantity:** [N] unit(s) +**Estimated Total:** $[X.XX] - $[Y.YY] (per unit) + +## Core Components + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| 1 | [MCU Board] | [specs] | $X.XX | [DigiKey](#) / [Amazon](#) | +| ... | ... | ... | ... | ... | + +## Sensors & Input + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| ... | ... | ... | ... | ... | + +## Output Devices + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| ... | ... | ... | ... | ... | + +## Power Components + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| ... | ... | ... | ... | ... | + +## Passive Components + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| ... | ... | ... | ... | ... | + +## Mechanical & Connectors + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| ... | ... | ... | ... | ... | + +## Tools Required (if not owned) +- [ ] Tool 1 +- [ ] Tool 2 + +## Compatibility Warnings +⚠️ [Any voltage/current/timing concerns] + +## Substitution Notes +πŸ’‘ [Alternative components if primary unavailable] +``` + +## Component Database + +### Microcontrollers + +| Board | Voltage | GPIO | Flash | Price Range | Best For | +|-------|---------|------|-------|-------------|----------| +| Arduino UNO R3 | 5V | 14 | 32KB | $12-25 | Beginners, most tutorials | +| Arduino Nano | 5V | 14 | 32KB | $3-20 | Compact projects | +| Arduino Mega | 5V | 54 | 256KB | $15-40 | Many I/O, large programs | +| ESP32 DevKit | 3.3V | 34 | 4MB | $5-15 | WiFi, Bluetooth, IoT | +| ESP32-C3 | 3.3V | 22 | 4MB | $4-10 | Low-cost WiFi | +| RP2040 Pico | 3.3V | 26 | 2MB | $4-6 | Dual-core, PIO | +| RP2040 Pico W | 3.3V | 26 | 2MB | $6-8 | Pico + WiFi | + +### Common Sensors + +| Sensor | Interface | Voltage | Price | Use Case | +|--------|-----------|---------|-------|----------| +| DHT22/AM2302 | 1-Wire | 3.3-5V | $3-8 | Temperature + humidity | +| BME280 | I2C/SPI | 3.3V | $5-15 | Temp + humidity + pressure | +| BMP280 | I2C/SPI | 3.3V | $2-8 | Temp + pressure (no humidity) | +| DS18B20 | 1-Wire | 3-5.5V | $2-5 | Waterproof temperature | +| HC-SR04 | GPIO | 5V | $1-3 | Ultrasonic distance | +| VL53L0X | I2C | 3.3V | $5-12 | Laser distance (accurate) | +| MPU6050 | I2C | 3.3V | $2-8 | Accelerometer + gyroscope | +| BNO055 | I2C | 3.3V | $25-35 | 9-DOF IMU (best accuracy) | +| VEML7700 | I2C | 3.3V | $4-8 | Ambient light (lux) | +| MAX30102 | I2C | 3.3V | $5-12 | Heart rate + SpO2 | + +### Displays + +| Display | Interface | Resolution | Price | Notes | +|---------|-----------|------------|-------|-------| +| 16x2 LCD | I2C | 16 chars | $3-8 | Need I2C backpack | +| 20x4 LCD | I2C | 20 chars | $5-12 | Larger text display | +| SSD1306 OLED | I2C | 128x64 | $3-8 | Sharp, no backlight needed | +| SSD1306 OLED | I2C | 128x32 | $3-6 | Compact OLED | +| ST7735 TFT | SPI | 128x160 | $5-10 | Color, fast refresh | +| ILI9341 TFT | SPI | 240x320 | $8-15 | Larger color display | +| E-Paper | SPI | Various | $15-40 | Low power, no refresh | + +### Motors & Actuators + +| Type | Driver Needed | Price | Notes | +|------|---------------|-------|-------| +| SG90 Servo | None (PWM) | $2-5 | 180Β°, weak torque | +| MG996R Servo | None (PWM) | $5-10 | Strong, metal gears | +| 28BYJ-48 Stepper | ULN2003 | $3-6 | Cheap, slow, weak | +| NEMA17 Stepper | A4988/DRV8825 | $8-15 | Strong, precise | +| DC Motor + Gearbox | L298N/TB6612 | $5-15 | High speed available | +| Linear Actuator | L298N | $15-40 | Push/pull motion | + +### Motor Drivers + +| Driver | Channels | Max Current | Voltage | Price | +|--------|----------|-------------|---------|-------| +| L298N | 2 | 2A/ch | 5-35V | $3-8 | +| TB6612FNG | 2 | 1.2A/ch | 4.5-13.5V | $3-8 | +| A4988 | 1 stepper | 2A | 8-35V | $2-5 | +| DRV8825 | 1 stepper | 2.5A | 8.2-45V | $3-6 | +| TMC2209 | 1 stepper | 2A | 4.75-28V | $8-15 | + +### Power Components + +| Component | Specs | Price | Use Case | +|-----------|-------|-------|----------| +| LM7805 | 5V 1A linear | $0.50 | Simple 5V reg | +| AMS1117-3.3 | 3.3V 1A linear | $0.30 | 3.3V from 5V | +| LM2596 Module | Adj. 3A buck | $2-4 | Efficient step-down | +| MT3608 Module | Adj. 2A boost | $1-3 | Step-up voltage | +| TP4056 Module | LiPo charger | $1-2 | Battery charging | +| 18650 Holder | 1-4 cells | $1-5 | Battery mounting | +| JST Connectors | 2-pin | $2-5/10pk | Battery connections | + +### Passive Components (Buy Kits!) + +| Kit Type | Typical Contents | Price | Recommendation | +|----------|------------------|-------|----------------| +| Resistor Kit | 600+ pcs, 1/4W | $8-15 | Get once, use forever | +| Capacitor Kit | Ceramic + electrolytic | $10-20 | Essential | +| LED Kit | 5mm various colors | $5-10 | Common needs | +| Button Kit | Tactile switches | $5-8 | Various sizes | +| Diode Kit | 1N4148, 1N4007, etc | $5-8 | Protection circuits | + +## Supplier Guide + +### Speed vs Cost Trade-offs + +| Supplier | Shipping | Price | Best For | +|----------|----------|-------|----------| +| **DigiKey** | 1-3 days | $$$ | Precise specs, datasheets, urgent | +| **Mouser** | 1-3 days | $$$ | Wide selection, quality | +| **Amazon** | 1-2 days | $$ | Quick delivery, returns easy | +| **Adafruit** | 3-5 days | $$$ | Quality, tutorials, support | +| **SparkFun** | 3-5 days | $$$ | Breakout boards, learning | +| **AliExpress** | 2-6 weeks | $ | Bulk, budget, clones | +| **LCSC** | 1-2 weeks | $$ | Chinese components, good quality | + +### Part Number Patterns + +``` +DigiKey: Descriptive codes + - 1N4007-TP β†’ 1N4007 diode + - SER0006 β†’ Servo motor + +Mouser: Manufacturer part numbers + - Search by exact MPN + +Amazon: ASIN codes + - Search by product name + specs + +AliExpress: Store + product ID + - Check reviews, sold count +``` + +## Compatibility Checks + +### Voltage Level Matrix + +``` + Can Connect To: +From: 3.3V Logic 5V Logic +───────────────────────────────────── +3.3V MCU βœ… Direct ⚠️ Level shifter +5V MCU ⚠️ Divider βœ… Direct +3.3V Sensor βœ… Direct ⚠️ May work* +5V Sensor ❌ Damage! βœ… Direct + +* Some 3.3V sensors are 5V tolerant - check datasheet +``` + +### Current Budget Check + +``` +Source Limits: +- Arduino 5V pin: 500mA max (from USB) +- Arduino GPIO: 40mA max per pin +- ESP32 3.3V: 500mA max +- ESP32 GPIO: 40mA max +- RP2040 GPIO: 16mA max + +Always calculate: +Total current = Ξ£(component currents) +If total > source limit β†’ external power needed +``` + +### I2C Address Conflicts + +Common I2C addresses to watch: +``` +0x3C - SSD1306 OLED +0x27 - PCF8574 LCD backpack +0x3F - PCF8574A LCD backpack +0x68 - MPU6050, DS3231 RTC +0x76 - BME280 (default) +0x77 - BME280 (alternate), BMP280 +0x48 - ADS1115 ADC +0x50 - AT24C32 EEPROM +``` + +## Output Format Options + +### Markdown Table (Default) +Best for documentation, GitHub READMEs. + +### CSV Export +```csv +Qty,Component,Specifications,Unit Price,Total,Supplier,Link +1,Arduino UNO R3,ATmega328P,15.00,15.00,Amazon,https://... +``` + +### Shopping Cart Links +Provide direct "Add to Cart" links where possible. + +## Example BOM Output + +See [references/example-bom.md](references/example-bom.md) for complete example. diff --git a/.agents/skills/bom-generator/assets/example-project.json b/.agents/skills/bom-generator/assets/example-project.json new file mode 100644 index 0000000..da75627 --- /dev/null +++ b/.agents/skills/bom-generator/assets/example-project.json @@ -0,0 +1,50 @@ +{ + "project_name": "Weather Station", + "version": "1.0", + "author": "Maker Student", + "date": "2026-01-04", + "items": [ + { + "name": "ESP32-DevKit", + "quantity": 1, + "reference": "U1", + "notes": "Main controller" + }, + { + "name": "BME280", + "quantity": 1, + "reference": "U2", + "notes": "Environmental sensor" + }, + { + "name": "OLED-128x64", + "quantity": 1, + "reference": "DISP1", + "notes": "Local display" + }, + { + "name": "TP4056", + "quantity": 1, + "reference": "U3", + "notes": "Battery charger" + }, + { + "name": "LiPo-1000mAh", + "quantity": 1, + "reference": "BAT1", + "notes": "Power source" + }, + { + "name": "Breadboard-830", + "quantity": 1, + "reference": "-", + "notes": "Prototyping" + }, + { + "name": "Jumper-Wires-Kit", + "quantity": 1, + "reference": "-", + "notes": "Connections" + } + ] +} diff --git a/.agents/skills/bom-generator/references/example-bom.md b/.agents/skills/bom-generator/references/example-bom.md new file mode 100644 index 0000000..ae40675 --- /dev/null +++ b/.agents/skills/bom-generator/references/example-bom.md @@ -0,0 +1,164 @@ +# Example BOM: Weather Station Project + +**Generated:** 2026-01-04 +**Target Board:** ESP32 DevKit +**Quantity:** 1 unit +**Estimated Total:** $35-55 (per unit) + +## Project Description +Battery-powered outdoor weather station with temperature, humidity, pressure sensors. Displays on OLED, logs to SD card, sends data via WiFi every 15 minutes. + +--- + +## Core Components + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| 1 | ESP32 DevKit V1 | ESP32-WROOM-32, 38 pins | $6-12 | [Amazon](https://amazon.com/dp/B07Q576VWZ) / [AliExpress](https://aliexpress.com/item/32959541446.html) | +| 1 | MicroSD Card Module | SPI interface, 3.3V | $1-3 | [Amazon](https://amazon.com/dp/B07BJ2P6X6) | +| 1 | MicroSD Card | 8-32GB, Class 10 | $5-10 | [Amazon](https://amazon.com/dp/B073JWXGNT) | + +## Sensors + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| 1 | BME280 | I2C, Temp/Humidity/Pressure, 3.3V | $5-12 | [Amazon](https://amazon.com/dp/B07KR24P6P) / [Adafruit](https://adafruit.com/product/2652) | +| 1 | BH1750 | I2C, Light sensor (lux), 3.3V | $2-5 | [Amazon](https://amazon.com/dp/B07DNYPLWD) | + +## Display + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| 1 | SSD1306 OLED | I2C, 128x64, 0.96", White | $4-8 | [Amazon](https://amazon.com/dp/B07D9H83WJ) | + +## Power Components + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| 1 | TP4056 Module | USB LiPo charger with protection | $1-3 | [Amazon](https://amazon.com/dp/B098T5N1X9) | +| 1 | 18650 Battery | 3.7V 2600mAh+, protected | $5-10 | [Amazon](https://amazon.com/dp/B07Y8FWKGJ) | +| 1 | 18650 Holder | Single cell, wire leads | $1-2 | [Amazon](https://amazon.com/dp/B07CWGN8SL) | +| 1 | MT3608 Boost | 3.7V β†’ 5V step-up | $1-3 | [Amazon](https://amazon.com/dp/B089JYBF25) | +| 1 | Slide Switch | SPDT, panel mount | $0.50 | [Amazon](https://amazon.com/dp/B07RQ7T2T3) | + +## Passive Components + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| 2 | 4.7kΞ© Resistor | 1/4W, for I2C pull-ups | $0.10 | From resistor kit | +| 2 | 100nF Capacitor | Ceramic, decoupling | $0.10 | From capacitor kit | +| 1 | 100Β΅F Capacitor | Electrolytic, 16V | $0.20 | From capacitor kit | + +## Mechanical & Connectors + +| Qty | Component | Specifications | Est. Price | Supplier Links | +|-----|-----------|----------------|------------|----------------| +| 1 | Project Enclosure | Weatherproof, ~100x68x50mm | $5-10 | [Amazon](https://amazon.com/dp/B07TYNYW1S) | +| 1 | Cable Glands | PG7, for wire entry | $2-5 | [Amazon](https://amazon.com/dp/B07JNC64M4) | +| 1 | Breadboard | 400 tie points (prototyping) | $2-4 | [Amazon](https://amazon.com/dp/B01EV6LJ7G) | +| 20 | Jumper Wires | M-M and M-F assortment | $3-6 | [Amazon](https://amazon.com/dp/B01EV70C78) | + +--- + +## Cost Summary + +| Category | Low Est. | High Est. | +|----------|----------|-----------| +| Core Components | $12 | $25 | +| Sensors | $7 | $17 | +| Display | $4 | $8 | +| Power | $9 | $18 | +| Passives | $0.50 | $1 | +| Mechanical | $10 | $20 | +| **TOTAL** | **$43** | **$89** | + +*Note: Prices vary by supplier and region. AliExpress is cheapest but slowest.* + +--- + +## Compatibility Warnings + +⚠️ **Voltage:** All sensors are 3.3V - ESP32 is 3.3V logic βœ… Compatible + +⚠️ **I2C Addresses:** +- BME280: 0x76 (default) or 0x77 +- BH1750: 0x23 (default) or 0x5C +- SSD1306: 0x3C (default) or 0x3D +- **No conflicts with default addresses** βœ… + +⚠️ **Current Budget:** +``` +Component Typical Current +───────────────────────────────────── +ESP32 (active) 80-240mA +ESP32 (deep sleep) 10Β΅A +BME280 0.1mA +BH1750 0.2mA +SSD1306 OLED 20mA +SD Card (write) 100mA peak +───────────────────────────────────── +Total Active: ~400mA max +Deep Sleep: ~0.2mA +``` +18650 @ 2600mAh β†’ 6.5 hours continuous, or months with proper sleep cycles + +--- + +## Substitution Notes + +πŸ’‘ **BME280 β†’ DHT22:** Cheaper ($3-5), but less accurate and no pressure. Wire is simpler (single GPIO). + +πŸ’‘ **SSD1306 β†’ 16x2 LCD:** Cheaper ($3), but needs more pins and I2C adapter. Less info display. + +πŸ’‘ **ESP32 β†’ ESP8266:** Cheaper ($3), fewer GPIO, no Bluetooth. Works for this project. + +πŸ’‘ **18650 β†’ 2x AA:** Simpler, no special charger needed. Shorter battery life, voltage less stable. + +--- + +## Tools Required + +If not already owned: +- [ ] Soldering iron + solder (for permanent build) +- [ ] Wire strippers +- [ ] Multimeter (essential for debugging) +- [ ] Hot glue gun (for securing components in enclosure) +- [ ] Drill + bits (for enclosure mounting holes) + +--- + +## Wiring Summary + +``` +ESP32 Component +────── ───────── +GPIO21 (SDA) β†’ BME280 SDA, BH1750 SDA, OLED SDA +GPIO22 (SCL) β†’ BME280 SCL, BH1750 SCL, OLED SCL +GPIO5 (CS) β†’ SD Card CS +GPIO18 (SCK) β†’ SD Card SCK +GPIO19 (MISO)β†’ SD Card MISO +GPIO23 (MOSI)β†’ SD Card MOSI +3.3V β†’ All VCC pins +GND β†’ All GND pins +``` + +--- + +## Order Checklist + +``` +β–‘ ESP32 DevKit +β–‘ BME280 sensor module +β–‘ BH1750 light sensor +β–‘ SSD1306 OLED display +β–‘ MicroSD card module +β–‘ MicroSD card (8GB+) +β–‘ TP4056 charger module +β–‘ 18650 battery (protected!) +β–‘ 18650 holder +β–‘ MT3608 boost converter +β–‘ Enclosure +β–‘ Cable glands +β–‘ Jumper wires +β–‘ Breadboard (if prototyping) +``` diff --git a/.agents/skills/bom-generator/scripts/generate_bom.py b/.agents/skills/bom-generator/scripts/generate_bom.py new file mode 100644 index 0000000..4fc00b5 --- /dev/null +++ b/.agents/skills/bom-generator/scripts/generate_bom.py @@ -0,0 +1,776 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "openpyxl", +# ] +# /// +""" +BOM Generator - Creates Bill of Materials in multiple formats + +Generates professional BOM spreadsheets (xlsx, csv) with: +- Component details and quantities +- Supplier links and pricing +- Category totals and grand total +- Formatted columns with formulas + +Usage: + uv run --no-project scripts/generate_bom.py --interactive + uv run --no-project scripts/generate_bom.py --json input.json --output bom.xlsx + uv run --no-project scripts/generate_bom.py --json input.json --format csv +""" + +import argparse +import json +import csv +import sys +from dataclasses import dataclass, field +from typing import Optional, List +from pathlib import Path +from datetime import datetime + +# Check for openpyxl +try: + from openpyxl import Workbook + from openpyxl.styles import Font, Alignment, Border, Side, PatternFill + from openpyxl.utils import get_column_letter + OPENPYXL_AVAILABLE = True +except ImportError: + OPENPYXL_AVAILABLE = False + print("Warning: openpyxl not installed. xlsx output disabled.", file=sys.stderr) + print("Install with: pip install openpyxl", file=sys.stderr) + + +# ============================================================================= +# Component Database +# ============================================================================= + +COMPONENT_DATABASE = { + # Microcontrollers + "ESP32-DevKit": { + "description": "ESP32 Development Board with WiFi/BLE", + "category": "Microcontroller", + "unit_price_usd": 8.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B08D5ZD528", + "AliExpress": "https://aliexpress.com/item/32902307791.html" + } + }, + "Arduino-Uno-R3": { + "description": "Arduino Uno R3 ATmega328P", + "category": "Microcontroller", + "unit_price_usd": 25.00, + "package": "Module", + "suppliers": { + "Arduino.cc": "https://store.arduino.cc/products/arduino-uno-rev3", + "Amazon": "https://amazon.com/dp/B008GRTSV6" + } + }, + "Arduino-Nano": { + "description": "Arduino Nano V3 ATmega328P", + "category": "Microcontroller", + "unit_price_usd": 5.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B0713XK923" + } + }, + "Raspberry-Pi-Pico": { + "description": "RP2040 Microcontroller Board", + "category": "Microcontroller", + "unit_price_usd": 4.00, + "package": "Module", + "suppliers": { + "Raspberry Pi": "https://www.raspberrypi.com/products/raspberry-pi-pico/", + "Adafruit": "https://www.adafruit.com/product/4864" + } + }, + + # Sensors + "BME280": { + "description": "Temperature/Humidity/Pressure Sensor (I2C)", + "category": "Sensor", + "unit_price_usd": 3.50, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07KR24P6P", + "AliExpress": "https://aliexpress.com/item/32849462236.html" + } + }, + "DHT22": { + "description": "Temperature/Humidity Sensor", + "category": "Sensor", + "unit_price_usd": 4.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B073F472JL" + } + }, + "MPU6050": { + "description": "6-Axis Accelerometer/Gyroscope (I2C)", + "category": "Sensor", + "unit_price_usd": 2.50, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B008BOPN40" + } + }, + "HC-SR04": { + "description": "Ultrasonic Distance Sensor", + "category": "Sensor", + "unit_price_usd": 2.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07RGB4W8V" + } + }, + "PIR-HC-SR501": { + "description": "PIR Motion Sensor", + "category": "Sensor", + "unit_price_usd": 1.50, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07KBWVJMP" + } + }, + + # Displays + "OLED-128x64": { + "description": "0.96\" OLED Display 128x64 (I2C)", + "category": "Display", + "unit_price_usd": 5.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B08ZY4YBHL", + "AliExpress": "https://aliexpress.com/item/32896971385.html" + } + }, + "LCD-16x2-I2C": { + "description": "16x2 LCD Display with I2C Backpack", + "category": "Display", + "unit_price_usd": 6.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07S7PJYM6" + } + }, + "TFT-1.8-ST7735": { + "description": "1.8\" TFT Display 128x160 (SPI)", + "category": "Display", + "unit_price_usd": 7.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B08FD643VZ" + } + }, + + # Communication + "NRF24L01": { + "description": "2.4GHz Wireless Transceiver", + "category": "Communication", + "unit_price_usd": 2.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B00O9O868G" + } + }, + "LoRa-SX1276": { + "description": "LoRa Radio Module 868/915MHz", + "category": "Communication", + "unit_price_usd": 12.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07V51M4VC" + } + }, + "GPS-NEO6M": { + "description": "GPS Module with Antenna", + "category": "Communication", + "unit_price_usd": 10.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07P8YMVNT" + } + }, + + # Power + "TP4056": { + "description": "LiPo Charging Module (USB-C)", + "category": "Power", + "unit_price_usd": 1.50, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07PZT3ZW2" + } + }, + "LM2596-DC-DC": { + "description": "Buck Converter Adjustable DC-DC", + "category": "Power", + "unit_price_usd": 2.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B01GJ0SC2C" + } + }, + "18650-Battery": { + "description": "18650 Li-ion Battery 3000mAh", + "category": "Power", + "unit_price_usd": 5.00, + "package": "Cell", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07SXRXVF3" + } + }, + "LiPo-1000mAh": { + "description": "LiPo Battery 3.7V 1000mAh", + "category": "Power", + "unit_price_usd": 6.00, + "package": "Cell", + "suppliers": { + "Adafruit": "https://www.adafruit.com/product/258" + } + }, + + # Actuators + "Servo-SG90": { + "description": "Micro Servo Motor SG90", + "category": "Actuator", + "unit_price_usd": 3.00, + "package": "Unit", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07MLR1498" + } + }, + "Relay-5V": { + "description": "5V Relay Module (1-Channel)", + "category": "Actuator", + "unit_price_usd": 2.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07BVXT1ZK" + } + }, + "Motor-Driver-L298N": { + "description": "L298N Dual H-Bridge Motor Driver", + "category": "Actuator", + "unit_price_usd": 4.00, + "package": "Module", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07BK1QL5T" + } + }, + + # Passive Components + "Resistor-Kit": { + "description": "Resistor Assortment Kit (600pcs)", + "category": "Passive", + "unit_price_usd": 10.00, + "package": "Kit", + "suppliers": { + "Amazon": "https://amazon.com/dp/B08FD1XVL6" + } + }, + "Capacitor-Kit": { + "description": "Ceramic Capacitor Kit (300pcs)", + "category": "Passive", + "unit_price_usd": 12.00, + "package": "Kit", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07PK78Y3H" + } + }, + "LED-Kit": { + "description": "LED Assortment Kit (500pcs)", + "category": "Passive", + "unit_price_usd": 8.00, + "package": "Kit", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07RBHSM6X" + } + }, + "Breadboard-830": { + "description": "830 Point Solderless Breadboard", + "category": "Passive", + "unit_price_usd": 5.00, + "package": "Unit", + "suppliers": { + "Amazon": "https://amazon.com/dp/B0135IQ0ZC" + } + }, + "Jumper-Wires-Kit": { + "description": "Jumper Wire Kit M-M, M-F, F-F", + "category": "Passive", + "unit_price_usd": 7.00, + "package": "Kit", + "suppliers": { + "Amazon": "https://amazon.com/dp/B07GD2BWPY" + } + } +} + + +@dataclass +class BOMItem: + """Single item in the BOM""" + reference: str # e.g., "U1", "R1" + name: str + description: str + quantity: int + category: str + unit_price_usd: float + package: str = "" + supplier: str = "" + supplier_link: str = "" + notes: str = "" + + @property + def total_price(self) -> float: + return self.quantity * self.unit_price_usd + + +@dataclass +class BOM: + """Complete Bill of Materials""" + project_name: str = "Untitled Project" + version: str = "1.0" + author: str = "" + date: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d")) + items: List[BOMItem] = field(default_factory=list) + + @property + def total_cost(self) -> float: + return sum(item.total_price for item in self.items) + + @property + def item_count(self) -> int: + return sum(item.quantity for item in self.items) + + def add_from_database(self, name: str, quantity: int = 1, + reference: str = "", notes: str = "", + supplier: str = "") -> bool: + """Add an item from the component database""" + if name not in COMPONENT_DATABASE: + return False + + comp = COMPONENT_DATABASE[name] + + # Select supplier + suppliers = comp.get("suppliers", {}) + if supplier and supplier in suppliers: + selected_supplier = supplier + supplier_link = suppliers[supplier] + elif suppliers: + selected_supplier = list(suppliers.keys())[0] + supplier_link = suppliers[selected_supplier] + else: + selected_supplier = "" + supplier_link = "" + + item = BOMItem( + reference=reference or f"X{len(self.items)+1}", + name=name, + description=comp["description"], + quantity=quantity, + category=comp["category"], + unit_price_usd=comp["unit_price_usd"], + package=comp.get("package", ""), + supplier=selected_supplier, + supplier_link=supplier_link, + notes=notes + ) + + self.items.append(item) + return True + + def add_custom(self, name: str, description: str, quantity: int, + category: str, unit_price: float, **kwargs) -> None: + """Add a custom item not in the database""" + item = BOMItem( + reference=kwargs.get("reference", f"X{len(self.items)+1}"), + name=name, + description=description, + quantity=quantity, + category=category, + unit_price_usd=unit_price, + package=kwargs.get("package", ""), + supplier=kwargs.get("supplier", ""), + supplier_link=kwargs.get("supplier_link", ""), + notes=kwargs.get("notes", "") + ) + self.items.append(item) + + def to_csv(self, filename: str) -> None: + """Export to CSV format""" + with open(filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + + # Header + writer.writerow([ + "Reference", "Name", "Description", "Quantity", + "Category", "Unit Price ($)", "Total ($)", + "Package", "Supplier", "Supplier Link", "Notes" + ]) + + # Items + for item in self.items: + writer.writerow([ + item.reference, item.name, item.description, item.quantity, + item.category, f"{item.unit_price_usd:.2f}", f"{item.total_price:.2f}", + item.package, item.supplier, item.supplier_link, item.notes + ]) + + # Total row + writer.writerow([]) + writer.writerow(["", "", "", self.item_count, "", "", f"{self.total_cost:.2f}", "", "", "", ""]) + + def to_xlsx(self, filename: str) -> bool: + """Export to Excel xlsx format with formatting""" + if not OPENPYXL_AVAILABLE: + print("Error: openpyxl not installed", file=sys.stderr) + return False + + wb = Workbook() + ws = wb.active + ws.title = "BOM" + + # Styles + header_font = Font(bold=True, color="FFFFFF") + header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") + border = Border( + left=Side(style='thin'), + right=Side(style='thin'), + top=Side(style='thin'), + bottom=Side(style='thin') + ) + currency_format = '$#,##0.00' + + # Project info + ws['A1'] = "Project:" + ws['B1'] = self.project_name + ws['A2'] = "Version:" + ws['B2'] = self.version + ws['A3'] = "Author:" + ws['B3'] = self.author + ws['A4'] = "Date:" + ws['B4'] = self.date + + ws['A1'].font = Font(bold=True) + ws['A2'].font = Font(bold=True) + ws['A3'].font = Font(bold=True) + ws['A4'].font = Font(bold=True) + + # Header row + headers = ["Ref", "Name", "Description", "Qty", "Category", + "Unit Price", "Total", "Package", "Supplier", "Notes"] + header_row = 6 + + for col, header in enumerate(headers, 1): + cell = ws.cell(row=header_row, column=col, value=header) + cell.font = header_font + cell.fill = header_fill + cell.border = border + cell.alignment = Alignment(horizontal='center') + + # Data rows + for row_num, item in enumerate(self.items, header_row + 1): + data = [ + item.reference, item.name, item.description, item.quantity, + item.category, item.unit_price_usd, item.total_price, + item.package, item.supplier, item.notes + ] + + for col, value in enumerate(data, 1): + cell = ws.cell(row=row_num, column=col, value=value) + cell.border = border + + # Format currency columns + if col in [6, 7]: + cell.number_format = currency_format + + # Total row + total_row = header_row + len(self.items) + 2 + ws.cell(row=total_row, column=3, value="TOTAL:").font = Font(bold=True) + ws.cell(row=total_row, column=4, value=self.item_count).font = Font(bold=True) + total_cell = ws.cell(row=total_row, column=7, value=self.total_cost) + total_cell.font = Font(bold=True) + total_cell.number_format = currency_format + + # Add formula for dynamic total + formula_row = header_row + 1 + end_row = header_row + len(self.items) + ws.cell(row=total_row, column=7, + value=f"=SUM(G{formula_row}:G{end_row})") + + # Adjust column widths + column_widths = [8, 25, 40, 6, 15, 12, 12, 10, 15, 30] + for i, width in enumerate(column_widths, 1): + ws.column_dimensions[get_column_letter(i)].width = width + + # Category summary sheet + ws2 = wb.create_sheet(title="By Category") + ws2['A1'] = "Category" + ws2['B1'] = "Items" + ws2['C1'] = "Total Cost" + ws2['A1'].font = Font(bold=True) + ws2['B1'].font = Font(bold=True) + ws2['C1'].font = Font(bold=True) + + categories = {} + for item in self.items: + if item.category not in categories: + categories[item.category] = {"count": 0, "cost": 0} + categories[item.category]["count"] += item.quantity + categories[item.category]["cost"] += item.total_price + + for row, (cat, data) in enumerate(sorted(categories.items()), 2): + ws2.cell(row=row, column=1, value=cat) + ws2.cell(row=row, column=2, value=data["count"]) + cost_cell = ws2.cell(row=row, column=3, value=data["cost"]) + cost_cell.number_format = currency_format + + # Save + wb.save(filename) + return True + + def to_markdown(self) -> str: + """Generate markdown BOM table""" + lines = [ + f"# Bill of Materials: {self.project_name}", + "", + f"**Version:** {self.version}", + f"**Author:** {self.author}", + f"**Date:** {self.date}", + "", + "## Components", + "", + "| Ref | Component | Description | Qty | Unit $ | Total $ | Supplier |", + "|-----|-----------|-------------|-----|--------|---------|----------|" + ] + + for item in self.items: + supplier_cell = f"[{item.supplier}]({item.supplier_link})" if item.supplier_link else item.supplier + lines.append( + f"| {item.reference} | {item.name} | {item.description} | " + f"{item.quantity} | ${item.unit_price_usd:.2f} | ${item.total_price:.2f} | {supplier_cell} |" + ) + + lines.extend([ + "", + f"**Total Components:** {self.item_count}", + f"**Estimated Cost:** ${self.total_cost:.2f}", + "", + "## By Category", + "" + ]) + + categories = {} + for item in self.items: + if item.category not in categories: + categories[item.category] = {"count": 0, "cost": 0} + categories[item.category]["count"] += item.quantity + categories[item.category]["cost"] += item.total_price + + for cat, data in sorted(categories.items()): + lines.append(f"- **{cat}:** {data['count']} items, ${data['cost']:.2f}") + + return "\n".join(lines) + + def to_dict(self) -> dict: + """Export to dictionary/JSON""" + return { + "project_name": self.project_name, + "version": self.version, + "author": self.author, + "date": self.date, + "items": [ + { + "reference": item.reference, + "name": item.name, + "description": item.description, + "quantity": item.quantity, + "category": item.category, + "unit_price_usd": item.unit_price_usd, + "total_price_usd": item.total_price, + "package": item.package, + "supplier": item.supplier, + "supplier_link": item.supplier_link, + "notes": item.notes + } + for item in self.items + ], + "summary": { + "total_items": self.item_count, + "total_cost_usd": round(self.total_cost, 2) + } + } + + @classmethod + def from_json(cls, filepath: str) -> 'BOM': + """Load BOM from JSON file""" + with open(filepath, 'r') as f: + data = json.load(f) + + bom = cls( + project_name=data.get("project_name", "Untitled"), + version=data.get("version", "1.0"), + author=data.get("author", ""), + date=data.get("date", datetime.now().strftime("%Y-%m-%d")) + ) + + for item in data.get("items", []): + if item.get("name") in COMPONENT_DATABASE: + bom.add_from_database( + name=item["name"], + quantity=item.get("quantity", 1), + reference=item.get("reference", ""), + notes=item.get("notes", ""), + supplier=item.get("supplier", "") + ) + else: + bom.add_custom( + name=item.get("name", "Unknown"), + description=item.get("description", ""), + quantity=item.get("quantity", 1), + category=item.get("category", "Other"), + unit_price=item.get("unit_price_usd", 0), + reference=item.get("reference", ""), + supplier=item.get("supplier", ""), + supplier_link=item.get("supplier_link", ""), + notes=item.get("notes", "") + ) + + return bom + + +def interactive_mode(): + """Run BOM generator in interactive mode""" + print("=" * 60) + print("BOM Generator - Interactive Mode") + print("=" * 60) + print() + + project_name = input("Project name [My Project]: ").strip() or "My Project" + version = input("Version [1.0]: ").strip() or "1.0" + author = input("Author: ").strip() + + bom = BOM(project_name=project_name, version=version, author=author) + + print("\nAvailable components in database:") + for i, name in enumerate(COMPONENT_DATABASE.keys(), 1): + print(f" {i:2}. {name}") + + print("\nEnter components (empty line to finish):") + print("Format: [quantity] [reference]") + + while True: + line = input("\nComponent: ").strip() + if not line: + break + + parts = line.split() + comp_input = parts[0] + quantity = int(parts[1]) if len(parts) > 1 else 1 + reference = parts[2] if len(parts) > 2 else "" + + # Handle numeric input + if comp_input.isdigit(): + idx = int(comp_input) - 1 + names = list(COMPONENT_DATABASE.keys()) + if 0 <= idx < len(names): + comp_input = names[idx] + else: + print("Invalid number") + continue + + if bom.add_from_database(comp_input, quantity, reference): + comp = COMPONENT_DATABASE[comp_input] + print(f" Added: {quantity}x {comp_input} (${comp['unit_price_usd']:.2f} each)") + else: + # Custom component + print(f" '{comp_input}' not in database. Enter details:") + desc = input(" Description: ").strip() + category = input(" Category: ").strip() or "Other" + price = float(input(" Unit price ($): ").strip() or "0") + bom.add_custom(comp_input, desc, quantity, category, price, reference=reference) + print(f" Added custom: {quantity}x {comp_input}") + + # Show summary + print("\n" + "=" * 60) + print(bom.to_markdown()) + + # Export + print("\nExport options:") + print("1. Excel (.xlsx)") + print("2. CSV (.csv)") + print("3. Markdown (.md)") + print("4. JSON (.json)") + print("5. All formats") + + choice = input("\nExport format [1]: ").strip() or "1" + base_name = project_name.replace(" ", "_") + + if choice in ["1", "5"]: + if bom.to_xlsx(f"{base_name}_BOM.xlsx"): + print(f"Saved: {base_name}_BOM.xlsx") + + if choice in ["2", "5"]: + bom.to_csv(f"{base_name}_BOM.csv") + print(f"Saved: {base_name}_BOM.csv") + + if choice in ["3", "5"]: + with open(f"{base_name}_BOM.md", 'w') as f: + f.write(bom.to_markdown()) + print(f"Saved: {base_name}_BOM.md") + + if choice in ["4", "5"]: + with open(f"{base_name}_BOM.json", 'w') as f: + json.dump(bom.to_dict(), f, indent=2) + print(f"Saved: {base_name}_BOM.json") + + +def main(): + parser = argparse.ArgumentParser(description="BOM Generator for Embedded Projects") + parser.add_argument("--interactive", "-i", action="store_true", help="Interactive mode") + parser.add_argument("--json", "-j", type=str, help="Load from JSON file") + parser.add_argument("--output", "-o", type=str, help="Output filename") + parser.add_argument("--format", "-f", type=str, choices=["xlsx", "csv", "md", "json"], + default="xlsx", help="Output format") + parser.add_argument("--list", "-l", action="store_true", help="List component database") + + args = parser.parse_args() + + if args.list: + print("Component Database:") + print("-" * 80) + for name, data in COMPONENT_DATABASE.items(): + print(f"{name:25} ${data['unit_price_usd']:6.2f} {data['description']}") + return + + if args.interactive: + interactive_mode() + return + + if args.json: + bom = BOM.from_json(args.json) + output = args.output or f"{bom.project_name.replace(' ', '_')}_BOM.{args.format}" + + if args.format == "xlsx": + bom.to_xlsx(output) + elif args.format == "csv": + bom.to_csv(output) + elif args.format == "md": + with open(output, 'w') as f: + f.write(bom.to_markdown()) + elif args.format == "json": + with open(output, 'w') as f: + json.dump(bom.to_dict(), f, indent=2) + + print(f"Generated: {output}") + return + + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/circuit-debugger/SKILL.md b/.agents/skills/circuit-debugger/SKILL.md new file mode 100644 index 0000000..a1142b6 --- /dev/null +++ b/.agents/skills/circuit-debugger/SKILL.md @@ -0,0 +1,239 @@ +--- +name: circuit-debugger +description: | + Systematic hardware debugging guide for Arduino/ESP32/RP2040 circuits. Use when user reports: circuit not working, components getting hot, no power, intermittent failures, unexpected behavior, sensor not responding, LED not lighting, motor not spinning. Guides through power checks, continuity testing, signal tracing, and component isolation using multimeter techniques. +--- + +# Circuit Debugger + +Systematic approach to diagnosing hardware issues in maker projects. + +## Resources + +This skill includes bundled tools and references: + +- **scripts/generate_debug_sketch.py** - Arduino sketch generator for I2C scanner, GPIO tester, ADC checker, PWM tester +- **references/measurement-procedures.md** - Comprehensive multimeter and oscilloscope guide + +## Quick Start + +**Generate I2C scanner:** +```bash +uv run --no-project scripts/generate_debug_sketch.py --i2c --output i2c_scanner.ino +``` + +**Generate GPIO tester:** +```bash +uv run --no-project scripts/generate_debug_sketch.py --gpio --pins 2,3,4,5 --output gpio_test.ino +``` + +**Generate all debug sketches:** +```bash +uv run --no-project scripts/generate_debug_sketch.py --all +``` + +**Interactive mode:** +```bash +uv run --no-project scripts/generate_debug_sketch.py --interactive +``` + +## Trigger Phrases +- "My circuit doesn't work" +- "Component is getting hot" +- "No power to the board" +- "Sensor not responding" +- "Intermittent/random failures" +- "Works sometimes, not others" + +## Debugging Protocol + +### Phase 1: Power System Check (Do First!) + +**Visual Inspection (30 seconds)** +``` +β–‘ Check for smoke, burn marks, or melted plastic +β–‘ Verify power LED on microcontroller is lit +β–‘ Look for loose wires or cold solder joints +β–‘ Confirm correct polarity on polarized components (LEDs, caps, diodes) +``` + +**Multimeter Tests (Set to DC Voltage)** +``` +Test Point | Expected Value | If Wrong +------------------------|-----------------|------------------ +VCC to GND on MCU | 3.3V or 5V | Check regulator, power source +Sensor VCC pin | Match datasheet | Check wiring, broken trace +Motor driver VCC | Logic + Motor V | Separate supplies needed? +Battery terminals | Rated voltage | Dead/discharged battery +``` + +**Common Power Issues:** +| Symptom | Likely Cause | Fix | +|---------|--------------|-----| +| No voltage anywhere | Disconnected power, blown fuse | Check continuity from source | +| Low voltage (< 4V when expecting 5V) | Overloaded supply, bad regulator | Reduce load, check current draw | +| Voltage drops under load | Undersized power supply | Calculate total current, upgrade PSU | +| Reverse polarity | Swapped wires | Check all connections, may have damaged components | + +### Phase 2: Ground Continuity + +**Critical Rule:** All grounds must be connected together. + +``` +Multimeter: Set to CONTINUITY (beep mode) + +Test these pairs - ALL should beep: +β–‘ Arduino GND ↔ Sensor GND +β–‘ Arduino GND ↔ Motor driver GND +β–‘ Arduino GND ↔ Display GND +β–‘ Arduino GND ↔ Power supply negative +β–‘ All breadboard GND rails connected +``` + +**Ground Problems Cause:** +- Erratic sensor readings +- I2C/SPI communication failures +- Motors behaving randomly +- Displays showing garbage + +### Phase 3: Signal Verification + +**Digital Signals (Set multimeter to DC Voltage)** +```cpp +// Add this debug code to verify pin states +void debugPins() { + Serial.println("=== Pin States ==="); + Serial.print("D2: "); Serial.println(digitalRead(2) ? "HIGH" : "LOW"); + Serial.print("D3: "); Serial.println(digitalRead(3) ? "HIGH" : "LOW"); + // Add more pins as needed +} +``` + +**Expected Readings:** +| Signal Type | HIGH | LOW | Floating (Bad!) | +|-------------|------|-----|-----------------| +| 5V Logic | 4.5-5.5V | 0-0.5V | 1-3V unstable | +| 3.3V Logic | 2.8-3.6V | 0-0.3V | 0.8-2V unstable | + +**I2C Troubleshooting:** +```cpp +// Run this I2C scanner first +#include + +void setup() { + Serial.begin(115200); + Wire.begin(); + + Serial.println("I2C Scanner"); + for (uint8_t addr = 1; addr < 127; addr++) { + Wire.beginTransmission(addr); + if (Wire.endTransmission() == 0) { + Serial.print("Found device at 0x"); + Serial.println(addr, HEX); + } + } +} +void loop() {} +``` + +| I2C Problem | Check | +|-------------|-------| +| No devices found | SDA/SCL swapped? Pull-ups present? Correct address? | +| Address conflict | Two devices same address? Check AD0/AD1 pins | +| Intermittent | Weak pull-ups (try 4.7kΞ©), long wires, noise | + +### Phase 4: Component Isolation + +**The Divide-and-Conquer Method:** +``` +1. Disconnect ALL external components +2. Verify MCU works alone (blink LED) +3. Add ONE component at a time +4. Test after EACH addition +5. When failure occurs, problem is last added component +``` + +**Component-Specific Tests:** + +**LEDs:** +``` +β–‘ Correct polarity? (long leg = anode = positive) +β–‘ Current limiting resistor present? (330Ξ©-1kΞ© typical) +β–‘ Test LED alone with battery + resistor +β–‘ PWM pin? Try digitalWrite first +``` + +**Motors/Servos:** +``` +β–‘ Never connect directly to MCU pin (use driver!) +β–‘ Separate power supply for motors +β–‘ Flyback diode on DC motors +β–‘ Check stall current vs driver rating +``` + +**Sensors:** +``` +β–‘ Correct operating voltage (3.3V vs 5V!) +β–‘ Level shifter needed for mixed voltage? +β–‘ Decoupling capacitor (100nF) near VCC pin +β–‘ Pull-up resistors for I2C (4.7kΞ© typical) +``` + +### Phase 5: Software vs Hardware + +**Quick Software Test:** +```cpp +// Minimal test - does the MCU even run? +void setup() { + pinMode(LED_BUILTIN, OUTPUT); + Serial.begin(115200); + Serial.println("MCU is alive!"); +} + +void loop() { + digitalWrite(LED_BUILTIN, HIGH); + delay(500); + digitalWrite(LED_BUILTIN, LOW); + delay(500); + Serial.println("heartbeat"); +} +``` + +| If This Works | If This Fails | +|---------------|---------------| +| Hardware likely OK, check your code | Check USB cable, bootloader, board selection | + +## Quick Reference: Common Failures + +| Symptom | First Check | Second Check | Third Check | +|---------|-------------|--------------|-------------| +| Nothing works | Power supply | USB cable | Board selection in IDE | +| Gets hot | Short circuit | Reversed polarity | Overcurrent | +| Works then stops | Power brownout | Overheating | Memory leak | +| Erratic behavior | Floating inputs | Missing grounds | Noise/interference | +| I2C fails | Pull-ups | Address | Wire length | +| Motor jerky | Power supply | PWM frequency | Driver current | + +## Multimeter Quick Guide + +``` +Measurement | Setting | Probes +---------------|--------------|------------------ +DC Voltage | VβŽ“ (20V) | Red=signal, Black=GND +Continuity | ))) or Ξ© | Either direction +Resistance | Ξ© | Component out of circuit! +Current | A or mA | IN SERIES (break circuit) +``` + +## When to Ask for Help + +If after this protocol you still can't find the issue: +1. Take clear photos of your wiring +2. Draw a schematic (even hand-drawn) +3. List exact components with part numbers +4. Share your complete code +5. Describe what you expected vs what happened + +## References +- See [references/multimeter-guide.md](references/multimeter-guide.md) for detailed measurement techniques +- See [references/common-mistakes.md](references/common-mistakes.md) for beginner pitfall gallery diff --git a/.agents/skills/circuit-debugger/references/common-mistakes.md b/.agents/skills/circuit-debugger/references/common-mistakes.md new file mode 100644 index 0000000..3d186d4 --- /dev/null +++ b/.agents/skills/circuit-debugger/references/common-mistakes.md @@ -0,0 +1,238 @@ +# Common Hardware Mistakes Gallery + +## Beginner Hall of Fame (We've All Done These!) + +### 1. Reversed LED Polarity +``` +WRONG: CORRECT: + [Short leg]─R─VCC [Long leg]─R─VCC + [Long leg]──GND [Short leg]──GND + +Remember: Long leg = Anode = Positive = + +Flat side of LED body = Cathode = Negative = - +``` +**Symptom:** LED doesn't light up +**Fix:** Flip the LED around + +### 2. Missing Current-Limiting Resistor +``` +WRONG (will burn out LED): + GPIO ──────[LED]────── GND + +CORRECT: + GPIO ──[330Ξ©]──[LED]── GND +``` +**Symptom:** LED very bright then dies, or GPIO pin damaged +**Fix:** Always use 220Ξ©-1kΞ© resistor with LEDs + +### 3. Breadboard Power Rails Not Connected +``` +Many breadboards have SPLIT power rails! + + [+]───────── [+]───────── + [-]───────── [-]───────── + ↑ + GAP HERE - not connected! + +Add jumper wire to connect both halves +``` +**Symptom:** Components on one side work, other side dead +**Fix:** Bridge the gap with jumper wires + +### 4. Wrong Breadboard Row +``` +Component leads must be in DIFFERENT rows: + +WRONG (shorted): CORRECT: + A B C D E A B C D E + 1 [===LED===] 1 [LED] + 2 [LED] +Both legs in row 1 Legs in rows 1 and 2 += short circuit = proper connection +``` +**Symptom:** Component doesn't work or gets hot +**Fix:** Ensure leads span multiple rows + +### 5. Forgetting Pull-Up Resistors on I2C +``` +I2C REQUIRES pull-ups! + + VCC + β”‚ + [4.7kΞ©] + β”‚ +MCU SDA ───┴─── Sensor SDA + +(Same for SCL line) +``` +**Symptom:** I2C scanner finds nothing, or intermittent communication +**Fix:** Add 4.7kΞ© pull-ups from SDA and SCL to VCC + +### 6. 5V Sensor on 3.3V MCU (or vice versa) +``` +ESP32 is 3.3V logic! + +WRONG: +5V sensor ──────────→ ESP32 GPIO + (can damage ESP32!) + +CORRECT: +5V sensor ──[Level Shifter]──→ ESP32 GPIO +``` +**Symptom:** Erratic behavior, sensor works briefly then MCU dies +**Fix:** Use level shifter or voltage divider + +### 7. Motor Connected Directly to GPIO +``` +WRONG (will damage MCU): +GPIO ───────[MOTOR]─── GND + ↑ + Motors need more current + than GPIO can provide! + +CORRECT: +GPIO ──[Driver]──[MOTOR]─── Motor Power + β”‚ + Separate power supply +``` +**Symptom:** Motor doesn't move, MCU resets, or GPIO burns out +**Fix:** Use motor driver (L298N, TB6612, L293D) + +### 8. Missing Flyback Diode on Inductive Loads +``` +Motors/relays generate voltage spikes when turned off! + +CORRECT (with flyback diode): + β”Œβ”€β”€β”€β”€[DIODE]────┐ + β”‚ ← stripe β”‚ + └──[MOTOR/RELAY]β”˜ + β”‚ β”‚ + Driver GND +``` +**Symptom:** Random resets, erratic behavior, damaged components +**Fix:** Add flyback diode (1N4001-1N4007) across motor/relay + +### 9. Servo Power from Arduino 5V Pin +``` +WRONG: +Arduino 5V ──→ Servo VCC + (can cause brownouts!) + +CORRECT: +External 5V ──→ Servo VCC + └─→ Arduino GND (connect grounds!) +``` +**Symptom:** Servo jitters, Arduino resets, erratic behavior +**Fix:** Use external 5V power supply for servos + +### 10. Floating Input Pins +``` +WRONG: +Button ──────→ GPIO (set as INPUT) + ↑ + Pin floats when button open + Reads random HIGH/LOW + +CORRECT: + VCC + β”‚ + [10kΞ©] Pull-up + β”‚ +Button ───┴──→ GPIO (INPUT) + β”‚ + GND + +Or use INPUT_PULLUP mode: + pinMode(BUTTON_PIN, INPUT_PULLUP); +``` +**Symptom:** Random triggering, noise-sensitive inputs +**Fix:** Use INPUT_PULLUP or external pull-up/down resistor + +## Intermediate Mistakes + +### 11. Ground Loops +``` +Multiple ground paths can create noise: + +WRONG: CORRECT: +MCU ─┬─ Sensor MCU ────┬─ Sensor + └─ Motor Driver β”‚ + └─ Power GND Star ground + all to one point +``` +**Symptom:** Noisy sensor readings, especially when motors run +**Fix:** Use star grounding, separate analog/digital grounds + +### 12. Inadequate Decoupling Capacitors +``` +Every IC needs decoupling! + + VCC + β”‚ + [100nF] ← Close to IC! + β”‚ + [IC] + β”‚ + GND +``` +**Symptom:** Occasional glitches, communication errors, resets +**Fix:** Add 100nF ceramic capacitor close to every IC's VCC pin + +### 13. Long I2C Wires +``` +I2C is designed for short distances (<30cm) + +Long wires = increased capacitance = signal degradation + +Solutions: +1. Shorter wires +2. Lower I2C speed (Wire.setClock(100000)) +3. Stronger pull-ups (2.2kΞ© instead of 4.7kΞ©) +4. Use I2C extender chip for long runs +``` +**Symptom:** I2C works close, fails when wires extended +**Fix:** Keep I2C wires short, or use I2C bus extender + +### 14. PWM Frequency Mismatch +``` +Some components need specific PWM frequencies: + +- LEDs: Any frequency >100Hz (no flicker) +- Motors: 1-20kHz typical +- Servos: 50Hz (20ms period) required! +- ESCs: Varies, check datasheet +``` +**Symptom:** Component behaves strangely, servo jitters +**Fix:** Match PWM frequency to component requirements + +### 15. Shared SPI Without Proper CS Handling +``` +Multiple SPI devices need separate Chip Select: + + CS1 CS2 + β”‚ β”‚ + [A] [B] + ↑ ↑ + MOSI/MISO/SCK shared + +IMPORTANT: Only ONE CS low at a time! +``` +**Symptom:** SPI devices interfere with each other +**Fix:** Ensure all CS pins HIGH before selecting device + +## Debug Checklist Template + +``` +β–‘ Power LED on MCU lit? +β–‘ Correct voltage at VCC? +β–‘ All grounds connected? +β–‘ Correct pin assignments in code? +β–‘ Component polarity correct? +β–‘ Resistors where needed? +β–‘ Pull-ups on I2C? +β–‘ Level shifters for mixed voltage? +β–‘ Decoupling caps near ICs? +β–‘ Flyback diodes on motors/relays? +β–‘ External power for high-current loads? +β–‘ No floating input pins? +``` diff --git a/.agents/skills/circuit-debugger/references/measurement-procedures.md b/.agents/skills/circuit-debugger/references/measurement-procedures.md new file mode 100644 index 0000000..cd4f8af --- /dev/null +++ b/.agents/skills/circuit-debugger/references/measurement-procedures.md @@ -0,0 +1,267 @@ +# Circuit Debugging Measurement Procedures + +## Multimeter Basics + +### Safety First +- Never measure resistance in a powered circuit +- Start with highest range when unknown +- Check meter leads for damage before use +- Use proper probes (not worn/broken tips) + +### Common Measurements + +## 1. Continuity Testing + +**Purpose:** Check if two points are electrically connected + +**Procedure:** +1. Turn off/disconnect power +2. Set multimeter to continuity mode (πŸ”Š symbol) +3. Touch probes to two points +4. Beep = connected, No beep = open + +**When to Use:** +- Verify solder joints +- Check for broken traces +- Test switches/buttons +- Find shorts + +**Expected Results:** +| Component | Should Beep? | +|-----------|--------------| +| Wire | Yes | +| Closed switch | Yes | +| Open switch | No | +| Diode (forward) | Yes (with resistance) | +| Diode (reverse) | No | +| Capacitor | Brief beep, then no | + +--- + +## 2. DC Voltage Measurement + +**Purpose:** Measure voltage between two points + +**Procedure:** +1. Set meter to DC voltage (VβŽ“) +2. Select appropriate range (or auto-range) +3. Black probe to ground/reference +4. Red probe to measurement point +5. Read display + +**Common Voltages:** +| Source | Expected | +|--------|----------| +| USB | 4.75-5.25V | +| LiPo battery | 3.0-4.2V | +| 3.3V regulator | 3.2-3.4V | +| 5V regulator | 4.9-5.1V | +| Arduino 5V pin | 4.5-5.5V | +| Arduino 3.3V pin | 3.1-3.5V | + +**Troubleshooting:** +- 0V = No power, broken connection +- Lower than expected = Overloaded, bad regulator +- Higher than expected = Check regulator, source + +--- + +## 3. Current Measurement + +**Purpose:** Measure current flowing through circuit + +**Procedure:** +1. **BREAK** the circuit where you want to measure +2. Set meter to DC current (AβŽ“) +3. Connect meter IN SERIES (current flows through meter) +4. Red to upstream, black to downstream +5. Read display + +**⚠️ WARNING:** +- Never connect ammeter in parallel! +- Use correct port (mA vs 10A) +- Start with highest range + +**Typical Currents:** +| State | Current | +|-------|---------| +| Arduino Uno idle | 40-50mA | +| ESP32 WiFi active | 80-250mA | +| ESP32 deep sleep | 10-150Β΅A | +| LED (typical) | 10-20mA | +| Servo idle | 10-20mA | +| Servo moving | 100-500mA | + +--- + +## 4. Resistance Measurement + +**Purpose:** Measure resistance of component or connection + +**Procedure:** +1. **POWER OFF** - Never measure resistance in live circuit! +2. Set meter to Ξ© (ohms) +3. Touch probes to component leads +4. Read display + +**Component Values:** +| Component | Typical Resistance | +|-----------|-------------------| +| Wire/trace | <1Ξ© | +| LED (forward) | 20-200Ξ© | +| Pull-up resistor | 1kΞ©-10kΞ© | +| Potentiometer | Varies with position | +| Thermistor | Varies with temp | +| Open circuit | OL (overload) | +| Short circuit | 0Ξ© | + +--- + +## 5. Diode/LED Testing + +**Purpose:** Test diode functionality and forward voltage + +**Procedure:** +1. Set meter to diode test mode (β–·|) +2. Red probe to anode (+), black to cathode (-) +3. Read forward voltage drop +4. Reverse probes - should show OL + +**Expected Forward Voltages:** +| Diode Type | Forward Voltage | +|------------|-----------------| +| Silicon (1N4148) | 0.6-0.7V | +| Schottky | 0.2-0.4V | +| Red LED | 1.8-2.2V | +| Green LED | 2.0-2.4V | +| Blue/White LED | 3.0-3.4V | +| Zener (reverse) | Zener voltage | + +--- + +## Common Debug Scenarios + +### Scenario 1: Device Not Powering On + +**Checklist:** +``` +β–‘ Check power source voltage +β–‘ Check regulator input voltage +β–‘ Check regulator output voltage +β–‘ Check for shorts (0Ξ© between VCC and GND) +β–‘ Feel for hot components +β–‘ Check fuse/polyfuse if present +``` + +### Scenario 2: Intermittent Operation + +**Checklist:** +``` +β–‘ Check all solder joints (cold joints?) +β–‘ Wiggle wires while measuring voltage +β–‘ Check connector seating +β–‘ Look for loose screws/standoffs touching traces +β–‘ Check power under load vs idle +``` + +### Scenario 3: Sensor Not Working + +**Checklist:** +``` +β–‘ Verify power at sensor VCC pin +β–‘ Check I2C/SPI connections with scope/logic analyzer +β–‘ Run I2C scanner to find address +β–‘ Verify pull-up resistors present for I2C +β–‘ Check signal levels match MCU voltage +``` + +### Scenario 4: Motor/Actuator Issues + +**Checklist:** +``` +β–‘ Measure motor supply voltage +β–‘ Check driver IC is getting logic power +β–‘ Verify PWM signal present +β–‘ Check for proper ground connection +β–‘ Test motor directly with power supply +β–‘ Check current - motor might be stalled +``` + +--- + +## Logic Level Reference + +| Logic Family | LOW | HIGH | Note | +|--------------|-----|------|------| +| 5V TTL | 0-0.8V | 2.0-5V | Arduino Uno | +| 5V CMOS | 0-1.5V | 3.5-5V | ATmega328 | +| 3.3V CMOS | 0-1.0V | 2.3-3.3V | ESP32, RP2040 | +| 1.8V CMOS | 0-0.6V | 1.2-1.8V | Some sensors | + +**Level Shifting Required When:** +- 5V Arduino ↔ 3.3V sensor +- 3.3V MCU ↔ 5V display +- Any mixed-voltage I2C bus + +--- + +## Oscilloscope Quick Reference + +### When Multimeter Isn't Enough: + +Use oscilloscope for: +- PWM signal verification +- Serial communication debugging +- Timing-critical signals +- Noise analysis +- I2C/SPI bus issues + +### Common Signals: + +**PWM:** +- Square wave +- Check frequency and duty cycle +- Look for clean edges + +**I2C:** +- SDA: Data, bidirectional +- SCL: Clock, master-driven +- Both idle HIGH (pull-ups) +- Look for ACK bits + +**UART/Serial:** +- Idle HIGH +- Start bit (LOW) +- 8 data bits +- Stop bit (HIGH) + +--- + +## Quick Debug Commands (Arduino Serial) + +```cpp +// Print all pin states +void debugPins() { + for (int i = 0; i < 14; i++) { + Serial.print("D"); Serial.print(i); + Serial.print(": "); Serial.println(digitalRead(i)); + } + for (int i = 0; i < 6; i++) { + Serial.print("A"); Serial.print(i); + Serial.print(": "); Serial.println(analogRead(i)); + } +} + +// Measure execution time +unsigned long start = micros(); +// ... code to measure ... +unsigned long duration = micros() - start; +Serial.print("Duration: "); Serial.print(duration); Serial.println(" Β΅s"); + +// Memory check +extern int __heap_start, *__brkval; +int freeMemory() { + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} +``` diff --git a/.agents/skills/circuit-debugger/references/multimeter-guide.md b/.agents/skills/circuit-debugger/references/multimeter-guide.md new file mode 100644 index 0000000..899dfc4 --- /dev/null +++ b/.agents/skills/circuit-debugger/references/multimeter-guide.md @@ -0,0 +1,165 @@ +# Multimeter Usage Guide for Beginners + +## Safety First +- **NEVER** measure current directly across a power source (creates short circuit!) +- Start with highest range, work down +- Discharge capacitors before measuring resistance +- Don't measure live mains voltage as a beginner + +## Essential Measurements + +### DC Voltage (Most Common) +**Setting:** VβŽ“ or VDC, 20V range for Arduino projects + +``` +How to measure: +1. Set dial to DC Voltage (V with straight line) +2. Select 20V range (covers 0-20V) +3. Black probe β†’ Ground/Negative +4. Red probe β†’ Point to measure +5. Read display + + Red probe here + ↓ + VCC ───[component]─── GND + ↑ + Black probe here +``` + +**Interpreting Results:** +| Reading | Meaning | +|---------|---------| +| 4.8-5.2V | Good 5V rail | +| 3.2-3.4V | Good 3.3V rail | +| 0V | No power or open circuit | +| Negative value | Probes reversed | +| Unstable/jumping | Floating or noisy | + +### Continuity Test +**Setting:** Diode/Continuity symbol (sounds like ))) ) + +``` +How to use: +1. Set dial to continuity (beep symbol) +2. Touch probes together β†’ should beep +3. Test connections β†’ beep = connected + +Testing a wire: + [====WIRE====] + ↑ ↑ + Probe Probe + + Beep = Good wire + No beep = Broken wire +``` + +**Use For:** +- Checking if wires are connected +- Finding broken traces on PCB +- Verifying solder joints +- Finding shorts (unexpected beeps!) + +### Resistance +**Setting:** Ξ© (Ohms) + +``` +IMPORTANT: Remove component from circuit first! +Otherwise you measure parallel paths, wrong value. + +How to measure: +1. Set to Ξ© range (start with 20kΞ©) +2. Touch probes to component leads +3. If shows "1" or "OL", increase range +4. Read value + +Common resistor values: +- 330Ξ© = LED current limiter +- 4.7kΞ© = I2C pull-up +- 10kΞ© = Common pull-up/down +``` + +### Current (Advanced - Use Carefully!) +**Setting:** A or mA + +``` +⚠️ WARNING: Must break circuit and insert meter IN SERIES! + +WRONG (will blow fuse or damage meter): + VCC ──[A]── GND ← SHORT CIRCUIT! + +CORRECT: + VCC ──[A]──[Load]── GND + ↑ + Meter measures current + flowing through load +``` + +**Current Measurement Setup:** +``` +Before: + VCC ─────[LED+R]───── GND + +After (insert meter): + VCC ──[mA]──[LED+R]── GND + ↑ + Break wire here, + insert meter in gap +``` + +## Troubleshooting with Multimeter + +### "Nothing Powers On" +``` +1. Check battery/power supply voltage + - Fresh 9V battery: 9.0-9.6V + - Drained 9V battery: <7V + - USB power: 4.75-5.25V + +2. Check voltage at MCU VCC pin + - If 0V: trace back to power source + - If correct: MCU may be damaged + +3. Check for shorts + - Continuity between VCC and GND + - Should NOT beep! If it does = short circuit +``` + +### "Component Gets Hot" +``` +1. Immediately disconnect power! +2. Check for reversed polarity +3. Check for short circuits +4. Verify component voltage rating +5. Calculate if current is excessive +``` + +### "Sensor Gives Wrong Values" +``` +1. Verify VCC voltage at sensor +2. Check signal voltage levels +3. Test with known-good values +4. Check for noise (unstable readings) +``` + +## Quick Reference Card + +| What to Measure | Setting | Probes | Expected | +|-----------------|---------|--------|----------| +| Arduino 5V rail | VDC 20V | Red=5V pin, Black=GND | 4.8-5.2V | +| Arduino 3.3V | VDC 20V | Red=3.3V, Black=GND | 3.2-3.4V | +| Battery | VDC 20V | Red=+, Black=- | Rated voltage | +| Wire intact? | Continuity | Both ends | Beep | +| Resistor value | Ξ© | Both leads | Marked value | +| LED polarity | Diode | Either way | Shows ~0.6-2V one way | +| I2C pull-up | Ξ© | SDA/SCL to VCC | ~4.7kΞ© | + +## Budget Multimeter Recommendations +- **Beginner:** Any $15-25 auto-ranging meter +- **Recommended:** AstroAI DM6000AR, Kaiweets HT118A +- **Pro:** Fluke 117 (expensive but lifetime tool) + +Key features needed: +- Auto-ranging (easier to use) +- Continuity beeper +- DC voltage to 20V+ +- Current measurement (mA range) diff --git a/.agents/skills/circuit-debugger/scripts/generate_debug_sketch.py b/.agents/skills/circuit-debugger/scripts/generate_debug_sketch.py new file mode 100644 index 0000000..547ab74 --- /dev/null +++ b/.agents/skills/circuit-debugger/scripts/generate_debug_sketch.py @@ -0,0 +1,1031 @@ +#!/usr/bin/env python3 +""" +Debug Sketch Generator - Creates diagnostic Arduino sketches + +Generates diagnostic code for common debugging tasks: +- I2C bus scanner +- SPI device tester +- GPIO pin tester +- Serial loopback test +- Voltage/ADC checker +- PWM output tester +- Interrupt tester + +Usage: + uv run --no-project scripts/generate_debug_sketch.py --i2c + uv run --no-project scripts/generate_debug_sketch.py --gpio --pins 2,3,4,5 + uv run --no-project scripts/generate_debug_sketch.py --adc --pins A0,A1,A2 + uv run --no-project scripts/generate_debug_sketch.py --all --output debug_suite.ino +""" + +import argparse +from typing import List, Optional + +# ============================================================================= +# Sketch Templates +# ============================================================================= + +I2C_SCANNER = '''/* + * I2C Bus Scanner + * Scans for all connected I2C devices and reports addresses + * + * Wiring: + * SDA -> A4 (Uno) or 21 (Mega) or GPIO21 (ESP32) or GP4 (Pico) + * SCL -> A5 (Uno) or 20 (Mega) or GPIO22 (ESP32) or GP5 (Pico) + * + * Common I2C addresses: + * 0x27, 0x3F - LCD displays + * 0x3C, 0x3D - OLED displays (SSD1306) + * 0x68 - DS3231 RTC, MPU6050 + * 0x76, 0x77 - BME280/BMP280 + * 0x48-0x4F - PCF8591, ADS1115 + * 0x20-0x27 - PCF8574 I/O expander + * 0x50-0x57 - EEPROM (24LC256) + */ + +#include + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); // Wait for serial (Leonardo/ESP32) + + Wire.begin(); + + Serial.println(); + Serial.println("========================================"); + Serial.println(" I2C Bus Scanner v1.0 "); + Serial.println("========================================"); + Serial.println(); + + scanI2C(); +} + +void loop() { + Serial.println("\\nPress any key to scan again..."); + while (!Serial.available()) delay(100); + while (Serial.available()) Serial.read(); + scanI2C(); +} + +void scanI2C() { + byte deviceCount = 0; + byte error; + + Serial.println("Scanning I2C bus (0x03 to 0x77)..."); + Serial.println(); + + // Print header + Serial.print(" "); + for (byte i = 0; i < 16; i++) { + Serial.print(" "); + if (i < 16) Serial.print(i, HEX); + } + Serial.println(); + + for (byte row = 0; row < 8; row++) { + Serial.print(row, HEX); + Serial.print("0: "); + + for (byte col = 0; col < 16; col++) { + byte addr = (row << 4) | col; + + if (addr < 0x03 || addr > 0x77) { + Serial.print(" --"); + } else { + Wire.beginTransmission(addr); + error = Wire.endTransmission(); + + if (error == 0) { + Serial.print(" "); + if (addr < 16) Serial.print("0"); + Serial.print(addr, HEX); + deviceCount++; + } else { + Serial.print(" --"); + } + } + } + Serial.println(); + } + + Serial.println(); + Serial.print("Found "); + Serial.print(deviceCount); + Serial.println(" device(s)"); + + if (deviceCount > 0) { + Serial.println("\\nDevice details:"); + for (byte addr = 0x03; addr <= 0x77; addr++) { + Wire.beginTransmission(addr); + if (Wire.endTransmission() == 0) { + Serial.print(" 0x"); + if (addr < 16) Serial.print("0"); + Serial.print(addr, HEX); + Serial.print(" - "); + Serial.println(identifyDevice(addr)); + } + } + } +} + +String identifyDevice(byte addr) { + // Common device identification + switch(addr) { + case 0x27: case 0x3F: return "LCD I2C (PCF8574)"; + case 0x3C: case 0x3D: return "OLED SSD1306"; + case 0x68: return "DS3231 RTC or MPU6050"; + case 0x69: return "MPU6050 (ALT)"; + case 0x76: case 0x77: return "BME280/BMP280"; + case 0x48: return "ADS1115/PCF8591"; + case 0x50: return "EEPROM 24LC256"; + case 0x57: return "MAX30102 Pulse Sensor"; + case 0x1E: return "HMC5883L Compass"; + case 0x53: return "ADXL345 Accelerometer"; + case 0x40: return "INA219 Current Sensor or HTU21D"; + case 0x44: return "SHT31 Humidity"; + case 0x29: return "VL53L0X ToF Sensor"; + default: + if (addr >= 0x20 && addr <= 0x27) return "PCF8574 I/O Expander"; + if (addr >= 0x50 && addr <= 0x57) return "EEPROM"; + return "Unknown device"; + } +} +''' + +GPIO_TESTER = '''/* + * GPIO Pin Tester + * Tests digital I/O functionality + * + * Features: + * - Output test (LED blink pattern) + * - Input test with pullup + * - Measures pin capacitance indication + * + * Test pins: {pins} + */ + +const int TEST_PINS[] = {{{pins_array}}}; +const int NUM_PINS = {num_pins}; + +void setup() {{ + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println(); + Serial.println("========================================"); + Serial.println(" GPIO Pin Tester v1.0 "); + Serial.println("========================================"); + Serial.println(); + + Serial.println("Testing pins: {pins}"); + Serial.println(); + + // Test each pin + for (int i = 0; i < NUM_PINS; i++) {{ + testPin(TEST_PINS[i]); + }} + + Serial.println("\\nOutput test - watch for LED blink pattern..."); + outputTest(); +}} + +void loop() {{ + // Continuous input monitoring + Serial.println("\\nMonitoring inputs (press any key to restart)..."); + + while (!Serial.available()) {{ + for (int i = 0; i < NUM_PINS; i++) {{ + pinMode(TEST_PINS[i], INPUT_PULLUP); + }} + + Serial.print("Inputs: "); + for (int i = 0; i < NUM_PINS; i++) {{ + Serial.print("D"); + Serial.print(TEST_PINS[i]); + Serial.print("="); + Serial.print(digitalRead(TEST_PINS[i])); + Serial.print(" "); + }} + Serial.println(); + delay(500); + }} + + while (Serial.available()) Serial.read(); + + Serial.println("\\n--- Restarting test ---\\n"); + for (int i = 0; i < NUM_PINS; i++) {{ + testPin(TEST_PINS[i]); + }} + outputTest(); +}} + +void testPin(int pin) {{ + Serial.print("Pin D"); + Serial.print(pin); + Serial.print(": "); + + // Test as output + pinMode(pin, OUTPUT); + digitalWrite(pin, HIGH); + delay(1); + digitalWrite(pin, LOW); + + // Test as input with pullup + pinMode(pin, INPUT_PULLUP); + delay(1); + int pullupVal = digitalRead(pin); + + // Test as input without pullup + pinMode(pin, INPUT); + delay(1); + int floatVal = digitalRead(pin); + + Serial.print("pullup="); + Serial.print(pullupVal); + Serial.print(" float="); + Serial.print(floatVal); + + // Diagnosis + if (pullupVal == HIGH && floatVal == LOW) {{ + Serial.println(" [OK - normal]"); + }} else if (pullupVal == HIGH && floatVal == HIGH) {{ + Serial.println(" [OK - pulled high externally]"); + }} else if (pullupVal == LOW) {{ + Serial.println(" [WARNING - pulled low or shorted to GND]"); + }} else {{ + Serial.println(" [OK]"); + }} +}} + +void outputTest() {{ + // Set all as outputs + for (int i = 0; i < NUM_PINS; i++) {{ + pinMode(TEST_PINS[i], OUTPUT); + }} + + // Blink pattern + for (int cycle = 0; cycle < 5; cycle++) {{ + // All on + for (int i = 0; i < NUM_PINS; i++) {{ + digitalWrite(TEST_PINS[i], HIGH); + }} + delay(200); + + // All off + for (int i = 0; i < NUM_PINS; i++) {{ + digitalWrite(TEST_PINS[i], LOW); + }} + delay(200); + }} + + // Sequential chase + for (int cycle = 0; cycle < 3; cycle++) {{ + for (int i = 0; i < NUM_PINS; i++) {{ + digitalWrite(TEST_PINS[i], HIGH); + delay(100); + digitalWrite(TEST_PINS[i], LOW); + }} + }} + + Serial.println("Output test complete"); +}} +''' + +ADC_CHECKER = '''/* + * ADC / Analog Input Checker + * Reads and displays analog values with voltage calculation + * + * Test pins: {pins} + * Reference: {vref}V (10-bit = 0-1023) + */ + +const int ADC_PINS[] = {{{pins_array}}}; +const int NUM_PINS = {num_pins}; +const float VREF = {vref}; // Reference voltage +const int ADC_MAX = 1023; // 10-bit ADC + +void setup() {{ + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println(); + Serial.println("========================================"); + Serial.println(" ADC Analog Checker v1.0 "); + Serial.println("========================================"); + Serial.println(); + + Serial.print("Reference voltage: "); + Serial.print(VREF); + Serial.println("V"); + Serial.print("Testing pins: {pins}"); + Serial.println(); + Serial.println(); + + // Print header + printHeader(); +}} + +void loop() {{ + // Read and display all channels + for (int i = 0; i < NUM_PINS; i++) {{ + int raw = analogRead(ADC_PINS[i]); + float voltage = (raw * VREF) / ADC_MAX; + float percent = (raw * 100.0) / ADC_MAX; + + // Pin label + Serial.print("A"); + Serial.print(ADC_PINS[i] - A0); + Serial.print(": "); + + // Raw value (padded) + if (raw < 1000) Serial.print(" "); + if (raw < 100) Serial.print(" "); + if (raw < 10) Serial.print(" "); + Serial.print(raw); + + Serial.print(" | "); + + // Voltage + Serial.print(voltage, 3); + Serial.print("V | "); + + // Percentage bar + int bars = percent / 5; // 20 chars max + Serial.print("["); + for (int b = 0; b < 20; b++) {{ + if (b < bars) Serial.print("#"); + else Serial.print(" "); + }} + Serial.print("] "); + Serial.print(percent, 1); + Serial.println("%"); + }} + + Serial.println("----------------------------------------"); + delay(500); +}} + +void printHeader() {{ + Serial.println("Pin | Raw | Voltage | Level"); + Serial.println("----------------------------------------"); +}} +''' + +PWM_TESTER = '''/* + * PWM Output Tester + * Tests PWM output on specified pins with varying duty cycles + * + * PWM Pins by board: + * Uno/Nano: 3, 5, 6, 9, 10, 11 + * Mega: 2-13, 44-46 + * ESP32: Any GPIO (LEDC) + * Pico: Any GPIO + * + * Test pins: {pins} + */ + +const int PWM_PINS[] = {{{pins_array}}}; +const int NUM_PINS = {num_pins}; + +int currentDuty = 0; +int direction = 1; +bool fadeMode = true; + +void setup() {{ + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println(); + Serial.println("========================================"); + Serial.println(" PWM Output Tester v1.0 "); + Serial.println("========================================"); + Serial.println(); + Serial.println("Testing pins: {pins}"); + Serial.println(); + Serial.println("Commands:"); + Serial.println(" 0-255: Set specific duty cycle"); + Serial.println(" f: Toggle fade mode"); + Serial.println(" s: Stop all outputs"); + Serial.println(); + + // Initialize pins + for (int i = 0; i < NUM_PINS; i++) {{ + pinMode(PWM_PINS[i], OUTPUT); + }} +}} + +void loop() {{ + // Check for serial commands + if (Serial.available()) {{ + String cmd = Serial.readStringUntil('\\n'); + cmd.trim(); + + if (cmd == "f") {{ + fadeMode = !fadeMode; + Serial.print("Fade mode: "); + Serial.println(fadeMode ? "ON" : "OFF"); + }} else if (cmd == "s") {{ + for (int i = 0; i < NUM_PINS; i++) {{ + analogWrite(PWM_PINS[i], 0); + }} + Serial.println("All PWM stopped"); + fadeMode = false; + }} else {{ + int duty = cmd.toInt(); + if (duty >= 0 && duty <= 255) {{ + currentDuty = duty; + fadeMode = false; + for (int i = 0; i < NUM_PINS; i++) {{ + analogWrite(PWM_PINS[i], currentDuty); + }} + Serial.print("Set duty cycle: "); + Serial.print(currentDuty); + Serial.print(" ("); + Serial.print((currentDuty * 100) / 255); + Serial.println("%)"); + }} + }} + }} + + // Fade mode - smooth ramp up/down + if (fadeMode) {{ + currentDuty += direction * 5; + if (currentDuty >= 255) {{ + currentDuty = 255; + direction = -1; + }} else if (currentDuty <= 0) {{ + currentDuty = 0; + direction = 1; + }} + + for (int i = 0; i < NUM_PINS; i++) {{ + analogWrite(PWM_PINS[i], currentDuty); + }} + + // Display + Serial.print("PWM: "); + Serial.print(currentDuty); + Serial.print(" ["); + int bars = currentDuty / 12; // ~21 chars + for (int b = 0; b < 21; b++) {{ + if (b < bars) Serial.print("="); + else Serial.print(" "); + }} + Serial.println("]"); + + delay(30); + }} +}} +''' + +SERIAL_LOOPBACK = '''/* + * Serial Loopback Tester + * Tests serial communication by sending and receiving + * + * For loopback test: Connect TX to RX directly + * For device test: Connect to external serial device + * + * Tests: + * 1. Self loopback (wire TX to RX) + * 2. Baud rate validation + * 3. Data integrity + */ + +// Test configuration +const long BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200}; +const int NUM_BAUDS = 5; + +void setup() { + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println(); + Serial.println("========================================"); + Serial.println(" Serial Loopback Tester v1.0 "); + Serial.println("========================================"); + Serial.println(); + Serial.println("Connect TX -> RX for loopback test"); + Serial.println("Or connect to target device"); + Serial.println(); + Serial.println("Commands:"); + Serial.println(" t - Run loopback test"); + Serial.println(" s - Send test string"); + Serial.println(" m - Monitor incoming data"); + Serial.println(); +} + +void loop() { + if (Serial.available()) { + char cmd = Serial.read(); + while (Serial.available()) Serial.read(); // Clear buffer + + switch (cmd) { + case 't': + case 'T': + loopbackTest(); + break; + case 's': + case 'S': + sendTestString(); + break; + case 'm': + case 'M': + monitorMode(); + break; + } + } +} + +void loopbackTest() { + Serial.println("\\n--- Loopback Test ---"); + Serial.println("Ensure TX is connected to RX"); + Serial.println(); + + // Wait for any existing data + delay(100); + while (Serial.available()) Serial.read(); + + // Test string + const char* testStr = "LOOPBACK_TEST_12345"; + int testLen = strlen(testStr); + + Serial.print("Sending: "); + Serial.println(testStr); + + // Send test string (will echo back through loopback) + Serial.print(testStr); + Serial.flush(); + + // Wait for response + delay(100); + + char response[50]; + int received = 0; + unsigned long start = millis(); + + while (received < testLen && (millis() - start) < 1000) { + if (Serial.available()) { + response[received++] = Serial.read(); + } + } + response[received] = '\\0'; + + Serial.print("Received: "); + Serial.println(response); + + // Compare + if (received == testLen && strcmp(response, testStr) == 0) { + Serial.println("Result: PASS - Loopback OK"); + } else if (received == 0) { + Serial.println("Result: FAIL - No response (check TX-RX connection)"); + } else { + Serial.print("Result: FAIL - Received "); + Serial.print(received); + Serial.print(" of "); + Serial.print(testLen); + Serial.println(" bytes"); + } +} + +void sendTestString() { + Serial.println("\\n--- Sending Test Pattern ---"); + + // ASCII printable range + Serial.println("ASCII printable characters:"); + for (char c = 32; c < 127; c++) { + Serial.print(c); + } + Serial.println(); + + // Numbers + Serial.println("\\nNumber sequence:"); + for (int i = 0; i < 10; i++) { + Serial.print(i); + } + Serial.println(); + + Serial.println("\\nTest complete"); +} + +void monitorMode() { + Serial.println("\\n--- Monitor Mode ---"); + Serial.println("Displaying incoming bytes (press any key to exit)..."); + Serial.println(); + + int byteCount = 0; + unsigned long lastPrint = 0; + + while (true) { + // Check for exit (data from USB serial) + // This is tricky in loopback mode... + + // Display any received data + while (Serial.available()) { + char c = Serial.read(); + + // Print as hex and ASCII + if (c >= 32 && c < 127) { + Serial.print(c); + } else { + Serial.print("["); + Serial.print((int)c, HEX); + Serial.print("]"); + } + + byteCount++; + } + + // Stats every 2 seconds + if (millis() - lastPrint > 2000) { + Serial.print("\\n["); + Serial.print(byteCount); + Serial.println(" bytes received]"); + lastPrint = millis(); + + // Exit after timeout with no data + if (byteCount == 0) { + Serial.println("No data - exiting monitor"); + break; + } + byteCount = 0; + } + + delay(10); + } +} +''' + +VOLTAGE_DIVIDER = '''/* + * Voltage Divider Calculator & Tester + * Calculates and tests voltage divider circuits + * + * Schematic: + * Vin ----[R1]----+----[R2]---- GND + * | + * Vout (to ADC) + * + * Formula: Vout = Vin * (R2 / (R1 + R2)) + * + * Connect Vout to: {adc_pin} + */ + +const int ADC_PIN = {adc_pin}; +const float VREF = {vref}; // ADC reference voltage +const int ADC_MAX = 1023; // 10-bit ADC + +// Resistor values (ohms) - adjust to match your circuit +float R1 = {r1}; // Top resistor (Vin side) +float R2 = {r2}; // Bottom resistor (GND side) + +// Calculated divider ratio +float dividerRatio; + +void setup() {{ + Serial.begin(115200); + while (!Serial) delay(10); + + Serial.println(); + Serial.println("========================================"); + Serial.println(" Voltage Divider Calculator v1.0 "); + Serial.println("========================================"); + Serial.println(); + + // Calculate ratio + dividerRatio = R2 / (R1 + R2); + float maxVin = VREF / dividerRatio; + + Serial.println("Configuration:"); + Serial.print(" R1 (top): "); + Serial.print(R1 / 1000, 1); + Serial.println(" kΞ©"); + Serial.print(" R2 (bottom): "); + Serial.print(R2 / 1000, 1); + Serial.println(" kΞ©"); + Serial.print(" Divider ratio: "); + Serial.println(dividerRatio, 4); + Serial.print(" ADC reference: "); + Serial.print(VREF); + Serial.println("V"); + Serial.print(" Max input voltage: "); + Serial.print(maxVin, 2); + Serial.println("V"); + Serial.println(); + Serial.println("Commands:"); + Serial.println(" r - Read voltage"); + Serial.println(" c - Continuous reading"); + Serial.println(" 1/2 - Adjust R1/R2 values"); + Serial.println(); +}} + +void loop() {{ + if (Serial.available()) {{ + char cmd = Serial.read(); + while (Serial.available()) Serial.read(); + + switch (cmd) {{ + case 'r': + case 'R': + readVoltage(); + break; + case 'c': + case 'C': + continuousRead(); + break; + case '1': + adjustR1(); + break; + case '2': + adjustR2(); + break; + }} + }} +}} + +void readVoltage() {{ + // Take multiple readings for stability + long sum = 0; + for (int i = 0; i < 10; i++) {{ + sum += analogRead(ADC_PIN); + delay(10); + }} + float avgRaw = sum / 10.0; + + float vout = (avgRaw * VREF) / ADC_MAX; + float vin = vout / dividerRatio; + + Serial.println("\\n--- Voltage Reading ---"); + Serial.print("ADC raw: "); + Serial.println(avgRaw, 1); + Serial.print("Vout (ADC): "); + Serial.print(vout, 3); + Serial.println("V"); + Serial.print("Vin (calculated): "); + Serial.print(vin, 2); + Serial.println("V"); +}} + +void continuousRead() {{ + Serial.println("\\nContinuous mode (any key to stop)..."); + + while (!Serial.available()) {{ + int raw = analogRead(ADC_PIN); + float vout = (raw * VREF) / ADC_MAX; + float vin = vout / dividerRatio; + + Serial.print("ADC:"); + Serial.print(raw); + Serial.print(" Vout:"); + Serial.print(vout, 3); + Serial.print("V Vin:"); + Serial.print(vin, 2); + Serial.println("V"); + + delay(500); + }} + while (Serial.available()) Serial.read(); +}} + +void adjustR1() {{ + Serial.println("\\nEnter R1 value in ohms:"); + while (!Serial.available()) delay(10); + R1 = Serial.parseFloat(); + updateRatio(); +}} + +void adjustR2() {{ + Serial.println("\\nEnter R2 value in ohms:"); + while (!Serial.available()) delay(10); + R2 = Serial.parseFloat(); + updateRatio(); +}} + +void updateRatio() {{ + dividerRatio = R2 / (R1 + R2); + Serial.print("New ratio: "); + Serial.println(dividerRatio, 4); + Serial.print("Max Vin: "); + Serial.print(VREF / dividerRatio, 2); + Serial.println("V"); +}} +''' + + +def generate_i2c_scanner() -> str: + """Generate I2C scanner sketch""" + return I2C_SCANNER + + +def generate_gpio_tester(pins: List[int]) -> str: + """Generate GPIO tester sketch""" + pins_str = ", ".join(map(str, pins)) + return GPIO_TESTER.format( + pins=pins_str, + pins_array=pins_str, + num_pins=len(pins) + ) + + +def generate_adc_checker(pins: List[str], vref: float = 5.0) -> str: + """Generate ADC checker sketch""" + # Convert pin names to Arduino pin numbers + pin_nums = [] + for p in pins: + if p.startswith('A'): + pin_nums.append(f"A{p[1:]}") + else: + pin_nums.append(f"A{p}") + + pins_str = ", ".join(pins) + pins_array = ", ".join(pin_nums) + + return ADC_CHECKER.format( + pins=pins_str, + pins_array=pins_array, + num_pins=len(pins), + vref=vref + ) + + +def generate_pwm_tester(pins: List[int]) -> str: + """Generate PWM tester sketch""" + pins_str = ", ".join(map(str, pins)) + return PWM_TESTER.format( + pins=pins_str, + pins_array=pins_str, + num_pins=len(pins) + ) + + +def generate_serial_loopback() -> str: + """Generate serial loopback tester""" + return SERIAL_LOOPBACK + + +def generate_voltage_divider(adc_pin: str = "A0", vref: float = 5.0, + r1: float = 30000, r2: float = 7500) -> str: + """Generate voltage divider calculator sketch""" + return VOLTAGE_DIVIDER.format( + adc_pin=adc_pin, + vref=vref, + r1=r1, + r2=r2 + ) + + +def interactive_mode(): + """Interactive sketch generator""" + print("=" * 60) + print("Debug Sketch Generator - Interactive Mode") + print("=" * 60) + print() + print("Available sketches:") + print(" 1. I2C Bus Scanner") + print(" 2. GPIO Pin Tester") + print(" 3. ADC/Analog Checker") + print(" 4. PWM Output Tester") + print(" 5. Serial Loopback Test") + print(" 6. Voltage Divider Tester") + print(" 7. Generate All") + print() + + choice = input("Select sketch (1-7): ").strip() + + sketch = "" + filename = "debug_sketch.ino" + + if choice == "1": + sketch = generate_i2c_scanner() + filename = "i2c_scanner.ino" + + elif choice == "2": + pins_input = input("Enter GPIO pins (comma-separated) [2,3,4,5]: ").strip() + pins = [int(p.strip()) for p in (pins_input or "2,3,4,5").split(",")] + sketch = generate_gpio_tester(pins) + filename = "gpio_tester.ino" + + elif choice == "3": + pins_input = input("Enter ADC pins (comma-separated) [A0,A1,A2]: ").strip() + pins = [p.strip() for p in (pins_input or "A0,A1,A2").split(",")] + vref = float(input("Reference voltage [5.0]: ").strip() or "5.0") + sketch = generate_adc_checker(pins, vref) + filename = "adc_checker.ino" + + elif choice == "4": + pins_input = input("Enter PWM pins (comma-separated) [3,5,6,9]: ").strip() + pins = [int(p.strip()) for p in (pins_input or "3,5,6,9").split(",")] + sketch = generate_pwm_tester(pins) + filename = "pwm_tester.ino" + + elif choice == "5": + sketch = generate_serial_loopback() + filename = "serial_loopback.ino" + + elif choice == "6": + adc = input("ADC pin [A0]: ").strip() or "A0" + vref = float(input("Reference voltage [5.0]: ").strip() or "5.0") + r1 = float(input("R1 (ohms) [30000]: ").strip() or "30000") + r2 = float(input("R2 (ohms) [7500]: ").strip() or "7500") + sketch = generate_voltage_divider(adc, vref, r1, r2) + filename = "voltage_divider.ino" + + elif choice == "7": + # Generate all into one file + sketches = [ + "// ===== I2C SCANNER =====", + generate_i2c_scanner(), + "\n// ===== GPIO TESTER =====", + generate_gpio_tester([2, 3, 4, 5]), + "\n// ===== ADC CHECKER =====", + generate_adc_checker(["A0", "A1", "A2"]), + ] + sketch = "\n\n".join(sketches) + filename = "debug_suite.ino" + print("\nNote: Multiple sketches generated. Copy desired section to use.") + + else: + print("Invalid choice") + return + + # Save + custom_name = input(f"\nOutput filename [{filename}]: ").strip() + if custom_name: + filename = custom_name if custom_name.endswith(".ino") else custom_name + ".ino" + + with open(filename, 'w') as f: + f.write(sketch) + + print(f"\nβœ“ Generated: {filename}") + print(f" Upload to Arduino and open Serial Monitor at 115200 baud") + + +def main(): + parser = argparse.ArgumentParser(description="Debug Sketch Generator") + parser.add_argument("--interactive", "-i", action="store_true", help="Interactive mode") + parser.add_argument("--i2c", action="store_true", help="Generate I2C scanner") + parser.add_argument("--gpio", action="store_true", help="Generate GPIO tester") + parser.add_argument("--adc", action="store_true", help="Generate ADC checker") + parser.add_argument("--pwm", action="store_true", help="Generate PWM tester") + parser.add_argument("--serial", action="store_true", help="Generate serial loopback") + parser.add_argument("--voltage", action="store_true", help="Generate voltage divider") + parser.add_argument("--pins", type=str, help="Comma-separated pin list") + parser.add_argument("--vref", type=float, default=5.0, help="Reference voltage") + parser.add_argument("--output", "-o", type=str, help="Output filename") + parser.add_argument("--all", action="store_true", help="Generate all sketches") + + args = parser.parse_args() + + if args.interactive: + interactive_mode() + return + + sketch = "" + filename = args.output or "debug_sketch.ino" + + if args.i2c: + sketch = generate_i2c_scanner() + filename = args.output or "i2c_scanner.ino" + elif args.gpio: + pins = [int(p) for p in (args.pins or "2,3,4,5").split(",")] + sketch = generate_gpio_tester(pins) + filename = args.output or "gpio_tester.ino" + elif args.adc: + pins = (args.pins or "A0,A1,A2").split(",") + sketch = generate_adc_checker(pins, args.vref) + filename = args.output or "adc_checker.ino" + elif args.pwm: + pins = [int(p) for p in (args.pins or "3,5,6,9").split(",")] + sketch = generate_pwm_tester(pins) + filename = args.output or "pwm_tester.ino" + elif args.serial: + sketch = generate_serial_loopback() + filename = args.output or "serial_loopback.ino" + elif args.voltage: + sketch = generate_voltage_divider("A0", args.vref) + filename = args.output or "voltage_divider.ino" + elif args.all: + sketches = [ + generate_i2c_scanner(), + generate_gpio_tester([2, 3, 4, 5]), + generate_adc_checker(["A0", "A1", "A2"]), + generate_pwm_tester([3, 5, 6, 9]), + generate_serial_loopback() + ] + # Save each separately + names = ["i2c_scanner.ino", "gpio_tester.ino", "adc_checker.ino", + "pwm_tester.ino", "serial_loopback.ino"] + for s, n in zip(sketches, names): + with open(n, 'w') as f: + f.write(s) + print(f"Generated: {n}") + return + else: + parser.print_help() + return + + with open(filename, 'w') as f: + f.write(sketch) + print(f"Generated: {filename}") + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/code-review-facilitator/SKILL.md b/.agents/skills/code-review-facilitator/SKILL.md new file mode 100644 index 0000000..9f04a9e --- /dev/null +++ b/.agents/skills/code-review-facilitator/SKILL.md @@ -0,0 +1,496 @@ +--- +name: code-review-facilitator +description: Automated code review for Arduino/ESP32/RP2040 projects focusing on best practices, memory safety, and common pitfalls. Use when user wants code feedback, says "review my code", needs help improving code quality, or before finalizing a project. Generates actionable checklists and specific improvement suggestions. +--- + +# Code Review Facilitator + +Provides systematic code review for microcontroller projects. + +## Resources + +This skill includes bundled tools: + +- **scripts/analyze_code.py** - Static analyzer detecting 15+ common Arduino issues + +## Quick Start + +**Analyze a file:** +```bash +uv run --no-project scripts/analyze_code.py sketch.ino +``` + +**Analyze entire project:** +```bash +uv run --no-project scripts/analyze_code.py --dir /path/to/project +``` + +**Interactive mode (paste code):** +```bash +uv run --no-project scripts/analyze_code.py --interactive +``` + +**Filter by severity:** +```bash +uv run --no-project scripts/analyze_code.py sketch.ino --severity warning +``` + +## When to Use +- "Review my code" +- "Is this code okay?" +- "How can I improve this?" +- Before publishing to GitHub +- After completing a feature +- When code "works but feels wrong" + +--- + +## Review Categories + +### 1. πŸ—οΈ Structure & Organization + +**Check For:** +``` +β–‘ Single responsibility - each function does ONE thing +β–‘ File organization - separate concerns (config, sensors, display, network) +β–‘ Consistent naming convention (camelCase for variables, UPPER_CASE for constants) +β–‘ Reasonable function length (< 50 lines ideally) +β–‘ Header comments explaining purpose +``` + +**Common Issues:** + +| Issue | Bad | Good | +|-------|-----|------| +| God function | 200-line `loop()` | Split into `readSensors()`, `updateDisplay()`, etc. | +| Mixed concerns | WiFi code in sensor file | Separate network.cpp/h | +| Unclear names | `int x, temp1, val;` | `int sensorReading, temperatureC;` | + +**Example Refactoring:** +```cpp +// ❌ Bad: Everything in loop() +void loop() { + // 50 lines of sensor reading + // 30 lines of display update + // 40 lines of network code +} + +// βœ… Good: Organized functions +void loop() { + SensorData data = readAllSensors(); + updateDisplay(data); + if (shouldTransmit()) { + sendToServer(data); + } + handleSleep(); +} +``` + +--- + +### 2. πŸ’Ύ Memory Safety + +**Critical Checks:** +``` +β–‘ No String class in time-critical code (use char arrays) +β–‘ Buffer sizes declared as constants +β–‘ Array bounds checking +β–‘ No dynamic memory allocation in loop() +β–‘ Static buffers for frequently used strings +``` + +**Memory Issues Table:** + +| Issue | Problem | Solution | +|-------|---------|----------| +| String fragmentation | Heap corruption over time | Use char arrays, snprintf() | +| Stack overflow | Large local arrays | Use static/global, reduce size | +| Buffer overflow | strcpy without bounds | Use strncpy, snprintf | +| Memory leak | malloc without free | Avoid dynamic allocation | + +**Safe String Handling:** +```cpp +// ❌ Dangerous: String class in loop +void loop() { + String msg = "Temp: " + String(temp) + "C"; // Fragments heap + Serial.println(msg); +} + +// βœ… Safe: Static buffer with snprintf +void loop() { + static char msg[32]; + snprintf(msg, sizeof(msg), "Temp: %.1fC", temp); + Serial.println(msg); +} + +// βœ… Safe: F() macro for flash strings +Serial.println(F("This string is in flash, not RAM")); +``` + +**Memory Monitoring:** +```cpp +// Add to setup() for debugging +Serial.print(F("Free heap: ")); +Serial.println(ESP.getFreeHeap()); + +// Periodic check in loop() +if (ESP.getFreeHeap() < 10000) { + Serial.println(F("WARNING: Low memory!")); +} +``` + +--- + +### 3. πŸ”’ Magic Numbers & Constants + +**Check For:** +``` +β–‘ No unexplained numbers in code +β–‘ Pin assignments in config.h +β–‘ Timing values named +β–‘ Threshold values documented +``` + +**Examples:** +```cpp +// ❌ Bad: Magic numbers everywhere +if (analogRead(A0) > 512) { + digitalWrite(4, HIGH); + delay(1500); +} + +// βœ… Good: Named constants +// config.h +#define MOISTURE_SENSOR_PIN A0 +#define PUMP_RELAY_PIN 4 +#define MOISTURE_THRESHOLD 512 // ~50% soil moisture +#define PUMP_RUN_TIME_MS 1500 // 1.5 second watering + +// main.ino +if (analogRead(MOISTURE_SENSOR_PIN) > MOISTURE_THRESHOLD) { + digitalWrite(PUMP_RELAY_PIN, HIGH); + delay(PUMP_RUN_TIME_MS); +} +``` + +--- + +### 4. ⚠️ Error Handling + +**Check For:** +``` +β–‘ Sensor initialization verified +β–‘ Network connections have timeouts +β–‘ File operations check return values +β–‘ Graceful degradation when components fail +β–‘ User feedback for errors (LED, serial, display) +``` + +**Error Handling Patterns:** +```cpp +// ❌ Bad: Assume everything works +void setup() { + bme.begin(0x76); // What if it fails? +} + +// βœ… Good: Check and handle failures +void setup() { + Serial.begin(115200); + + if (!bme.begin(0x76)) { + Serial.println(F("BME280 not found!")); + errorBlink(ERROR_SENSOR); // Visual feedback + // Either halt or continue without sensor + sensorAvailable = false; + } + + // WiFi with timeout + WiFi.begin(SSID, PASSWORD); + unsigned long startAttempt = millis(); + while (WiFi.status() != WL_CONNECTED) { + if (millis() - startAttempt > WIFI_TIMEOUT_MS) { + Serial.println(F("WiFi failed - continuing offline")); + wifiAvailable = false; + break; + } + delay(500); + } +} +``` + +--- + +### 5. ⏱️ Timing & Delays + +**Check For:** +``` +β–‘ No blocking delay() in main loop (except simple projects) +β–‘ millis() overflow handled (after 49 days) +β–‘ Debouncing for buttons/switches +β–‘ Rate limiting for sensors/network +``` + +**Non-Blocking Pattern:** +```cpp +// ❌ Bad: Blocking delays +void loop() { + readSensor(); + delay(1000); // Blocks everything for 1 second +} + +// βœ… Good: Non-blocking with millis() +unsigned long previousMillis = 0; +const unsigned long INTERVAL = 1000; + +void loop() { + unsigned long currentMillis = millis(); + + // Handle button immediately (responsive) + checkButton(); + + // Sensor reading at interval + if (currentMillis - previousMillis >= INTERVAL) { + previousMillis = currentMillis; + readSensor(); + } +} + +// βœ… millis() overflow safe (works after 49 days) +// The subtraction handles overflow automatically with unsigned math +``` + +**Debouncing:** +```cpp +// Button debouncing +const unsigned long DEBOUNCE_MS = 50; +unsigned long lastDebounce = 0; +int lastButtonState = HIGH; +int buttonState = HIGH; + +void checkButton() { + int reading = digitalRead(BUTTON_PIN); + + if (reading != lastButtonState) { + lastDebounce = millis(); + } + + if ((millis() - lastDebounce) > DEBOUNCE_MS) { + if (reading != buttonState) { + buttonState = reading; + if (buttonState == LOW) { + handleButtonPress(); + } + } + } + lastButtonState = reading; +} +``` + +--- + +### 6. πŸ”Œ Hardware Interactions + +**Check For:** +``` +β–‘ Pin modes set in setup() +β–‘ Pull-up/pull-down resistors considered +β–‘ Voltage levels compatible (3.3V vs 5V) +β–‘ Current limits respected +β–‘ Proper power sequencing +``` + +**Pin Configuration:** +```cpp +// ❌ Bad: Missing or incorrect pin modes +digitalWrite(LED_PIN, HIGH); // Works by accident on some boards + +// βœ… Good: Explicit configuration +void setup() { + // Outputs + pinMode(LED_PIN, OUTPUT); + pinMode(RELAY_PIN, OUTPUT); + + // Inputs with pull-up (button connects to GND) + pinMode(BUTTON_PIN, INPUT_PULLUP); + + // Analog input (no pinMode needed but document it) + // SENSOR_PIN is analog input - no pinMode required + + // Set safe initial states + digitalWrite(RELAY_PIN, LOW); // Relay off at start +} +``` + +--- + +### 7. πŸ“‘ Network & Communication + +**Check For:** +``` +β–‘ Credentials not hardcoded (use config file) +β–‘ Connection retry logic +β–‘ Timeout handling +β–‘ Secure connections (HTTPS where possible) +β–‘ Data validation +``` + +**Secure Credential Handling:** +```cpp +// ❌ Bad: Credentials in main code +WiFi.begin("MyNetwork", "password123"); + +// βœ… Good: Separate config file (add to .gitignore) +// config.h +#ifndef CONFIG_H +#define CONFIG_H + +#define WIFI_SSID "your-ssid" +#define WIFI_PASSWORD "your-password" +#define API_KEY "your-api-key" + +#endif + +// .gitignore +config.h +``` + +--- + +### 8. πŸ”‹ Power Efficiency + +**Check For:** +``` +β–‘ Unused peripherals disabled +β–‘ Appropriate sleep modes used +β–‘ WiFi off when not needed +β–‘ LED brightness reduced (PWM) +β–‘ Sensor power controlled +``` + +**Power Optimization:** +```cpp +// ESP32 power management +void goToSleep(int seconds) { + WiFi.disconnect(true); + WiFi.mode(WIFI_OFF); + btStop(); + + esp_sleep_enable_timer_wakeup(seconds * 1000000ULL); + esp_deep_sleep_start(); +} + +// Sensor power control +#define SENSOR_POWER_PIN 25 + +void readSensorWithPowerControl() { + digitalWrite(SENSOR_POWER_PIN, HIGH); // Power on + delay(100); // Stabilization time + + int value = analogRead(SENSOR_PIN); + + digitalWrite(SENSOR_POWER_PIN, LOW); // Power off + return value; +} +``` + +--- + +## Review Checklist Generator + +Generate project-specific checklist: + +```markdown +## Code Review Checklist for [Project Name] + +### Critical (Must Fix) +- [ ] Memory: No String in loop() +- [ ] Safety: All array accesses bounds-checked +- [ ] Error: Sensor init failures handled + +### Important (Should Fix) +- [ ] No magic numbers +- [ ] Non-blocking delays where possible +- [ ] Timeouts on all network operations + +### Nice to Have +- [ ] F() macro for string literals +- [ ] Consistent naming convention +- [ ] Comments for complex logic + +### Platform-Specific (ESP32) +- [ ] WiFi reconnection logic +- [ ] Brownout detector consideration +- [ ] Deep sleep properly configured +``` + +--- + +## Code Smell Detection + +### Automatic Red Flags + +| Pattern | Severity | Action | +|---------|----------|--------| +| `String +` in loop() | πŸ”΄ Critical | Replace with snprintf | +| `delay(>100)` in loop() | 🟑 Warning | Consider millis() | +| `while(1)` without yield() | πŸ”΄ Critical | Add yield() or refactor | +| Hardcoded credentials | πŸ”΄ Critical | Move to config.h | +| `malloc/new` without `free/delete` | πŸ”΄ Critical | Track allocations | +| `sprintf` (not snprintf) | 🟑 Warning | Use snprintf for safety | +| Global variables without `volatile` for ISR | πŸ”΄ Critical | Add volatile keyword | + +--- + +## Review Response Template + +```markdown +## Code Review Summary + +**Overall Assessment:** β­β­β­β˜†β˜† (3/5) + +### πŸ”΄ Critical Issues (Fix Before Use) +1. **Memory leak in line 45** - String concatenation in loop() + - Current: `String msg = "Value: " + String(val);` + - Fix: Use `snprintf(buffer, sizeof(buffer), "Value: %d", val);` + +### 🟑 Important Issues (Fix Soon) +1. **Missing error handling** - BME280 init not checked +2. **Magic number** - `delay(1500)` unexplained + +### 🟒 Suggestions (Nice to Have) +1. Consider adding F() macro to Serial.print strings +2. Function `readAllSensors()` could be split + +### βœ… Good Practices Found +- Clear variable naming +- Consistent formatting +- Good use of constants in config.h + +### Recommended Next Steps +1. Fix critical memory issue first +2. Add sensor error handling +3. Run memory monitoring to verify fix +``` + +--- + +## Quick Reference Commands + +```cpp +// Memory debugging +Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap()); +Serial.printf("Min free heap: %d bytes\n", ESP.getMinFreeHeap()); + +// Stack high water mark (FreeRTOS) +Serial.printf("Stack remaining: %d bytes\n", uxTaskGetStackHighWaterMark(NULL)); + +// Find I2C devices +void scanI2C() { + for (byte addr = 1; addr < 127; addr++) { + Wire.beginTransmission(addr); + if (Wire.endTransmission() == 0) { + Serial.printf("Found device at 0x%02X\n", addr); + } + } +} +``` diff --git a/.agents/skills/code-review-facilitator/scripts/analyze_code.py b/.agents/skills/code-review-facilitator/scripts/analyze_code.py new file mode 100644 index 0000000..fa0dd3a --- /dev/null +++ b/.agents/skills/code-review-facilitator/scripts/analyze_code.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +""" +Arduino Code Analyzer - Static analysis for Arduino/C++ sketches + +Analyzes code for common issues: +- Memory problems (String usage, large arrays) +- Blocking code (delay in loops) +- Missing volatile for ISR variables +- Pin conflicts +- Power efficiency issues + +Usage: + uv run --no-project scripts/analyze_code.py sketch.ino + uv run --no-project scripts/analyze_code.py --dir /path/to/project + uv run --no-project scripts/analyze_code.py --interactive +""" + +import argparse +import re +import os +from dataclasses import dataclass +from typing import List, Dict, Optional +from pathlib import Path + +# ============================================================================= +# Issue Definitions +# ============================================================================= + +@dataclass +class Issue: + """Code issue found during analysis""" + severity: str # error, warning, info + category: str + line_number: int + description: str + suggestion: str + code_snippet: str = "" + + +# Analysis rules +RULES = { + # Memory Issues + "string_in_loop": { + "pattern": r'void\s+loop\s*\(\s*\).*?String\s+\w+', + "multiline": True, + "severity": "warning", + "category": "memory", + "description": "String object created inside loop() - causes memory fragmentation", + "suggestion": "Move String declarations outside loop() or use char arrays" + }, + "string_concat_loop": { + "pattern": r'for\s*\(.*?\).*?(?:\w+\s*\+=\s*|String.*?\+)', + "multiline": True, + "severity": "warning", + "category": "memory", + "description": "String concatenation in loop - allocates memory repeatedly", + "suggestion": "Pre-allocate buffer or use sprintf() with char array" + }, + "large_array": { + "pattern": r'(?:int|float|double|long)\s+\w+\s*\[\s*(\d{3,})\s*\]', + "severity": "warning", + "category": "memory", + "description": "Large array may exhaust RAM", + "suggestion": "Use PROGMEM for constants, or consider external storage" + }, + + # Blocking Code + "delay_in_loop": { + "pattern": r'void\s+loop\s*\(\s*\).*?delay\s*\(\s*(\d+)\s*\)', + "multiline": True, + "severity": "info", + "category": "performance", + "description": "delay() blocks execution - consider millis() for non-blocking", + "suggestion": "Use millis()-based timing for responsive code" + }, + "long_delay": { + "pattern": r'delay\s*\(\s*(\d{4,})\s*\)', + "severity": "warning", + "category": "performance", + "description": "Long delay ({0}ms) blocks all code execution", + "suggestion": "Use millis() timing pattern for delays > 100ms" + }, + + # ISR Issues + "non_volatile_isr": { + "pattern": r'(?:attachInterrupt|ISR\s*\().*?(\w+)', + "severity": "error", + "category": "correctness", + "description": "Variable used in ISR may not be declared volatile", + "suggestion": "Declare shared ISR variables as: volatile int variableName;" + }, + "serial_in_isr": { + "pattern": r'(?:ISR\s*\(|void\s+\w+ISR\s*\().*?Serial\.', + "multiline": True, + "severity": "error", + "category": "correctness", + "description": "Serial operations inside ISR can cause crashes", + "suggestion": "Set a flag in ISR, handle Serial in loop()" + }, + "delay_in_isr": { + "pattern": r'(?:ISR\s*\(|void\s+\w+ISR\s*\().*?delay\s*\(', + "multiline": True, + "severity": "error", + "category": "correctness", + "description": "delay() does not work inside ISR", + "suggestion": "ISRs should be fast - set flag and exit" + }, + + # Digital I/O + "pinmode_in_loop": { + "pattern": r'void\s+loop\s*\(\s*\).*?pinMode\s*\(', + "multiline": True, + "severity": "warning", + "category": "performance", + "description": "pinMode() called repeatedly in loop()", + "suggestion": "Move pinMode() to setup() - only needs to run once" + }, + "analog_read_speed": { + "pattern": r'for\s*\(.*?\).*?analogRead\s*\(', + "multiline": True, + "severity": "info", + "category": "performance", + "description": "analogRead() is slow (~100Β΅s per read)", + "suggestion": "Consider averaging or reducing read frequency" + }, + + # Power Efficiency + "no_sleep": { + "pattern": r'void\s+loop\s*\(\s*\).*?while\s*\(\s*(?:1|true)\s*\)', + "multiline": True, + "severity": "info", + "category": "power", + "description": "Busy wait loop - wastes power", + "suggestion": "Use sleep modes for battery-powered projects" + }, + "adc_always_on": { + "pattern": r'analogRead\s*\(', + "severity": "info", + "category": "power", + "description": "ADC consumes power even between reads", + "suggestion": "Disable ADC when not needed for power savings" + }, + + # Common Bugs + "assignment_in_condition": { + "pattern": r'if\s*\(\s*\w+\s*=\s*[^=]', + "severity": "error", + "category": "correctness", + "description": "Assignment (=) in if condition - likely meant comparison (==)", + "suggestion": "Use == for comparison: if (x == 5)" + }, + "floating_point_comparison": { + "pattern": r'if\s*\(.*?(?:float|double)\s+.*?==', + "multiline": True, + "severity": "warning", + "category": "correctness", + "description": "Direct floating-point comparison may fail", + "suggestion": "Use tolerance: if (abs(a - b) < 0.001)" + }, + "unsigned_negative": { + "pattern": r'(?:byte|unsigned)\s+\w+\s*=\s*-', + "severity": "error", + "category": "correctness", + "description": "Negative value assigned to unsigned type", + "suggestion": "Use signed type (int) or ensure value is positive" + }, + + # Best Practices + "magic_numbers": { + "pattern": r'(?:pinMode|digitalWrite|analogWrite|analogRead)\s*\(\s*\d+\s*[,\)]', + "severity": "info", + "category": "style", + "description": "Magic number used for pin - harder to maintain", + "suggestion": "Use named constants: const int LED_PIN = 13;" + }, + "missing_f_macro": { + "pattern": r'Serial\.print(?:ln)?\s*\(\s*"[^"]{20,}"', + "severity": "info", + "category": "memory", + "description": "Long string literal uses RAM", + "suggestion": "Use F() macro: Serial.println(F(\"text\"))" + }, + "no_serial_check": { + "pattern": r'Serial\.begin.*?Serial\.print', + "multiline": True, + "severity": "info", + "category": "robustness", + "description": "Serial used without checking if ready", + "suggestion": "Add: while (!Serial) delay(10); after Serial.begin()" + } +} + + +def analyze_file(filepath: str) -> List[Issue]: + """Analyze a single file for issues""" + issues = [] + + try: + with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + lines = content.split('\n') + except Exception as e: + return [Issue("error", "file", 0, f"Could not read file: {e}", "Check file path")] + + for rule_name, rule in RULES.items(): + pattern = rule["pattern"] + flags = re.DOTALL if rule.get("multiline") else 0 + + # Search in full content for multiline patterns + if rule.get("multiline"): + matches = re.finditer(pattern, content, flags | re.IGNORECASE) + for match in matches: + # Calculate line number + line_num = content[:match.start()].count('\n') + 1 + + # Get snippet + snippet_start = max(0, match.start() - 20) + snippet_end = min(len(content), match.end() + 20) + snippet = content[snippet_start:snippet_end].replace('\n', ' ')[:60] + + description = rule["description"] + if '{0}' in description and match.groups(): + description = description.format(*match.groups()) + + issues.append(Issue( + severity=rule["severity"], + category=rule["category"], + line_number=line_num, + description=description, + suggestion=rule["suggestion"], + code_snippet=snippet + )) + else: + # Line-by-line search + for line_num, line in enumerate(lines, 1): + if re.search(pattern, line, re.IGNORECASE): + description = rule["description"] + match = re.search(pattern, line) + if match and match.groups() and '{0}' in description: + description = description.format(*match.groups()) + + issues.append(Issue( + severity=rule["severity"], + category=rule["category"], + line_number=line_num, + description=description, + suggestion=rule["suggestion"], + code_snippet=line.strip()[:60] + )) + + return issues + + +def analyze_directory(dirpath: str) -> Dict[str, List[Issue]]: + """Analyze all .ino, .cpp, .h files in directory""" + results = {} + + for ext in ['*.ino', '*.cpp', '*.c', '*.h']: + for filepath in Path(dirpath).rglob(ext): + issues = analyze_file(str(filepath)) + if issues: + results[str(filepath)] = issues + + return results + + +def format_report(issues: List[Issue], filename: str = "") -> str: + """Format issues as readable report""" + if not issues: + return "βœ… No issues found!" + + lines = [ + "=" * 60, + f"Code Analysis Report{': ' + filename if filename else ''}", + "=" * 60, + "" + ] + + # Group by severity + errors = [i for i in issues if i.severity == "error"] + warnings = [i for i in issues if i.severity == "warning"] + infos = [i for i in issues if i.severity == "info"] + + # Summary + lines.append(f"Summary: {len(errors)} errors, {len(warnings)} warnings, {len(infos)} suggestions") + lines.append("") + + # Errors first + if errors: + lines.append("πŸ”΄ ERRORS (must fix)") + lines.append("-" * 40) + for issue in errors: + lines.append(f" Line {issue.line_number}: {issue.description}") + lines.append(f" Fix: {issue.suggestion}") + if issue.code_snippet: + lines.append(f" Code: {issue.code_snippet}") + lines.append("") + + if warnings: + lines.append("🟑 WARNINGS (should fix)") + lines.append("-" * 40) + for issue in warnings: + lines.append(f" Line {issue.line_number}: {issue.description}") + lines.append(f" Fix: {issue.suggestion}") + lines.append("") + + if infos: + lines.append("πŸ”΅ SUGGESTIONS (nice to have)") + lines.append("-" * 40) + for issue in infos: + lines.append(f" Line {issue.line_number}: {issue.description}") + lines.append(f" Tip: {issue.suggestion}") + lines.append("") + + return "\n".join(lines) + + +def interactive_mode(): + """Interactive code analysis""" + print("=" * 60) + print("Arduino Code Analyzer - Interactive Mode") + print("=" * 60) + print() + print("Paste your code (press Enter twice when done):") + print() + + lines = [] + empty_count = 0 + while True: + line = input() + if not line: + empty_count += 1 + if empty_count >= 2: + break + else: + empty_count = 0 + lines.append(line) + + # Save to temp file and analyze + temp_file = "_temp_analysis.ino" + with open(temp_file, 'w') as f: + f.write('\n'.join(lines)) + + issues = analyze_file(temp_file) + os.remove(temp_file) + + print(format_report(issues, "pasted code")) + + +def main(): + parser = argparse.ArgumentParser(description="Arduino Code Analyzer") + parser.add_argument("file", nargs="?", help="File to analyze") + parser.add_argument("--dir", "-d", type=str, help="Directory to analyze") + parser.add_argument("--interactive", "-i", action="store_true", help="Interactive mode") + parser.add_argument("--json", "-j", action="store_true", help="Output as JSON") + parser.add_argument("--severity", "-s", type=str, help="Min severity: error, warning, info") + + args = parser.parse_args() + + if args.interactive: + interactive_mode() + return + + if args.dir: + results = analyze_directory(args.dir) + if args.json: + import json + print(json.dumps({k: [vars(i) for i in v] for k, v in results.items()}, indent=2)) + else: + for filepath, issues in results.items(): + print(format_report(issues, filepath)) + print() + elif args.file: + issues = analyze_file(args.file) + + # Filter by severity if specified + if args.severity: + severity_order = {"error": 0, "warning": 1, "info": 2} + min_level = severity_order.get(args.severity, 2) + issues = [i for i in issues if severity_order.get(i.severity, 2) <= min_level] + + if args.json: + import json + print(json.dumps([vars(i) for i in issues], indent=2)) + else: + print(format_report(issues, args.file)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/datasheet-interpreter/SKILL.md b/.agents/skills/datasheet-interpreter/SKILL.md new file mode 100644 index 0000000..b70e3c3 --- /dev/null +++ b/.agents/skills/datasheet-interpreter/SKILL.md @@ -0,0 +1,444 @@ +--- +name: datasheet-interpreter +description: Extracts key specifications from component datasheet PDFs for maker projects. Use when user shares a datasheet PDF URL, asks about component specs, needs pin assignments, I2C addresses, timing requirements, or register maps. Downloads and parses PDF to extract essentials. Complements datasheet-parser for quick lookups. +--- + +# Datasheet Interpreter + +Extracts actionable specifications from component datasheet PDFs. + +## Resources + +### Scripts +- **scripts/extract_specs.py** - Downloads datasheet PDFs from URLs and extracts specs using pattern matching + +### References +- **references/common-parameters.md** - Guide to understanding datasheet parameters + +### Quick Start +```bash +# Extract specs from a datasheet URL +uv run --no-project scripts/extract_specs.py --url "https://www.sparkfun.com/datasheets/Sensors/Temperature/DHT22.pdf" + +# Extract specs with markdown output +uv run --no-project scripts/extract_specs.py --url "https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf" --format markdown + +# Extract from local PDF file +uv run --no-project scripts/extract_specs.py --file "local_datasheet.pdf" + +# Interactive mode +uv run --no-project scripts/extract_specs.py --interactive +``` + +--- + +## When to Use +- "What's the I2C address for this chip?" +- "How do I connect this sensor?" +- "What pins do what on this module?" +- User shares a PDF datasheet URL +- Need timing specs or register info +- Quick reference without full driver generation + +## Relationship to Other Skills +- **datasheet-parser**: Full driver library generation (lexus2k-style) +- **datasheet-interpreter** (this): Quick spec extraction for immediate use + +--- + +## Information Extraction Workflow + +### Step 1: Request Datasheet + +``` +Please provide: +1. Component name (e.g., "BME280", "MPU6050") +2. Datasheet PDF file or URL (manufacturer version preferred) +3. What specific information you need: + β–‘ Pin assignments / pinout + β–‘ I2C/SPI address and protocol + β–‘ Operating conditions (voltage, current) + β–‘ Timing requirements + β–‘ Register map + β–‘ Example circuit + β–‘ All of the above +``` + +### Step 2: Validate PDF Quality + +**Check For:** +- Searchable text (not scanned image) +- Official manufacturer datasheet (not clone/distributor summary) +- Revision/version number noted +- Complete document (includes register tables if applicable) + +**If PDF Issues:** +``` +⚠️ This appears to be a [scanned image | partial datasheet | third-party summary]. + +For best results, please provide the official manufacturer datasheet from: +- [Manufacturer website link] +- Or search: "[component] datasheet pdf site:manufacturer.com" +``` + +--- + +## Extraction Templates + +### Quick Reference Card + +Generate this for any component: + +```markdown +# [Component Name] Quick Reference + +## Basic Info +- **Manufacturer:** [name] +- **Part Number:** [full part number with variants] +- **Datasheet Version:** [rev/date] +- **Description:** [one-line description] + +## Electrical Characteristics +| Parameter | Min | Typ | Max | Unit | +|-----------|-----|-----|-----|------| +| Supply Voltage (VDD) | | | | V | +| Operating Current | | | | mA | +| Sleep Current | | | | Β΅A | +| Operating Temp | | | | Β°C | + +## Communication Interface +- **Protocol:** [I2C / SPI / UART / GPIO] +- **Address:** [0xNN (7-bit)] or [selectable via pin] +- **Max Clock:** [frequency] +- **Logic Levels:** [3.3V / 5V tolerant] + +## Pinout +| Pin | Name | Type | Description | +|-----|------|------|-------------| +| 1 | VDD | Power | Supply voltage | +| 2 | GND | Power | Ground | +| ... | ... | ... | ... | + +## Key Registers (if applicable) +| Address | Name | R/W | Description | +|---------|------|-----|-------------| +| 0x00 | WHO_AM_I | R | Device ID (expect 0xNN) | +| ... | ... | ... | ... | + +## Arduino Wiring +| Component Pin | Arduino Uno | ESP32 | Pico | +|---------------|-------------|-------|------| +| VCC | 3.3V | 3.3V | 3V3 | +| GND | GND | GND | GND | +| SDA | A4 | GPIO21 | GP4 | +| SCL | A5 | GPIO22 | GP5 | + +## Important Notes +- [Critical warnings from datasheet] +- [Application notes] +``` + +--- + +## Common Component Categories + +### I2C Sensors + +**Extract:** +``` +β–‘ I2C address (7-bit format, note if configurable) +β–‘ WHO_AM_I register address and expected value +β–‘ Configuration register settings +β–‘ Data registers (where to read measurements) +β–‘ Resolution and range options +β–‘ Conversion time / measurement rate +``` + +**I2C Address Quick Reference:** +| Component | Address (7-bit) | Config Pin | Alt Address | +|-----------|-----------------|------------|-------------| +| BME280 | 0x76 | SDOβ†’GND | 0x77 (SDOβ†’VDD) | +| BME680 | 0x76 | SDOβ†’GND | 0x77 (SDOβ†’VDD) | +| MPU6050 | 0x68 | AD0β†’GND | 0x69 (AD0β†’VDD) | +| SHT30 | 0x44 | ADDRβ†’GND | 0x45 (ADDRβ†’VDD) | +| BH1750 | 0x23 | ADDRβ†’GND | 0x5C (ADDRβ†’VDD) | +| VL53L0X | 0x29 | - | Configurable via software | +| INA219 | 0x40 | A0,A1 | 0x40-0x4F | +| ADS1115 | 0x48 | ADDRβ†’GND | 0x49-0x4B | + +### SPI Devices + +**Extract:** +``` +β–‘ SPI mode (CPOL, CPHA) +β–‘ Max clock frequency +β–‘ Data order (MSB/LSB first) +β–‘ Command format +β–‘ Chip select behavior (active low?) +``` + +### Motor Drivers + +**Extract:** +``` +β–‘ Logic voltage (VCC) +β–‘ Motor voltage range (VM) +β–‘ Current per channel (continuous/peak) +β–‘ Control interface (PWM frequency, step/dir) +β–‘ Protection features (thermal, overcurrent) +β–‘ H-bridge or half-bridge +``` + +### Display Modules + +**Extract:** +``` +β–‘ Resolution (width x height) +β–‘ Controller IC (SSD1306, ILI9341, etc.) +β–‘ Interface (I2C, SPI, parallel) +β–‘ Memory map / pixel format +β–‘ Initialization sequence +β–‘ Refresh rate +``` + +--- + +## Critical Spec Finder + +### For Power Planning +``` +Search datasheet for: +- "Electrical Characteristics" table +- "Power Consumption" or "Current Consumption" +- "Typical Operating Conditions" +- Sleep/standby modes and their currents +``` + +**Template Output:** +```markdown +## Power Specs for [Component] + +### Operating Modes +| Mode | Typical Current | Conditions | +|------|-----------------|------------| +| Active | XXX mA | [freq, voltage, etc.] | +| Idle | XXX Β΅A | | +| Sleep/Standby | XXX Β΅A | | +| Deep Sleep | XXX nA | | + +### Wake-up Time +- From sleep: XXX ms +- From deep sleep: XXX ms +``` + +### For Timing Requirements +``` +Search datasheet for: +- "Timing Characteristics" +- "AC Characteristics" +- Setup/hold times +- Clock specifications +``` + +### For Pin Tolerance +``` +Search datasheet for: +- "Absolute Maximum Ratings" +- "ESD protection" +- "5V tolerant" mentions +- Input voltage specifications +``` + +--- + +## Register Map Extraction + +### Structured Format +```cpp +// [Component] Register Definitions +// Extracted from datasheet v[X.Y], p.[N] + +// === REGISTER ADDRESSES === +#define REG_NAME 0x00 // Description + +// === BITFIELD DEFINITIONS === +// REG_NAME (0x00) - [Description] +// Bit 7-6: FIELD_A - [description] +// Bit 5: FIELD_B - [description] +// Bit 4-0: FIELD_C - [description] + +#define REG_NAME_FIELD_A_MASK 0xC0 +#define REG_NAME_FIELD_A_POS 6 +#define REG_NAME_FIELD_B_BIT 5 +#define REG_NAME_FIELD_C_MASK 0x1F +``` + +### Common Register Patterns + +**Configuration Register:** +``` +- Enable/disable features +- Set operating mode +- Configure interrupt behavior +``` + +**Status Register:** +``` +- Data ready flags +- Error/fault indicators +- Interrupt sources +``` + +**Data Registers:** +``` +- Usually consecutive addresses +- Note byte order (MSB first or LSB first) +- Signed vs unsigned interpretation +``` + +--- + +## Wiring Diagram Generator + +Generate text-based wiring diagrams: + +``` + [Component] [Arduino Uno] + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ VCC ────┼────────── 3.3V β”‚ + β”‚ GND ────┼────────── GND β”‚ + β”‚ SDA ────┼────────── A4 (SDA) β”‚ + β”‚ SCL ────┼────────── A5 (SCL) β”‚ + β”‚ INT ────┼────────── D2 (INT0) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + + Notes: + - Add 4.7kΞ© pull-ups on SDA/SCL if not on module + - INT is open-drain, needs pull-up +``` + +--- + +## Example Code Generator + +Based on extracted specs, generate minimal working example: + +```cpp +/* + * [Component] Basic Example + * Generated from datasheet interpretation + * + * Wiring: + * - VCC β†’ 3.3V + * - GND β†’ GND + * - SDA β†’ A4 (Arduino Uno) / GPIO21 (ESP32) + * - SCL β†’ A5 (Arduino Uno) / GPIO22 (ESP32) + */ + +#include + +#define COMPONENT_ADDR 0x00 // From datasheet p.X +#define REG_WHO_AM_I 0x00 // Expected: 0xNN +#define REG_CTRL 0x00 // Configuration +#define REG_DATA 0x00 // Data output + +void setup() { + Serial.begin(115200); + Wire.begin(); + + // Verify communication + Wire.beginTransmission(COMPONENT_ADDR); + if (Wire.endTransmission() != 0) { + Serial.println("Component not found!"); + while(1); + } + + // Read device ID + uint8_t id = readReg(REG_WHO_AM_I); + Serial.print("Device ID: 0x"); + Serial.println(id, HEX); + + // Initialize + writeReg(REG_CTRL, 0x00); // [Configuration value from datasheet] +} + +void loop() { + // Read data + int16_t data = readReg16(REG_DATA); + Serial.println(data); + delay(100); +} + +uint8_t readReg(uint8_t reg) { + Wire.beginTransmission(COMPONENT_ADDR); + Wire.write(reg); + Wire.endTransmission(false); + Wire.requestFrom(COMPONENT_ADDR, (uint8_t)1); + return Wire.read(); +} + +void writeReg(uint8_t reg, uint8_t val) { + Wire.beginTransmission(COMPONENT_ADDR); + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); +} + +int16_t readReg16(uint8_t reg) { + Wire.beginTransmission(COMPONENT_ADDR); + Wire.write(reg); + Wire.endTransmission(false); + Wire.requestFrom(COMPONENT_ADDR, (uint8_t)2); + int16_t val = Wire.read() << 8; // MSB first (check datasheet!) + val |= Wire.read(); + return val; +} +``` + +--- + +## Quick Lookup Database + +### Common Questions Answered + +**"What voltage does [component] need?"** +β†’ Check "Absolute Maximum Ratings" and "Recommended Operating Conditions" + +**"Is it 3.3V or 5V?"** +β†’ Look for "VDD" or "VCC" in electrical characteristics +β†’ Check if I/O pins are "5V tolerant" + +**"How fast can I run the I2C?"** +β†’ Find "I2C Clock Frequency" in timing specs (usually 100kHz standard, 400kHz fast) + +**"How do I wake it from sleep?"** +β†’ Look for "Power Management" or "Operating Modes" section + +**"Why won't it respond at the address I expected?"** +β†’ Check address selection pins (AD0, ADDR, SA0) +β†’ Some datasheets show 8-bit address (divide by 2 for 7-bit) + +--- + +## Datasheet Reading Tips + +### Where to Find Key Info + +| Information | Section Name | +|-------------|--------------| +| I2C Address | "Serial Interface" or "Communication" | +| Voltage/Current | "Electrical Characteristics" | +| Pinout | "Pin Configuration" or "Package Information" | +| Registers | "Register Map" or "Memory Map" | +| Timing | "Timing Characteristics" or "AC Characteristics" | +| Example Circuit | "Application Information" or "Typical Application" | + +### Red Flags in Datasheets + +- 🚩 Missing absolute maximum ratings +- 🚩 No version/revision number +- 🚩 Inconsistent register descriptions +- 🚩 Machine-translated text +- 🚩 Missing timing diagrams for protocols + +β†’ If you see these, try to find a better quality datasheet from the original manufacturer diff --git a/.agents/skills/datasheet-interpreter/references/common-parameters.md b/.agents/skills/datasheet-interpreter/references/common-parameters.md new file mode 100644 index 0000000..97a96a4 --- /dev/null +++ b/.agents/skills/datasheet-interpreter/references/common-parameters.md @@ -0,0 +1,129 @@ +# Common Datasheet Parameters Reference + +Quick reference for understanding component datasheets. + +## Electrical Ratings + +### Voltage Parameters +| Parameter | Symbol | Meaning | +|-----------|--------|---------| +| Operating Voltage | VCC, VDD | Normal supply voltage range | +| Max Voltage | Vmax | Absolute maximum (do not exceed!) | +| Logic High | VIH | Minimum voltage for logic HIGH | +| Logic Low | VIL | Maximum voltage for logic LOW | +| Output High | VOH | Output voltage when HIGH | +| Output Low | VOL | Output voltage when LOW | + +### Current Parameters +| Parameter | Symbol | Meaning | +|-----------|--------|---------| +| Operating Current | ICC, IDD | Normal current draw | +| Standby Current | ISB | Current in sleep/standby mode | +| Max Current | Imax | Maximum safe continuous current | +| Source Current | IOH | Max current pin can source (output HIGH) | +| Sink Current | IOL | Max current pin can sink (output LOW) | + +## Communication Interface Parameters + +### I2C +| Parameter | Typical Value | Notes | +|-----------|---------------|-------| +| Standard Mode | 100 kHz | Basic speed | +| Fast Mode | 400 kHz | Common for sensors | +| Fast Mode+ | 1 MHz | Higher performance | +| High Speed | 3.4 MHz | Requires special setup | +| Pull-up | 2.2kΞ©-10kΞ© | Required on SDA/SCL | + +### SPI +| Parameter | Range | Notes | +|-----------|-------|-------| +| Clock Speed | 1-20+ MHz | Device dependent | +| Modes | 0, 1, 2, 3 | CPOL/CPHA settings | +| Bit Order | MSB/LSB first | Check datasheet | + +### UART +| Parameter | Common Values | Notes | +|-----------|---------------|-------| +| Baud Rate | 9600, 115200 | Must match both ends | +| Data Bits | 8 | Standard | +| Stop Bits | 1 | Standard | +| Parity | None | Unless specified | + +## Timing Parameters + +| Parameter | Symbol | Meaning | +|-----------|--------|---------| +| Startup Time | tPU | Time from power-on to ready | +| Conversion Time | tCONV | ADC/sensor measurement time | +| Response Time | tRESP | Time to respond to input | +| Refresh Rate | - | How often output updates | + +## Temperature Ratings + +| Range | Min | Max | Notes | +|-------|-----|-----|-------| +| Commercial | 0Β°C | 70Β°C | Consumer devices | +| Industrial | -40Β°C | 85Β°C | Robust applications | +| Extended | -40Β°C | 125Β°C | Automotive/harsh | + +## Common Unit Conversions + +| From | To | Multiply by | +|------|----|-------------| +| mA | Β΅A | 1000 | +| Β΅A | mA | 0.001 | +| mW | W | 0.001 | +| MHz | kHz | 1000 | +| ms | Β΅s | 1000 | +| Β΅s | ms | 0.001 | + +## Absolute Maximum Ratings ⚠️ + +**CRITICAL**: These are DAMAGE LIMITS, not operating conditions! +- Never exceed these values even momentarily +- Recommended operating conditions are LOWER +- No safety margin included + +## Interpreting Accuracy Specs + +### Β±X% vs Β±X Units +- **Β±5%**: Error is percentage of reading +- **Β±2Β°C**: Error is fixed amount regardless of reading + +### Full Scale (FS) vs Reading +- **Β±1% FS**: Error relative to maximum range +- **Β±1% rdg**: Error relative to actual reading + +## Common Gotchas + +1. **5V vs 3.3V Logic** + - Many modern sensors are 3.3V ONLY + - Check "Absolute Maximum" voltage + - Use level shifters if needed + +2. **I2C Addresses** + - May be shown as 7-bit or 8-bit + - 7-bit: 0x3C, 8-bit: 0x78 (same device) + - Some addresses are configurable + +3. **Current Draw** + - Datasheet shows typical, actual may vary + - Add 20% margin for design + - Check peak vs average current + +4. **Pin Names** + - VCC/VDD = Power positive + - VSS/GND = Ground + - NC = Not Connected + - DNC = Do Not Connect + +## Quick Lookup: Popular Components + +| Component | Voltage | Interface | Address | +|-----------|---------|-----------|---------| +| DHT22 | 3.3-5V | 1-Wire | N/A | +| BME280 | 3.3V | I2C/SPI | 0x76/0x77 | +| SSD1306 | 3.3V | I2C/SPI | 0x3C/0x3D | +| MPU6050 | 3.3V | I2C | 0x68/0x69 | +| DS3231 | 3.3-5V | I2C | 0x68 | +| nRF24L01 | 3.3V | SPI | N/A | diff --git a/.agents/skills/datasheet-interpreter/scripts/extract_specs.py b/.agents/skills/datasheet-interpreter/scripts/extract_specs.py new file mode 100644 index 0000000..055da09 --- /dev/null +++ b/.agents/skills/datasheet-interpreter/scripts/extract_specs.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "httpx", +# "pdfplumber", +# ] +# /// +""" +Datasheet Spec Extractor - Extracts key specifications from component datasheet PDFs + +Downloads datasheet PDFs from URLs and extracts: +- Voltage/current ratings +- Pin configurations +- I2C/SPI addresses +- Operating temperature +- Timing parameters + +Usage: + uv run --no-project scripts/extract_specs.py --url "https://example.com/datasheet.pdf" + uv run --no-project scripts/extract_specs.py --url "https://example.com/datasheet.pdf" --format markdown + uv run --no-project scripts/extract_specs.py --file "local_datasheet.pdf" + uv run --no-project scripts/extract_specs.py --interactive +""" + +import argparse +import re +import sys +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +import httpx +import pdfplumber + + +# ============================================================================= +# Extraction Patterns - Regex for common datasheet parameters +# ============================================================================= + +PATTERNS = { + # Voltage patterns + "supply_voltage": [ + r"(?:VCC|VDD|Supply|Operating)\s*(?:Voltage)?[\s:=]+(\d+\.?\d*)\s*(?:to|[-–])\s*(\d+\.?\d*)\s*V", + r"(\d+\.?\d*)\s*V\s*(?:to|[-–])\s*(\d+\.?\d*)\s*V\s*(?:supply|operating|input)", + r"(?:Power Supply|Input Voltage)[\s:]+(\d+\.?\d*)\s*[-–]\s*(\d+\.?\d*)\s*V", + ], + + # Current patterns + "supply_current": [ + r"(?:Supply|Operating|Active)\s*Current[\s:=]+(\d+\.?\d*)\s*(mA|Β΅A|uA|ΞΌA)", + r"(?:ICC|IDD)[\s:=]+(\d+\.?\d*)\s*(mA|Β΅A|uA|ΞΌA)", + r"Current\s*Consumption[\s:=]+(\d+\.?\d*)\s*(mA|Β΅A|uA|ΞΌA)", + ], + + # Standby/Sleep current + "standby_current": [ + r"(?:Standby|Sleep|Power.?Down)\s*(?:Current|Mode)?[\s:=]+(\d+\.?\d*)\s*(mA|Β΅A|uA|ΞΌA|nA)", + r"(?:ISB|ISBY)[\s:=]+(\d+\.?\d*)\s*(Β΅A|uA|ΞΌA|nA)", + ], + + # I2C Address + "i2c_address": [ + r"(?:I2C|IΒ²C|IIC)\s*(?:Address|Addr)[\s:=]*(0x[0-9A-Fa-f]{2})", + r"(?:Slave|Device)\s*Address[\s:=]*(0x[0-9A-Fa-f]{2})", + r"Address[\s:=]*(0x[0-9A-Fa-f]{2})\s*(?:\(7.?bit\))?", + r"(0x[0-9A-Fa-f]{2})\s*(?:\(W\)|write|read)", + ], + + # SPI Clock + "spi_clock": [ + r"(?:SPI|SCLK|SCK)\s*(?:Clock|Frequency)?[\s:=]+(?:up to\s*)?(\d+\.?\d*)\s*(MHz|kHz)", + r"(?:Serial Clock|fSCL)[\s:=]+(\d+\.?\d*)\s*(MHz|kHz)", + ], + + # Operating Temperature + "temperature_range": [ + r"(?:Operating|Ambient)\s*Temperature[\s:=]*([-]?\d+)\s*Β°?C?\s*(?:to|[-–])\s*([-]?\d+)\s*Β°?C", + r"(?:TA|TOPR)[\s:=]*([-]?\d+)\s*(?:to|[-–])\s*([-]?\d+)\s*Β°?C", + r"([-]?\d+)\s*Β°C\s*to\s*([-]?\d+)\s*Β°C\s*(?:operating|ambient)", + ], + + # Resolution (for ADCs, sensors) + "resolution": [ + r"(?:Resolution|ADC)[\s:=]+(\d+)\s*(?:-)?bit", + r"(\d+)\s*(?:-)?bit\s*(?:resolution|ADC|DAC)", + ], + + # Package/Pins + "package": [ + r"(?:Package|Pkg)[\s:=]+(\w+[-]?\d+)", + r"(SO(?:IC)?[-]?\d+|DIP[-]?\d+|QFN[-]?\d+|TQFP[-]?\d+|BGA[-]?\d+)", + ], + + # Accuracy (for sensors) + "accuracy": [ + r"(?:Accuracy|Typical Accuracy)[\s:=]+[Β±]?(\d+\.?\d*)\s*(%|Β°C|Β°|LSB)", + r"[Β±](\d+\.?\d*)\s*(%|Β°C)\s*(?:accuracy|typical)", + ], +} + + +@dataclass +class ExtractedSpecs: + """Container for extracted specifications""" + source: str + supply_voltage: Optional[str] = None + supply_current: Optional[str] = None + standby_current: Optional[str] = None + i2c_addresses: list = field(default_factory=list) + spi_clock: Optional[str] = None + temperature_range: Optional[str] = None + resolution: Optional[str] = None + package: Optional[str] = None + accuracy: Optional[str] = None + raw_text_sample: str = "" + tables: list = field(default_factory=list) + + +def download_pdf(url: str, timeout: float = 30.0) -> bytes: + """Download PDF from URL with progress indication""" + print(f"Downloading: {url}") + + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + } + + with httpx.Client(follow_redirects=True, timeout=timeout) as client: + response = client.get(url, headers=headers) + response.raise_for_status() + + content_type = response.headers.get("content-type", "") + if "pdf" not in content_type.lower() and not url.lower().endswith(".pdf"): + print(f"Warning: Content-Type is '{content_type}', may not be a PDF") + + print(f"Downloaded: {len(response.content):,} bytes") + return response.content + + +def extract_text_from_pdf(pdf_path: Path, max_pages: int = 20) -> tuple[str, list]: + """Extract text and tables from PDF""" + all_text = [] + all_tables = [] + + with pdfplumber.open(pdf_path) as pdf: + page_count = min(len(pdf.pages), max_pages) + print(f"Processing {page_count} pages...") + + for i, page in enumerate(pdf.pages[:max_pages]): + # Extract text + text = page.extract_text() or "" + all_text.append(text) + + # Extract tables (first 5 pages only - usually contain specs) + if i < 5: + tables = page.extract_tables() + for table in tables: + if table and len(table) > 1: # Has header + data + all_tables.append(table) + + return "\n".join(all_text), all_tables + + +def extract_specs(text: str, tables: list, source: str) -> ExtractedSpecs: + """Extract specifications using regex patterns""" + specs = ExtractedSpecs(source=source) + + # Normalize text for better matching + text_normalized = text.replace('\n', ' ').replace(' ', ' ') + + # Extract voltage + for pattern in PATTERNS["supply_voltage"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.supply_voltage = f"{match.group(1)}-{match.group(2)}V" + break + + # Extract supply current + for pattern in PATTERNS["supply_current"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.supply_current = f"{match.group(1)} {match.group(2)}" + break + + # Extract standby current + for pattern in PATTERNS["standby_current"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.standby_current = f"{match.group(1)} {match.group(2)}" + break + + # Extract I2C addresses (can have multiple) + for pattern in PATTERNS["i2c_address"]: + matches = re.findall(pattern, text_normalized, re.IGNORECASE) + for addr in matches: + if addr not in specs.i2c_addresses: + specs.i2c_addresses.append(addr) + + # Extract SPI clock + for pattern in PATTERNS["spi_clock"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.spi_clock = f"{match.group(1)} {match.group(2)}" + break + + # Extract temperature range + for pattern in PATTERNS["temperature_range"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.temperature_range = f"{match.group(1)}Β°C to {match.group(2)}Β°C" + break + + # Extract resolution + for pattern in PATTERNS["resolution"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.resolution = f"{match.group(1)}-bit" + break + + # Extract package + for pattern in PATTERNS["package"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.package = match.group(1) + break + + # Extract accuracy + for pattern in PATTERNS["accuracy"]: + match = re.search(pattern, text_normalized, re.IGNORECASE) + if match: + specs.accuracy = f"Β±{match.group(1)}{match.group(2)}" + break + + # Store raw text sample (first 500 chars for context) + specs.raw_text_sample = text[:500].strip() + + # Store relevant tables + specs.tables = tables[:3] # First 3 tables + + return specs + + +def format_text(specs: ExtractedSpecs) -> str: + """Format extracted specs as plain text""" + lines = [ + "=" * 60, + " DATASHEET SPEC EXTRACTION", + "=" * 60, + "", + f"Source: {specs.source}", + "", + "EXTRACTED SPECIFICATIONS:", + "-" * 40, + ] + + if specs.supply_voltage: + lines.append(f" Supply Voltage: {specs.supply_voltage}") + if specs.supply_current: + lines.append(f" Supply Current: {specs.supply_current}") + if specs.standby_current: + lines.append(f" Standby Current: {specs.standby_current}") + if specs.i2c_addresses: + lines.append(f" I2C Address(es): {', '.join(specs.i2c_addresses)}") + if specs.spi_clock: + lines.append(f" SPI Clock: {specs.spi_clock}") + if specs.temperature_range: + lines.append(f" Temperature: {specs.temperature_range}") + if specs.resolution: + lines.append(f" Resolution: {specs.resolution}") + if specs.package: + lines.append(f" Package: {specs.package}") + if specs.accuracy: + lines.append(f" Accuracy: {specs.accuracy}") + + # Check if nothing was extracted + has_specs = any([ + specs.supply_voltage, specs.supply_current, specs.standby_current, + specs.i2c_addresses, specs.spi_clock, specs.temperature_range, + specs.resolution, specs.package, specs.accuracy + ]) + + if not has_specs: + lines.append(" (No specifications auto-extracted)") + lines.append(" Try reading the datasheet manually for specs") + + lines.append("") + + # Show tables if found + if specs.tables: + lines.append("SPECIFICATION TABLES FOUND:") + lines.append("-" * 40) + for i, table in enumerate(specs.tables[:2], 1): + lines.append(f"\nTable {i}:") + for row in table[:5]: # First 5 rows + if row: + lines.append(" " + " | ".join(str(cell or "")[:20] for cell in row)) + if len(table) > 5: + lines.append(f" ... ({len(table) - 5} more rows)") + + lines.append("") + lines.append("RAW TEXT SAMPLE (first 300 chars):") + lines.append("-" * 40) + lines.append(specs.raw_text_sample[:300]) + + return "\n".join(lines) + + +def format_markdown(specs: ExtractedSpecs) -> str: + """Format extracted specs as markdown""" + lines = [ + "# Datasheet Specification Extract", + "", + f"**Source:** `{specs.source}`", + "", + "## Extracted Specifications", + "", + "| Parameter | Value |", + "|-----------|-------|", + ] + + if specs.supply_voltage: + lines.append(f"| Supply Voltage | {specs.supply_voltage} |") + if specs.supply_current: + lines.append(f"| Supply Current | {specs.supply_current} |") + if specs.standby_current: + lines.append(f"| Standby Current | {specs.standby_current} |") + if specs.i2c_addresses: + lines.append(f"| I2C Address | {', '.join(specs.i2c_addresses)} |") + if specs.spi_clock: + lines.append(f"| SPI Clock | {specs.spi_clock} |") + if specs.temperature_range: + lines.append(f"| Operating Temp | {specs.temperature_range} |") + if specs.resolution: + lines.append(f"| Resolution | {specs.resolution} |") + if specs.package: + lines.append(f"| Package | {specs.package} |") + if specs.accuracy: + lines.append(f"| Accuracy | {specs.accuracy} |") + + lines.append("") + + # Tables + if specs.tables: + lines.append("## Specification Tables") + lines.append("") + for i, table in enumerate(specs.tables[:2], 1): + lines.append(f"### Table {i}") + lines.append("") + # Convert to markdown table + if table and len(table) > 0: + header = table[0] + lines.append("| " + " | ".join(str(h or "")[:20] for h in header) + " |") + lines.append("| " + " | ".join(["---"] * len(header)) + " |") + for row in table[1:6]: + lines.append("| " + " | ".join(str(c or "")[:20] for c in row) + " |") + if len(table) > 6: + lines.append(f"*... {len(table) - 6} more rows*") + lines.append("") + + return "\n".join(lines) + + +def process_url(url: str, output_format: str = "text") -> str: + """Main processing function for URL input""" + try: + # Download PDF + pdf_bytes = download_pdf(url) + + # Save to temp file + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp: + tmp.write(pdf_bytes) + tmp_path = Path(tmp.name) + + try: + # Extract text and tables + text, tables = extract_text_from_pdf(tmp_path) + + # Extract specs + specs = extract_specs(text, tables, url) + + # Format output + if output_format == "markdown": + return format_markdown(specs) + else: + return format_text(specs) + finally: + # Cleanup temp file + tmp_path.unlink(missing_ok=True) + + except httpx.HTTPError as e: + return f"Error downloading PDF: {e}" + except Exception as e: + return f"Error processing PDF: {e}" + + +def process_file(file_path: str, output_format: str = "text") -> str: + """Process local PDF file""" + path = Path(file_path) + if not path.exists(): + return f"Error: File not found: {file_path}" + + try: + text, tables = extract_text_from_pdf(path) + specs = extract_specs(text, tables, str(path)) + + if output_format == "markdown": + return format_markdown(specs) + else: + return format_text(specs) + except Exception as e: + return f"Error processing PDF: {e}" + + +def interactive_mode(): + """Interactive mode for spec extraction""" + print("=" * 60) + print("Datasheet Spec Extractor - Interactive Mode") + print("=" * 60) + print() + print("Enter a datasheet PDF URL or local file path") + print("Type 'quit' or 'q' to exit") + print() + + while True: + source = input("PDF URL or path> ").strip() + + if not source: + continue + if source.lower() in ['quit', 'exit', 'q']: + break + + print() + + if source.startswith(('http://', 'https://')): + result = process_url(source) + else: + result = process_file(source) + + print(result) + print() + + +def main(): + parser = argparse.ArgumentParser( + description="Extract specifications from component datasheet PDFs", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + uv run --no-project scripts/extract_specs.py --url "https://www.sparkfun.com/datasheets/Sensors/Temperature/DHT22.pdf" + uv run --no-project scripts/extract_specs.py --url "https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf" --format markdown + uv run --no-project scripts/extract_specs.py --file "local_datasheet.pdf" + uv run --no-project scripts/extract_specs.py --interactive + """ + ) + parser.add_argument("--url", "-u", type=str, help="URL of datasheet PDF to download and extract") + parser.add_argument("--file", "-f", type=str, help="Local PDF file path") + parser.add_argument("--format", type=str, default="text", + choices=["text", "markdown"], help="Output format") + parser.add_argument("--interactive", "-i", action="store_true", help="Interactive mode") + + args = parser.parse_args() + + if args.interactive: + interactive_mode() + return + + if args.url: + result = process_url(args.url, args.format) + print(result) + return + + if args.file: + result = process_file(args.file, args.format) + print(result) + return + + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/error-message-explainer/SKILL.md b/.agents/skills/error-message-explainer/SKILL.md new file mode 100644 index 0000000..494ea1c --- /dev/null +++ b/.agents/skills/error-message-explainer/SKILL.md @@ -0,0 +1,421 @@ +--- +name: error-message-explainer +description: Interprets Arduino/ESP32/RP2040 compiler errors in plain English for beginners. Use when user shares error messages, compilation failures, upload problems, or asks "what does this error mean". Covers common errors like undefined references, type mismatches, missing libraries, and board-specific issues. +--- + +# Error Message Explainer + +Translates cryptic compiler errors into actionable fixes for Arduino/ESP32/RP2040 projects. + +## Resources + +This skill includes bundled tools: + +- **scripts/parse_errors.py** - Automated error analysis with 20+ error patterns + +## Quick Start + +**Analyze error from file:** +```bash +uv run --no-project scripts/parse_errors.py --file error_log.txt +``` + +**Analyze single error:** +```bash +uv run --no-project scripts/parse_errors.py --message "error: 'LED' was not declared in this scope" +``` + +**Interactive mode:** +```bash +uv run --no-project scripts/parse_errors.py --interactive +``` + +**Pipe from compiler:** +```bash +arduino-cli compile 2>&1 | uv run --no-project scripts/parse_errors.py --stdin +``` + +## How to Use This Skill + +When user pastes an error message: +1. Identify the error type from patterns below +2. Explain what it means in simple terms +3. Show the specific fix with code example +4. Explain WHY this error happens (educational value) + +## Common Compilation Errors + +### 1. "'xyz' was not declared in this scope" +``` +error: 'xyz' was not declared in this scope +``` + +**Meaning:** Compiler doesn't recognize the name `xyz`. + +**Common Causes & Fixes:** + +| Cause | Fix | +|-------|-----| +| Typo in variable/function name | Check spelling, C++ is case-sensitive! | +| Variable used before declaration | Move declaration before first use | +| Missing `#include` | Add required library header | +| Function defined after it's called | Add forward declaration or move function up | + +**Example - Typo:** +```cpp +// WRONG +int ledpin = 13; // lowercase 'p' +digitalWrite(ledPin, HIGH); // uppercase 'P' - DIFFERENT! + +// CORRECT +int ledPin = 13; +digitalWrite(ledPin, HIGH); +``` + +**Example - Missing Include:** +```cpp +// WRONG - Servo not defined! +Servo myServo; + +// CORRECT +#include +Servo myServo; +``` + +--- + +### 2. "expected ';' before..." +``` +error: expected ';' before 'xyz' +``` + +**Meaning:** Missing semicolon on the previous line. + +**Fix:** Add `;` at end of the line ABOVE the error. + +```cpp +// WRONG - error points to line 3 +int x = 5 // ← missing ; here! +int y = 10; + +// CORRECT +int x = 5; +int y = 10; +``` + +**Pro Tip:** The error line number is where compiler noticed the problem, not where the missing `;` is! + +--- + +### 3. "expected ')' before..." or "expected '}' before..." +``` +error: expected ')' before ';' +error: expected '}' at end of input +``` + +**Meaning:** Mismatched parentheses or braces. + +**Common Patterns:** +```cpp +// WRONG - unmatched parenthesis +if (x > 5 { // missing ) +} + +// WRONG - unmatched brace +void setup() { + Serial.begin(115200); +// missing closing } + +// CORRECT +if (x > 5) { +} + +void setup() { + Serial.begin(115200); +} +``` + +**Debugging Tip:** Use IDE auto-format (Ctrl+T) to reveal mismatches. + +--- + +### 4. "invalid conversion from 'const char*' to 'char*'" +``` +error: invalid conversion from 'const char*' to 'char*' +``` + +**Meaning:** Trying to modify a string literal (which is read-only). + +```cpp +// WRONG +char* message = "Hello"; // string literals are const! +message[0] = 'h'; // can't modify! + +// CORRECT (if you need to modify) +char message[] = "Hello"; // creates modifiable copy +message[0] = 'h'; // OK! + +// CORRECT (if read-only is fine) +const char* message = "Hello"; +``` + +--- + +### 5. "no matching function for call to..." +``` +error: no matching function for call to 'SomeClass::method(int, int, int)' +note: candidate: void SomeClass::method(int, int) +``` + +**Meaning:** Function called with wrong number or type of arguments. + +```cpp +// WRONG - too many arguments +myServo.write(90, 100); // write() takes only 1 argument! + +// CORRECT +myServo.write(90); +``` + +**Read the "note:" lines** - they show what arguments ARE accepted. + +--- + +### 6. "multiple definition of 'xyz'" +``` +error: multiple definition of 'xyz' +``` + +**Meaning:** Same variable/function defined in multiple files. + +**Fixes:** +```cpp +// In header file (.h), use 'extern': +extern int globalVar; // declaration only + +// In ONE .cpp file, define it: +int globalVar = 0; // actual definition +``` + +Or for functions in header: +```cpp +// WRONG - defined in header, included multiple times +int add(int a, int b) { return a + b; } + +// CORRECT - use 'inline' +inline int add(int a, int b) { return a + b; } +``` + +--- + +### 7. "'xyz' does not name a type" +``` +error: 'WiFiClient' does not name a type +``` + +**Meaning:** Class/type not recognized. + +**Fixes:** +| Board | Library | Include | +|-------|---------|---------| +| ESP32 | WiFi | `#include ` | +| ESP8266 | WiFi | `#include ` | +| Arduino + WiFi Shield | WiFi | `#include ` | + +```cpp +// WRONG +WiFiClient client; // WiFiClient unknown! + +// CORRECT for ESP32 +#include +WiFiClient client; +``` + +--- + +### 8. "redefinition of 'xyz'" +``` +error: redefinition of 'int x' +``` + +**Meaning:** Variable declared twice in same scope. + +```cpp +// WRONG +int count = 0; +int count = 0; // redefinition! + +// WRONG in loops +for (int i = 0; i < 10; i++) { + int i = 5; // shadows loop variable! +} + +// CORRECT +int count = 0; // declare once +count = 5; // assign without 'int' +``` + +--- + +## Upload Errors + +### 9. "avrdude: stk500_recv(): programmer is not responding" +``` +avrdude: stk500_recv(): programmer is not responding +avrdude: stk500_getsync() attempt X of 10: not in sync +``` + +**Meaning:** Arduino IDE can't communicate with the board. + +**Fixes (try in order):** +1. βœ… Correct board selected? (Tools β†’ Board) +2. βœ… Correct port selected? (Tools β†’ Port) +3. βœ… USB cable is data cable (not charge-only)? +4. βœ… Try different USB port +5. βœ… Nothing connected to pins 0/1 (TX/RX)? +6. βœ… Press reset button during upload +7. βœ… Install/reinstall USB drivers + +--- + +### 10. "A fatal error occurred: Failed to connect to ESP32" +``` +A fatal error occurred: Failed to connect to ESP32: +Timed out waiting for packet header +``` + +**Meaning:** ESP32 not entering bootloader mode. + +**Fix:** Hold BOOT button while uploading: +1. Click Upload in IDE +2. When "Connecting..." appears, hold BOOT button +3. Release when upload starts +4. Some boards: hold BOOT, press EN/RST, release BOOT + +--- + +### 11. "Sketch too big" +``` +Sketch too big; see https://support.arduino.cc/... +Sketch uses 34816 bytes (107%) of program storage space. +Maximum is 32256 bytes. +``` + +**Meaning:** Program doesn't fit in flash memory. + +**Fixes:** +```cpp +// 1. Use F() macro for strings (saves RAM + sometimes Flash) +Serial.println(F("This string in flash")); // instead of RAM + +// 2. Remove unused libraries +// Each #include adds code even if not used + +// 3. Use smaller data types +uint8_t x = 5; // instead of int (2 bytes saved) + +// 4. Choose board with more flash (ESP32 has 4MB vs Arduino's 32KB) +``` + +--- + +## Library Errors + +### 12. "fatal error: xyz.h: No such file or directory" +``` +fatal error: Adafruit_BME280.h: No such file or directory +``` + +**Meaning:** Library not installed. + +**Fix:** +1. Sketch β†’ Include Library β†’ Manage Libraries +2. Search for library name +3. Click Install +4. If not in Library Manager: download ZIP, Sketch β†’ Include Library β†’ Add .ZIP Library + +--- + +### 13. "exit status 1 / Error compiling for board..." +``` +exit status 1 +Error compiling for board Arduino Uno. +``` + +**Meaning:** Generic error - scroll UP to find the real error message. + +**The actual error is ABOVE this line!** Look for lines containing `error:`. + +--- + +## Type Errors + +### 14. "cannot convert 'String' to 'const char*'" +``` +error: cannot convert 'String' to 'const char*' +``` + +**Meaning:** Function expects C-string but got Arduino String. + +```cpp +// WRONG +String myString = "hello"; +someFunction(myString); // if someFunction expects const char* + +// CORRECT +String myString = "hello"; +someFunction(myString.c_str()); // convert to C-string +``` + +--- + +### 15. "invalid operands to binary expression" +``` +error: invalid operands of types 'const char*' and 'const char*' to binary 'operator+' +``` + +**Meaning:** Can't use `+` with C-strings. + +```cpp +// WRONG +const char* a = "hello"; +const char* b = " world"; +const char* c = a + b; // doesn't work! + +// CORRECT - use String +String a = "hello"; +String b = " world"; +String c = a + b; // works! + +// Or use snprintf +char c[20]; +snprintf(c, sizeof(c), "%s%s", a, b); +``` + +--- + +## Quick Reference Table + +| Error Contains | Likely Problem | Quick Fix | +|----------------|----------------|-----------| +| "not declared" | Typo or missing include | Check spelling, add #include | +| "expected ';'" | Missing semicolon | Add ; to line ABOVE error | +| "expected ')'" | Unmatched parenthesis | Count ( and ) | +| "expected '}'" | Unmatched brace | Count { and } | +| "no matching function" | Wrong arguments | Check function signature | +| "does not name a type" | Missing library | Add #include | +| "multiple definition" | Defined in multiple files | Use extern | +| "stk500" | Upload failed | Check board/port/cable | +| "No such file" | Library not installed | Install via Library Manager | +| "too big" | Out of flash | Use F(), remove unused code | + +## Debugging Strategy + +``` +1. Read the FIRST error (later ones often cascade) +2. Note the FILE and LINE NUMBER +3. Look at that line AND the line above +4. Check for common patterns above +5. Fix ONE error at a time, recompile +6. Repeat until clean +``` diff --git a/.agents/skills/error-message-explainer/scripts/parse_errors.py b/.agents/skills/error-message-explainer/scripts/parse_errors.py new file mode 100644 index 0000000..a6ce3a2 --- /dev/null +++ b/.agents/skills/error-message-explainer/scripts/parse_errors.py @@ -0,0 +1,555 @@ +#!/usr/bin/env python3 +""" +Error Message Parser - Analyzes Arduino/PlatformIO error messages + +Parses compiler errors and provides: +- Plain English explanation +- Likely causes +- Fix suggestions +- Code examples + +Usage: + uv run --no-project scripts/parse_errors.py --file error_log.txt + uv run --no-project scripts/parse_errors.py --message "error: 'LED' was not declared" + uv run --no-project scripts/parse_errors.py --interactive + cat error.txt | uv run --no-project scripts/parse_errors.py --stdin +""" + +import argparse +import re +import json +from dataclasses import dataclass +from typing import List, Optional, Tuple + +# ============================================================================= +# Error Pattern Database +# ============================================================================= + +ERROR_PATTERNS = { + # Declaration/Scope Errors + r"'(\w+)' was not declared in this scope": { + "type": "undeclared_identifier", + "explanation": "The compiler doesn't recognize '{0}'. This happens when you try to use a variable, function, or constant that hasn't been defined yet.", + "causes": [ + "Typo in variable or function name", + "Forgot to declare the variable", + "Missing #include for a library", + "Variable declared inside a function but used outside", + "Case sensitivity issue (Arduino is case-sensitive)" + ], + "fixes": [ + "Check spelling - Arduino is case-sensitive (LED vs led)", + "Declare the variable before using it: int {0};", + "Add the required #include at top of file", + "If it's a constant, add: #define {0} value", + "If it's a pin, use: const int {0} = pin_number;" + ] + }, + + r"'(\w+)' does not name a type": { + "type": "unknown_type", + "explanation": "The compiler doesn't recognize '{0}' as a valid type. Types define what kind of data a variable can hold.", + "causes": [ + "Missing #include for a library that defines this type", + "Typo in the type name", + "Using a class/struct before it's defined", + "Library not installed" + ], + "fixes": [ + "Add #include at top of sketch", + "Check the library documentation for correct type name", + "Install the library: Sketch β†’ Include Library β†’ Manage Libraries", + "Common types: int, float, char, byte, String, bool" + ] + }, + + # Missing Semicolon/Syntax + r"expected ('.*?'|';'|'\)'|'\}'|',' |declaration) before": { + "type": "syntax_error", + "explanation": "The compiler expected {0} but found something else. This usually means there's a missing punctuation mark on a previous line.", + "causes": [ + "Missing semicolon ; at end of previous line", + "Missing closing brace } or parenthesis )", + "Missing comma in function call or array", + "Unclosed string (missing quote)" + ], + "fixes": [ + "Add semicolon to the END of the previous line", + "Count your braces - every { needs a matching }", + "Count parentheses - every ( needs a matching )", + "Check the line ABOVE the error, not the error line" + ] + }, + + r"expected '\)' before '(\w+)'": { + "type": "missing_parenthesis", + "explanation": "Missing closing parenthesis. The function call or expression isn't properly closed.", + "causes": [ + "Forgot closing ) in function call", + "Mismatched parentheses in complex expression", + "Missing comma between function arguments" + ], + "fixes": [ + "Add ) to close the function call", + "Check for matching pairs of parentheses", + "Use editor highlighting to match brackets" + ] + }, + + # Type Mismatches + r"invalid conversion from '(\w+\*?)' to '(\w+\*?)'": { + "type": "type_mismatch", + "explanation": "You're trying to use a {0} where a {1} is expected. These types aren't compatible.", + "causes": [ + "Passing wrong type to function", + "Assigning incompatible values", + "Mixing pointers with non-pointers", + "String vs char array confusion" + ], + "fixes": [ + "Check function documentation for expected parameter types", + "Use proper type conversion: (int)value or String(value)", + "For strings: use .c_str() to convert String to char*", + "Make sure variable types match what function expects" + ] + }, + + # Function Errors + r"too few arguments to function '(\w+)'": { + "type": "missing_arguments", + "explanation": "Function '{0}' requires more arguments than you provided.", + "causes": [ + "Forgot to include required parameters", + "Using wrong function overload", + "Misread documentation" + ], + "fixes": [ + "Check function documentation for required parameters", + "Look at function definition to see all parameters", + "Add missing arguments in correct order" + ] + }, + + r"too many arguments to function '(\w+)'": { + "type": "extra_arguments", + "explanation": "Function '{0}' received more arguments than it accepts.", + "causes": [ + "Added extra parameters by mistake", + "Using wrong function overload", + "Comma inside a string being interpreted as separator" + ], + "fixes": [ + "Remove extra arguments", + "Check function documentation for correct usage", + "Verify you're calling the right function" + ] + }, + + r"'class (\w+)' has no member named '(\w+)'": { + "type": "no_member", + "explanation": "The '{0}' library/class doesn't have a function or variable called '{1}'.", + "causes": [ + "Typo in method name", + "Using method from wrong library version", + "Method doesn't exist in this library", + "Object type is wrong" + ], + "fixes": [ + "Check library documentation for correct method names", + "Verify library version matches examples", + "Look for similar method names (case-sensitive)", + "Make sure object is correct type" + ] + }, + + # Memory Errors + r"section.*will not fit in region": { + "type": "memory_overflow", + "explanation": "Your code is too large to fit in the microcontroller's memory.", + "causes": [ + "Using too many libraries", + "Large arrays or strings", + "Debug/print statements taking space", + "Using wrong board selection" + ], + "fixes": [ + "Use F() macro for strings: Serial.println(F(\"text\"))", + "Use PROGMEM for constant arrays", + "Remove unused libraries and code", + "Use smaller data types (byte vs int)", + "Verify correct board is selected in Tools menu" + ] + }, + + r"data.*will not fit|RAM.*overflow": { + "type": "ram_overflow", + "explanation": "Your program uses more RAM than available. Variables and runtime data exceed memory limits.", + "causes": [ + "Large arrays or buffers", + "Many String objects", + "Recursive functions", + "Large local variables" + ], + "fixes": [ + "Use PROGMEM for constant data", + "Use smaller buffers", + "Avoid String class, use char arrays", + "Make large arrays global instead of local", + "Use byte instead of int where possible" + ] + }, + + # Library Errors + r"No such file or directory.*#include.*<(\w+)": { + "type": "missing_library", + "explanation": "The library '{0}' is not installed.", + "causes": [ + "Library not installed", + "Typo in library name", + "Wrong include path", + "Library in wrong folder" + ], + "fixes": [ + "Install library: Sketch β†’ Include Library β†’ Manage Libraries", + "Search for '{0}' in Library Manager", + "Check for correct library name (case-sensitive)", + "Manually install: download .zip, Sketch β†’ Include Library β†’ Add .ZIP" + ] + }, + + r"multiple definition of `(\w+)'": { + "type": "multiple_definition", + "explanation": "'{0}' is defined more than once in your code.", + "causes": [ + "Same variable/function in multiple files", + "Header file included multiple times without guards", + "Copy-paste error creating duplicates" + ], + "fixes": [ + "Remove duplicate definitions", + "Use 'extern' keyword for variables shared between files", + "Add include guards to header files", + "Search your code for duplicate names" + ] + }, + + # Board/Upload Errors + r"avrdude.*programmer.*not responding": { + "type": "upload_fail", + "explanation": "Cannot communicate with the Arduino board.", + "causes": [ + "Wrong COM port selected", + "USB cable is data-only (no data lines)", + "Driver not installed", + "Board not connected", + "Another program using the port" + ], + "fixes": [ + "Select correct port: Tools β†’ Port", + "Try a different USB cable (use data cable, not charge-only)", + "Unplug and replug the Arduino", + "Close Serial Monitor and any other serial programs", + "Try a different USB port", + "Install/reinstall Arduino drivers" + ] + }, + + r"ser_open\(\).*can't open device": { + "type": "port_error", + "explanation": "Cannot open the serial port. The port may be in use or disconnected.", + "causes": [ + "Port used by another program", + "Arduino disconnected", + "Wrong port selected", + "Permission denied (Linux/Mac)" + ], + "fixes": [ + "Close other programs using serial port", + "Check Arduino is plugged in", + "Select correct port in Tools β†’ Port", + "On Linux: add user to dialout group" + ] + }, + + # ESP32/ESP8266 Specific + r"Brownout detector was triggered": { + "type": "brownout", + "explanation": "The ESP32 detected insufficient power and reset to protect itself.", + "causes": [ + "USB port cannot supply enough current", + "Power-hungry peripherals", + "WiFi transmission spikes", + "Bad USB cable with high resistance" + ], + "fixes": [ + "Use powered USB hub", + "Add external power supply (5V 1A minimum)", + "Use shorter/better quality USB cable", + "Add large capacitor (100-470Β΅F) near ESP32 power pins", + "Reduce WiFi transmit power in code" + ] + }, + + r"rst:0x1.*POWERON_RESET.*rst:0x10.*RTCWDT_RTC_RESET": { + "type": "watchdog_reset", + "explanation": "ESP32 watchdog timer reset - code is stuck in a loop or blocking for too long.", + "causes": [ + "Infinite loop without yield()", + "Blocking code in callbacks", + "WiFi.begin() without connection timeout" + ], + "fixes": [ + "Add yield() or delay(1) in long loops", + "Don't block in interrupt handlers", + "Add timeouts to network operations", + "Use async/non-blocking patterns" + ] + } +} + +# Common Arduino keywords that might be typos +COMMON_TYPOS = { + "Led": "LED", + "led": "LED", + "HIGH": "HIGH", + "high": "HIGH", + "Low": "LOW", + "low": "LOW", + "input": "INPUT", + "output": "OUTPUT", + "serial": "Serial", + "SERIAL": "Serial", + "String": "String", + "string": "String", + "Delay": "delay", + "DELAY": "delay", + "pinmode": "pinMode", + "PinMode": "pinMode", + "digitalwrite": "digitalWrite", + "DigitalWrite": "digitalWrite", + "digitalread": "digitalRead", + "DigitalRead": "digitalRead", + "analogwrite": "analogWrite", + "AnalogWrite": "analogWrite", + "analogread": "analogRead", + "AnalogRead": "analogRead" +} + + +@dataclass +class ParsedError: + """Parsed error information""" + original: str + error_type: str + explanation: str + causes: List[str] + fixes: List[str] + line_number: Optional[int] = None + file_name: Optional[str] = None + identifier: Optional[str] = None + + +def extract_location(error_line: str) -> Tuple[Optional[str], Optional[int]]: + """Extract file name and line number from error""" + # Pattern: /path/to/file.ino:123:45: error: + match = re.search(r'([^/\\:]+\.(?:ino|cpp|c|h)):(\d+):', error_line) + if match: + return match.group(1), int(match.group(2)) + return None, None + + +def parse_error(error_text: str) -> List[ParsedError]: + """Parse error message and return explanation""" + results = [] + + # Split into lines and process + lines = error_text.strip().split('\n') + + for line in lines: + # Skip non-error lines + if 'error:' not in line.lower() and 'warning:' not in line.lower(): + continue + + file_name, line_num = extract_location(line) + + # Try to match against known patterns + for pattern, info in ERROR_PATTERNS.items(): + match = re.search(pattern, line, re.IGNORECASE) + if match: + # Extract captured groups for formatting + groups = match.groups() + + explanation = info["explanation"] + if groups: + try: + explanation = explanation.format(*groups) + except (IndexError, KeyError): + pass + + fixes = [] + for fix in info["fixes"]: + if groups: + try: + fix = fix.format(*groups) + except (IndexError, KeyError): + pass + fixes.append(fix) + + results.append(ParsedError( + original=line, + error_type=info["type"], + explanation=explanation, + causes=info["causes"], + fixes=fixes, + line_number=line_num, + file_name=file_name, + identifier=groups[0] if groups else None + )) + break + else: + # No pattern matched - generic error + results.append(ParsedError( + original=line, + error_type="unknown", + explanation="Error not in database. See original message.", + causes=["Check the exact error message"], + fixes=["Search online for this specific error"], + line_number=line_num, + file_name=file_name + )) + + return results + + +def check_typo(identifier: str) -> Optional[str]: + """Check if identifier might be a typo of common Arduino keyword""" + if identifier in COMMON_TYPOS: + return COMMON_TYPOS[identifier] + + # Case-insensitive check + lower = identifier.lower() + for typo, correct in COMMON_TYPOS.items(): + if typo.lower() == lower: + return correct + + return None + + +def format_report(errors: List[ParsedError], verbose: bool = True) -> str: + """Format error analysis as readable report""" + if not errors: + return "No errors found in input." + + lines = [ + "=" * 60, + " ERROR ANALYSIS REPORT", + "=" * 60, + "" + ] + + for i, err in enumerate(errors, 1): + lines.append(f"Error #{i}: {err.error_type.upper()}") + lines.append("-" * 40) + + if err.file_name: + loc = f"{err.file_name}" + if err.line_number: + loc += f", line {err.line_number}" + lines.append(f"Location: {loc}") + + lines.append("") + lines.append(f"πŸ” What happened:") + lines.append(f" {err.explanation}") + + # Check for typo suggestion + if err.identifier: + suggestion = check_typo(err.identifier) + if suggestion: + lines.append(f" πŸ’‘ Did you mean: {suggestion}") + + if verbose: + lines.append("") + lines.append("πŸ“‹ Possible causes:") + for cause in err.causes[:3]: + lines.append(f" β€’ {cause}") + + lines.append("") + lines.append("πŸ”§ How to fix:") + for fix in err.fixes[:3]: + lines.append(f" β€’ {fix}") + + lines.append("") + lines.append(f"Original: {err.original[:80]}...") + lines.append("") + + return "\n".join(lines) + + +def interactive_mode(): + """Interactive error analysis""" + print("=" * 60) + print("Error Message Explainer - Interactive Mode") + print("=" * 60) + print() + print("Paste your error message (press Enter twice when done):") + print() + + lines = [] + while True: + line = input() + if not line and lines and not lines[-1]: + break + lines.append(line) + + error_text = "\n".join(lines) + + if not error_text.strip(): + print("No error message provided.") + return + + errors = parse_error(error_text) + report = format_report(errors) + print(report) + + +def main(): + parser = argparse.ArgumentParser(description="Arduino Error Message Parser") + parser.add_argument("--interactive", "-i", action="store_true", help="Interactive mode") + parser.add_argument("--file", "-f", type=str, help="Read errors from file") + parser.add_argument("--message", "-m", type=str, help="Single error message") + parser.add_argument("--stdin", action="store_true", help="Read from stdin") + parser.add_argument("--json", "-j", action="store_true", help="Output as JSON") + parser.add_argument("--brief", "-b", action="store_true", help="Brief output") + + args = parser.parse_args() + + error_text = "" + + if args.interactive: + interactive_mode() + return + + if args.file: + with open(args.file, 'r') as f: + error_text = f.read() + elif args.message: + error_text = args.message + elif args.stdin: + import sys + error_text = sys.stdin.read() + else: + parser.print_help() + return + + errors = parse_error(error_text) + + if args.json: + import dataclasses + output = [dataclasses.asdict(e) for e in errors] + print(json.dumps(output, indent=2)) + else: + print(format_report(errors, verbose=not args.brief)) + + +if __name__ == "__main__": + main() diff --git a/.agents/skills/readme-generator/SKILL.md b/.agents/skills/readme-generator/SKILL.md new file mode 100644 index 0000000..b1744c4 --- /dev/null +++ b/.agents/skills/readme-generator/SKILL.md @@ -0,0 +1,478 @@ +--- +name: readme-generator +description: Auto-generates professional README.md files for Arduino/ESP32/RP2040 projects following open-source best practices. Use when user wants to document their project for GitHub, needs help writing a README, or says "make my project shareable". Follows awesome-readme standards with sections for Overview, Hardware, Software, Setup, Usage, Troubleshooting, and Contributing. +--- + +# README Generator + +Creates professional, beginner-friendly README files for maker projects. + +## Resources + +This skill includes bundled tools: + +- **scripts/generate_readme.py** - Full README generator with wiring diagrams and templates + +## Quick Start + +**Interactive mode:** +```bash +uv run --no-project scripts/generate_readme.py --interactive +``` + +**Quick generation:** +```bash +uv run --no-project scripts/generate_readme.py --project "Weather Station" --board "ESP32" --output README.md +``` + +**Scan existing project:** +```bash +uv run --no-project scripts/generate_readme.py --scan /path/to/arduino/project --output README.md +``` + +## When to Use +- "Help me document this project" +- "I want to share this on GitHub" +- "Write a README for my project" +- User has working project, needs documentation +- Before publishing to GitHub/Instructables + +## Information Gathering + +### Ask User For: +``` +1. Project name and one-line description +2. What problem does it solve / why did you build it? +3. Main features (3-5 bullet points) +4. Hardware components used +5. Software libraries required +6. Any photos/videos/GIFs available? +7. License preference (MIT recommended for open source) +8. Target audience (beginners/intermediate/advanced) +``` + +### Auto-Extract From Code: +- Pin assignments from config.h +- Library includes +- WiFi/Bluetooth features +- Sensor types + +--- + +## README Template + +Generate using this structure (based on awesome-readme best practices): + +```markdown +# 🎯 [Project Name] + +![Project Status](https://img.shields.io/badge/status-active-brightgreen) +![Platform](https://img.shields.io/badge/platform-ESP32-blue) +![License](https://img.shields.io/badge/license-MIT-green) + +> One-line description of what this project does and why it's useful. + +![Project Photo/GIF](images/project-demo.gif) + +## πŸ“‹ Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Hardware Components](#hardware-components) +- [Wiring Diagram](#wiring-diagram) +- [Software Dependencies](#software-dependencies) +- [Installation](#installation) +- [Configuration](#configuration) +- [Usage](#usage) +- [Troubleshooting](#troubleshooting) +- [Contributing](#contributing) +- [License](#license) +- [Acknowledgments](#acknowledgments) + +## πŸ” Overview + +[2-3 paragraphs explaining:] +- What the project does +- Why you built it / what problem it solves +- Who it's for (target audience) + +### Demo + +[Embed video or GIF showing project in action] + +## ✨ Features + +- βœ… Feature 1 - brief description +- βœ… Feature 2 - brief description +- βœ… Feature 3 - brief description +- 🚧 Planned: Feature 4 (coming soon) + +## πŸ”§ Hardware Components + +| Component | Quantity | Purpose | Notes | +|-----------|----------|---------|-------| +| [MCU Board] | 1 | Main controller | [version/variant] | +| [Sensor 1] | 1 | [function] | [I2C address, etc.] | +| [Display] | 1 | User interface | [resolution] | +| ... | ... | ... | ... | + +**Estimated Cost:** $XX-XX + +### Where to Buy + +- [Component 1](link) - Amazon/AliExpress +- [Component 2](link) - Adafruit/SparkFun + +## πŸ“ Wiring Diagram + +![Wiring Diagram](images/wiring-diagram.png) + +### Pin Connections + +| MCU Pin | Component | Pin | Function | +|---------|-----------|-----|----------| +| GPIO21 | BME280 | SDA | I2C Data | +| GPIO22 | BME280 | SCL | I2C Clock | +| GPIO4 | LED | Anode | Status indicator | +| ... | ... | ... | ... | + +## πŸ’» Software Dependencies + +### Required Software + +- [Arduino IDE](https://www.arduino.cc/en/software) (v2.0+) or [PlatformIO](https://platformio.org/) +- [Board package] - [installation link] + +### Required Libraries + +| Library | Version | Purpose | Install via | +|---------|---------|---------|-------------| +| [Library1] | >=1.0.0 | [function] | Library Manager | +| [Library2] | >=2.3.0 | [function] | Library Manager | +| ... | ... | ... | ... | + +## πŸ“¦ Installation + +### Option 1: Arduino IDE + +1. **Install Arduino IDE** + - Download from [arduino.cc](https://www.arduino.cc/en/software) + +2. **Add Board Support** (if using ESP32/RP2040) + ``` + File β†’ Preferences β†’ Additional Board Manager URLs: + https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + ``` + Then: Tools β†’ Board β†’ Boards Manager β†’ Search "[board]" β†’ Install + +3. **Install Required Libraries** + - Sketch β†’ Include Library β†’ Manage Libraries + - Search and install each library from the table above + +4. **Clone or Download This Repository** + ```bash + git clone https://github.com/[username]/[repo-name].git + ``` + Or download ZIP and extract + +5. **Open the Project** + - Open `[project-name].ino` in Arduino IDE + +### Option 2: PlatformIO (Recommended for Advanced Users) + +1. Install [VS Code](https://code.visualstudio.com/) + [PlatformIO extension](https://platformio.org/install/ide?install=vscode) + +2. Clone and open: + ```bash + git clone https://github.com/[username]/[repo-name].git + cd [repo-name] + code . + ``` + +3. PlatformIO will automatically install dependencies from `platformio.ini` + +## βš™οΈ Configuration + +Before uploading, customize `config.h`: + +```cpp +// === NETWORK SETTINGS === +#define WIFI_SSID "your-wifi-name" +#define WIFI_PASSWORD "your-wifi-password" + +// === HARDWARE PINS === +#define LED_PIN 4 +#define SENSOR_SDA 21 +#define SENSOR_SCL 22 + +// === FEATURE FLAGS === +#define ENABLE_OLED true +#define ENABLE_WIFI true +#define DEBUG_MODE true +``` + +### Environment-Specific Settings + +| Setting | Development | Production | +|---------|-------------|------------| +| DEBUG_MODE | true | false | +| SERIAL_BAUD | 115200 | 9600 | +| SLEEP_INTERVAL | 10s | 300s | + +## πŸš€ Usage + +### Basic Operation + +1. **Power On** - Connect USB or battery +2. **Wait for Boot** - Status LED blinks during initialization +3. **[Normal Operation]** - Description of what happens + +### LED Status Indicators + +| LED State | Meaning | +|-----------|---------| +| Solid Green | Normal operation | +| Blinking Blue | WiFi connecting | +| Red Flash | Error (check serial) | + +### Serial Monitor + +Open Serial Monitor at 115200 baud to see: +``` +[BOOT] Project Name v1.0.0 +[INFO] Initializing sensors... +[OK] BME280 found at 0x76 +[INFO] Connecting to WiFi... +[OK] Connected: 192.168.1.100 +[DATA] Temp: 23.5Β°C, Humidity: 45% +``` + +### Web Interface (if applicable) + +Navigate to `http://[device-ip]` to access: +- Real-time sensor readings +- Configuration panel +- Data export + +## ❓ Troubleshooting + +### Common Issues + +
+Upload fails: "Failed to connect" + +**ESP32:** Hold BOOT button while clicking Upload, release when "Connecting..." appears. + +**Arduino:** Check correct COM port selected in Tools β†’ Port. +
+ +
+Sensor not detected + +1. Check wiring (SDA/SCL not swapped?) +2. Run I2C scanner sketch to verify address +3. Add pull-up resistors (4.7kΞ©) if not on module +4. Check voltage compatibility (3.3V vs 5V) +
+ +
+WiFi won't connect + +1. Verify SSID/password in config.h (case-sensitive!) +2. 2.4GHz only (ESP32 doesn't support 5GHz) +3. Check router isn't blocking new devices +4. Try moving closer to router +
+ +
+Random resets + +1. Power supply too weak - use 500mA+ source +2. Add 100Β΅F capacitor near MCU +3. Check for short circuits +4. Disable brownout detector (ESP32) +
+ +### Still Stuck? + +1. Check [Issues](https://github.com/[username]/[repo]/issues) for similar problems +2. Open a new issue with: + - Hardware setup (board, sensors) + - Error messages (full serial output) + - Steps to reproduce + +## 🀝 Contributing + +Contributions are welcome! Here's how: + +### Reporting Bugs + +1. Check existing issues first +2. Use the bug report template +3. Include serial output and hardware details + +### Suggesting Features + +1. Open an issue with `[Feature Request]` prefix +2. Describe use case and expected behavior + +### Pull Requests + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/amazing-feature` +3. Make your changes +4. Test thoroughly +5. Commit: `git commit -m 'Add amazing feature'` +6. Push: `git push origin feature/amazing-feature` +7. Open a Pull Request + +### Code Style + +- Use meaningful variable names +- Comment complex logic +- Follow existing formatting +- Test on actual hardware before PR + +## πŸ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +``` +MIT License + +Copyright (c) [year] [author] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +``` + +## πŸ™ Acknowledgments + +- [Library/Project] - for [what it provides] +- [Person/Tutorial] - inspiration/guidance +- [Community] - testing and feedback + +## πŸ“¬ Contact + +**[Your Name]** - [@twitter_handle](https://twitter.com/handle) - email@example.com + +Project Link: [https://github.com/[username]/[repo-name]](https://github.com/[username]/[repo-name]) + +--- + +⭐ If this project helped you, please give it a star! +``` + +--- + +## Section Guidelines + +### Good vs Bad Examples + +**Project Description:** +``` +❌ Bad: "An Arduino project with sensors" +βœ… Good: "Battery-powered environmental monitor that tracks temperature, + humidity, and air quality, sending alerts when thresholds are exceeded" +``` + +**Features:** +``` +❌ Bad: "Has WiFi" +βœ… Good: "πŸ“Ά WiFi connectivity with automatic reconnection and OTA updates" +``` + +**Installation Steps:** +``` +❌ Bad: "Install the libraries and upload" +βœ… Good: Step-by-step with screenshots, version numbers, exact menu paths +``` + +### Visual Assets + +**Recommended:** +- Project photo (hero image) +- Wiring diagram (Fritzing or hand-drawn) +- Demo GIF (< 5MB, 10-15 seconds) +- Schematic (KiCad export) + +**Creating GIFs:** +- Use ScreenToGif (Windows) or Peek (Linux) +- Optimize with ezgif.com +- Keep under 5MB for GitHub + +### Badges + +```markdown + +![Build Status](https://img.shields.io/badge/build-passing-brightgreen) +![Version](https://img.shields.io/badge/version-1.0.0-blue) +![License](https://img.shields.io/badge/license-MIT-green) + + +![Arduino](https://img.shields.io/badge/platform-Arduino-00979D?logo=arduino) +![ESP32](https://img.shields.io/badge/platform-ESP32-blue) +![Raspberry Pi](https://img.shields.io/badge/platform-RPi_Pico-C51A4A) + + +![GitHub stars](https://img.shields.io/github/stars/user/repo) +![GitHub forks](https://img.shields.io/github/forks/user/repo) +``` + +--- + +## File Structure Recommendation + +``` +project-name/ +β”œβ”€β”€ README.md # Main documentation +β”œβ”€β”€ LICENSE # MIT license file +β”œβ”€β”€ .gitignore # Ignore build files +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ main.ino # Main sketch +β”‚ └── config.h # User configuration +β”œβ”€β”€ lib/ # Local libraries (optional) +β”œβ”€β”€ docs/ +β”‚ β”œβ”€β”€ WIRING.md # Detailed wiring guide +β”‚ β”œβ”€β”€ API.md # API documentation (if applicable) +β”‚ └── CHANGELOG.md # Version history +β”œβ”€β”€ images/ +β”‚ β”œβ”€β”€ project-photo.jpg +β”‚ β”œβ”€β”€ wiring-diagram.png +β”‚ └── demo.gif +β”œβ”€β”€ hardware/ # PCB/enclosure files (optional) +β”‚ β”œβ”€β”€ schematic.pdf +β”‚ └── enclosure.stl +└── examples/ # Additional example sketches + └── basic/ +``` + +--- + +## Quick README Checklist + +Before publishing, verify: + +``` +β–‘ Project name is clear and memorable +β–‘ One-line description explains the "what" and "why" +β–‘ Hero image/GIF shows project in action +β–‘ All hardware components listed with links +β–‘ Wiring diagram included +β–‘ All libraries listed with versions +β–‘ Step-by-step installation instructions +β–‘ Configuration section explains all settings +β–‘ Usage section shows expected output +β–‘ Troubleshooting covers common issues +β–‘ License file present +β–‘ Contact information included +β–‘ No broken links +β–‘ Spelling/grammar checked +``` diff --git a/.agents/skills/readme-generator/scripts/generate_readme.py b/.agents/skills/readme-generator/scripts/generate_readme.py new file mode 100644 index 0000000..439d6f3 --- /dev/null +++ b/.agents/skills/readme-generator/scripts/generate_readme.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +""" +README Generator - Creates professional README.md for Arduino/maker projects + +Analyzes project structure and generates documentation including: +- Project description +- Hardware requirements +- Wiring diagrams (ASCII) +- Installation instructions +- Usage examples +- License + +Usage: + uv run --no-project scripts/generate_readme.py --interactive + uv run --no-project scripts/generate_readme.py --project "Weather Station" --board "ESP32" + uv run --no-project scripts/generate_readme.py --scan /path/to/project +""" + +import argparse +import os +import re +from datetime import datetime +from dataclasses import dataclass, field +from typing import List, Optional, Dict + +# ============================================================================= +# Templates +# ============================================================================= + +README_TEMPLATE = '''# {project_name} + +{badges} + +{description} + +## πŸ“‹ Features + +{features} + +## πŸ”§ Hardware Required + +{hardware_list} + +### Wiring Diagram + +{wiring_diagram} + +## πŸ“¦ Dependencies + +{dependencies} + +## πŸš€ Installation + +{installation} + +## πŸ“– Usage + +{usage} + +## βš™οΈ Configuration + +{configuration} + +## πŸ› Troubleshooting + +{troubleshooting} + +## πŸ“„ License + +{license} + +--- +{footer} +''' + +ASCII_WIRING_TEMPLATES = { + "i2c": '''``` + {board} + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ VIN 5V │─────────────┐ + β”‚ GND │───────────┐ β”‚ + β”‚ SDA {sda} │────────┐ β”‚ β”‚ + β”‚ SCL {scl} │──────┐ β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ β”‚ + {device} β”‚ β”‚ β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ + β”‚ VCC β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”Όβ”€β”Όβ”€β”˜ + β”‚ GND β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”Όβ”€β”˜ + β”‚ SDA β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”˜ + β”‚ SCL β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +```''', + + "spi": '''``` + {board} + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ VIN 5V │─────────────┐ + β”‚ GND │───────────┐ β”‚ + β”‚ MOSI {mosi}│────────┐ β”‚ β”‚ + β”‚ MISO {miso}│──────┐ β”‚ β”‚ β”‚ + β”‚ SCK {sck}│────┐ β”‚ β”‚ β”‚ β”‚ + β”‚ CS {cs}│──┐ β”‚ β”‚ β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + {device} β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + β”‚ VCC β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”Όβ”€β”Όβ”€β”Όβ”€β”Όβ”€β”˜ + β”‚ GND β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”Όβ”€β”Όβ”€β”Όβ”€β”˜ + β”‚ DIN/MOSI β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”Όβ”€β”Όβ”€β”˜ + β”‚ DOUT/MISO β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”Όβ”€β”˜ + β”‚ CLK/SCK β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”˜ + β”‚ CS/SS β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +```''', + + "simple_led": '''``` + {board} LED + Resistor + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Pin {pin} │─────────┬─────[{resistor}Ξ©]β”€β”€β”€β”€β–Ίβ”œβ”€β”€ + β”‚ GND β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +```''', + + "servo": '''``` + {board} Servo + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ VIN 5V │─────────│ Red β”‚ + β”‚ GND │─────────│ Brown β”‚ + β”‚ Pin {pin} │─────────│ Orange β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +```''', + + "generic": '''``` +Connections: +{connections} +```''' +} + +BOARD_PINOUTS = { + "Arduino Uno": {"sda": "A4", "scl": "A5", "mosi": "11", "miso": "12", "sck": "13"}, + "Arduino Nano": {"sda": "A4", "scl": "A5", "mosi": "11", "miso": "12", "sck": "13"}, + "Arduino Mega": {"sda": "20", "scl": "21", "mosi": "51", "miso": "50", "sck": "52"}, + "ESP32": {"sda": "21", "scl": "22", "mosi": "23", "miso": "19", "sck": "18"}, + "ESP8266": {"sda": "D2", "scl": "D1", "mosi": "D7", "miso": "D6", "sck": "D5"}, + "Raspberry Pi Pico": {"sda": "GP4", "scl": "GP5", "mosi": "GP19", "miso": "GP16", "sck": "GP18"}, +} + + +@dataclass +class ProjectInfo: + """Project information""" + name: str = "My Arduino Project" + description: str = "An Arduino-based project." + board: str = "Arduino Uno" + features: List[str] = field(default_factory=list) + hardware: List[Dict] = field(default_factory=list) + libraries: List[str] = field(default_factory=list) + wiring_type: str = "generic" + connections: List[Dict] = field(default_factory=list) + license: str = "MIT" + author: str = "" + version: str = "1.0.0" + + +def scan_ino_file(filepath: str) -> Dict: + """Scan .ino file for libraries and pin definitions""" + info = { + "libraries": [], + "pins": [], + "functions": [] + } + + try: + with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + except: + return info + + # Find includes + includes = re.findall(r'#include\s*[<"]([^>"]+)[>"]', content) + info["libraries"] = [inc.replace('.h', '') for inc in includes] + + # Find pin definitions + pin_defs = re.findall(r'(?:const\s+int|#define)\s+(\w*(?:PIN|LED|BTN|BUTTON|SERVO|MOTOR)\w*)\s*[=\s]+(\d+)', + content, re.IGNORECASE) + info["pins"] = [(name, num) for name, num in pin_defs] + + # Check for I2C usage + if 'Wire' in content or 'Wire.h' in str(includes): + info["uses_i2c"] = True + + # Check for SPI usage + if 'SPI' in content or 'SPI.h' in str(includes): + info["uses_spi"] = True + + return info + + +def generate_wiring_diagram(project: ProjectInfo) -> str: + """Generate ASCII wiring diagram""" + pinout = BOARD_PINOUTS.get(project.board, BOARD_PINOUTS["Arduino Uno"]) + + if project.wiring_type == "i2c" and project.hardware: + device = project.hardware[0].get("name", "I2C Device") + return ASCII_WIRING_TEMPLATES["i2c"].format( + board=project.board, + device=device, + sda=pinout["sda"], + scl=pinout["scl"] + ) + + elif project.wiring_type == "spi" and project.hardware: + device = project.hardware[0].get("name", "SPI Device") + return ASCII_WIRING_TEMPLATES["spi"].format( + board=project.board, + device=device, + mosi=pinout["mosi"], + miso=pinout["miso"], + sck=pinout["sck"], + cs="10" + ) + + elif project.connections: + # Generic connection list + lines = [] + for conn in project.connections: + lines.append(f" {project.board} {conn.get('from', '?')} ──── {conn.get('to', '?')} {conn.get('device', '')}") + return ASCII_WIRING_TEMPLATES["generic"].format( + connections="\n".join(lines) + ) + + return "_Add wiring diagram here_" + + +def generate_hardware_list(project: ProjectInfo) -> str: + """Generate hardware requirements list""" + lines = [f"- 1x {project.board}"] + + for hw in project.hardware: + qty = hw.get("qty", 1) + name = hw.get("name", "Component") + note = hw.get("note", "") + line = f"- {qty}x {name}" + if note: + line += f" ({note})" + lines.append(line) + + # Add common items + lines.extend([ + "- USB cable for programming", + "- Breadboard and jumper wires" + ]) + + return "\n".join(lines) + + +def generate_dependencies(project: ProjectInfo) -> str: + """Generate dependencies section""" + if not project.libraries: + return "No external libraries required." + + lines = ["Install these libraries via Arduino Library Manager:"] + lines.append("") + for lib in project.libraries: + lines.append(f"- `{lib}`") + + lines.extend([ + "", + "**To install:**", + "1. Open Arduino IDE", + "2. Go to Sketch β†’ Include Library β†’ Manage Libraries", + "3. Search for each library and click Install" + ]) + + return "\n".join(lines) + + +def generate_installation(project: ProjectInfo) -> str: + """Generate installation instructions""" + return f'''1. **Clone or download** this repository + ```bash + git clone https://github.com/yourusername/{project.name.lower().replace(" ", "-")}.git + ``` + +2. **Open the project** in Arduino IDE + - File β†’ Open β†’ Select the `.ino` file + +3. **Install required libraries** (see Dependencies section) + +4. **Select your board** + - Tools β†’ Board β†’ {project.board} + +5. **Select the port** + - Tools β†’ Port β†’ (select your Arduino's port) + +6. **Upload the sketch** + - Click the Upload button (β†’) or press Ctrl+U''' + + +def generate_usage(project: ProjectInfo) -> str: + """Generate usage instructions""" + return f'''1. Connect the hardware according to the wiring diagram +2. Upload the code to your {project.board} +3. Open Serial Monitor at 115200 baud +4. The system will start automatically + +### Serial Commands + +| Command | Description | +|---------|-------------| +| `status` | Show current status | +| `help` | List available commands | + +_Customize this section based on your project's functionality._''' + + +def generate_readme(project: ProjectInfo) -> str: + """Generate complete README""" + + # Generate badges + badges = f"![Version](https://img.shields.io/badge/version-{project.version}-blue) " + badges += f"![Board](https://img.shields.io/badge/board-{project.board.replace(' ', '%20')}-green) " + badges += f"![License](https://img.shields.io/badge/license-{project.license}-orange)" + + # Generate features list + features = "" + if project.features: + features = "\n".join(f"- βœ… {f}" for f in project.features) + else: + features = "- βœ… Feature 1\n- βœ… Feature 2\n- βœ… Feature 3" + + # Generate sections + readme = README_TEMPLATE.format( + project_name=project.name, + badges=badges, + description=project.description, + features=features, + hardware_list=generate_hardware_list(project), + wiring_diagram=generate_wiring_diagram(project), + dependencies=generate_dependencies(project), + installation=generate_installation(project), + usage=generate_usage(project), + configuration="_Add configuration options here_", + troubleshooting='''| Problem | Solution | +|---------|----------| +| Won't compile | Check library installations | +| No serial output | Verify baud rate is 115200 | +| Device not detected | Check wiring connections |''', + license=f"This project is licensed under the {project.license} License.", + footer=f"Made with ❀️ for the maker community β€’ {datetime.now().year}" + ) + + return readme + + +def interactive_mode(): + """Interactive README generator""" + print("=" * 60) + print("README Generator - Interactive Mode") + print("=" * 60) + print() + + project = ProjectInfo() + + # Basic info + project.name = input("Project name [My Arduino Project]: ").strip() or "My Arduino Project" + project.description = input("Short description: ").strip() or "An Arduino-based project." + + # Board selection + print("\nAvailable boards:") + boards = list(BOARD_PINOUTS.keys()) + for i, b in enumerate(boards, 1): + print(f" {i}. {b}") + choice = input(f"Select board (1-{len(boards)}) [1]: ").strip() or "1" + try: + project.board = boards[int(choice) - 1] + except: + project.board = "Arduino Uno" + + # Features + print("\nEnter features (one per line, empty to finish):") + while True: + feat = input(" - ").strip() + if not feat: + break + project.features.append(feat) + + # Hardware + print("\nEnter hardware components (empty name to finish):") + while True: + name = input(" Component name: ").strip() + if not name: + break + qty = input(" Quantity [1]: ").strip() or "1" + project.hardware.append({"name": name, "qty": int(qty)}) + + # Libraries + print("\nEnter required libraries (empty to finish):") + while True: + lib = input(" Library: ").strip() + if not lib: + break + project.libraries.append(lib) + + # Wiring type + print("\nWiring diagram type:") + print(" 1. I2C device") + print(" 2. SPI device") + print(" 3. Generic/Custom") + wtype = input("Select (1-3) [3]: ").strip() or "3" + project.wiring_type = {"1": "i2c", "2": "spi", "3": "generic"}.get(wtype, "generic") + + # Generate + readme = generate_readme(project) + + # Output + filename = input("\nOutput filename [README.md]: ").strip() or "README.md" + with open(filename, 'w') as f: + f.write(readme) + + print(f"\nβœ“ Generated: {filename}") + print(f" Project: {project.name}") + print(f" Board: {project.board}") + + +def main(): + parser = argparse.ArgumentParser(description="README Generator for Arduino Projects") + parser.add_argument("--interactive", "-i", action="store_true", help="Interactive mode") + parser.add_argument("--project", "-p", type=str, help="Project name") + parser.add_argument("--board", "-b", type=str, default="Arduino Uno", help="Board type") + parser.add_argument("--description", "-d", type=str, help="Project description") + parser.add_argument("--scan", "-s", type=str, help="Scan directory for .ino files") + parser.add_argument("--output", "-o", type=str, default="README.md", help="Output file") + + args = parser.parse_args() + + if args.interactive: + interactive_mode() + return + + project = ProjectInfo( + name=args.project or "My Arduino Project", + board=args.board, + description=args.description or "An Arduino-based project." + ) + + # Scan directory if provided + if args.scan: + for root, dirs, files in os.walk(args.scan): + for f in files: + if f.endswith('.ino'): + info = scan_ino_file(os.path.join(root, f)) + project.libraries.extend(info.get("libraries", [])) + if info.get("uses_i2c"): + project.wiring_type = "i2c" + elif info.get("uses_spi"): + project.wiring_type = "spi" + project.libraries = list(set(project.libraries)) # Remove duplicates + + readme = generate_readme(project) + + with open(args.output, 'w') as f: + f.write(readme) + + print(f"Generated: {args.output}") + + +if __name__ == "__main__": + main() diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ef953a1 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,36 @@ +# Buzzer Synth Project β€” Copilot Instructions + +## Obsidian Documentation (graphthulhu MCP) + +**After every code change session**, update the Obsidian vault via the graphthulhu MCP server: + +1. **Update [[Buzzer Change Log]]** β€” append new changes with date, files modified, and rationale +2. **Update [[Buzzer Known Issues and Plans]]** β€” mark resolved issues, add new ones +3. **Update [[Buzzer File Map]]** β€” if any files were created, deleted, or significantly changed +4. **Update [[Buzzer Sound Engine Deep Dive]]** β€” if sound.cpp, fm_synth.cpp, or envelope behavior changed +5. **Cross-link new content** β€” use `[[Page Name]]` wiki-links in all pages + +### Obsidian page hierarchy: +- **Buzzer Synth Project** (parent) β€” project overview + documentation index + - Mozzi Library Reference β€” Mozzi API, tables, patterns + - Buzzer File Map β€” every file with purpose and key functions + - Buzzer Sound Engine Deep Dive β€” voice system, mixing, envelopes, bugs + - Buzzer Hardware Reference β€” pins, wiring, resource budget + - Buzzer Protocol Reference β€” serial command table + - Buzzer Change Log β€” modification history + - Buzzer Known Issues and Plans β€” bugs and roadmap + +### When to create NEW pages: +- New major subsystem added (e.g., EQ module β†’ "Buzzer EQ System") +- New hardware component added +- Keep pages focused β€” split if a page exceeds ~500 lines + +## Project Conventions + +- **Mozzi**: version 2.0.3. `` in main.cpp ONLY. All others use ``. +- **Wavetables**: All includes in wavetable.cpp only. Expose via accessor functions. +- **PROGMEM**: Any const array > 8 bytes. Use `pgm_read_byte()` / `pgm_read_word()`. +- **audioUpdate()**: NO float, NO division, NO analogRead, NO Serial. Bit shifts only. +- **RAM budget**: ~176 bytes free. Every new variable must be justified. +- **Protocol**: Binary, 2 Mbaud. New commands need matching Python GUI code. +- **Build**: `pio run -e uno` to compile. Check RAM/Flash usage in output. diff --git a/.gitignore b/.gitignore index b52a892..6220ac8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ __pycache__ AKWF_MOZZI_MODIFIED -midi \ No newline at end of file +midi +MeeBleeps-Freaq-FM-Synth-master diff --git a/README.md b/README.md index 0c2afd6..d1b75a4 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ flowchart TD subgraph Firmware["βš™οΈ Firmware Modules"] INIT["Setup & Initialisation"]:::firmware - CTRL["Control Loop
128 Hz"]:::firmware + CTRL["Control Loop
64 Hz"]:::firmware SND["Sound Engine
8 voices"]:::firmware REC["Recorder
EEPROM storage"]:::firmware PLR["Melody Player
13 presets"]:::firmware diff --git a/include/char_digits.h b/include/char_digits.h new file mode 100644 index 0000000..a1a0895 --- /dev/null +++ b/include/char_digits.h @@ -0,0 +1,11 @@ +#ifndef CHAR_DIGITS_H +#define CHAR_DIGITS_H + +#include + +// 7-segment display digit lookup table (0-9) +static const uint8_t CHAR_DIGITS[] = { + 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f +}; + +#endif // CHAR_DIGITS_H diff --git a/include/config.h b/include/config.h index 181db77..b1249d4 100644 --- a/include/config.h +++ b/include/config.h @@ -15,12 +15,16 @@ // Mozzi audio output: Pin 9 (Timer1 PWM) on Arduino Uno // DO NOT USE pin 9 for other purposes -constexpr uint8_t PIN_POT = A5; // Volume potentiometer +// Buttons (analog pins used as digital inputs) constexpr uint8_t PIN_BTN_REC = A0; constexpr uint8_t PIN_BTN_STOP = A1; constexpr uint8_t PIN_BTN_PLAY = A2; constexpr uint8_t PIN_BTN_CLEAR = A3; +// Potentiometers +// A4: Available for future use +constexpr uint8_t PIN_POT = A5; // Synth parameter pot β€” volume is hardwired, no code needed + // RGB LED (Must be PWM Pins) constexpr uint8_t PIN_LED_R = 3; constexpr uint8_t PIN_LED_G = 5; @@ -35,12 +39,16 @@ constexpr uint8_t PIN_LED_PLAY_STATUS = 8; // Steady ON when playing constexpr uint8_t FEEDBACK_FLASH_DURATION_TICKS = 20; // ~150ms at 128Hz +// 7-Segment Display +constexpr uint8_t PIN_DISPLAY_CLK = 11; +constexpr uint8_t PIN_DISPLAY_DIO = 12; + // ============================================================================ // VISUALIZER CONFIGURATION // ============================================================================ // Visualizer constexpr uint8_t VISUALIZER_MAX_BRIGHTNESS = - 20; // 0-255 (Keep low for USB power safety) + 40; // 0-255 (Keep low for USB power safety) constexpr uint8_t VISUALIZER_PASSIVE_HUE_INC = 1; // Passive rainbow speed // Update interval (ms) @@ -60,8 +68,10 @@ constexpr uint8_t CMD_BUFFER_SIZE = 32; // SOUND CONFIGURATION // ============================================================================ -constexpr uint8_t MAX_VOLUME = 100; -constexpr uint8_t DEFAULT_VOLUME = 7; +// Number of simultaneous FM voices (polyphony). +// Each voice uses ~40 bytes RAM + 2 Oscil<2048> + 2 ADSR (amp + mod). +// ATmega328P budget: 2 voices with dual envelopes = tight but workable. +constexpr uint8_t NUM_VOICES = 2; // ============================================================================ // MOZZI CONFIGURATION @@ -71,12 +81,6 @@ constexpr uint8_t DEFAULT_VOLUME = 7; // Higher = smoother envelopes but more CPU. 64 is Mozzi default. #define MOZZI_CONTROL_RATE 64 -// ============================================================================ -// POLYPHONY CONFIGURATION -// ============================================================================ - -constexpr uint8_t MAX_VOICES = 8; // Officially recommended for this hardware is 4. 8 Works fine if notes per second stays below 20 - // ============================================================================ // RECORDING CONFIGURATION // ============================================================================ @@ -84,34 +88,14 @@ constexpr uint8_t MAX_VOICES = 8; // Officially recommended for this hardware is constexpr uint16_t EEPROM_START_ADDR = 2; // Reserve 0-1 for count constexpr uint16_t EEPROM_SIZE = 1024; // ATmega328P has 1KB EEPROM + + // ============================================================================ -// INSTRUMENTS (Envelope Profiles) +// PITCH / TRANSPOSE // ============================================================================ -constexpr uint8_t NUM_INSTRUMENTS = 5; -constexpr uint8_t INSTRUMENT_CUSTOM = 4; - -// Envelope times in milliseconds -struct EnvelopeProfile { - uint16_t attackMs; - uint16_t decayMs; - uint8_t sustainLevel; // 0-255 (Mozzi ADSR uses byte levels) - uint16_t sustainLenght; // ms - uint16_t releaseMs; - int8_t octaveShift; // Semitone offset (multiples of 12 for whole octaves) -}; - -// Instrument profiles stored in PROGMEM -// Piano: instant attack, moderate decay, medium sustain, short release -// Organ: instant attack, no decay, full sustain, short release -// Staccato: instant attack, fast decay, no sustain, instant release -// Pad: slow attack, slow decay, high sustain, medium release -const EnvelopeProfile INSTRUMENTS[] PROGMEM = { - // attack, decay, sustain, sustainLenght, release, octaveShift - {5, 100, 20, 5000, 50, 0}, // Piano (Snappy but safe) - {10, 50, 250, 10000, 100, 0}, // Organ - {5, 20, 0, 500, 50, 0}, // Staccato - {20, 50, 180, 8000, 100, 0}, // Pad -}; +// Global pitch transpose in semitones (-24 to +24 = -2 to +2 octaves) +// Applied to all notes regardless of instrument +constexpr int8_t DEFAULT_PITCH_TRANSPOSE = 0; // No transpose #endif // CONFIG_H diff --git a/include/digit_tables.h b/include/digit_tables.h new file mode 100644 index 0000000..6abb567 --- /dev/null +++ b/include/digit_tables.h @@ -0,0 +1,20 @@ +#ifndef DIGIT_TABLES_H +#define DIGIT_TABLES_H + +#include +#include + +// Lookup tables for extracting hundreds, tens, ones digits from 0-255 without division/modulo +static const uint8_t DIGIT_HUNDREDS[256] PROGMEM = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 +}; + +static const uint8_t DIGIT_TENS[256] PROGMEM = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5 +}; + +static const uint8_t DIGIT_ONES[256] PROGMEM = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5 +}; + +#endif // DIGIT_TABLES_H diff --git a/include/display.h b/include/display.h new file mode 100644 index 0000000..fba3a17 --- /dev/null +++ b/include/display.h @@ -0,0 +1,24 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#include + +void setupDisplay(); +void updateDisplay(); + +// Select which synth parameter (0-2) the pot is currently editing. +// FM mode: 0=Intensity, 1=Rate, 2=Ratio +void displaySelectParam(uint8_t paramIdx); + +// Returns the currently selected parameter index (0-2). +// Returns 0 when in note-view (no param explicitly selected). +uint8_t displayGetSelectedParam(); + +// Called by Sound::controlUpdate() β€” routes A5 pot value to the selected parameter +// and updates the display. +void displayUpdatePot(int potValue); + +// Show carrier wave name on display (button 3 press), auto-timeout to note view. +void displayShowWave(uint8_t waveId); + +#endif diff --git a/include/freq_utils.h b/include/freq_utils.h deleted file mode 100644 index c5e3b87..0000000 --- a/include/freq_utils.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef FREQ_TABLE_H -#define FREQ_TABLE_H - -#include - -// Simple mtof implementation or table lookup -// Standard mtof: 440 * pow(2, (note-69)/12.0) - -// Function to convert MIDI note to Hz -uint16_t midiToFreq(uint8_t note) { - // A4 = 69 = 440Hz - // f = 440 * 2^((n-69)/12) - return 440.0 * pow(2.0, (float)(note - 69) / 12.0); -} - -// Function to convert Hz to MIDI note -uint8_t freqToMidi(uint16_t freq) { - if (freq == 0) - return 0; - // n = 12 * log2(f/440) + 69 - - float f = freq; - return (uint8_t)(12.0 * log(f / 440.0) / log(2.0) + 69.0 + 0.5); -} - -#endif diff --git a/include/melodies.h b/include/melodies.h index 2ced9da..880e9ef 100644 --- a/include/melodies.h +++ b/include/melodies.h @@ -195,7 +195,7 @@ const Note PROGMEM melody_jingle[] = { const uint16_t melody_jingle_length = sizeof(melody_jingle) / sizeof(Note); // ============================================================================ -// Melody Index - For easy selection +// Melody Index // ============================================================================ enum MelodyID { @@ -205,13 +205,8 @@ enum MelodyID { MELODY_IMPERIAL, MELODY_PACMAN, MELODY_BIRTHDAY, - MELODY_ZELDA_SECRET, - MELODY_GAMEOVER, - MELODY_ALERT, - MELODY_SUCCESS, - MELODY_FAILURE, - MELODY_NOTIFY, MELODY_JINGLE, + MELODY_SIMPLE, MELODY_COUNT // Total number of melodies }; @@ -219,16 +214,17 @@ enum MelodyID { const char melodyName_mario[] PROGMEM = "Mario"; const char melodyName_tetris[] PROGMEM = "Tetris"; const char melodyName_nokia[] PROGMEM = "Nokia"; -const char melodyName_imperial[] PROGMEM = "Imperial March"; +const char melodyName_imperial[] PROGMEM = "Imperial"; const char melodyName_pacman[] PROGMEM = "Pacman"; -const char melodyName_birthday[] PROGMEM = "Happy Birthday"; -const char melodyName_zelda[] PROGMEM = "Zelda Secret"; -const char melodyName_gameover[] PROGMEM = "Game Over"; -const char melodyName_alert[] PROGMEM = "Alert"; -const char melodyName_success[] PROGMEM = "Success"; -const char melodyName_failure[] PROGMEM = "Failure"; -const char melodyName_notify[] PROGMEM = "Notify"; -const char melodyName_jingle[] PROGMEM = "Jingle Bells"; +const char melodyName_birthday[] PROGMEM = "Birthday"; +const char melodyName_jingle[] PROGMEM = "Jingle"; +const char melodyName_simple[] PROGMEM = "Simple"; + +const Note PROGMEM melody_simple[] = { + {NOTE_C5, 200}, {NOTE_E5, 200}, {NOTE_G5, 400}, {NOTE_REST, 200}, + {NOTE_G5, 200}, {NOTE_E5, 200}, {NOTE_C5, 400} +}; +const uint16_t melody_simple_length = sizeof(melody_simple) / sizeof(Note); const char* const melodyNames[] PROGMEM = { melodyName_mario, @@ -237,13 +233,8 @@ const char* const melodyNames[] PROGMEM = { melodyName_imperial, melodyName_pacman, melodyName_birthday, - melodyName_zelda, - melodyName_gameover, - melodyName_alert, - melodyName_success, - melodyName_failure, - melodyName_notify, - melodyName_jingle + melodyName_jingle, + melodyName_simple }; #endif // MELODIES_H diff --git a/include/protocol.h b/include/protocol.h index b76ecb6..dad2427 100644 --- a/include/protocol.h +++ b/include/protocol.h @@ -1,8 +1,35 @@ /** * @file protocol.h - * @brief Serial protocol handler + * @brief Serial protocol handler β€” binary, 2Mbaud * - * Handles command parsing from serial input using interrupt-driven reception. + * COMMAND MAP (MeeBleeps-style FM engine): + * 0x00 STOP_ALL β€” no args + * 0x01 REC_START β€” no args + * 0x02 REC_STOP β€” no args + * 0x03 REC_PLAY β€” no args + * 0x04 REC_CLEAR β€” no args + * 0x05 SET_ADSR β€” 7 bytes: [A_hi, A_lo, D_hi, D_lo, S, R_hi, R_lo] + * 0x06 SET_VOL β€” 1 byte (0-100) + * 0x08 PLAY_MELODY β€” 1 byte (ID) + * 0x0A SET_PITCH β€” 1 byte (signed int8, semitones) + * 0x0E SET_MOD_AMOUNT β€” 2 bytes big-endian (0-1023) + * 0x0F SET_LFO_RATE β€” 2 bytes big-endian (0-1023) + * 0x10 SET_MOD_RATIO β€” 2 bytes big-endian (0-1023) + * 0x11 GET_PARAMS β€” no args β€” triggers SYNC: responses + * 0x18 LOAD_FM_PRESET β€” 1 byte (preset ID 0-6) + * 0x19 SET_CARRIER_WAVE β€” 1 byte (wave ID 0-5) + * 0x1A SET_MOD_WAVE β€” 1 byte (wave ID 0-5) + * 0x1B SET_FM_MODE β€” 1 byte (0-3) + * 0x1C SET_MOD_SHAPE β€” 2 bytes big-endian (0-1023) + * 0x1D SET_LFO_DEPTH β€” 1 byte (0-255) + * 0x1E SET_LFO_WAVEFORM β€” 1 byte (0-5) + * 0x1F SET_MOD_ENV_ATTACK β€” 2 bytes big-endian (ms) + * 0x20 SET_MOD_ENV_DECAY β€” 2 bytes big-endian (ms) + * 0x21 SET_MOD_ENV_SUSTAIN β€” 1 byte (0-255) + * + * MIDI (standard 3-byte): + * 0x90 Note ON [note, velocity] + * 0x80 Note OFF [note, velocity] */ #ifndef PROTOCOL_H @@ -12,43 +39,70 @@ #include // ============================================================================ -// BINARY PROTOCOL MAP +// BINARY COMMAND OPCODES // ============================================================================ -// Range [0x00 - 0x14]: System Commands -constexpr uint8_t CMD_STOP_ALL = 0x00; -constexpr uint8_t CMD_REC_START = 0x01; -constexpr uint8_t CMD_REC_STOP = 0x02; -constexpr uint8_t CMD_REC_PLAY = 0x03; -constexpr uint8_t CMD_REC_CLEAR = 0x04; -constexpr uint8_t CMD_SET_ADSR = 0x05; // Followed by 10 bytes: [A_hi, A_lo, D_hi, D_lo, S, SL_hi, SL_lo, R_hi, R_lo, Oct] -constexpr uint8_t CMD_SET_VOL = 0x06; // Followed by 1 byte -constexpr uint8_t CMD_SET_INST = 0x07; // Followed by 1 byte -constexpr uint8_t CMD_PLAY_MELODY = 0x08; // Followed by 1 byte (ID) -constexpr uint8_t CMD_SET_WAVEFORM = 0x09; // Followed by 1 byte (ID) - -// Notes are mapped directly: -// 0x15 - 0x6C (21-108) = Note OFF (Note = Val) -// 0x95 - 0xEC (149-236) = Note ON (Note = Val - 128) +constexpr uint8_t CMD_STOP_ALL = 0x00; +constexpr uint8_t CMD_REC_START = 0x01; +constexpr uint8_t CMD_REC_STOP = 0x02; +constexpr uint8_t CMD_REC_PLAY = 0x03; +constexpr uint8_t CMD_REC_CLEAR = 0x04; +constexpr uint8_t CMD_SET_ADSR = 0x05; // 7 bytes +constexpr uint8_t CMD_SET_VOL = 0x06; // 1 byte +constexpr uint8_t CMD_PLAY_MELODY = 0x08; // 1 byte +constexpr uint8_t CMD_SET_PITCH = 0x0A; // 1 byte (signed) +constexpr uint8_t CMD_SET_MOD_AMOUNT = 0x0E; // 2 bytes +constexpr uint8_t CMD_SET_LFO_RATE = 0x0F; // 2 bytes +constexpr uint8_t CMD_SET_MOD_RATIO = 0x10; // 2 bytes +constexpr uint8_t CMD_GET_PARAMS = 0x11; // no args +constexpr uint8_t CMD_LOAD_FM_PRESET = 0x18; // 1 byte (0-6) +constexpr uint8_t CMD_SET_CARRIER_WAVE = 0x19; // 1 byte (0-5) +constexpr uint8_t CMD_SET_MOD_WAVE = 0x1A; // 1 byte (0-5) +constexpr uint8_t CMD_SET_FM_MODE = 0x1B; // 1 byte (0-3) +constexpr uint8_t CMD_SET_MOD_SHAPE = 0x1C; // 2 bytes +constexpr uint8_t CMD_SET_LFO_DEPTH = 0x1D; // 1 byte (0-255) +constexpr uint8_t CMD_SET_LFO_WAVEFORM = 0x1E; // 1 byte (0-5) +constexpr uint8_t CMD_SET_MOD_ENV_ATTACK = 0x1F; // 2 bytes +constexpr uint8_t CMD_SET_MOD_ENV_DECAY = 0x20; // 2 bytes +constexpr uint8_t CMD_SET_MOD_ENV_SUSTAIN = 0x21; // 1 byte (0-255) +constexpr uint8_t CMD_SET_VIBRATO_DEPTH = 0x22; // 1 byte (0-255) +constexpr uint8_t CMD_SET_VIBRATO_SPEED = 0x23; // 1 byte (0-255) +constexpr uint8_t CMD_SET_PORTAMENTO = 0x24; // 1 byte (0-255) // ============================================================================ -// COMMAND TYPES (Internal) +// COMMAND TYPES (internal) // ============================================================================ enum class CmdType : uint8_t { - NONE, - NOTE_ON, - NOTE_OFF, - SET_VOLUME, - SET_INSTRUMENT, - SET_WAVEFORM, - SET_ADSR, - REC_START, - REC_STOP, - REC_PLAY, - REC_CLEAR, - STOP_ALL, - MELODY_PLAY + NONE, + NOTE_ON, + NOTE_OFF, + SET_VOLUME, + SET_ADSR, + SET_PITCH, + REC_START, + REC_STOP, + REC_PLAY, + REC_CLEAR, + STOP_ALL, + MELODY_PLAY, + SET_MOD_AMOUNT, + SET_LFO_RATE, + SET_MOD_RATIO, + GET_PARAMS, + LOAD_FM_PRESET, + SET_CARRIER_WAVE, + SET_MOD_WAVE, + SET_FM_MODE, + SET_MOD_SHAPE, + SET_LFO_DEPTH, + SET_LFO_WAVEFORM, + SET_MOD_ENV_ATTACK, + SET_MOD_ENV_DECAY, + SET_MOD_ENV_SUSTAIN, + SET_VIBRATO_DEPTH, + SET_VIBRATO_SPEED, + SET_PORTAMENTO, }; // ============================================================================ @@ -56,17 +110,15 @@ enum class CmdType : uint8_t { // ============================================================================ struct Command { - CmdType type; - uint16_t value1; - uint16_t value2; - uint8_t sustain; - uint16_t sustainlenght; - uint16_t release; - int8_t octave; + CmdType type; + uint16_t value1; + uint16_t value2; + uint8_t velocity; + uint8_t sustain; + uint16_t release; - Command() - : type(CmdType::NONE), value1(0), value2(0), sustain(0), sustainlenght(0), release(0), - octave(0) {} + Command() : type(CmdType::NONE), value1(0), value2(0), + velocity(0), sustain(0), release(0) {} }; // ============================================================================ @@ -74,11 +126,11 @@ struct Command { // ============================================================================ namespace Protocol { -void init(); -bool hasCommand(); -Command getCommand(); -void respond(const __FlashStringHelper* key, const char* value = nullptr); -void respond(const __FlashStringHelper* key, uint16_t value); + void init(); + bool hasCommand(); + Command getCommand(); + void respond(const __FlashStringHelper* key, const char* value = nullptr); + void respond(const __FlashStringHelper* key, uint16_t value); } // namespace Protocol #endif // PROTOCOL_H diff --git a/include/recorder.h b/include/recorder.h index 3bca5c6..23a1dc3 100644 --- a/include/recorder.h +++ b/include/recorder.h @@ -20,7 +20,8 @@ // Packed structure for EEPROM (6 bytes) struct RecordedNote { uint16_t deltaMs; // Time since last note (max 65535ms) - uint16_t frequency; // Hz + uint8_t note; // MIDI Note (0-127) + uint8_t velocity; // MIDI Velocity (0-127) uint16_t duration; // ms (0 = sustained until next) }; // Total: 6 Bytes @@ -66,7 +67,7 @@ bool isRecording(); /** * @brief Record a note start (captured at KeyDown) */ -void startNote(uint16_t freq); +void startNote(uint8_t note, uint8_t velocity); /** * @brief Record a note stop (captured at KeyUp) diff --git a/include/sound.h b/include/sound.h index baa682a..1ff9f2d 100644 --- a/include/sound.h +++ b/include/sound.h @@ -1,153 +1,116 @@ /** * @file sound.h - * @brief Sound module with Mozzi wavetable synthesis and ADSR envelopes - * - * Uses Mozzi library for audio generation with: - * - Wavetable oscillators (sine wave for piano-like tone) - * - ADSR envelope shaping per voice - * - Up to MAX_VOICES polyphonic voices - * - * Mozzi takes over Timer0 and Timer1. Audio output on pin 9. - * TODO! this might not be correct atm, research mozzi docs since we're using 1pin mode not hifi mode + * @brief Sound module β€” MeeBleeps-style 2-op FM synthesis */ #ifndef SOUND_H #define SOUND_H #include "config.h" +#include "sound/fm_synth.h" #include +namespace Sound { + // ============================================================================ -// SOUND MODULE API +// INITIALIZATION // ============================================================================ -namespace Sound { - -/** - * @brief Initialize Mozzi audio engine - * Must be called in setup() AFTER Serial.begin() - */ void init(); -/** - * @brief Set master volume (0-10) - */ -void setVolume(uint8_t vol); +// ============================================================================ +// VOLUME +// ============================================================================ -/** - * @brief Get current master volume - */ +void setVolume(uint8_t vol); // 0-100 uint8_t getVolume(); -/** - * @brief Set waveform type (0-4) - * 0=Triangle, 1=Sawtooth, 2=Sine, 3=Square, 4=Piano - */ -void setWaveform(uint8_t waveId); - -/** - * @brief Set instrument (envelope profile 0-3) - * 0=Piano, 1=Organ, 2=Staccato, 3=Pad - */ -void setInstrument(uint8_t inst); - -/** - * @brief Get current instrument - */ -uint8_t getInstrument(); +// ============================================================================ +// PITCH TRANSPOSE +// ============================================================================ -/** - * @brief Get instrument name - */ -const char *getInstrumentName(); +void setPitchTranspose(int8_t semitones); +int8_t getPitchTranspose(); -/** - * @brief Set custom envelope parameters - * @param a Attack (ms) - * @param d Decay (ms) - * @param s Sustain (0-255) - * @param sl Sustain length (ms) - how long to hold the sustain level before auto-releasing (0 for infinite sustain until noteOff) - * @param r Release (ms) - * @param o Octave shift (semitones, use multiples of 12 for whole octaves: -36,-24,-12,0,12,24,36) - */ -void setCustomEnvelope(uint16_t a, uint16_t d, uint8_t s, uint16_t sl, uint16_t r, int8_t o); +// ============================================================================ +// NOTE CONTROL +// ============================================================================ -/** - * @brief Parse note string to frequency (e.g., "C4", "A#5", "440") - * @param noteStr Note string - * @return Frequency in Hz, 0 if invalid - */ -uint16_t parseNote(const char *noteStr); +void noteOn(uint8_t midiNote, uint8_t velocity = 127); +void noteOnFreq(uint16_t freqHz, uint8_t velocity = 127); +void noteOff(uint16_t freq); +void noteOffMidi(uint8_t midiNote); +void noteOff(); +void playTimed(uint8_t midiNote, uint16_t durationMs, uint8_t velocity = 127); +void stop(); +bool isPlaying(); -/** - * @brief Apply current instrument offset to raw frequency - * @param freq Raw frequency in Hz - * @return Adjusted frequency based on instrument - */ -uint16_t applyInstrument(uint16_t freq); +uint16_t getLatestNoteFreq(); +uint8_t getLatestMidiNote(); +uint16_t mtof(uint8_t note); +uint8_t getEnvelopeLevel(); -/** - * @brief Play note with sustain (until noteOff called) - * Allocates a voice and starts ADSR envelope - * @param freq Frequency in Hz - */ -void noteOn(uint16_t freq); - -/** - * @brief Stop specific note (frequency) with smooth ADSR release - * @param freq Frequency in Hz - */ -void noteOff(uint16_t freq); +// ============================================================================ +// FM PARAMETER SETTERS (forwarded to FM:: namespace) +// ============================================================================ -/** - * @brief Stop all notes with smooth ADSR release - */ -void noteOff(); +// Modulation +void setModAmount(uint16_t amount); +void setModRatio(uint16_t ratio); +void setFMMode(uint8_t mode); +void setModEnvAttack(uint16_t ms); +void setModEnvDecay(uint16_t ms); +void setModEnvSustain(uint8_t level); +void setModShape(uint16_t val); + +// LFO +void setLFODepth(uint8_t depth); +void setLFORate(uint16_t rate); +void setLFOWaveform(uint8_t id); + +// Amp ADSR +void setAmpADSR(uint16_t a, uint16_t d, uint8_t s, uint16_t r); + +// Performance (vibrato / portamento) +void setVibratoDepth(uint8_t depth); +void setVibratoSpeed(uint8_t speed); +void setPortamento(uint8_t speed); + +// Presets & wavetables +void loadFMPreset(uint8_t id); +void setCarrierWave(uint8_t id); +void setModulatorWave(uint8_t id); +uint8_t nextCarrierWave(); +uint8_t nextFMMode(); + +const FM::FMParams& getFMParams(); -/** - * @brief Play note for specific duration then auto-release - * @param freq Frequency in Hz - * @param durationMs Duration in milliseconds - */ -void playTimed(uint16_t freq, uint16_t durationMs); +// ============================================================================ +// MOZZI CALLBACKS +// ============================================================================ -/** - * @brief Immediate silence (all voices off) - */ -void stop(); +void controlUpdate(); +int audioUpdate(); -/** - * @brief Check if any voice is currently playing - */ -bool isPlaying(); +// ============================================================================ +// TIMING UTILITIES +// ============================================================================ -/** - * @brief Get frequency of the most recently triggered note - */ -uint16_t getPrimaryFreq(); +inline uint32_t ticksToMs(uint16_t ticks) { + return (uint32_t(ticks) * 125UL) >> 3; +} -/** - * @brief Get the current envelope level of the primary voice (0-255) - */ -uint8_t getEnvelopeLevel(); +inline uint16_t msToTicks(uint16_t ms) { + uint32_t ticks = (uint32_t(ms) * 4195UL) >> 16; + return ticks > 0 ? (uint16_t)ticks : 1; +} -/** - * @brief Read volume from potentiometer and update if changed - * Uses Mozzi-compatible analog read - * @return true if volume changed - */ -bool updateVolumeFromPot(); +// ============================================================================ +// TIMING STATE +// ============================================================================ -/** - * @brief Called from Mozzi's updateControl() β€” manages envelopes and timing - */ -void controlUpdate(); +extern volatile uint16_t controlTicks; -/** - * @brief Called from Mozzi's updateAudio() β€” generates audio samples - * @return Audio sample for Mozzi output - */ -int audioUpdate(); } // namespace Sound #endif // SOUND_H diff --git a/include/sound/fm_synth.h b/include/sound/fm_synth.h new file mode 100644 index 0000000..90e8a0c --- /dev/null +++ b/include/sound/fm_synth.h @@ -0,0 +1,158 @@ +/** + * @file fm_synth.h + * @brief Polyphonic 2-op FM synthesis β€” MeeBleeps-style engine with mod envelope, + * 4 FM ratio modes, LFO depth/waveform, and 1-knob mod shape macro. + */ + +#ifndef SOUND_FM_SYNTH_H +#define SOUND_FM_SYNTH_H + +#include +#include + +namespace FM { + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +constexpr uint16_t MOD_AMOUNT_MAX = 1023; +constexpr uint16_t MOD_RATIO_MAX = 1023; +constexpr uint16_t LFO_RATE_MAX = 1023; +constexpr uint8_t FM_NUM_PRESETS = 12; + +// FM ratio modes (from MeeBleeps) +constexpr uint8_t FM_MODE_EXPONENTIAL = 0; // quantised harmonic multiples +constexpr uint8_t FM_MODE_LINEAR_HIGH = 1; // carrier * ratio/100 +constexpr uint8_t FM_MODE_LINEAR_LOW = 2; // carrier * ratio/10000 +constexpr uint8_t FM_MODE_FREE = 3; // absolute frequency (0-2000 Hz) +constexpr uint8_t FM_MODE_COUNT = 4; + +// Wavetable IDs (carrier, modulator, LFO) β€” matches MeeBleeps order +constexpr uint8_t WAVE_SIN = 0; +constexpr uint8_t WAVE_SAW = 1; +constexpr uint8_t WAVE_REVSAW = 2; +constexpr uint8_t WAVE_SQUARE = 3; +constexpr uint8_t WAVE_PSEUDORANDOM = 4; +constexpr uint8_t WAVE_NULL = 5; +constexpr uint8_t WAVE_COUNT = 6; + +// Mod shape macro range +constexpr uint16_t MAX_MOD_SHAPE = 1023; + +// Minimum modulation envelope time (prevents inaudible envelopes) +constexpr uint16_t MIN_MOD_ENV_TIME = 30; + +// ============================================================================ +// PRESET STRUCT (lives in PROGMEM) +// ============================================================================ + +struct FMPreset { + char name[12]; + uint16_t modAmount; // 0-1023 + uint16_t modRatio; // 0-1023 (interpreted per fmMode) + uint8_t fmMode; // FM_MODE_EXPONENTIAL..FM_MODE_FREE + uint16_t modEnvAttack; // ms + uint16_t modEnvDecay; // ms + uint8_t modEnvSustain; // 0-255 + uint8_t lfoDepth; // 0-255 + uint16_t lfoRate; // 0-1023 mapped to Hz + uint8_t lfoWaveform; // WAVE_SIN..WAVE_NULL + uint8_t carrierWave; // WAVE_SIN..WAVE_NULL + uint8_t modWave; // WAVE_SIN..WAVE_NULL + uint16_t ampAttack; // ms + uint16_t ampDecay; // ms + uint8_t ampSustain; // 0-255 + uint16_t ampRelease; // ms + uint8_t vibratoDepth; // 0-255 (pitch mod depth) + uint8_t vibratoSpeed; // 0-255 (vibrato LFO rate) + uint8_t portamento; // 0-255 (glide speed, 0=instant) +}; + +extern const FMPreset FM_PRESETS[FM_NUM_PRESETS] PROGMEM; + +// ============================================================================ +// LIVE PARAMETER STATE (RAM, one copy β€” shared across all voices) +// ============================================================================ + +struct FMParams { + uint16_t modAmount; // FM modulation depth (0-1023) + uint16_t modRatio; // FM ratio (0-1023, mode-dependent) + uint8_t fmMode; // FM_MODE_EXPONENTIAL..FM_MODE_FREE + uint16_t modEnvAttack; // mod envelope attack (ms) + uint16_t modEnvDecay; // mod envelope decay (ms) + uint8_t modEnvSustain; // mod envelope sustain (0-255) + uint16_t modShape; // 1-knob macro (0-1023) + uint8_t lfoDepth; // LFO β†’ mod amount (0-255) + uint16_t lfoRate; // LFO speed (0-1023) + uint8_t lfoWaveform; // WAVE_SIN..WAVE_NULL + uint8_t carrierWave; // WAVE_SIN..WAVE_NULL + uint8_t modWave; // WAVE_SIN..WAVE_NULL + uint16_t ampAttack; // amp envelope attack (ms) + uint16_t ampDecay; // amp envelope decay (ms) + uint8_t ampSustain; // amp envelope sustain (0-255) + uint16_t ampRelease; // amp envelope release (ms) + uint8_t vibratoDepth; // pitch vibrato depth (0-255) + uint8_t vibratoSpeed; // pitch vibrato speed (0-255) + uint8_t portamento; // glide speed (0-255, 0=instant) + uint8_t presetId; // 255 = custom +}; + +// ============================================================================ +// API +// ============================================================================ + +void init(uint32_t noiseSeed = 0); + +// Note control β€” voice-allocated +void noteOn(uint8_t midiNote, uint8_t velocity); +void noteOnFreq(uint16_t freqHz, uint8_t velocity); +void noteOff(uint8_t midiNote); +void allNotesOff(); +void stop(); +bool isPlaying(); +uint8_t getActiveVoiceCount(); + +// Modulation parameters +void setModAmount(uint16_t val); // 0-1023 +void setModRatio(uint16_t val); // 0-1023 +void setFMMode(uint8_t mode); // FM_MODE_EXPONENTIAL..FM_MODE_FREE +void setModEnvAttack(uint16_t ms); +void setModEnvDecay(uint16_t ms); +void setModEnvSustain(uint8_t level); +void setModShape(uint16_t val); // 1-knob macro (0-1023) + +// LFO +void setLFODepth(uint8_t depth); // 0-255 +void setLFORate(uint16_t rate); // 0-1023 +void setLFOWaveform(uint8_t id); // WAVE_SIN..WAVE_NULL + +// Amp envelope +void setAmpADSR(uint16_t attack, uint16_t decay, uint8_t sustain, uint16_t release); + +// Performance (vibrato / portamento) +void setVibratoDepth(uint8_t depth); // 0-255 +void setVibratoSpeed(uint8_t speed); // 0-255 +void setPortamento(uint8_t speed); // 0-255 (0=instant) + +// Wavetable selection (WAVE_SIN..WAVE_NULL β€” applies to all voices) +void setCarrierWave(uint8_t id); +void setModulatorWave(uint8_t id); +uint8_t nextCarrierWave(); +uint8_t nextFMMode(); + +// Preset loader +void loadPreset(uint8_t id); + +// Getters +const FMParams& getParams(); +uint16_t getCarrierFreq(); +uint16_t midiToFreqHz(uint8_t note); + +// Mozzi callbacks +void updateControl(); +int updateAudio(); + +} // namespace FM + +#endif // SOUND_FM_SYNTH_H diff --git a/include/sound/tables/nullwaveform2048_int8.h b/include/sound/tables/nullwaveform2048_int8.h new file mode 100644 index 0000000..7ab09ff --- /dev/null +++ b/include/sound/tables/nullwaveform2048_int8.h @@ -0,0 +1,153 @@ +/* + NULLWAVEFORM2018_int8.h - defines a table representing a zero waveform + + used to silence the oscillator + + can be replaced with any values you like, but must be 2048 cells to match the other tables +*/ +#ifndef NULLWAVEFORM2048_H_ +#define NULLWAVEFORM2048_H_ + +#include + +#define NULLWAVEFORM2048_NUM_CELLS 2048 +#define NULLWAVEFORM2048_SAMPLERATE 2048 + +/** @ingroup tables +reverse saw table +*/ + +CONSTTABLE_STORAGE(int8_t) NULLWAVEFORM2048_DATA [2048] = + { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + }; + + +#endif /* NULLWAVEFORM2048_H_ */ diff --git a/include/sound/tables/pseudorandom2048_int8.h b/include/sound/tables/pseudorandom2048_int8.h new file mode 100644 index 0000000..4cc1bc4 --- /dev/null +++ b/include/sound/tables/pseudorandom2048_int8.h @@ -0,0 +1,291 @@ +/* + pseudorandom2048_int8.h - defines a table to semi-random values with a slight smoothing between them + + used for the LFO/carrier/modulator + + can be replaced with any values you like, but must be 2048 cells to match the other tables +*/ + +#ifndef PSEUDORANDOM2048_H_ +#define PSEUDORANDOM2048_H_ + +#include + +#define PSEUDORANDOM2048_NUM_CELLS 2048 +#define PSEUDORANDOM2048_SAMPLERATE 2048 + +/** @ingroup tables +random table. smoothed +*/ +CONSTTABLE_STORAGE(int8_t) PSEUDORANDOM2048_DATA [2048] = + { + 41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + ,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + ,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + ,35,29,24,18,12,6,0,-6,-11,-17,-23,-29,-35,-40,-46,-52 + ,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52 + ,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52 + ,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52 + ,-52,-51,-51,-51,-50,-50,-50,-50,-49,-49,-49,-48,-48,-48,-47,-47 + ,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47 + ,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47 + ,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47 + ,-47,-48,-48,-49,-49,-50,-50,-51,-51,-51,-52,-52,-53,-53,-54,-54 + ,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54 + ,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54 + ,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54 + ,-55,-56,-56,-57,-58,-59,-59,-60,-61,-62,-62,-63,-64,-65,-65,-66 + ,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66 + ,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66 + ,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66 + ,-61,-57,-52,-47,-42,-38,-33,-28,-23,-19,-14,-9,-4,1,5,10 + ,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10 + ,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10 + ,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10 + ,10,9,9,9,8,8,8,8,7,7,7,6,6,6,5,5 + ,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 + ,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 + ,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 + ,11,17,23,29,35,41,47,54,60,66,72,78,84,90,96,102 + ,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102 + ,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102 + ,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102 + ,95,89,82,76,69,63,56,50,43,36,30,23,17,10,4,-3 + ,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 + ,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 + ,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 + ,-2,-1,0,1,1,2,3,4,5,6,7,8,8,9,10,11 + ,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11 + ,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11 + ,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11 + ,14,17,19,22,25,28,31,34,36,39,42,45,48,50,53,56 + ,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56 + ,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56 + ,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56 + ,60,64,67,71,75,79,83,87,90,94,98,102,106,109,113,117 + ,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117 + ,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117 + ,112,107,102,97,92,87,82,77,71,66,61,56,51,46,41,36 + ,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36 + ,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36 + ,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36 + ,28,20,12,4,-4,-12,-20,-28,-35,-43,-51,-59,-67,-75,-83,-91 + ,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91 + ,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91 + ,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91 + ,-78,-65,-51,-38,-25,-12,1,15,28,41,54,67,80,94,107,120 + ,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120 + ,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120 + ,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120 + ,117,114,110,107,104,101,98,95,91,88,85,82,79,75,72,69 + ,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69 + ,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69 + ,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69 + ,61,53,46,38,30,22,14,7,-1,-9,-17,-25,-33,-40,-48,-56 + ,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56 + ,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56 + ,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56 + ,-45,-33,-22,-11,1,12,24,35,46,58,69,81,92,103,115,126 + ,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126 + ,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126 + ,123,120,116,113,110,107,104,101,97,94,91,88,85,81,78,75 + ,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75 + ,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75 + ,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75 + ,66,57,48,39,30,21,12,3,-7,-16,-25,-34,-43,-52,-61,-70 + ,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70 + ,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70 + ,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70 + ,-61,-51,-42,-32,-23,-13,-4,6,15,24,34,43,53,62,72,81 + ,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81 + ,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81 + ,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81 + ,68,56,43,31,18,5,-7,-20,-33,-45,-58,-71,-83,-96,-108,-121 + ,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121 + ,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121 + ,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121 + ,-115,-108,-102,-96,-89,-83,-76,-70,-64,-57,-51,-45,-38,-32,-25,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-15,-12,-8,-4,-1,3,7,11,14,18,22,25,29,33,36,40 + ,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40 + ,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40 + ,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40 + ,38,35,33,30,28,25,23,20,18,15,13,10,8,5,3,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,7,15,22,30,37,44,52,59,66,74,81,89,96,103,111,118 + ,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118 + ,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118 + ,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118 + ,114,110,107,103,99,95,91,88,84,80,76,72,68,65,61,57 + ,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57 + ,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57 + ,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57 + ,60,63,66,69,72,75,78,81,84,87,90,93,96,99,102,105 + ,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105 + ,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105 + ,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105 + ,101,97,94,90,86,82,78,75,71,67,63,59,55,52,48,44 + ,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44 + ,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44 + ,40,36,32,28,24,20,16,12,7,3,-1,-5,-9,-13,-17,-21 + ,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21 + ,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21 + ,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21 + ,-18,-16,-13,-11,-8,-5,-3,0,3,5,8,11,13,16,18,21 + ,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21 + ,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21 + ,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21 + ,15,9,4,-2,-8,-14,-20,-26,-31,-37,-43,-49,-55,-60,-66,-72 + ,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72 + ,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72 + ,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72 + ,-72,-65,-58,-51,-44,-37,-30,-23,-16,-9,-2,5,12,19,26,33 + + }; +/* + +// random table. not smoothed + +CONSTTABLE_STORAGE(int8_t) PSEUDORANDOM2048_DATA [2048] = + { + 41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + ,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + ,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + ,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41 + ,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52 + ,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52 + ,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52 + ,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52,-52 + ,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47 + ,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47 + ,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47 + ,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47,-47 + ,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54 + ,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54 + ,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54 + ,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54,-54 + ,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66 + ,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66 + ,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66 + ,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66,-66 + ,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10 + ,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10 + ,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10 + ,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 + ,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 + ,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 + ,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 + ,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102 + ,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102 + ,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102 + ,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102,102 + ,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 + ,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 + ,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 + ,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3,-3 + ,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11 + ,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11 + ,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11 + ,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11,11 + ,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56 + ,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56 + ,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56 + ,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56,56 + ,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117 + ,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117 + ,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117,117 + ,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36 + ,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36 + ,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36 + ,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36 + ,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91 + ,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91 + ,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91 + ,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91,-91 + ,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120 + ,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120 + ,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120 + ,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120,120 + ,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69 + ,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69 + ,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69 + ,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69 + ,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56 + ,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56 + ,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56 + ,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56,-56 + ,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126 + ,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126 + ,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126,126 + ,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75 + ,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75 + ,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75 + ,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75,75 + ,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70 + ,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70 + ,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70 + ,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70,-70 + ,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81 + ,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81 + ,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81 + ,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81,81 + ,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121 + ,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121 + ,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121 + ,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121,-121 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19,-19 + ,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40 + ,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40 + ,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40 + ,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 + ,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118 + ,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118 + ,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118 + ,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118,118 + ,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57 + ,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57 + ,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57 + ,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57,57 + ,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105 + ,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105 + ,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105 + ,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105 + ,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44 + ,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44 + ,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44,44 + ,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21 + ,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21 + ,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21 + ,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21,-21 + ,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21 + ,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21 + ,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21 + ,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21 + ,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72 + ,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72 + ,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72 + ,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72,-72 + ,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19,19 + + + }; +*/ + +#endif /* PSEUDORANDOM2048_H_ */ diff --git a/include/sound/tables/revsaw2048_int8.h b/include/sound/tables/revsaw2048_int8.h new file mode 100644 index 0000000..c3e4165 --- /dev/null +++ b/include/sound/tables/revsaw2048_int8.h @@ -0,0 +1,153 @@ +/* + revsaw2018_int8.h - defines a table representing a reverse-saw waveform + + used for the LFO/carrier/modulator + + can be replaced with any values you like, but must be 2048 cells to match the other tables +*/ +#ifndef REVSAW2048_H_ +#define REVSAW2048_H_ + +#include + +#define REVSAW2048_NUM_CELLS 2048 +#define REVSAW2048_SAMPLERATE 2048 + +/** @ingroup tables +reverse saw table +*/ + +CONSTTABLE_STORAGE(int8_t) REVSAW2048_DATA [2048] = + { + 127,127,127,127,127,126,126,126,126,126,126,126,126,125,125,125 + ,125,125,125,125,125,124,124,124,124,124,124,124,124,123,123,123 + ,123,123,123,123,123,122,122,122,122,122,122,122,122,121,121,121 + ,121,121,121,121,121,120,120,120,120,120,120,120,120,119,119,119 + ,119,119,119,119,119,118,118,118,118,118,118,118,118,117,117,117 + ,117,117,117,117,117,116,116,116,116,116,116,116,116,115,115,115 + ,115,115,115,115,115,114,114,114,114,114,114,114,114,113,113,113 + ,113,113,113,113,113,112,112,112,112,112,112,112,112,111,111,111 + ,111,111,111,111,111,110,110,110,110,110,110,110,110,109,109,109 + ,109,109,109,109,109,108,108,108,108,108,108,108,108,107,107,107 + ,107,107,107,107,107,106,106,106,106,106,106,106,106,105,105,105 + ,105,105,105,105,105,104,104,104,104,104,104,104,104,103,103,103 + ,103,103,103,103,103,102,102,102,102,102,102,102,102,101,101,101 + ,101,101,101,101,101,100,100,100,100,100,100,100,100,99,99,99 + ,99,99,99,99,99,98,98,98,98,98,98,98,98,97,97,97 + ,97,97,97,97,97,96,96,96,96,96,96,96,96,95,95,95 + ,95,95,95,95,95,95,94,94,94,94,94,94,94,94,93,93 + ,93,93,93,93,93,93,92,92,92,92,92,92,92,92,91,91 + ,91,91,91,91,91,91,90,90,90,90,90,90,90,90,89,89 + ,89,89,89,89,89,89,88,88,88,88,88,88,88,88,87,87 + ,87,87,87,87,87,87,86,86,86,86,86,86,86,86,85,85 + ,85,85,85,85,85,85,84,84,84,84,84,84,84,84,83,83 + ,83,83,83,83,83,83,82,82,82,82,82,82,82,82,81,81 + ,81,81,81,81,81,81,80,80,80,80,80,80,80,80,79,79 + ,79,79,79,79,79,79,78,78,78,78,78,78,78,78,77,77 + ,77,77,77,77,77,77,76,76,76,76,76,76,76,76,75,75 + ,75,75,75,75,75,75,74,74,74,74,74,74,74,74,73,73 + ,73,73,73,73,73,73,72,72,72,72,72,72,72,72,71,71 + ,71,71,71,71,71,71,70,70,70,70,70,70,70,70,69,69 + ,69,69,69,69,69,69,68,68,68,68,68,68,68,68,67,67 + ,67,67,67,67,67,67,66,66,66,66,66,66,66,66,65,65 + ,65,65,65,65,65,65,64,64,64,64,64,64,64,64,63,63 + ,63,63,63,63,63,63,63,62,62,62,62,62,62,62,62,61 + ,61,61,61,61,61,61,61,60,60,60,60,60,60,60,60,59 + ,59,59,59,59,59,59,59,58,58,58,58,58,58,58,58,57 + ,57,57,57,57,57,57,57,56,56,56,56,56,56,56,56,55 + ,55,55,55,55,55,55,55,54,54,54,54,54,54,54,54,53 + ,53,53,53,53,53,53,53,52,52,52,52,52,52,52,52,51 + ,51,51,51,51,51,51,51,50,50,50,50,50,50,50,50,49 + ,49,49,49,49,49,49,49,48,48,48,48,48,48,48,48,47 + ,47,47,47,47,47,47,47,46,46,46,46,46,46,46,46,45 + ,45,45,45,45,45,45,45,44,44,44,44,44,44,44,44,43 + ,43,43,43,43,43,43,43,42,42,42,42,42,42,42,42,41 + ,41,41,41,41,41,41,41,40,40,40,40,40,40,40,40,39 + ,39,39,39,39,39,39,39,38,38,38,38,38,38,38,38,37 + ,37,37,37,37,37,37,37,36,36,36,36,36,36,36,36,35 + ,35,35,35,35,35,35,35,34,34,34,34,34,34,34,34,33 + ,33,33,33,33,33,33,33,32,32,32,32,32,32,32,32,31 + ,31,31,31,31,31,31,31,31,30,30,30,30,30,30,30,30 + ,29,29,29,29,29,29,29,29,28,28,28,28,28,28,28,28 + ,27,27,27,27,27,27,27,27,26,26,26,26,26,26,26,26 + ,25,25,25,25,25,25,25,25,24,24,24,24,24,24,24,24 + ,23,23,23,23,23,23,23,23,22,22,22,22,22,22,22,22 + ,21,21,21,21,21,21,21,21,20,20,20,20,20,20,20,20 + ,19,19,19,19,19,19,19,19,18,18,18,18,18,18,18,18 + ,17,17,17,17,17,17,17,17,16,16,16,16,16,16,16,16 + ,15,15,15,15,15,15,15,15,14,14,14,14,14,14,14,14 + ,13,13,13,13,13,13,13,13,12,12,12,12,12,12,12,12 + ,11,11,11,11,11,11,11,11,10,10,10,10,10,10,10,10 + ,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,8 + ,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,6 + ,5,5,5,5,5,5,5,5,4,4,4,4,4,4,4,4 + ,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2 + ,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0 + ,-1,-1,-1,-1,-1,-1,-1,-1,-1,-2,-2,-2,-2,-2,-2,-2 + ,-2,-3,-3,-3,-3,-3,-3,-3,-3,-4,-4,-4,-4,-4,-4,-4 + ,-4,-5,-5,-5,-5,-5,-5,-5,-5,-6,-6,-6,-6,-6,-6,-6 + ,-6,-7,-7,-7,-7,-7,-7,-7,-7,-8,-8,-8,-8,-8,-8,-8 + ,-8,-9,-9,-9,-9,-9,-9,-9,-9,-10,-10,-10,-10,-10,-10,-10 + ,-10,-11,-11,-11,-11,-11,-11,-11,-11,-12,-12,-12,-12,-12,-12,-12 + ,-12,-13,-13,-13,-13,-13,-13,-13,-13,-14,-14,-14,-14,-14,-14,-14 + ,-14,-15,-15,-15,-15,-15,-15,-15,-15,-16,-16,-16,-16,-16,-16,-16 + ,-16,-17,-17,-17,-17,-17,-17,-17,-17,-18,-18,-18,-18,-18,-18,-18 + ,-18,-19,-19,-19,-19,-19,-19,-19,-19,-20,-20,-20,-20,-20,-20,-20 + ,-20,-21,-21,-21,-21,-21,-21,-21,-21,-22,-22,-22,-22,-22,-22,-22 + ,-22,-23,-23,-23,-23,-23,-23,-23,-23,-24,-24,-24,-24,-24,-24,-24 + ,-24,-25,-25,-25,-25,-25,-25,-25,-25,-26,-26,-26,-26,-26,-26,-26 + ,-26,-27,-27,-27,-27,-27,-27,-27,-27,-28,-28,-28,-28,-28,-28,-28 + ,-28,-29,-29,-29,-29,-29,-29,-29,-29,-30,-30,-30,-30,-30,-30,-30 + ,-30,-31,-31,-31,-31,-31,-31,-31,-31,-32,-32,-32,-32,-32,-32,-32 + ,-32,-32,-33,-33,-33,-33,-33,-33,-33,-33,-34,-34,-34,-34,-34,-34 + ,-34,-34,-35,-35,-35,-35,-35,-35,-35,-35,-36,-36,-36,-36,-36,-36 + ,-36,-36,-37,-37,-37,-37,-37,-37,-37,-37,-38,-38,-38,-38,-38,-38 + ,-38,-38,-39,-39,-39,-39,-39,-39,-39,-39,-40,-40,-40,-40,-40,-40 + ,-40,-40,-41,-41,-41,-41,-41,-41,-41,-41,-42,-42,-42,-42,-42,-42 + ,-42,-42,-43,-43,-43,-43,-43,-43,-43,-43,-44,-44,-44,-44,-44,-44 + ,-44,-44,-45,-45,-45,-45,-45,-45,-45,-45,-46,-46,-46,-46,-46,-46 + ,-46,-46,-47,-47,-47,-47,-47,-47,-47,-47,-48,-48,-48,-48,-48,-48 + ,-48,-48,-49,-49,-49,-49,-49,-49,-49,-49,-50,-50,-50,-50,-50,-50 + ,-50,-50,-51,-51,-51,-51,-51,-51,-51,-51,-52,-52,-52,-52,-52,-52 + ,-52,-52,-53,-53,-53,-53,-53,-53,-53,-53,-54,-54,-54,-54,-54,-54 + ,-54,-54,-55,-55,-55,-55,-55,-55,-55,-55,-56,-56,-56,-56,-56,-56 + ,-56,-56,-57,-57,-57,-57,-57,-57,-57,-57,-58,-58,-58,-58,-58,-58 + ,-58,-58,-59,-59,-59,-59,-59,-59,-59,-59,-60,-60,-60,-60,-60,-60 + ,-60,-60,-61,-61,-61,-61,-61,-61,-61,-61,-62,-62,-62,-62,-62,-62 + ,-62,-62,-63,-63,-63,-63,-63,-63,-63,-63,-64,-64,-64,-64,-64,-64 + ,-64,-64,-64,-65,-65,-65,-65,-65,-65,-65,-65,-66,-66,-66,-66,-66 + ,-66,-66,-66,-67,-67,-67,-67,-67,-67,-67,-67,-68,-68,-68,-68,-68 + ,-68,-68,-68,-69,-69,-69,-69,-69,-69,-69,-69,-70,-70,-70,-70,-70 + ,-70,-70,-70,-71,-71,-71,-71,-71,-71,-71,-71,-72,-72,-72,-72,-72 + ,-72,-72,-72,-73,-73,-73,-73,-73,-73,-73,-73,-74,-74,-74,-74,-74 + ,-74,-74,-74,-75,-75,-75,-75,-75,-75,-75,-75,-76,-76,-76,-76,-76 + ,-76,-76,-76,-77,-77,-77,-77,-77,-77,-77,-77,-78,-78,-78,-78,-78 + ,-78,-78,-78,-79,-79,-79,-79,-79,-79,-79,-79,-80,-80,-80,-80,-80 + ,-80,-80,-80,-81,-81,-81,-81,-81,-81,-81,-81,-82,-82,-82,-82,-82 + ,-82,-82,-82,-83,-83,-83,-83,-83,-83,-83,-83,-84,-84,-84,-84,-84 + ,-84,-84,-84,-85,-85,-85,-85,-85,-85,-85,-85,-86,-86,-86,-86,-86 + ,-86,-86,-86,-87,-87,-87,-87,-87,-87,-87,-87,-88,-88,-88,-88,-88 + ,-88,-88,-88,-89,-89,-89,-89,-89,-89,-89,-89,-90,-90,-90,-90,-90 + ,-90,-90,-90,-91,-91,-91,-91,-91,-91,-91,-91,-92,-92,-92,-92,-92 + ,-92,-92,-92,-93,-93,-93,-93,-93,-93,-93,-93,-94,-94,-94,-94,-94 + ,-94,-94,-94,-95,-95,-95,-95,-95,-95,-95,-95,-96,-96,-96,-96,-96 + ,-96,-96,-96,-96,-97,-97,-97,-97,-97,-97,-97,-97,-98,-98,-98,-98 + ,-98,-98,-98,-98,-99,-99,-99,-99,-99,-99,-99,-99,-100,-100,-100,-100 + ,-100,-100,-100,-100,-101,-101,-101,-101,-101,-101,-101,-101,-102,-102,-102,-102 + ,-102,-102,-102,-102,-103,-103,-103,-103,-103,-103,-103,-103,-104,-104,-104,-104 + ,-104,-104,-104,-104,-105,-105,-105,-105,-105,-105,-105,-105,-106,-106,-106,-106 + ,-106,-106,-106,-106,-107,-107,-107,-107,-107,-107,-107,-107,-108,-108,-108,-108 + ,-108,-108,-108,-108,-109,-109,-109,-109,-109,-109,-109,-109,-110,-110,-110,-110 + ,-110,-110,-110,-110,-111,-111,-111,-111,-111,-111,-111,-111,-112,-112,-112,-112 + ,-112,-112,-112,-112,-113,-113,-113,-113,-113,-113,-113,-113,-114,-114,-114,-114 + ,-114,-114,-114,-114,-115,-115,-115,-115,-115,-115,-115,-115,-116,-116,-116,-116 + ,-116,-116,-116,-116,-117,-117,-117,-117,-117,-117,-117,-117,-118,-118,-118,-118 + ,-118,-118,-118,-118,-119,-119,-119,-119,-119,-119,-119,-119,-120,-120,-120,-120 + ,-120,-120,-120,-120,-121,-121,-121,-121,-121,-121,-121,-121,-122,-122,-122,-122 + ,-122,-122,-122,-122,-123,-123,-123,-123,-123,-123,-123,-123,-124,-124,-124,-124 + ,-124,-124,-124,-124,-125,-125,-125,-125,-125,-125,-125,-125,-126,-126,-126,-126 + ,-126,-126,-126,-126,-127,-127,-127,-127,-127,-127,-127,-127,-128,-128,-128,-128 + }; + + +#endif /* REVSAW2048_H_ */ diff --git a/include/sound/tables/square2048_int8.h b/include/sound/tables/square2048_int8.h new file mode 100644 index 0000000..6910d51 --- /dev/null +++ b/include/sound/tables/square2048_int8.h @@ -0,0 +1,153 @@ +/* + SQUARE2018_int8.h - defines a table representing a square waveform + + used for the LFO/carrier/modulator + + can be replaced with any values you like, but must be 2048 cells to match the other tables +*/ +#ifndef SQUARE2048_H_ +#define SQUARE2048_H_ + +#include + +#define SQUARE2048_NUM_CELLS 2048 +#define SQUARE2048_SAMPLERATE 2048 + +/** @ingroup tables +reverse saw table +*/ + +CONSTTABLE_STORAGE(int8_t) SQUARE2048_DATA [2048] = + { + 127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127,127 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + ,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128,-128 + }; + + +#endif /* SQUARE2048_H_ */ diff --git a/include/sound/wavetable.h b/include/sound/wavetable.h new file mode 100644 index 0000000..c1819fc --- /dev/null +++ b/include/sound/wavetable.h @@ -0,0 +1,17 @@ +/** + * @file wavetable.h + * @brief FM oscillator table accessors β€” sine and cosine only + */ + +#ifndef SOUND_WAVETABLE_H +#define SOUND_WAVETABLE_H + +#include + +/// Sine 2048-sample table (FM carrier) +const int8_t* getSin2048Data(); + +/// Cosine 2048-sample table (FM modulator + LFO) +const int8_t* getCos2048Data(); + +#endif diff --git a/include/tables/akwf_aguitarr.h b/include/tables/akwf_aguitarr.h deleted file mode 100644 index 3a592cb..0000000 --- a/include/tables/akwf_aguitarr.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef AKWF_aguitar_0001_512_H_ -#define AKWF_aguitar_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_aguitar_0001_512_NUM_CELLS 512 -#define AKWF_aguitar_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_aguitar_0001_512_DATA -[] = {3, 7, 13, 19, 25, 31, 37, 43, 50, 56, 61, 67, 73, 79, 84, 89, 93, 98, 103, -107, 110, 113, 115, 118, 119, 122, 124, 124, 125, 126, 127, 127, 127, 127, 127, -127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 126, 126, 126, 125, 124, 123, -121, 121, 120, 118, 117, 116, 114, 111, 110, 107, 104, 102, 100, 96, 93, 90, 86, -82, 79, 74, 71, 66, 62, 57, 51, 47, 42, 38, 32, 27, 20, 16, 10, 5, -1, -6, -12, --17, -22, -27, -32, -36, -41, -44, -48, -51, -54, -58, -60, -63, -65, -67, -70, --72, -74, -75, -77, -79, -81, -82, -84, -85, -87, -88, -90, -92, -93, -94, -96, --97, -98, -100, -101, -102, -103, -104, -105, -107, -108, -109, -109, -111, --113, -114, -114, -115, -116, -116, -117, -119, -119, -120, -120, -121, -121, --120, -121, -120, -120, -119, -118, -118, -118, -117, -116, -116, -114, -115, --114, -113, -112, -111, -112, -112, -111, -110, -109, -109, -108, -107, -106, --106, -105, -104, -103, -102, -100, -99, -98, -95, -94, -93, -89, -88, -86, -83, --81, -78, -76, -73, -71, -69, -66, -64, -61, -58, -55, -53, -48, -47, -43, -40, --37, -34, -30, -27, -25, -23, -19, -16, -15, -12, -8, -6, -4, 0, 2, 4, 6, 10, -12, 16, 19, 23, 25, 28, 32, 35, 39, 43, 46, 50, 53, 55, 59, 63, 65, 67, 70, 72, -75, 77, 78, 80, 83, 84, 86, 88, 89, 92, 94, 96, 98, 99, 102, 104, 106, 108, 109, -111, 111, 113, 114, 114, 114, 114, 114, 113, 113, 112, 112, 111, 110, 109, 108, -107, 105, 104, 103, 102, 102, 101, 99, 98, 97, 95, 94, 91, 88, 85, 83, 80, 75, -72, 67, 63, 60, 54, 51, 46, 42, 40, 36, 33, 31, 29, 27, 26, 24, 24, 24, 23, 22, -22, 20, 20, 19, 17, 15, 13, 11, 8, 4, 0, -3, -8, -13, -18, -23, -25, -31, -35, --38, -42, -44, -48, -49, -51, -53, -53, -55, -55, -56, -56, -56, -55, -56, -55, --55, -55, -56, -56, -56, -57, -56, -56, -57, -57, -56, -55, -54, -54, -52, -51, --49, -47, -45, -42, -41, -37, -35, -33, -30, -28, -24, -23, -21, -18, -17, -15, --15, -13, -13, -12, -11, -11, -9, -9, -8, -6, -7, -6, -4, -4, -3, -1, 0, 1, 2, -2, 3, 4, 4, 5, 6, 6, 5, 6, 6, 6, 6, 5, 5, 6, 6, 6, 6, 7, 8, 8, 9, 12, 13, 14, -16, 17, 20, 21, 22, 24, 25, 26, 26, 27, 28, 27, 26, 24, 22, 21, 19, 16, 13, 9, -6, 1, -3, -6, -10, -15, -19, -23, -26, -31, -34, -37, -40, -43, -45, -47, -48, --49, -49, -49, -51, -50, -49, -48, -48, -47, -45, -44, -43, -42, -40, -38, -38, --36, -35, -34, -34, -33, -32, -32, -31, -32, -32, -32, -33, -33, -33, -34, -33, --34, -34, -34, -34, -34, -34, -33, -31, -30, -28, -26, -23, -20, -17, -14, -9, --5, -1, }; - -#endif /* AKWF_aguitar_0001_512_H_ */ diff --git a/include/tables/akwf_cello.h b/include/tables/akwf_cello.h deleted file mode 100644 index 1d9e286..0000000 --- a/include/tables/akwf_cello.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef AKWF_cello_0001_512_H_ -#define AKWF_cello_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_cello_0001_512_NUM_CELLS 512 -#define AKWF_cello_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_cello_0001_512_DATA [] -= {0, 1, 3, 7, 8, 10, 13, 14, 17, 21, 24, 27, 28, 29, 29, 29, 28, 28, 27, 26, -25, 24, 21, 20, 18, 17, 14, 13, 12, 12, 13, 16, 19, 24, 29, 36, 43, 49, 53, 58, -62, 65, 69, 72, 73, 74, 73, 73, 73, 74, 77, 79, 84, 88, 92, 98, 104, 111, 116, -122, 126, 127, 127, 126, 123, 119, 117, 114, 112, 109, 108, 105, 102, 98, 93, -90, 87, 85, 83, 82, 78, 76, 74, 70, 67, 63, 60, 57, 54, 51, 48, 45, 42, 37, 33, -28, 22, 18, 12, 6, 1, -5, -11, -17, -22, -27, -30, -33, -35, -36, -37, -40, -43, --47, -51, -57, -63, -69, -74, -82, -88, -96, -102, -106, -110, -113, -115, -114, --114, -113, -109, -107, -104, -103, -103, -102, -101, -99, -98, -95, -94, -96, --98, -100, -100, -103, -103, -105, -105, -105, -103, -100, -96, -91, -86, -81, --77, -73, -69, -66, -63, -64, -66, -69, -72, -75, -78, -81, -83, -82, -82, -82, --82, -79, -76, -73, -70, -68, -64, -64, -62, -59, -57, -55, -52, -51, -48, -47, --45, -43, -40, -37, -35, -31, -29, -27, -23, -20, -15, -10, -3, 2, 7, 10, 12, -13, 15, 17, 21, 23, 26, 28, 29, 30, 31, 33, 34, 35, 35, 34, 32, 30, 29, 27, 27, -25, 24, 22, 19, 18, 18, 17, 16, 15, 14, 10, 6, 2, -2, -7, -10, -13, -16, -19, --20, -21, -23, -21, -20, -19, -17, -16, -15, -14, -17, -18, -19, -20, -20, -21, --21, -21, -22, -22, -21, -20, -18, -14, -9, -5, -1, 3, 8, 14, 19, 21, 24, 25, -28, 28, 26, 25, 24, 25, 23, 22, 23, 27, 29, 28, 29, 31, 32, 33, 32, 33, 35, 39, -43, 44, 43, 45, 47, 46, 44, 45, 46, 48, 49, 49, 50, 50, 50, 50, 53, 59, 64, 66, -70, 73, 72, 71, 71, 74, 79, 80, 79, 78, 79, 81, 79, 76, 72, 71, 70, 66, 61, 59, -60, 58, 50, 44, 41, 38, 39, 39, 36, 32, 28, 22, 13, 4, -3, -9, -13, -17, -22, --23, -23, -22, -20, -15, -10, -5, 0, 4, 8, 8, 8, 8, 9, 6, 3, -1, -3, -4, -2, -1, -0, 4, 9, 15, 20, 24, 29, 37, 43, 46, 48, 47, 43, 38, 31, 22, 12, 4, -4, -11, --18, -25, -30, -34, -37, -39, -41, -41, -44, -48, -53, -57, -64, -70, -76, -80, --86, -92, -96, -99, -100, -99, -98, -95, -90, -86, -83, -80, -75, -69, -64, -60, --55, -52, -50, -48, -47, -46, -44, -41, -39, -37, -35, -32, -30, -25, -23, -19, --15, -10, -6, -3, -1, 1, 1, 1, -2, -6, -7, -8, -9, -10, -9, -9, -7, -7, -3, 2, -8, 13, 18, 22, 24, 24, 22, 20, 16, 13, 11, 9, 6, 5, 1, -1, -3, -4, -3, -3, -2, --1, -2, -2, -3, -5, -6, -8, -10, -11, -14, -16, -16, -15, -15, -15, -12, -11, --10, -7, -7, -4, -3, -1, -1, -1, }; - -#endif /* AKWF_cello_0001_512_H_ */ diff --git a/include/tables/akwf_clarinett.h b/include/tables/akwf_clarinett.h deleted file mode 100644 index 6d59fc5..0000000 --- a/include/tables/akwf_clarinett.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef AKWF_clarinett_0001_512_H_ -#define AKWF_clarinett_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_clarinett_0001_512_NUM_CELLS 512 -#define AKWF_clarinett_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) -AKWF_clarinett_0001_512_DATA [] = {0, 1, 4, 5, 6, 8, 11, 12, 12, 14, 16, 17, 19, -20, 23, 23, 25, 25, 26, 26, 27, 29, 29, 31, 30, 30, 30, 30, 29, 27, 26, 26, 24, -24, 22, 21, 19, 17, 17, 15, 16, 15, 15, 15, 15, 16, 17, 18, 20, 22, 23, 25, 28, -30, 33, 35, 38, 41, 44, 46, 50, 54, 56, 59, 61, 65, 68, 70, 73, 75, 78, 80, 82, -84, 87, 88, 90, 92, 94, 97, 98, 101, 103, 106, 108, 109, 111, 113, 115, 117, -119, 121, 123, 123, 125, 125, 127, 127, 127, 127, 127, 126, 124, 122, 120, 119, -117, 114, 112, 108, 104, 102, 98, 96, 92, 90, 86, 83, 81, 78, 75, 73, 71, 68, -66, 64, 62, 59, 56, 54, 52, 48, 47, 45, 42, 40, 37, 35, 34, 31, 28, 26, 25, 23, -20, 18, 15, 13, 9, 8, 5, 2, 1, -1, -5, -6, -8, -10, -12, -13, -16, -17, -18, --18, -20, -20, -21, -21, -22, -23, -23, -23, -23, -23, -23, -25, -25, -26, -27, --27, -27, -28, -27, -28, -28, -30, -29, -28, -29, -28, -27, -27, -25, -24, -23, --20, -19, -18, -16, -15, -13, -10, -9, -6, -4, -1, 2, 4, 5, 6, 9, 11, 11, 12, -14, 16, 16, 17, 17, 18, 18, 17, 17, 16, 16, 15, 15, 14, 14, 13, 12, 11, 11, 10, -9, 9, 9, 8, 7, 7, 6, 7, 6, 6, 7, 6, 5, 5, 5, 4, 4, 2, 1, 0, -2, -3, -5, -7, -8, --10, -11, -12, -14, -15, -16, -17, -19, -20, -20, -20, -21, -22, -23, -22, -23, --23, -23, -23, -24, -25, -24, -25, -25, -25, -25, -26, -27, -28, -29, -29, -29, --29, -31, -31, -33, -34, -34, -35, -36, -35, -35, -36, -36, -37, -37, -38, -39, --40, -39, -39, -40, -41, -42, -42, -41, -43, -44, -45, -47, -48, -50, -53, -54, --56, -58, -61, -64, -65, -68, -70, -73, -76, -79, -81, -84, -86, -87, -89, -90, --92, -93, -96, -96, -98, -98, -99, -100, -99, -101, -102, -102, -104, -104, --105, -105, -106, -106, -107, -107, -106, -107, -107, -109, -108, -110, -108, --109, -108, -108, -108, -108, -107, -107, -106, -105, -104, -102, -101, -99, --98, -97, -95, -93, -91, -89, -86, -85, -81, -80, -77, -74, -71, -69, -66, -62, --60, -55, -52, -49, -45, -42, -39, -36, -31, -27, -24, -19, -17, -13, -9, -6, --2, 2, 5, 9, 12, 15, 19, 23, 27, 30, 32, 35, 38, 41, 43, 45, 46, 47, 49, 50, 52, -53, 53, 55, 55, 54, 55, 54, 54, 54, 53, 52, 51, 49, 48, 46, 44, 42, 40, 37, 35, -33, 30, 28, 26, 23, 22, 19, 18, 17, 15, 12, 11, 10, 8, 6, 3, 2, 0, -2, -3, -4, --5, -8, -10, -12, -13, -15, -15, -16, -18, -19, -19, -21, -22, -23, -23, -24, --25, -24, -24, -24, -24, -24, -24, -24, -23, -23, -21, -20, -19, -18, -15, -14, --12, -10, -8, -7, -6, -4, -1, }; - -#endif /* AKWF_clarinett_0001_512_H_ */ diff --git a/include/tables/akwf_epiano.h b/include/tables/akwf_epiano.h deleted file mode 100644 index 5f329e1..0000000 --- a/include/tables/akwf_epiano.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef AKWF_EPIANO_H_ -#define AKWF_EPIANO_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_EPIANO_NUM_CELLS 512 -#define AKWF_EPIANO_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_EPIANO_DATA[] = { - 1, 5, 9, 15, 18, 22, 27, 31, 36, 41, 46, 50, 55, 60, 64, - 68, 73, 77, 82, 85, 90, 94, 96, 100, 103, 106, 109, 112, 114, 117, - 119, 120, 122, 123, 124, 126, 127, 127, 127, 127, 127, 127, 127, 126, 125, - 126, 126, 124, 123, 122, 120, 119, 118, 116, 115, 113, 111, 111, 109, 107, - 105, 104, 102, 100, 98, 97, 95, 94, 91, 89, 88, 87, 85, 83, 82, - 80, 79, 76, 76, 75, 72, 70, 69, 69, 67, 65, 63, 62, 61, 60, - 59, 57, 55, 55, 53, 53, 51, 50, 49, 48, 47, 46, 45, 43, 43, - 40, 40, 40, 38, 38, 36, 36, 35, 34, 33, 31, 31, 30, 30, 29, - 28, 27, 26, 26, 24, 23, 23, 21, 21, 20, 20, 19, 17, 16, 15, - 15, 14, 13, 12, 12, 10, 10, 9, 8, 7, 6, 6, 4, 4, 2, - 1, 0, 0, -1, -1, -3, -4, -4, -6, -6, -7, -9, -9, -10, -12, - -12, -13, -13, -15, -15, -16, -18, -18, -19, -19, -21, -22, -23, -24, -25, - -25, -26, -28, -28, -29, -30, -30, -32, -33, -34, -34, -34, -37, -36, -38, - -40, -40, -41, -42, -42, -43, -44, -46, -45, -47, -48, -49, -50, -51, -52, - -53, -53, -54, -55, -57, -57, -58, -59, -60, -61, -62, -63, -64, -64, -66, - -67, -68, -68, -69, -70, -71, -72, -73, -74, -75, -77, -77, -77, -78, -80, - -80, -81, -82, -82, -83, -85, -85, -86, -86, -88, -87, -88, -88, -89, -90, - -92, -92, -93, -93, -93, -94, -95, -95, -95, -95, -96, -96, -97, -96, -98, - -97, -97, -97, -98, -96, -97, -97, -97, -96, -96, -96, -95, -94, -95, -93, - -92, -92, -90, -90, -88, -86, -86, -84, -82, -80, -78, -76, -74, -71, -68, - -66, -63, -60, -57, -53, -49, -46, -43, -39, -34, -31, -26, -23, -18, -13, - -9, -4, 1, 5, 9, 14, 18, 23, 28, 31, 36, 40, 44, 48, 50, - 54, 58, 60, 63, 66, 68, 72, 72, 75, 76, 78, 78, 80, 80, 81, - 82, 81, 82, 82, 82, 80, 80, 79, 78, 77, 77, 76, 73, 72, 70, - 69, 68, 67, 64, 62, 61, 58, 57, 55, 52, 51, 48, 47, 45, 43, - 40, 39, 36, 36, 33, 31, 30, 27, 25, 24, 22, 20, 18, 17, 16, - 14, 12, 10, 10, 6, 6, 4, 3, 1, 0, -1, -3, -4, -4, -6, - -8, -8, -9, -11, -12, -12, -13, -15, -17, -17, -18, -20, -21, -22, -22, - -23, -24, -25, -26, -27, -27, -29, -29, -30, -32, -31, -32, -34, -35, -35, - -36, -36, -37, -38, -40, -40, -41, -41, -43, -43, -44, -44, -46, -46, -47, - -48, -49, -49, -49, -50, -52, -52, -53, -53, -54, -54, -56, -56, -56, -58, - -58, -58, -59, -58, -59, -61, -60, -61, -61, -61, -61, -61, -62, -62, -60, - -60, -61, -61, -59, -59, -59, -58, -57, -56, -55, -54, -53, -52, -51, -50, - -48, -46, -44, -42, -39, -37, -35, -31, -29, -26, -24, -19, -17, -13, -9, - -6, -2}; - -#endif diff --git a/include/tables/akwf_flute.h b/include/tables/akwf_flute.h deleted file mode 100644 index 5ca1a06..0000000 --- a/include/tables/akwf_flute.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef AKWF_flute_0001_512_H_ -#define AKWF_flute_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_flute_0001_512_NUM_CELLS 512 -#define AKWF_flute_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_flute_0001_512_DATA [] -= {0, 2, 4, 5, 6, 7, 10, 11, 13, 13, 14, 15, 16, 17, 17, 17, 18, 19, 19, 20, 22, -21, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 25, 26, 25, 25, 26, 26, -26, 27, 28, 28, 29, 29, 31, 32, 34, 34, 34, 36, 37, 38, 38, 39, 40, 41, 41, 43, -43, 44, 45, 45, 45, 47, 47, 46, 46, 47, 47, 47, 48, 48, 48, 49, 49, 50, 51, 50, -50, 50, 51, 51, 51, 51, 51, 51, 52, 53, 54, 55, 56, 55, 57, 58, 59, 58, 59, 61, -60, 62, 63, 63, 64, 65, 66, 67, 69, 69, 72, 72, 74, 74, 76, 77, 79, 80, 81, 82, -83, 85, 86, 86, 87, 89, 90, 91, 92, 94, 94, 95, 96, 97, 98, 98, 100, 101, 102, -103, 104, 106, 106, 108, 110, 111, 111, 113, 114, 114, 115, 117, 117, 118, 118, -120, 121, 122, 124, 124, 125, 126, 126, 127, 127, 127, 127, 127, 127, 127, 127, -127, 127, 126, 127, 126, 125, 125, 124, 122, 122, 121, 120, 118, 118, 117, 115, -115, 114, 114, 113, 112, 112, 111, 112, 110, 110, 110, 109, 108, 108, 107, 106, -106, 106, 105, 104, 104, 103, 102, 100, 100, 98, 97, 95, 94, 91, 90, 88, 86, 84, -81, 78, 76, 73, 70, 67, 65, 61, 59, 55, 53, 51, 48, 46, 43, 41, 38, 35, 33, 31, -27, 24, 22, 19, 17, 14, 12, 9, 6, 3, 2, -2, -4, -6, -7, -9, -12, -13, -15, -16, --17, -18, -20, -20, -21, -22, -22, -22, -24, -24, -24, -23, -25, -23, -24, -24, --23, -24, -24, -25, -26, -27, -27, -27, -27, -28, -28, -29, -29, -30, -31, -32, --33, -34, -35, -35, -36, -38, -39, -40, -42, -42, -44, -45, -47, -47, -49, -50, --50, -51, -52, -52, -53, -53, -54, -52, -52, -54, -53, -53, -54, -55, -55, -55, --56, -56, -56, -57, -57, -58, -58, -58, -60, -60, -61, -61, -62, -61, -63, -64, --63, -64, -65, -65, -67, -67, -68, -68, -68, -69, -69, -69, -70, -72, -72, -72, --73, -74, -74, -74, -75, -75, -76, -77, -79, -80, -80, -81, -82, -82, -83, -85, --86, -86, -87, -90, -91, -93, -94, -95, -96, -97, -98, -99, -100, -101, -104, --105, -106, -107, -109, -110, -112, -112, -114, -114, -115, -115, -116, -117, --117, -117, -117, -118, -117, -116, -117, -117, -116, -117, -117, -116, -117, --118, -117, -119, -119, -119, -120, -120, -120, -121, -122, -122, -123, -124, --124, -124, -125, -124, -125, -124, -124, -125, -123, -124, -123, -124, -122, --122, -122, -121, -120, -119, -118, -116, -115, -113, -111, -110, -107, -106, --105, -103, -101, -100, -99, -98, -95, -95, -93, -92, -90, -89, -87, -87, -85, --84, -83, -82, -80, -80, -78, -77, -77, -75, -74, -73, -71, -70, -69, -67, -65, --63, -61, -59, -56, -55, -52, -50, -48, -45, -43, -40, -37, -34, -31, -29, -26, --24, -21, -19, -15, -13, -11, -8, -5, -3, -2, 0, }; - -#endif /* AKWF_flute_0001_512_H_ */ diff --git a/include/tables/akwf_fmsynth.h b/include/tables/akwf_fmsynth.h deleted file mode 100644 index bdae809..0000000 --- a/include/tables/akwf_fmsynth.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef AKWF_fmsynth_0001_512_H_ -#define AKWF_fmsynth_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_fmsynth_0001_512_NUM_CELLS 512 -#define AKWF_fmsynth_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_fmsynth_0001_512_DATA -[] = {2, 8, 13, 20, 26, 32, 37, 44, 51, 57, 62, 67, 73, 79, 84, 90, 95, 98, 102, -106, 110, 113, 115, 117, 119, 122, 123, 124, 124, 125, 126, 126, 126, 127, 126, -127, 127, 127, 127, 127, 127, 126, 126, 126, 125, 125, 124, 123, 124, 122, 121, -120, 118, 116, 114, 112, 110, 108, 106, 105, 102, 100, 98, 97, 95, 93, 92, 90, -89, 87, 86, 85, 85, 85, 84, 85, 85, 86, 86, 87, 89, 90, 91, 93, 97, 99, 101, -104, 106, 110, 112, 115, 118, 121, 123, 125, 126, 126, 126, 126, 123, 120, 114, -106, 97, 86, 72, 56, 35, 13, -14, -35, -55, -71, -85, -96, -105, -112, -117, --121, -125, -127, -127, -128, -128, -128, -126, -126, -124, -124, -122, -122, --120, -120, -119, -118, -117, -117, -116, -116, -118, -117, -118, -118, -119, --120, -120, -121, -122, -123, -124, -124, -126, -127, -127, -127, -128, -127, --127, -127, -127, -126, -126, -124, -123, -122, -120, -118, -117, -116, -113, --112, -110, -110, -109, -109, -108, -108, -108, -108, -110, -112, -113, -115, --117, -118, -120, -122, -124, -124, -126, -127, -127, -126, -126, -125, -123, --121, -118, -115, -111, -107, -102, -98, -92, -86, -81, -75, -68, -62, -57, -49, --43, -38, -32, -25, -20, -14, -9, -3, 1, 7, 11, 16, 20, 24, 29, 32, 35, 39, 42, -46, 47, 50, 53, 53, 55, 56, 57, 57, 57, 59, 59, 58, 57, 56, 56, 54, 53, 51, 50, -49, 46, 44, 41, 39, 36, 34, 31, 29, 27, 24, 22, 18, 16, 13, 11, 9, 7, 4, 2, 1, -0, -2, -3, -4, -4, -5, -4, -4, -5, -4, -4, -2, -2, 0, 3, 4, 6, 8, 12, 15, 18, -21, 25, 30, 33, 38, 43, 48, 53, 58, 63, 68, 75, 81, 86, 92, 97, 102, 107, 112, -117, 119, 123, 125, 126, 127, 127, 126, 123, 119, 114, 108, 101, 93, 85, 75, 64, -53, 40, 30, 17, 5, -5, -15, -24, -30, -36, -39, -43, -46, -48, -47, -48, -48, --45, -43, -40, -36, -31, -26, -19, -12, -3, 5, 14, 24, 32, 41, 49, 57, 64, 71, -77, 83, 88, 92, 97, 100, 105, 107, 110, 112, 114, 116, 116, 118, 118, 118, 119, -119, 118, 119, 118, 117, 116, 115, 112, 111, 108, 104, 99, 95, 88, 81, 72, 61, -49, 34, 15, -4, -24, -42, -59, -74, -87, -98, -107, -114, -120, -124, -127, --127, -127, -127, -124, -121, -119, -115, -111, -107, -102, -97, -92, -87, -83, --78, -74, -69, -65, -61, -57, -54, -51, -48, -45, -42, -40, -38, -37, -36, -34, --34, -33, -33, -33, -33, -33, -34, -34, -36, -37, -38, -40, -42, -44, -46, -49, --51, -53, -56, -58, -60, -63, -66, -68, -71, -73, -76, -78, -80, -82, -84, -86, --88, -89, -91, -91, -93, -94, -95, -95, -95, -94, -95, -94, -93, -92, -91, -89, --87, -85, -83, -81, -78, -75, -71, -67, -64, -59, -56, -51, -45, -41, -36, -30, --24, -19, -14, -7, -2, }; - -#endif /* AKWF_fmsynth_0001_512_H_ */ diff --git a/include/tables/akwf_nes_pulse.h b/include/tables/akwf_nes_pulse.h deleted file mode 100644 index 16c00eb..0000000 --- a/include/tables/akwf_nes_pulse.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef AKWF_nes_pulse_12_5_512_H_ -#define AKWF_nes_pulse_12_5_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_nes_pulse_12_5_512_NUM_CELLS 512 -#define AKWF_nes_pulse_12_5_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) -AKWF_nes_pulse_12_5_512_DATA [] = {61, 106, 97, 103, 99, 102, 99, 102, 99, 101, -100, 101, 100, 101, 100, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, -100, 100, 101, 100, 101, 100, 101, 100, 101, 100, 100, 99, 101, 100, 101, 100, -100, 101, 100, 100, 101, 101, 101, 101, 100, 101, 99, 102, 99, 102, 97, 103, 96, -104, 94, 109, 89, 119, 34, -120, -91, -106, -97, -102, -99, -101, -101, -100, --100, -99, -101, -98, -102, -99, -101, -99, -101, -99, -102, -99, -102, -99, --101, -100, -101, -100, -101, -99, -101, -100, -101, -100, -100, -101, -100, --101, -99, -101, -100, -100, -100, -100, -100, -100, -100, -101, -99, -101, --100, -100, -101, -100, -100, -101, -100, -100, -100, -101, -99, -100, -100, --100, -101, -101, -100, -100, -100, -100, -101, -100, -100, -100, -100, -101, --101, -100, -100, -100, -100, -101, -100, -100, -100, -100, -99, -101, -100, --101, -100, -101, -100, -100, -101, -100, -100, -100, -100, -101, -101, -99, --100, -100, -101, -100, -101, -100, -100, -101, -100, -100, -101, -100, -99, --100, -100, -100, -100, -101, -101, -100, -100, -100, -100, -101, -100, -100, --100, -100, -100, -100, -100, -100, -100, -101, -100, -100, -101, -100, -100, --100, -100, -100, -100, -100, -100, -100, -101, -101, -100, -100, -100, -101, --101, -100, -100, -100, -101, -100, -101, -101, -100, -100, -100, -100, -101, --100, -100, -100, -101, -100, -101, -100, -100, -100, -100, -100, -100, -100, --101, -100, -101, -100, -101, -101, -100, -100, -100, -101, -101, -100, -100, --100, -100, -100, -100, -101, -101, -100, -100, -100, -101, -101, -101, -100, --100, -100, -101, -101, -100, -101, -100, -101, -100, -101, -101, -100, -100, --101, -101, -101, -100, -101, -100, -100, -100, -100, -100, -100, -100, -101, --100, -100, -100, -101, -101, -100, -100, -101, -100, -100, -100, -100, -100, --100, -100, -99, -100, -101, -101, -100, -101, -101, -100, -101, -100, -100, --100, -100, -101, -101, -100, -101, -100, -100, -101, -101, -100, -101, -100, --101, -100, -100, -101, -100, -101, -100, -101, -100, -100, -99, -100, -100, --101, -100, -100, -100, -100, -100, -101, -100, -100, -100, -100, -99, -100, --100, -100, -100, -101, -101, -100, -100, -100, -100, -100, -101, -100, -100, --100, -100, -100, -101, -99, -100, -100, -100, -99, -100, -100, -100, -100, --100, -101, -100, -101, -99, -100, -100, -100, -101, -100, -101, -100, -100, --100, -100, -100, -100, -100, -101, -100, -100, -101, -101, -100, -101, -101, --100, -99, -100, -100, -101, -100, -100, -100, -101, -101, -100, -100, -100, --100, -100, -100, -101, -101, -101, -101, -99, -100, -100, -100, -100, -100, --100, -101, -100, -100, -100, -100, -100, -100, -100, -101, -100, -100, -100, --100, -100, -100, -100, -100, -101, -101, -100, -100, -100, -100, -100, -101, --100, -100, -100, -100, -100, -100, -101, -100, -100, -100, -100, -100, -100, --100, -100, -101, -101, -100, -101, -100, -100, -100, -101, -100, -100, -101, --100, -100, -100, -100, -100, -102, -100, -102, -98, -102, -98, -103, -97, -103, --97, -104, -96, -106, -94, -109, -83, }; - -#endif /* AKWF_nes_pulse_12_5_512_H_ */ diff --git a/include/tables/akwf_oboe.h b/include/tables/akwf_oboe.h deleted file mode 100644 index d821353..0000000 --- a/include/tables/akwf_oboe.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef AKWF_oboe_0001_512_H_ -#define AKWF_oboe_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_oboe_0001_512_NUM_CELLS 512 -#define AKWF_oboe_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_oboe_0001_512_DATA [] -= {2, 4, 7, 10, 12, 14, 17, 21, 23, 25, 28, 30, 33, 34, 37, 39, 42, 44, 45, 47, -50, 51, 54, 56, 57, 59, 61, 63, 65, 66, 68, 69, 70, 73, 74, 74, 76, 77, 79, 80, -82, 82, 83, 85, 86, 88, 88, 89, 91, 91, 93, 94, 94, 95, 96, 98, 98, 99, 99, 100, -102, 103, 104, 104, 105, 106, 107, 108, 109, 109, 110, 111, 112, 113, 114, 114, -114, 115, 115, 117, 117, 117, 119, 119, 120, 120, 121, 121, 121, 122, 122, 123, -123, 124, 123, 124, 124, 125, 125, 126, 126, 125, 125, 126, 125, 126, 127, 126, -127, 127, 127, 127, 126, 127, 126, 126, 127, 126, 126, 126, 125, 125, 125, 125, -125, 125, 124, 124, 123, 123, 122, 122, 122, 121, 120, 121, 120, 119, 119, 118, -117, 117, 116, 116, 114, 114, 113, 111, 110, 110, 108, 108, 107, 106, 104, 102, -102, 101, 100, 98, 97, 95, 94, 93, 92, 90, 88, 88, 85, 85, 83, 82, 80, 78, 76, -75, 74, 71, 71, 69, 67, 65, 64, 63, 60, 59, 57, 54, 53, 52, 50, 47, 46, 44, 42, -41, 39, 37, 35, 33, 32, 32, 30, 27, 27, 25, 23, 22, 21, 19, 18, 16, 14, 13, 13, -11, 9, 9, 6, 5, 4, 3, 2, 0, -1, -1, -3, -5, -5, -6, -7, -8, -8, -10, -10, -11, --12, -13, -13, -14, -16, -16, -17, -17, -19, -19, -19, -20, -21, -21, -22, -22, --22, -24, -23, -24, -24, -25, -26, -27, -28, -27, -29, -30, -30, -30, -31, -32, --33, -33, -34, -35, -36, -37, -37, -37, -38, -39, -39, -39, -40, -40, -42, -41, --42, -43, -43, -44, -45, -45, -46, -46, -46, -47, -47, -48, -49, -49, -49, -49, --50, -50, -51, -50, -52, -52, -52, -53, -53, -53, -53, -53, -53, -53, -54, -54, --54, -53, -54, -55, -53, -55, -54, -54, -54, -54, -54, -54, -52, -53, -53, -53, --53, -53, -53, -53, -52, -52, -52, -52, -51, -51, -50, -51, -51, -51, -50, -50, --50, -50, -50, -50, -50, -50, -50, -50, -49, -49, -49, -49, -49, -50, -50, -50, --50, -50, -50, -50, -51, -50, -51, -52, -53, -54, -53, -54, -55, -55, -57, -57, --57, -59, -60, -60, -61, -63, -62, -63, -65, -65, -67, -68, -69, -70, -71, -72, --73, -75, -76, -77, -77, -79, -81, -82, -83, -85, -86, -87, -89, -91, -93, -93, --95, -98, -99, -100, -102, -103, -106, -106, -108, -109, -111, -113, -114, -115, --116, -118, -118, -119, -120, -121, -122, -124, -123, -125, -125, -126, -126, --127, -128, -128, -128, -128, -128, -128, -127, -127, -128, -128, -127, -128, --128, -128, -127, -127, -126, -126, -125, -124, -122, -121, -120, -119, -118, --116, -115, -113, -112, -109, -107, -105, -104, -102, -99, -98, -95, -93, -91, --89, -86, -83, -81, -78, -76, -73, -70, -68, -65, -61, -60, -57, -54, -50, -48, --45, -42, -39, -36, -33, -31, -28, -25, -22, -19, -17, -13, -11, -9, -5, -2, 0, -}; - -#endif /* AKWF_oboe_0001_512_H_ */ diff --git a/include/tables/akwf_oscchip.h b/include/tables/akwf_oscchip.h deleted file mode 100644 index 378cc7f..0000000 --- a/include/tables/akwf_oscchip.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef AKWF_oscchip_0001_512_H_ -#define AKWF_oscchip_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_oscchip_0001_512_NUM_CELLS 512 -#define AKWF_oscchip_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_oscchip_0001_512_DATA -[] = {86, 127, 113, 124, 117, 123, 119, 122, 120, 122, 120, 121, 120, 120, 121, -118, 114, 114, 114, 112, 113, 113, 113, 113, 113, 112, 113, 113, 114, 113, 113, -111, 105, 104, 104, 103, 104, 104, 103, 105, 105, 104, 104, 104, 103, 104, 104, -103, 98, 98, 99, 98, 98, 98, 98, 98, 98, 99, 97, 98, 98, 99, 99, 95, 90, 90, 91, -91, 91, 90, 91, 91, 90, 91, 90, 91, 90, 91, 90, 89, 87, 87, 86, 87, 86, 86, 87, -87, 87, 87, 86, 86, 86, 86, 86, 82, 76, 77, 76, 76, 76, 76, 75, 76, 76, 76, 75, -76, 75, 75, 75, 70, 64, 65, 65, 64, 65, 65, 65, 65, 64, 64, 65, 64, 64, 64, 65, -62, 60, 61, 61, 61, 61, 61, 60, 61, 61, 60, 61, 61, 61, 60, 60, 51, 46, 46, 46, -47, 45, 46, 46, 46, 46, 46, 46, 47, 46, 46, 46, 41, 39, 38, 39, 39, 39, 39, 39, -39, 39, 38, 39, 39, 39, 38, 38, 39, 38, 36, 37, 37, 38, 37, 36, 37, 36, 36, 37, -36, 36, 37, 36, 35, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29, 29, 29, 29, 30, 29, -27, 21, 20, 20, 20, 20, 21, 21, 20, 20, 21, 21, 21, 21, 20, 21, 18, 12, 10, 12, -12, 11, 12, 11, 11, 11, 11, 12, 11, 12, 11, 11, 10, 8, 7, 8, 8, 8, 8, 8, 8, 7, -7, 7, 8, 8, 8, 8, 5, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, -4, -9, -8, --9, -9, -8, -8, -8, -9, -9, -9, -8, -8, -8, -8, -7, -18, -29, -30, -30, -30, --31, -30, -30, -30, -29, -30, -30, -30, -29, -29, -29, -32, -33, -33, -33, -33, --34, -33, -33, -34, -33, -33, -33, -34, -32, -33, -33, -39, -44, -44, -44, -44, --44, -44, -44, -43, -44, -44, -44, -44, -43, -43, -44, -45, -48, -48, -48, -46, --47, -47, -47, -48, -47, -47, -47, -47, -48, -48, -47, -54, -58, -58, -58, -58, --57, -58, -57, -57, -58, -57, -58, -57, -58, -57, -58, -63, -68, -66, -66, -66, --66, -66, -66, -66, -66, -66, -66, -66, -66, -66, -67, -74, -77, -77, -76, -76, --77, -76, -77, -77, -77, -77, -77, -77, -76, -76, -76, -77, -78, -79, -78, -78, --78, -78, -78, -78, -78, -78, -79, -79, -78, -79, -78, -81, -85, -86, -85, -86, --85, -85, -85, -85, -85, -85, -86, -85, -85, -85, -85, -87, -88, -89, -88, -89, --89, -88, -88, -87, -88, -88, -88, -88, -88, -89, -87, -89, -92, -91, -91, -92, --91, -91, -91, -91, -91, -91, -91, -91, -91, -92, -91, -92, -95, -95, -95, -95, --94, -95, -95, -95, -95, -95, -95, -93, -95, -94, -94, -96, -98, -98, -98, -98, --98, -98, -98, -98, -98, -98, -98, -97, -98, -99, -97, -102, -104, -105, -104, --105, -104, -105, -104, -105, -103, -105, -102, -106, -102, -108, -95, -15, }; - -#endif /* AKWF_oscchip_0001_512_H_ */ diff --git a/include/tables/akwf_piano.h b/include/tables/akwf_piano.h deleted file mode 100644 index 300cfe2..0000000 --- a/include/tables/akwf_piano.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef AKWF_piano_0001_512_H_ -#define AKWF_piano_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_piano_0001_512_NUM_CELLS 512 -#define AKWF_piano_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_piano_0001_512_DATA [] -= {0, 4, 8, 13, 16, 21, 25, 28, 31, 33, 33, 32, 32, 31, 31, 30, 30, 32, 33, 35, -37, 38, 41, 44, 44, 46, 48, 48, 49, 49, 48, 46, 45, 42, 39, 36, 33, 32, 30, 31, -34, 36, 42, 50, 58, 68, 78, 88, 98, 106, 113, 120, 123, 125, 124, 123, 119, 114, -108, 101, 94, 85, 78, 72, 66, 59, 54, 51, 46, 42, 37, 33, 27, 22, 16, 10, 3, -2, --5, -7, -7, -4, 1, 8, 16, 25, 35, 45, 54, 62, 69, 75, 80, 83, 84, 85, 83, 80, -77, 72, 65, 58, 52, 44, 37, 30, 23, 17, 12, 6, 1, -5, -13, -19, -25, -32, -39, --45, -53, -60, -66, -72, -77, -83, -86, -89, -91, -92, -93, -92, -90, -89, -87, --84, -82, -80, -78, -76, -74, -71, -68, -66, -64, -62, -59, -56, -53, -49, -42, --36, -28, -20, -9, 1, 10, 20, 30, 37, 44, 50, 53, 56, 57, 56, 54, 52, 49, 45, -43, 41, 38, 38, 37, 37, 37, 39, 40, 42, 45, 47, 49, 51, 53, 56, 59, 64, 69, 75, -81, 87, 92, 97, 99, 102, 101, 100, 96, 93, 88, 82, 75, 68, 60, 53, 47, 40, 35, -32, 29, 27, 25, 24, 25, 25, 27, 29, 32, 36, 38, 41, 43, 45, 47, 48, 48, 49, 47, -46, 44, 41, 38, 35, 31, 27, 24, 21, 18, 15, 12, 9, 5, 3, 0, -2, -3, -6, -8, -9, --10, -12, -13, -12, -12, -11, -10, -8, -7, -5, -5, -5, -6, -6, -7, -9, -9, -12, --16, -21, -27, -33, -41, -51, -59, -66, -72, -77, -80, -81, -82, -81, -78, -76, --72, -70, -68, -66, -66, -64, -62, -61, -60, -58, -56, -55, -54, -54, -54, -55, --57, -59, -61, -63, -66, -67, -68, -67, -66, -62, -55, -48, -39, -29, -20, -12, --4, 3, 9, 14, 17, 20, 23, 24, 25, 25, 23, 23, 19, 15, 11, 4, -2, -10, -19, -26, --34, -40, -45, -48, -50, -49, -47, -44, -40, -35, -30, -24, -17, -10, -2, 6, 15, -25, 33, 43, 52, 60, 67, 73, 77, 80, 81, 79, 76, 72, 66, 62, 57, 53, 51, 47, 47, -46, 47, 48, 48, 50, 51, 53, 55, 56, 58, 60, 64, 67, 71, 75, 79, 83, 84, 84, 84, -82, 79, 75, 69, 65, 59, 53, 47, 41, 35, 29, 21, 16, 8, 2, -4, -9, -14, -19, -21, --25, -27, -28, -29, -31, -32, -33, -35, -39, -41, -46, -50, -56, -63, -69, -75, --82, -88, -93, -97, -100, -104, -107, -109, -113, -116, -120, -122, -125, -126, --128, -128, -127, -123, -118, -112, -104, -95, -85, -74, -64, -54, -45, -38, --33, -29, -27, -27, -28, -32, -36, -42, -49, -57, -65, -72, -80, -87, -92, -96, --99, -100, -102, -101, -101, -98, -98, -96, -95, -94, -93, -92, -91, -89, -86, --84, -82, -78, -74, -70, -64, -60, -55, -48, -41, -35, -30, -23, -19, -15, -14, --12, -12, -12, -14, -14, -14, -13, -9, -6, -1, }; - -#endif /* AKWF_piano_0001_512_H_ */ diff --git a/include/tables/akwf_violin.h b/include/tables/akwf_violin.h deleted file mode 100644 index cb01c98..0000000 --- a/include/tables/akwf_violin.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef AKWF_violin_0001_512_H_ -#define AKWF_violin_0001_512_H_ - -#if ARDUINO >= 100 -#include "Arduino.h" -#else -#include "WProgram.h" -#endif -#include - -#define AKWF_violin_0001_512_NUM_CELLS 512 -#define AKWF_violin_0001_512_SAMPLERATE 512 - -const int8_t __attribute__((section(".progmem.data"))) AKWF_violin_0001_512_DATA -[] = {1, 2, 2, 3, 2, 2, 2, 1, 1, 2, 3, 5, 9, 15, 22, 31, 39, 47, 55, 63, 73, 83, -91, 100, 108, 114, 120, 125, 127, 127, 127, 124, 120, 115, 109, 102, 95, 86, 79, -70, 62, 55, 48, 42, 38, 31, 28, 27, 26, 26, 29, 33, 37, 43, 48, 55, 61, 66, 72, -78, 82, 88, 92, 96, 98, 99, 99, 98, 98, 99, 99, 99, 99, 100, 100, 101, 102, 102, -103, 102, 101, 98, 96, 92, 88, 81, 77, 69, 63, 56, 49, 42, 33, 25, 18, 10, 2, --6, -15, -23, -31, -40, -48, -56, -61, -66, -70, -73, -75, -76, -76, -78, -77, --77, -76, -76, -74, -74, -73, -73, -72, -71, -71, -69, -68, -69, -69, -70, -70, --72, -73, -76, -77, -79, -83, -86, -89, -92, -95, -97, -101, -102, -104, -105, --106, -106, -106, -105, -103, -101, -96, -93, -89, -84, -79, -73, -66, -60, -53, --48, -43, -36, -31, -26, -21, -16, -12, -8, -7, -5, -4, -3, -4, -4, -5, -6, -7, --8, -9, -11, -12, -14, -15, -17, -20, -22, -25, -27, -30, -32, -35, -37, -38, --41, -41, -43, -41, -41, -38, -35, -31, -26, -21, -15, -10, -3, 2, 10, 15, 23, -28, 33, 38, 43, 46, 50, 53, 54, 55, 57, 58, 58, 57, 57, 58, 57, 55, 54, 53, 52, -50, 49, 48, 46, 43, 41, 39, 36, 34, 32, 30, 28, 27, 26, 24, 24, 24, 24, 23, 23, -24, 23, 24, 23, 23, 23, 21, 21, 20, 19, 19, 17, 18, 16, 16, 15, 15, 15, 16, 14, -16, 16, 17, 19, 20, 22, 24, 26, 29, 31, 33, 34, 37, 38, 38, 39, 37, 36, 34, 32, -28, 25, 21, 16, 12, 8, 5, 1, -2, -5, -6, -8, -8, -7, -5, -3, 0, 3, 6, 9, 12, 16, -19, 22, 23, 27, 28, 28, 28, 28, 27, 27, 26, 26, 24, 23, 23, 21, 21, 21, 20, 20, -20, 19, 20, 20, 19, 18, 17, 15, 14, 11, 10, 7, 5, 2, -1, -3, -5, -6, -9, -9, --10, -10, -8, -8, -7, -5, -4, -1, 0, 1, 2, 2, 2, 2, 2, 1, 0, -1, -3, -4, -7, -7, --9, -10, -12, -14, -14, -16, -16, -17, -17, -16, -16, -15, -14, -13, -13, -12, --11, -9, -9, -7, -7, -7, -6, -4, -4, -4, -4, -3, -2, -2, -1, -1, 0, -1, 0, -1, --1, -1, 0, -1, -1, -1, -2, -2, -3, -4, -5, -6, -8, -10, -11, -12, -15, -18, -21, --23, -24, -25, -25, -26, -26, -25, -25, -25, -24, -24, -23, -22, -22, -23, -23, --24, -25, -25, -26, -28, -31, -35, -37, -41, -45, -50, -55, -59, -64, -68, -73, --76, -79, -82, -85, -87, -88, -87, -87, -84, -80, -78, -72, -67, -62, -57, -51, --47, -42, -37, -33, -31, -29, -28, -29, -29, -30, -33, -36, -40, -44, -47, -49, --52, -54, -55, -56, -55, -53, -48, -44, -40, -35, -30, -25, -20, -17, -12, -9, --5, -3, 0, }; - -#endif /* AKWF_violin_0001_512_H_ */ diff --git a/include/tables/noise2048_int8.h b/include/tables/noise2048_int8.h new file mode 100644 index 0000000..955de09 --- /dev/null +++ b/include/tables/noise2048_int8.h @@ -0,0 +1,140 @@ +#ifndef NOISE2048_INT8_H_ +#define NOISE2048_INT8_H_ + +#include + +#define NOISE2048_NUM_CELLS 2048 +#define NOISE2048_SAMPLERATE 2048 + +const int8_t __attribute__((section(".progmem.data"))) NOISE2048_DATA [] = { +-71, -116, 12, -3, -14, -57, -76, -84, 88, -112, -113, -81, -17, -9, -115, -27, +86, -16, 101, 14, -125, -47, 88, 46, 14, -49, -18, 44, -76, -81, 66, -79, +55, 48, 7, -106, 107, -65, 65, -88, 22, 57, -30, -93, -105, -12, 20, -88, +-9, -77, 66, 14, 104, 58, -45, 61, 53, -21, 8, -92, -41, -3, -45, 108, +66, 10, -16, 38, -100, -11, -112, 33, 77, 9, -95, -20, 33, -20, 127, 74, +106, -55, 7, -57, -2, 6, 91, 76, 57, -16, -58, 124, -82, -104, -72, -50, +-47, 88, -96, 69, 67, 111, 0, -123, -70, 8, 46, -71, 22, 94, -48, 104, +-127, 6, -37, -74, 24, -27, -50, 63, -46, -128, 37, 122, -119, -71, 57, 29, +-6, -99, -5, -88, -85, 120, -93, -64, -63, 115, -44, 7, 88, -20, -26, 31, +76, 63, 96, 103, -67, -2, -13, -96, 45, -118, -11, -16, -125, -92, -98, -11, +-94, -112, 41, -92, -7, 14, 120, -19, -61, 114, -4, 114, 80, -31, -80, -79, +92, 53, 88, 82, 111, -101, -78, -97, 78, 45, -73, -1, -30, -31, 101, -57, +88, -35, 14, 108, -1, -90, 98, -78, -103, -121, -81, -7, -43, 80, 120, 118, +-19, 77, -98, -44, 66, -127, 71, 7, 104, 18, 88, 121, -49, -31, 23, -17, +-99, -97, 32, -99, -103, 116, -48, -99, -87, -33, -93, -94, -8, 78, -67, -2, +-108, -87, 86, 33, 5, -24, 32, -6, 7, 74, -61, 25, 106, 33, -91, -124, +106, -77, -91, -19, 7, -61, 50, -93, -3, 61, 17, -48, 96, 26, -124, 25, +-75, -60, 7, -69, -74, -49, 11, 16, -21, 47, -24, 7, 122, 0, -102, -81, +88, 13, -106, -127, 42, -62, 6, -46, 98, 90, -124, -71, -90, -52, -110, 61, +-53, 92, -63, -107, 29, 58, -108, 55, -21, -1, -76, 53, 80, -49, -7, -45, +-38, 83, -116, -37, 42, 82, -1, 8, -47, -73, 67, -109, 112, -15, -26, 107, +51, 28, -12, -14, -116, -30, 76, 40, 14, -93, 14, 51, 76, 41, -114, -69, +5, -37, 7, -109, -73, 94, 48, 32, 95, -69, 69, -31, 2, -106, 95, -128, +-28, 58, 92, -93, 41, 32, -65, 25, 30, 81, 39, 78, 23, -63, -30, 87, +66, -39, 26, 79, -128, 27, 18, -21, 92, 36, 110, 98, 98, -19, 114, -42, +-85, 17, 43, -81, -8, 30, -13, -27, -53, -116, -105, -3, 115, -91, 105, 84, +-29, 68, 125, 76, -4, -53, -126, -74, 89, -16, -38, 109, -103, -1, -66, 105, +-60, 109, 34, 98, 90, 100, -47, 115, 102, 4, -2, 13, 120, -6, 12, 97, +-89, 18, -8, 11, 43, 35, -87, -58, -51, -10, 68, -50, -19, -96, 84, 80, +41, 110, 84, -97, -23, 87, 71, -118, 66, 116, -125, 52, 24, 71, 86, -16, +121, -16, 11, 95, 120, -114, 71, 44, 79, -44, 111, -63, -115, 73, -115, -86, +91, -59, 108, -35, -103, 5, 66, 39, -20, 104, 39, 44, 66, 14, 87, 1, +-87, 112, -119, -102, 51, -14, -93, -108, -113, -2, -26, -118, -50, -6, -64, 114, +-70, -17, 110, 3, 60, -43, -70, -45, 31, -73, -115, 31, 64, 75, -27, -90, +-4, -76, 26, -67, -107, 49, 91, 61, -93, 46, -122, 87, 122, -74, 93, 57, +107, -50, 94, -38, 10, 119, 110, 95, 9, 37, -3, -84, 14, 102, -4, 109, +66, 44, -114, 125, 38, -35, 121, -20, 53, 4, 46, 15, 13, -123, -31, -85, +-5, 80, 122, -5, 115, 123, 101, -120, -81, 22, -15, 79, -4, 28, 60, 114, +48, 89, 41, 52, 104, 10, 28, 0, -10, -67, -30, 33, -67, -34, -30, -18, +119, 13, 16, -77, -29, 23, -12, 56, -37, 26, -121, -64, 12, -105, -101, 21, +-64, 123, -76, -122, 17, 112, 117, 97, 46, -34, -102, 1, 116, -70, -95, 77, +123, -91, -101, -51, -52, 27, -85, -1, -68, 85, -13, 66, 102, 98, 24, 91, +28, -98, -78, -22, -20, 7, -87, -48, -6, -40, -90, -48, -127, 81, 102, 112, +21, -112, -10, 19, 16, 104, -92, -9, 7, -27, 89, -70, -13, -52, 8, -56, +-92, -98, -44, 29, 19, 96, -65, 111, 27, 78, 11, 124, 96, -87, -108, 93, +37, 0, -115, -82, -11, -118, 9, -108, -39, 112, 98, 14, -36, 95, 123, -82, +112, 50, 81, 42, 36, -75, -46, 40, 82, 125, 19, 77, -110, 104, -83, 33, +1, 37, -69, 78, -128, 108, 83, -101, -32, 57, 127, 98, -102, -24, 8, -61, +19, 96, 120, -66, -114, -6, -47, 31, -121, 80, -81, -13, -70, 108, -68, -50, +127, 21, 11, 84, 119, 113, -4, 105, -54, 68, -31, -59, -93, 13, 84, 46, +8, -127, 16, 24, 122, -52, 100, 119, 48, 42, 65, 105, 36, -32, -6, 68, +-9, 82, -106, 34, 114, 67, 69, -51, 125, -110, -64, 41, -77, 97, -77, 105, +-121, -55, 81, -49, -90, 112, 7, 45, 75, -87, 40, 66, 34, 121, -110, -93, +-8, 19, -12, -82, 94, -78, -77, 99, -43, 25, -114, -105, 38, -100, 22, 55, +63, 92, -54, -3, 82, -36, -41, -39, -88, 67, -5, 126, -55, -10, 108, 2, +107, 2, -124, 110, 19, -48, -91, 98, 48, 25, 89, 0, 105, 26, -27, 68, +119, -74, -7, 67, 55, 23, 23, -117, 74, 12, -124, -103, 126, 18, -11, 52, +-16, -31, 0, -58, -79, -108, 30, 97, -111, 58, -61, -82, 23, 39, 84, -39, +-26, -61, 59, 11, -44, 3, 118, 23, 45, -70, 111, -90, -56, -13, 75, 59, +-82, 74, -121, 7, -65, 104, 60, 6, 67, 62, -73, -9, 113, -116, 39, -15, +-96, 109, 26, 81, -69, -57, -105, -109, 27, 124, -69, -79, -8, -59, 70, 104, +61, 86, -49, 84, -78, 122, 80, 15, -112, 61, -17, 99, 99, -8, 57, -78, +60, 55, -97, 75, 13, -31, -66, 104, -82, -20, -118, -103, 42, -4, -64, -23, +-93, -22, -18, -9, 40, -53, -127, 13, -54, -62, 0, -39, -72, -115, -61, -121, +55, -7, 37, -120, -39, 7, -102, -64, 87, -70, -96, 115, 101, 57, -73, 103, +-15, -106, 26, 106, -113, -97, 117, 77, 90, -73, 123, 99, -91, -87, 36, -53, +-95, -64, 12, 38, 67, 22, 104, 92, -78, -70, -18, 92, 103, -12, 83, 45, +104, 76, 84, -80, 32, 90, 32, 2, 63, -50, 114, -94, -82, -85, -81, 93, +-79, 62, -62, -98, 40, -66, 82, 53, 88, -102, 19, 31, 52, -75, -20, -49, +118, -14, -73, 51, 60, -70, 14, -13, 91, -115, 8, -114, -36, 11, 30, 45, +51, -125, -36, -55, 77, -93, -56, -113, -82, -18, 64, 86, 104, 46, -48, 61, +31, 38, -85, -102, -49, -48, -103, -87, 11, 98, 89, 120, 98, 84, 11, -18, +-70, 48, 92, -72, 17, 121, 29, -105, -16, 74, -100, -125, -24, 26, -20, -58, +2, 20, 39, -67, -125, 126, 92, -39, -62, 66, -11, 53, -92, 75, -107, 95, +-119, 107, -89, 32, 91, 79, 85, 20, -70, 79, -118, 38, -41, 107, 57, -83, +95, -74, -4, 95, 77, -88, 74, 30, 45, -15, 42, -42, -89, -70, -29, 50, +51, -53, -8, -76, -54, 3, -27, -40, -50, -90, -38, 124, 109, 101, 37, 33, +-51, 97, -94, 112, 98, 27, 12, -100, 52, -91, 30, 108, 103, -109, -99, 60, +18, -89, -82, 68, 108, -107, 102, -32, 36, 115, -51, -97, 102, -76, 47, -85, +-40, -108, -2, 96, 96, -47, 58, 62, 16, 70, 81, 45, -102, 43, -95, 40, +-80, 69, 17, 1, -52, 42, -87, -56, 51, 30, 72, -62, -85, 30, 64, 40, +-63, -81, 88, 57, -119, 57, 30, -36, -19, 46, 120, -30, -13, -58, -49, -89, +23, -77, -109, 44, -61, 64, -50, -45, -36, -44, 96, -106, 82, 58, -7, 99, +17, 101, -9, -6, 30, 112, -29, 60, 97, 108, 16, 67, 86, -46, -26, -58, +0, -102, 118, 62, -76, -65, 17, -86, -46, 11, 102, -53, 95, -82, -15, 102, +50, -115, 84, -101, 74, 63, -8, 69, -87, 63, -14, -114, 35, -78, 43, -54, +-58, -109, 18, 113, -57, 112, 101, -126, -88, -119, 3, -18, -52, 88, -72, 19, +-7, 26, -66, -104, -6, 86, 106, -96, -72, 127, -120, -5, -55, 21, 91, -128, +52, -5, 85, -33, -85, 56, -94, -118, 71, 112, -106, 70, 63, 1, -120, 54, +-94, 48, -5, -75, 42, -60, -106, 52, 45, -39, 109, 117, -35, -59, -96, 106, +-110, 22, -25, -106, -26, -107, 33, 30, 75, 114, 1, -110, -31, 18, 54, -104, +41, 11, -65, 60, 95, 76, 97, 69, 45, -33, 126, 126, 60, 8, -86, 89, +-88, 92, -36, 22, 36, -76, -88, 39, 23, 28, 100, 90, -43, 99, 51, 100, +-107, 52, 94, 12, -99, -90, 79, 58, -47, -113, -55, 96, -111, -64, -94, -8, +59, 57, 68, -112, -50, 102, 61, 62, 99, -89, -58, 59, 75, 32, 14, -1, +-70, -115, -33, 127, 70, -68, 6, 5, 100, -19, 18, 123, -26, -66, -59, -91, +103, -40, 99, -84, 35, 49, -95, 20, 25, -48, -39, 57, -14, -66, -26, -57, +-7, 124, -115, 56, 60, 111, -62, -84, -95, 30, 75, 117, 82, 81, -91, -64, +34, -91, 102, 110, 48, -63, -35, -62, 93, -100, -65, -50, 27, -44, -46, 37, +-13, 49, 17, -88, 0, -28, 12, -64, 27, -81, -42, -50, -41, 44, -107, -114, +-87, -105, 7, -21, 85, -113, 126, 20, 26, 119, -3, 79, 24, 104, -91, -98, +-48, 97, 84, 119, 109, -24, 46, -55, 32, 35, 48, 76, -62, 61, -74, 35, +-5, 110, -66, 8, 102, -2, -56, -79, -103, 20, 68, 86, -1, -47, 39, 32, +-31, -47, 127, 111, 127, 29, 126, -117, -82, 73, 106, -5, -18, 52, -104, -103, +16, 125, 112, 18, -124, -73, 92, -60, 7, 59, 78, 59, -105, 77, -102, -29, +57, 19, -91, 69, 102, 15, -68, -63, -79, 73, 63, 45, 59, -55, -27, 77, +-108, -105, -109, -58, 42, 114, 105, -52, -57, 39, 35, -45, 73, 25, 44, 122, +25, 115, -120, 60, 41, -72, 85, 29, -115, 114, 7, -12, -102, 117, -41, 66, +-53, -4, -112, -72, -31, -119, 97, 32, 86, -51, 83, -24, 82, 113, -97, -58, +-22, 38, 116, 64, 32, -40, 107, 47, 53, 7, 119, -30, -2, 14, 24, -13, +24, 19, -22, 122, 34, 117, 50, 12, 19, -66, 66, 74, 48, -54, 20, -107, +19, -88, 49, 98, 3, 117, -19, -25, 10, 11, -58, -73, -6, -4, -103, -13, +-9, -102, -77, 83, 41, 113, -77, -58, -126, -48, 80, 115, 116, -26, 19, 36, +18, -98, -83, -10, -109, -39, 85, -38, -110, 75, 125, -33, 20, -109, -124, 24, +-74, 43, 17, 104, 124, -60, 111, 11, -30, -71, 41, -45, 106, 3, -33, -121, +44, 23, -30, -39, 79, 90, 39, -84, 77, -80, -34, -57, 116, 37, -1, -125, +5, 68, -8, 100, 8, 40, 26, -123, 5, 56, -8, -97, -68, 110, 29, -47, +79, 31, -68, 23, 60, -15, -16, -60, 116, -50, 104, 63, 84, 113, -17, -2, +-87, 100, 57, -89, -71, -97, -25, -52, -44, 39, 98, -69, -23, 122, -82, 100, +-100, 104, -61, 84, 105, -99, 108, 29, -117, 74, 2, -127, -17, -91, -105, 88, +48, -96, -98, -93, 113, -112, 18, 81, -36, -59, 87, 63, 67, 101, 65, 64, +-87, -60, 50, -68, -37, 73, -63, -14, -127, -117, 24, 109, 89, 66, -11, -2, +107, 49, -49, 13, -32, -71, -112, 86, -120, -5, -23, -94, -77, -111, 100, -104, +-3, -106, 77, 96, -9, -17, -100, -57, 20, -9, 35, 36, -7, 26, -55, -15, +83, 25, 12, -97, -39, 90, 125, -104, 48, 67, 35, 85, 81, -52, 25, 64, +-34, 114, -5, -13, 25, -55, 109, -99, 83, 85, -60, 70, -4, 2, -24, 41, +-88, 102, 61, -81, -31, -102, 9, 65, -108, -91, -32, -17, 117, -22, 42, 27, +-121, -20, -31, -68, 117, -4, -24, 75, -6, 36, 16, 66, 110, 55, 29, 5 +}; + +#endif // NOISE2048_INT8_H_ diff --git a/platformio.ini b/platformio.ini index 849d1ac..02a3692 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,6 +15,16 @@ framework = arduino monitor_speed = 2000000 lib_deps = - sensorium/Mozzi@^2.0.0 + sensorium/Mozzi + smougenot/TM1637@0.0.0-alpha+sha.9486982048 build_flags = -O3 -flto -DMOZZI_ANALOG_READ_RESOLUTION=10 + +[env:uno_debug] +extends = env:uno +lib_deps = + sensorium/Mozzi + smougenot/TM1637@0.0.0-alpha+sha.9486982048 + jdolinay/avr-debugger @ ~1.4 +debug_tool = avr-stub +debug_port = /dev/ttyACM1 \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..befaf7f --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,50 @@ +{ + "version": 1, + "skills": { + "arduino-code-generator": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "6102f632dee7865e2e44ac72b59ab33e5993e56aaa10a76c800eea60c2882625" + }, + "arduino-project-builder": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "36b40762569f5e9355063abedcd9d6026bb137f1d2bef7e1ba5b41c5193acf10" + }, + "arduino-serial-monitor": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "5b831c24c4e468bb2334133b3c0c9605993b0243fc1b7956d48b4c34d7c15f91" + }, + "bom-generator": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "79716e8d7cadc27544d293ade24c42dd568245e54939b3e4819419a205c92795" + }, + "circuit-debugger": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "a52991fc792b7ce924654f3242e055b302cc6a668a8937ea8dec0b7edba2f55d" + }, + "code-review-facilitator": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "bcb9420f86055768e265a9b762e84ad60f1c00c2140b2b5c98f4033348c21985" + }, + "datasheet-interpreter": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "a8362fe4d53c340f11e66c8964f48b07728111535a96353027eb1dc646d0ea91" + }, + "error-message-explainer": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "5d42517ce2a648ab23f4ebcf8c147d421dd80a2453fd8cda0fae05677a295269" + }, + "readme-generator": { + "source": "wedsamuel1230/arduino-skills", + "sourceType": "github", + "computedHash": "d6024318d1d1521cb7f767095e06cd448e3ea58b5035288576e7eab210f02690" + } + } +} diff --git a/src/buttons.cpp b/src/buttons.cpp index b84c755..209f388 100644 --- a/src/buttons.cpp +++ b/src/buttons.cpp @@ -1,41 +1,40 @@ #include "buttons.h" #include "config.h" -#include "player.h" +#include "display.h" #include "protocol.h" -#include "recorder.h" #include "sound.h" -extern void triggerFeedbackLed(); // Forward declaration or include leds.h later +extern void triggerFeedbackLed(); + +// Access global tick counter from sound.cpp (Optimization 1) +using Sound::controlTicks; // SIMPLIFIED BUTTON HANDLING -// Polled at 128Hz from updateControl() - natural debouncing from INPUT_PULLUP +// Polled at 64Hz from updateControl() - natural debouncing from INPUT_PULLUP struct Button { const uint8_t pin; - const char *name; bool wasPressed = false; - unsigned long lastTriggerTime = 0; + uint16_t lastTriggerTick = 0; // Prevent retriggering for 200ms after activation - static const unsigned long COOLDOWN = 200; + // 200ms at 64Hz = 12.8 ticks, round to 13 + static const uint8_t COOLDOWN_TICKS = 13; - Button(uint8_t p, const char *n) : pin(p), name(n) {} + Button(uint8_t p) : pin(p) {} bool update() { // INPUT_PULLUP: HIGH = Released, LOW = Pressed bool isPressed = (digitalRead(pin) == LOW); - unsigned long now = millis(); + uint32_t now = controlTicks; - // Edge detection: trigger on press (not release) if (isPressed && !wasPressed) { - // Check cooldown to prevent accidental double-triggers - if (now - lastTriggerTime > COOLDOWN) { + if (now - lastTriggerTick > COOLDOWN_TICKS) { wasPressed = true; - lastTriggerTime = now; - return true; // TRIGGER ACTION + lastTriggerTick = now; + return true; } } - // Update state if (!isPressed) { wasPressed = false; } @@ -44,62 +43,50 @@ struct Button { } }; -Button btnRec(PIN_BTN_REC, "REC"); -Button btnStop(PIN_BTN_STOP, "STOP"); -Button btnPlay(PIN_BTN_PLAY, "PLAY"); -Button btnClear(PIN_BTN_CLEAR, "CLEAR"); +// Buttons A0-A3: I=Intensity, r=Rate, n=Ratio, W=Wave(cycle carrier) +Button btn0(PIN_BTN_REC); // A0 β†’ Param 0 +Button btn1(PIN_BTN_STOP); // A1 β†’ Param 1 +Button btn2(PIN_BTN_PLAY); // A2 β†’ Param 2 +Button btn3(PIN_BTN_CLEAR); // A3 β†’ Param 3 void setupButtons() { - // Use INPUT_PULLUP to prevent floating pins (crosstalk) - pinMode(PIN_BTN_REC, INPUT_PULLUP); - pinMode(PIN_BTN_STOP, INPUT_PULLUP); - pinMode(PIN_BTN_PLAY, INPUT_PULLUP); + pinMode(PIN_BTN_REC, INPUT_PULLUP); + pinMode(PIN_BTN_STOP, INPUT_PULLUP); + pinMode(PIN_BTN_PLAY, INPUT_PULLUP); pinMode(PIN_BTN_CLEAR, INPUT_PULLUP); + + // Mozzi startMozzi() may disable digital input for analog pins (A0-A5) + // using DIDR0. Re-enable these pins used by buttons explicitly. + DIDR0 &= ~((1 << ADC0D) | (1 << ADC1D) | (1 << ADC2D) | (1 << ADC3D)); } void checkButtons() { - static uint8_t scanIndex = 0; - - switch (scanIndex) { - case 0: - if (btnRec.update()) { - Protocol::respond(F("BTN_PRESS:REC")); - triggerFeedbackLed(); - Player::stop(); - Recorder::startRecording(); - } - break; - case 1: - if (btnStop.update()) { - Protocol::respond(F("BTN_PRESS:STOP")); - triggerFeedbackLed(); - Recorder::stopRecording(); - Recorder::stopPlayback(); - Player::stop(); - Sound::stop(); - } - break; - case 2: - if (btnPlay.update()) { - Protocol::respond(F("BTN_PRESS:PLAY")); - triggerFeedbackLed(); - Player::stop(); - Recorder::stopRecording(); - Recorder::startPlayback(); - } - break; - case 3: - if (btnClear.update()) { - Protocol::respond(F("BTN_PRESS:CLEAR")); - triggerFeedbackLed(); - Recorder::stopRecording(); - Recorder::stopPlayback(); - Recorder::clear(); - } - break; + if (btn0.update()) { + Protocol::respond(F("BTN_PRESS:0")); + triggerFeedbackLed(); + displaySelectParam(0); } - scanIndex++; - if (scanIndex > 3) - scanIndex = 0; + if (btn1.update()) { + Protocol::respond(F("BTN_PRESS:1")); + triggerFeedbackLed(); + displaySelectParam(1); + } + + if (btn2.update()) { + Protocol::respond(F("BTN_PRESS:2")); + triggerFeedbackLed(); + uint8_t newMode = Sound::nextFMMode(); + displayShowWave(newMode); // repurpose wave display for mode + Protocol::respond(F("SYNC:FM"), (uint16_t)newMode); + } + + if (btn3.update()) { + Protocol::respond(F("BTN_PRESS:3")); + triggerFeedbackLed(); + uint8_t newWave = Sound::nextCarrierWave(); + displayShowWave(newWave); + Protocol::respond(F("SYNC:CW"), (uint16_t)newWave); + } } + diff --git a/src/display.cpp b/src/display.cpp new file mode 100644 index 0000000..1d2b649 --- /dev/null +++ b/src/display.cpp @@ -0,0 +1,194 @@ +#include "display.h" +#include "config.h" +#include "sound.h" +#include "protocol.h" +#include + +TM1637Display tm(PIN_DISPLAY_CLK, PIN_DISPLAY_DIO); + +#include "char_digits.h" + +#define CHAR_C 0x39 +#define CHAR_D 0x5e // 'd' +#define CHAR_E 0x79 +#define CHAR_F 0x71 // 'F' +#define CHAR_G 0x3d // 'G' +#define CHAR_A 0x77 // 'A' +#define CHAR_B 0x7c // 'b' +#define CHAR_SHARP 0x63 // degree symbol for sharp (upper circle) + +uint8_t getNoteSegment(uint8_t noteIndex) { + switch(noteIndex) { + case 0: return CHAR_C; + case 1: return CHAR_C; // C# + case 2: return CHAR_D; + case 3: return CHAR_D; // D# + case 4: return CHAR_E; + case 5: return CHAR_F; + case 6: return CHAR_F; // F# + case 7: return CHAR_G; + case 8: return CHAR_G; // G# + case 9: return CHAR_A; + case 10: return CHAR_A; // A# + case 11: return CHAR_B; + } + return 0; +} + +bool isSharp(uint8_t noteIndex) { + return noteIndex == 1 || noteIndex == 3 || noteIndex == 6 || noteIndex == 8 || noteIndex == 10; +} + +void setupDisplay() { + tm.setBrightness(0x0f); // Max brightness + tm.clear(); +} + +// Display state machine for parameter editing and note view +enum DisplayState { + DISPLAY_NOTE, + DISPLAY_PARAM_A, + DISPLAY_PARAM_B, + DISPLAY_PARAM_C, + DISPLAY_WAVE // carrier wave name (no pot) +}; + +static DisplayState displayState = DISPLAY_NOTE; +static uint16_t displayTimeoutTicks = 0; +static int lastPotValue = -1; +static const uint8_t debounceThreshold = 5; + +// Cached FM parameter values (0=ModAmount, 1=ModRatio) +static uint16_t fmParamVals[2] = {512, 0}; + +// Wave name 7-segment patterns (4 digits each) in PROGMEM +// SIN: S-I-n, SAW: S-A-U, rSA: r-S-A (RevSaw), SQr: S-q-r, Prn: P-r-n (Pseudorandom), nUL: n-U-L (Null) +static const uint8_t WAVE_SEGS[6][4] PROGMEM = { + {0x6D, 0x06, 0x54, 0x00}, // SIN: S I n _ + {0x6D, 0x77, 0x3E, 0x00}, // SAW: S A U _ + {0x50, 0x6D, 0x77, 0x00}, // rSA: r S A _ + {0x6D, 0x67, 0x50, 0x00}, // SQr: S q r _ + {0x73, 0x50, 0x54, 0x00}, // Prn: P r n _ + {0x54, 0x3E, 0x38, 0x00}, // nUL: n U L _ +}; +static uint8_t displayWaveId = 0; + +void displaySelectParam(uint8_t paramIdx) { + if (paramIdx > 1) return; // only 0-1 valid (wave uses displayShowWave) + displayState = (DisplayState)(DISPLAY_PARAM_A + paramIdx); + displayTimeoutTicks = 320; // 5s at 64Hz + lastPotValue = -1; // Force update on next pot read +} + +void displayShowWave(uint8_t waveId) { + if (waveId >= FM::WAVE_COUNT) waveId = 0; + displayWaveId = waveId; + displayState = DISPLAY_WAVE; + displayTimeoutTicks = 320; // 5s at 64Hz +} + +uint8_t displayGetSelectedParam() { + if (displayState == DISPLAY_NOTE) return 0; + return (uint8_t)(displayState - DISPLAY_PARAM_A); +} + +// Called from controlUpdate() with raw A5 pot value (0-1023). +void displayUpdatePot(int potValue) { + if (potValue < 0) potValue = 0; + if (potValue > 1023) potValue = 1023; + + if (abs(potValue - lastPotValue) < debounceThreshold) return; + lastPotValue = potValue; + + if (displayState == DISPLAY_NOTE) return; + + uint8_t idx = (uint8_t)(displayState - DISPLAY_PARAM_A); + displayTimeoutTicks = 320; + + switch (idx) { + case 0: { // Mod Amount: 0-1023 + uint16_t val = (uint16_t)potValue; + if (val != fmParamVals[0]) { + fmParamVals[0] = val; + Sound::setModAmount(val); + Protocol::respond(F("SYNC:MA"), val); + } + break; + } + case 1: { // Mod Ratio: 0-1023 + uint16_t val = (uint16_t)potValue; + if (val != fmParamVals[1]) { + fmParamVals[1] = val; + Sound::setModRatio(val); + Protocol::respond(F("SYNC:MR"), val); + } + break; + } + } +} + +// Render the 4-digit display for param editing or note view +void updateDisplay() { + static uint8_t tickCount = 0; + tickCount++; + if (tickCount < 6) return; + tickCount = 0; + + if (displayState != DISPLAY_NOTE) { + if (displayTimeoutTicks > 0) { + displayTimeoutTicks--; + } else { + displayState = DISPLAY_NOTE; + } + } + + uint8_t segments[4] = {0, 0, 0, 0}; + + if (displayState == DISPLAY_NOTE) { + uint8_t midiNote = Sound::getLatestMidiNote(); + if (midiNote == 0) { tm.clear(); return; } + uint8_t div12 = (midiNote * 43) >> 9; + uint8_t noteIdx = midiNote - (div12 * 12); + int8_t octave = div12 - 1; + segments[0] = getNoteSegment(noteIdx); + segments[1] = isSharp(noteIdx) ? CHAR_SHARP : 0; + segments[2] = 0; + segments[3] = (octave >= 0 && octave <= 9) ? CHAR_DIGITS[octave] : 0; + } else { + if (displayState == DISPLAY_WAVE) { + // Show wave name from PROGMEM segment patterns + for (uint8_t i = 0; i < 4; i++) + segments[i] = pgm_read_byte(&WAVE_SEGS[displayWaveId][i]); + } else { + // FM: A(mount) / r(atio) + static const uint8_t FM_CHARS[2] = { + 0x77, // 'A' (mod Amount) + 0x50 // 'r' (mod Ratio) + }; + uint8_t idx = (uint8_t)(displayState - DISPLAY_PARAM_A); + segments[0] = FM_CHARS[idx]; + uint16_t value = fmParamVals[idx]; + if (value >= 100) { + segments[1] = CHAR_DIGITS[value / 100 % 10]; + segments[2] = CHAR_DIGITS[value / 10 % 10]; + segments[3] = CHAR_DIGITS[value % 10]; + } else if (value >= 10) { + segments[1] = 0; + segments[2] = CHAR_DIGITS[value / 10 % 10]; + segments[3] = CHAR_DIGITS[value % 10]; + } else { + segments[1] = 0; + segments[2] = 0; + segments[3] = CHAR_DIGITS[value]; + } + } + } + + static uint8_t lastSegments[4] = {255, 255, 255, 255}; + bool changed = false; + for (uint8_t i = 0; i < 4; i++) { + if (segments[i] != lastSegments[i]) { changed = true; lastSegments[i] = segments[i]; } + } + if (changed) tm.setSegments(segments); +} + diff --git a/src/leds.cpp b/src/leds.cpp index 5a324ee..db2749f 100644 --- a/src/leds.cpp +++ b/src/leds.cpp @@ -98,10 +98,10 @@ void setRGB(uint8_t h, uint8_t s, uint8_t v) { } // Calculate required ticks for update delay -// MOZZI_CONTROL_RATE = 128Hz -> 1 tick = 7.8125ms -// Ticks = DelayMs / 7.8125 +// MOZZI_CONTROL_RATE = 64Hz -> 1 tick = 15.625ms +// Ticks = DelayMs / 15.625 constexpr uint8_t VISUALIZER_UPDATE_TICKS = - (uint8_t)(VISUALIZER_UPDATE_MS * 128 / 1000); + (uint8_t)(VISUALIZER_UPDATE_MS * 64 / 1000); void updateRGB() { // PERFORMANCE FIX: Use tick counter instead of millis() @@ -120,19 +120,10 @@ void updateRGB() { // Basic State bool soundActive = Sound::isPlaying(); - uint8_t vol = Sound::getVolume(); // 0-100 - - // Calculate Target Brightness - // "Dim not zero": Minimum brightness - uint8_t minBright = 15; - - // "More volume = brighter": Dynamic range scaled by volume - // Scale vol (0-100) to multiplier (0-200 roughly) - uint8_t volScale = vol * 2; uint8_t hue = 0; uint8_t sat = 255; - uint8_t val = minBright; // Start with baseline + uint8_t val = 15; // Minimum baseline brightness static uint8_t rainbowHue = 0; @@ -141,24 +132,22 @@ void updateRGB() { // Full Spectrum Mapping: C2 (65Hz) to C6 (1046Hz) -> Hue 0 (Red) to 215 // (Purple) Avoid 255 (Red again) to keep distinct endpoint - uint16_t freq = Sound::getPrimaryFreq(); - if (freq < 65) + uint16_t freq = Sound::getLatestNoteFreq(); + if (freq < 65) { hue = 0; // Red - else if (freq > 1046) + } else if (freq > 1046) { hue = 215; // Purple - else { - // Linear Map: (freq - 65) * 215 / (1046 - 65) - hue = (uint8_t)map(freq, 65, 1046, 0, 215); + } else { + // Linear Map: (freq - 65) * 215 / (1046 - 65) via O(1) bitshift math + // Equivalent to map(freq, 65, 1046, 0, 215) + hue = (uint8_t)(((freq - 65) * 56) >> 8); } // ENVELOPE -> BRIGHTNESS - // "Volume directly sets how bright" - // Brightness = EnvelopeLevel * MasterVolume - uint8_t level = Sound::getEnvelopeLevel(); // 0-255 - // Scale: (Level * Vol * 2) >> 8 - val = (level * volScale) >> 8; + // Direct envelope-to-brightness (hw pot controls actual volume) + val = level; // Sync rainbow rainbowHue = hue; diff --git a/src/main.cpp b/src/main.cpp index db75b0b..012cfa8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,17 +1,10 @@ /** * @file main.cpp * @brief Main entry point for the Arduino Piano project. - * - * Coordinates: - * - Mozzi Audio Engine (Sound) - * - Recording & Playback (Recorder, Player) - * - Serial Communication (Protocol) - * - User Input (Buttons - via buttons.h) - * - Feedback (LEDs - via leds.h) */ #include -// #include for sd card if have time +#include // --- MODULES --- #include "buttons.h" @@ -20,98 +13,138 @@ #include "player.h" #include "protocol.h" #include "recorder.h" +#include "display.h" #include "sound.h" -// Mozzi must be included in main.cpp for its hidden magic -#include - -// Wrapper for mozziAnalogRead so sound.cpp can use it without template linking errors -int getPotValue(uint8_t pin) { - return mozziAnalogRead<10>(pin); +// Called from sound.cpp to seed PRNG from floating A4 pin +uint32_t getNoiseSeed() { + return mozziAnalogRead(A4) + ((uint32_t)mozziAnalogRead(A4) << 16); } // ============================================================================ // COMMAND HANDLING // ============================================================================ -// Forward declaration for Mozzi callback -AudioOutput updateAudio(); - void handleCommand(Command cmd) { switch (cmd.type) { - /* - case CmdType::PING: -// Protocol::respond(F("PONG")); + case CmdType::NOTE_ON: + Sound::noteOn(cmd.value2, cmd.velocity); + Recorder::startNote(cmd.value2, cmd.velocity); break; - */ - - case CmdType::NOTE_ON: - Sound::noteOn(cmd.value1); - if (Recorder::isRecording()) { - Recorder::startNote(cmd.value1); - } - // No triggerFeedbackLed() for Note events - break; - - case CmdType::NOTE_OFF: - Sound::noteOff(cmd.value1); - if (Recorder::isRecording()) { + case CmdType::NOTE_OFF: + Sound::noteOffMidi(cmd.value2); Recorder::stopNote(); + break; + case CmdType::SET_VOLUME: + Sound::setVolume((uint8_t)cmd.value1); + break; + case CmdType::SET_ADSR: + Sound::setAmpADSR(cmd.value1, cmd.value2, cmd.sustain, cmd.release); + break; + case CmdType::SET_PITCH: + Sound::setPitchTranspose((int8_t)cmd.value1); + break; + case CmdType::MELODY_PLAY: + Player::play((uint8_t)cmd.value1); + break; + case CmdType::SET_MOD_AMOUNT: + Sound::setModAmount(cmd.value1); + break; + case CmdType::SET_LFO_RATE: + Sound::setLFORate(cmd.value1); + break; + case CmdType::SET_MOD_RATIO: + Sound::setModRatio(cmd.value1); + break; + case CmdType::SET_FM_MODE: + Sound::setFMMode((uint8_t)cmd.value1); + break; + case CmdType::SET_MOD_SHAPE: + Sound::setModShape(cmd.value1); + break; + case CmdType::SET_LFO_DEPTH: + Sound::setLFODepth((uint8_t)cmd.value1); + break; + case CmdType::SET_LFO_WAVEFORM: + Sound::setLFOWaveform((uint8_t)cmd.value1); + break; + case CmdType::SET_MOD_ENV_ATTACK: + Sound::setModEnvAttack(cmd.value1); + break; + case CmdType::SET_MOD_ENV_DECAY: + Sound::setModEnvDecay(cmd.value1); + break; + case CmdType::SET_MOD_ENV_SUSTAIN: + Sound::setModEnvSustain((uint8_t)cmd.value1); + break; + case CmdType::SET_VIBRATO_DEPTH: + Sound::setVibratoDepth((uint8_t)cmd.value1); + break; + case CmdType::SET_VIBRATO_SPEED: + Sound::setVibratoSpeed((uint8_t)cmd.value1); + break; + case CmdType::SET_PORTAMENTO: + Sound::setPortamento((uint8_t)cmd.value1); + break; + case CmdType::LOAD_FM_PRESET: + Sound::loadFMPreset((uint8_t)cmd.value1); + break; + case CmdType::SET_CARRIER_WAVE: + Sound::setCarrierWave((uint8_t)cmd.value1); + break; + case CmdType::SET_MOD_WAVE: + Sound::setModulatorWave((uint8_t)cmd.value1); + break; + case CmdType::REC_START: + Recorder::startRecording(); + break; + case CmdType::REC_STOP: + Recorder::stopRecording(); + break; + case CmdType::REC_PLAY: + Recorder::startPlayback(); + break; + case CmdType::REC_CLEAR: + Recorder::clear(); + break; + case CmdType::STOP_ALL: + Player::stop(); + Recorder::stop(); + Sound::stop(); + break; + case CmdType::GET_PARAMS: { + const FM::FMParams& p = Sound::getFMParams(); + // Modulation + Protocol::respond(F("SYNC:MA"), p.modAmount); + Protocol::respond(F("SYNC:MR"), p.modRatio); + Protocol::respond(F("SYNC:FM"), (uint16_t)p.fmMode); + Protocol::respond(F("SYNC:MS"), p.modShape); + // Mod envelope + Protocol::respond(F("SYNC:VA"), p.modEnvAttack); + Protocol::respond(F("SYNC:VD"), p.modEnvDecay); + Protocol::respond(F("SYNC:VS"), (uint16_t)p.modEnvSustain); + // LFO + Protocol::respond(F("SYNC:LD"), (uint16_t)p.lfoDepth); + Protocol::respond(F("SYNC:LR"), p.lfoRate); + Protocol::respond(F("SYNC:LW"), (uint16_t)p.lfoWaveform); + // Wavetables + Protocol::respond(F("SYNC:CW"), (uint16_t)p.carrierWave); + Protocol::respond(F("SYNC:MW"), (uint16_t)p.modWave); + // Amp ADSR + Protocol::respond(F("SYNC:EA"), p.ampAttack); + Protocol::respond(F("SYNC:ED"), p.ampDecay); + Protocol::respond(F("SYNC:ES"), (uint16_t)p.ampSustain); + Protocol::respond(F("SYNC:ER"), p.ampRelease); + // Preset + Protocol::respond(F("SYNC:FP"), (uint16_t)p.presetId); + // Performance + Protocol::respond(F("SYNC:VB"), (uint16_t)p.vibratoDepth); + Protocol::respond(F("SYNC:VP"), (uint16_t)p.vibratoSpeed); + Protocol::respond(F("SYNC:PT"), (uint16_t)p.portamento); + break; } - // No triggerFeedbackLed() for Note events - break; - - case CmdType::SET_INSTRUMENT: - Sound::setInstrument(cmd.value1); - break; - - case CmdType::SET_WAVEFORM: - Sound::setWaveform(cmd.value1); - break; - - case CmdType::SET_ADSR: - Sound::setCustomEnvelope(cmd.value1, cmd.value2, cmd.sustain, cmd.sustainlenght, cmd.release, - cmd.octave); - break; - - case CmdType::REC_START: - Recorder::startRecording(); - triggerFeedbackLed(); - break; - - case CmdType::REC_STOP: - Recorder::stopRecording(); - triggerFeedbackLed(); - break; - - case CmdType::REC_PLAY: - Recorder::startPlayback(); - triggerFeedbackLed(); - break; - - case CmdType::REC_CLEAR: - Recorder::clear(); - triggerFeedbackLed(); - break; - - case CmdType::MELODY_PLAY: - // Stop any current playback/recording - Recorder::stopPlayback(); - Recorder::stopRecording(); - Player::play(cmd.value1); - Protocol::respond(F("PLAYING"), cmd.value1); - triggerFeedbackLed(); - break; - - case CmdType::STOP_ALL: - Sound::stop(); - Player::stop(); - Recorder::stopPlayback(); - Protocol::respond(F("STOP")); - break; - - default: - break; + default: + break; } } @@ -120,23 +153,13 @@ void handleCommand(Command cmd) { // ============================================================================ void setup() { - // Initialize serial first + Serial.begin(SERIAL_BAUD); Protocol::init(); - - // Initialize buttons and LEDs + startMozzi(); setupButtons(); + setupDisplay(); setupLeds(); - - // Start Mozzi audio engine (must be in main.cpp where MozziGuts.h is - // included) - startMozzi(MOZZI_CONTROL_RATE); - - // Initialize Mozzi sound voices and envelopes Sound::init(); - - // Initialize other modules - Recorder::init(); - Player::init(); } // ============================================================================ @@ -144,8 +167,6 @@ void setup() { // ============================================================================ void loop() { - // Mozzi wiki: loop() should ONLY contain audioHook() - // All other logic goes in updateControl() audioHook(); } @@ -154,38 +175,24 @@ void loop() { // ============================================================================ void updateControl() { - // Check buttons (High Speed Polling for zero latency) checkButtons(); - - // Sound engine: ADSR updates, timed note checks, pot reading Sound::controlUpdate(); + Player::update(); + Recorder::update(); + + // Read parameter pot (A5) and feed to display/param system + int potVal = (int)mozziAnalogRead(PIN_POT); + displayUpdatePot(potVal); - // Update LEDs Visualizer + updateDisplay(); updateLeds(); - // Handle serial commands if (Protocol::hasCommand()) { Command cmd = Protocol::getCommand(); handleCommand(cmd); } - - // Player and recorder timing - Player::update(); - Recorder::update(); - - // Check for melody completion - static bool wasPlaying = false; - if (wasPlaying && !Player::isPlaying()) { - Protocol::respond(F("Melody complete")); - } - wasPlaying = Player::isPlaying(); - - // Check for recording playback completion - static bool wasRecPlaying = false; - if (wasRecPlaying && !Recorder::isPlaying()) { - Protocol::respond(F("Playback complete")); - } - wasRecPlaying = Recorder::isPlaying(); } -AudioOutput updateAudio() { return MonoOutput::from8Bit(Sound::audioUpdate()); } +AudioOutput updateAudio() { + return MonoOutput::from16Bit(Sound::audioUpdate()); +} \ No newline at end of file diff --git a/src/player.cpp b/src/player.cpp index fe8345c..58ff914 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -1,6 +1,6 @@ /** * @file player.cpp - * @brief Melody player implementation using millis-based timing + * @brief Melody player implementation using tick-based timing (Optimization 1) */ #include "player.h" @@ -8,6 +8,11 @@ #include "protocol.h" #include "sound.h" +// All timing is handled via the Sound namespace (centralized O(1) tick counters) +using Sound::controlTicks; +using Sound::ticksToMs; +using Sound::msToTicks; + // ============================================================================ // INTERNAL STATE // ============================================================================ @@ -15,72 +20,42 @@ namespace { bool playing = false; uint16_t noteIndex = 0; -unsigned long noteStartTime = 0; -uint16_t noteDurationMs = 0; +uint32_t noteStartMs = 0; // Optimization 1: Track absolute MS +uint16_t noteDurationMs = 0; // Optimization 1: Track exact MS const Note *melodyData = nullptr; uint16_t melodyLength = 0; -// Get melody data by ID +// Melody lookup table for O(1) access by MelodyID index. +// Must stay in sync with the MelodyID enum in melodies.h. +struct MelodyInfo { + const Note* data; + uint16_t length; +}; + +const MelodyInfo MELODY_TABLE[MELODY_COUNT] PROGMEM = { + {melody_mario, melody_mario_length}, + {melody_tetris, melody_tetris_length}, + {melody_nokia, melody_nokia_length}, + {melody_imperial, melody_imperial_length}, + {melody_pacman, melody_pacman_length}, + {melody_birthday, melody_birthday_length}, + {melody_jingle, melody_jingle_length}, + {melody_simple, melody_simple_length}, +}; + +// Load melody data by ID β€” O(1) PROGMEM table lookup. void loadMelody(uint8_t id) { - switch (id) { - case 0: - melodyData = melody_mario; - melodyLength = melody_mario_length; - break; - case 1: - melodyData = melody_tetris; - melodyLength = melody_tetris_length; - break; - case 2: - melodyData = melody_nokia; - melodyLength = melody_nokia_length; - break; - case 3: - melodyData = melody_imperial; - melodyLength = melody_imperial_length; - break; - case 4: - melodyData = melody_pacman; - melodyLength = melody_pacman_length; - break; - case 5: - melodyData = melody_birthday; - melodyLength = melody_birthday_length; - break; - case 6: - melodyData = melody_zelda_secret; - melodyLength = melody_zelda_secret_length; - break; - case 7: - melodyData = melody_gameover; - melodyLength = melody_gameover_length; - break; - case 8: - melodyData = melody_alert; - melodyLength = melody_alert_length; - break; - case 9: - melodyData = melody_success; - melodyLength = melody_success_length; - break; - case 10: - melodyData = melody_failure; - melodyLength = melody_failure_length; - break; - case 11: - melodyData = melody_notify; - melodyLength = melody_notify_length; - break; - case 12: - melodyData = melody_jingle; - melodyLength = melody_jingle_length; - break; - default: + if (id >= MELODY_COUNT) { melodyData = nullptr; melodyLength = 0; - break; + return; } + + MelodyInfo info; + memcpy_P(&info, &MELODY_TABLE[id], sizeof(MelodyInfo)); + melodyData = info.data; + melodyLength = info.length; } // Play note from PROGMEM with instrument applied @@ -88,13 +63,10 @@ void playMelodyNote(uint16_t index) { Note note; memcpy_P(¬e, &melodyData[index], sizeof(Note)); - noteDurationMs = note.duration; - noteStartTime = millis(); + noteDurationMs = note.duration; // Use exact MS if (note.frequency > 0 && Sound::getVolume() > 0) { - // Apply current instrument to melody note - uint16_t adjustedFreq = Sound::applyInstrument(note.frequency); - Sound::noteOn(adjustedFreq); + Sound::noteOnFreq(note.frequency); // Mimic GUI Key Input for PC Visualizer // Protocol: REQ: NOTE:ON: @@ -129,7 +101,8 @@ void Player::play(uint8_t id) { noteIndex = 0; playing = true; - // Start first note + // Start first note EXACTLY now + noteStartMs = ticksToMs(controlTicks); playMelodyNote(0); } @@ -154,15 +127,18 @@ void Player::listMelodies() { uint8_t Player::getMelodyCount() { return MELODY_COUNT; } // ============================================================================ -// UPDATE (called from loop, millis-based timing) +// UPDATE (called from updateControl, tick-based timing) // ============================================================================ void Player::update() { if (!playing || melodyData == nullptr) return; - // Check if current note finished - if (millis() - noteStartTime >= noteDurationMs) { + // Use ticksToMs to bypass all integer truncation drift across notes! + uint32_t nowMs = ticksToMs(controlTicks); + uint32_t elapsed = nowMs - noteStartMs; + + if (elapsed >= noteDurationMs) { noteIndex++; // Check if melody done @@ -172,6 +148,14 @@ void Player::update() { return; } + // Accumulate target MS perfectly to prevent drift (prevents melody lag) + noteStartMs += noteDurationMs; + + // If for some reason we lagged heavily (e.g. CPU busy), snap to now + if (nowMs > noteStartMs + 65) { + noteStartMs = nowMs; + } + // Play next note playMelodyNote(noteIndex); } diff --git a/src/protocol.cpp b/src/protocol.cpp index f8448af..91cfb86 100644 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -1,11 +1,10 @@ /** * @file protocol.cpp - * @brief Binary Protocol implementation (Optimized) + * @brief Binary protocol parser (MeeBleeps-style FM engine) */ #include "protocol.h" #include "sound.h" -#include // ============================================================================ // BINARY PROTOCOL PARSER @@ -13,35 +12,29 @@ namespace { // Parser State -enum class State { IDLE, ARG1, ARG_ADSR }; +enum class State { IDLE, ARG1, ARG_MULTI, ARG_MIDI_N, ARG_MIDI_V }; State state = State::IDLE; -// Buffers -uint8_t adsrBuffer[8]; -uint8_t adsrIndex = 0; +// Multi-byte argument buffer (reused for ADSR 7-byte and 2-byte commands) +uint8_t argBuffer[7]; +uint8_t argIndex = 0; +uint8_t argExpected = 0; // how many bytes we're waiting for // Parsed command ready for main loop volatile bool cmdReady = false; Command pendingCmd; +uint8_t currentMidiStatus = 0; +uint8_t currentMidiNote = 0; -// Pre-calculated integer MIDI to Freq conversion table (Notes 21-127) -// Formula: 440 * 2^((mn-69)/12) -const uint16_t MTOF_TABLE[107] PROGMEM = { - 28, 29, 31, 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 58, 62, 65, 69, 73, 78, 82, 87, 92, 98, 104, 110, 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 233, 247, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951, 4186, 4435, 4699, 4978, 5274, 5588, 5920, 6272, 6645, 7040, 7459, 7902, 8372, 8870, 9397, 9956, 10548, 11175, 11840, 12544 -}; - -uint16_t mtof(uint8_t note) { - if (note < 21 || note > 127) - return 0; // Piano + High Synths range - return pgm_read_word(&MTOF_TABLE[note - 21]); -} } // namespace + + // ============================================================================ // INITIALIZATION // ============================================================================ -void Protocol::init() { Serial.begin(SERIAL_BAUD); } +void Protocol::init() { /* Serial already started in main setup() */ } // ============================================================================ // COMMAND HANDLING @@ -53,7 +46,7 @@ bool Protocol::hasCommand() { switch (state) { case State::IDLE: - if (byte <= 0x14) { + if (byte <= 0x24) { // System Command if (byte == CMD_STOP_ALL) { pendingCmd.type = CmdType::STOP_ALL; @@ -76,61 +69,132 @@ bool Protocol::hasCommand() { cmdReady = true; return true; } else if (byte == CMD_SET_ADSR) { - state = State::ARG_ADSR; // Wait for 8 bytes - adsrIndex = 0; + pendingCmd.type = CmdType::SET_ADSR; + state = State::ARG_MULTI; + argIndex = 0; + argExpected = 7; } else if (byte == CMD_SET_VOL) { pendingCmd.type = CmdType::SET_VOLUME; - state = State::ARG1; // Wait for 1 byte - } else if (byte == CMD_SET_INST) { - pendingCmd.type = CmdType::SET_INSTRUMENT; - state = State::ARG1; // Wait for 1 byte + state = State::ARG1; } else if (byte == CMD_PLAY_MELODY) { pendingCmd.type = CmdType::MELODY_PLAY; - state = State::ARG1; // Wait for 1 byte - } else if (byte == CMD_SET_WAVEFORM) { - pendingCmd.type = CmdType::SET_WAVEFORM; - state = State::ARG1; // Wait for 1 byte + state = State::ARG1; + } else if (byte == CMD_SET_PITCH) { + pendingCmd.type = CmdType::SET_PITCH; + state = State::ARG1; + } else if (byte == CMD_SET_MOD_AMOUNT) { + pendingCmd.type = CmdType::SET_MOD_AMOUNT; + state = State::ARG_MULTI; + argIndex = 0; + argExpected = 2; + } else if (byte == CMD_SET_LFO_RATE) { + pendingCmd.type = CmdType::SET_LFO_RATE; + state = State::ARG_MULTI; + argIndex = 0; + argExpected = 2; + } else if (byte == CMD_SET_MOD_RATIO) { + pendingCmd.type = CmdType::SET_MOD_RATIO; + state = State::ARG_MULTI; + argIndex = 0; + argExpected = 2; + } else if (byte == CMD_GET_PARAMS) { + pendingCmd.type = CmdType::GET_PARAMS; + cmdReady = true; + return true; + } else if (byte == CMD_LOAD_FM_PRESET) { + pendingCmd.type = CmdType::LOAD_FM_PRESET; + state = State::ARG1; + } else if (byte == CMD_SET_CARRIER_WAVE) { + pendingCmd.type = CmdType::SET_CARRIER_WAVE; + state = State::ARG1; + } else if (byte == CMD_SET_MOD_WAVE) { + pendingCmd.type = CmdType::SET_MOD_WAVE; + state = State::ARG1; + } else if (byte == CMD_SET_FM_MODE) { + pendingCmd.type = CmdType::SET_FM_MODE; + state = State::ARG1; + } else if (byte == CMD_SET_MOD_SHAPE) { + pendingCmd.type = CmdType::SET_MOD_SHAPE; + state = State::ARG_MULTI; + argIndex = 0; + argExpected = 2; + } else if (byte == CMD_SET_LFO_DEPTH) { + pendingCmd.type = CmdType::SET_LFO_DEPTH; + state = State::ARG1; + } else if (byte == CMD_SET_LFO_WAVEFORM) { + pendingCmd.type = CmdType::SET_LFO_WAVEFORM; + state = State::ARG1; + } else if (byte == CMD_SET_MOD_ENV_ATTACK) { + pendingCmd.type = CmdType::SET_MOD_ENV_ATTACK; + state = State::ARG_MULTI; + argIndex = 0; + argExpected = 2; + } else if (byte == CMD_SET_MOD_ENV_DECAY) { + pendingCmd.type = CmdType::SET_MOD_ENV_DECAY; + state = State::ARG_MULTI; + argIndex = 0; + argExpected = 2; + } else if (byte == CMD_SET_MOD_ENV_SUSTAIN) { + pendingCmd.type = CmdType::SET_MOD_ENV_SUSTAIN; + state = State::ARG1; + } else if (byte == CMD_SET_VIBRATO_DEPTH) { + pendingCmd.type = CmdType::SET_VIBRATO_DEPTH; + state = State::ARG1; + } else if (byte == CMD_SET_VIBRATO_SPEED) { + pendingCmd.type = CmdType::SET_VIBRATO_SPEED; + state = State::ARG1; + } else if (byte == CMD_SET_PORTAMENTO) { + pendingCmd.type = CmdType::SET_PORTAMENTO; + state = State::ARG1; } - } else if (byte >= 0x15 && byte <= 0x7F) { - // Note OFF (21-127) - // Map 0x15 (21) -> Note 21 - uint8_t note = byte; // The byte is the note number - pendingCmd.type = CmdType::NOTE_OFF; - pendingCmd.value1 = mtof(note); // Pass freq to stop? Or note? - // Sound::noteOff() usually stops everything or specific note. - // We will pass the Frequency as value1 for consistency. - cmdReady = true; - return true; - } else if (byte >= 0x95 && byte <= 0xFF) { - // Note ON (149-255) -> Map to 21-127 - uint8_t note = byte - 128; // e.g. 149 - 128 = 21 - pendingCmd.type = CmdType::NOTE_ON; - pendingCmd.value1 = mtof(note); - cmdReady = true; - return true; + } else if (byte >= 0x80 && byte <= 0x8F) { + currentMidiStatus = 0x80; + state = State::ARG_MIDI_N; + } else if (byte >= 0x90 && byte <= 0x9F) { + currentMidiStatus = 0x90; + state = State::ARG_MIDI_N; } break; + case State::ARG_MIDI_N: + currentMidiNote = byte; + state = State::ARG_MIDI_V; + break; + + case State::ARG_MIDI_V: + if (currentMidiStatus == 0x90 && byte > 0) { + pendingCmd.type = CmdType::NOTE_ON; + } else { + pendingCmd.type = CmdType::NOTE_OFF; + } + pendingCmd.value1 = Sound::mtof(currentMidiNote); + pendingCmd.value2 = currentMidiNote; + pendingCmd.velocity = byte; + state = State::IDLE; + cmdReady = true; + return true; + case State::ARG1: - // Single argument commands (Vol, Inst) pendingCmd.value1 = byte; state = State::IDLE; cmdReady = true; return true; - case State::ARG_ADSR: - adsrBuffer[adsrIndex++] = byte; - if (adsrIndex >= 10) { - // [A_hi, A_lo, D_hi, D_lo, S, SL_hi, SL_lo, R_hi, R_lo, Oct] - pendingCmd.type = CmdType::SET_ADSR; - // Reconstruct 16-bit values - pendingCmd.value1 = (uint16_t)((adsrBuffer[0] << 8) | adsrBuffer[1]); // Attack - pendingCmd.value2 = (uint16_t)((adsrBuffer[2] << 8) | adsrBuffer[3]); // Decay - pendingCmd.sustain = adsrBuffer[4]; // Sustain - pendingCmd.sustainlenght = (uint16_t)((adsrBuffer[5] << 8) | adsrBuffer[6]); // Sustain length - pendingCmd.release = (uint16_t)((adsrBuffer[7] << 8) | adsrBuffer[8]); // Release - pendingCmd.octave = (int8_t)adsrBuffer[9]; // Octave shift (signed) - + case State::ARG_MULTI: + if (argIndex < sizeof(argBuffer)) { + argBuffer[argIndex++] = byte; + } + if (argIndex >= argExpected) { + if (argExpected == 2) { + // 2-byte big-endian β†’ value1 + pendingCmd.value1 = (uint16_t)((argBuffer[0] << 8) | argBuffer[1]); + } else if (argExpected == 7) { + // ADSR: [A_hi, A_lo, D_hi, D_lo, S, R_hi, R_lo] + pendingCmd.value1 = (uint16_t)((argBuffer[0] << 8) | argBuffer[1]); + pendingCmd.value2 = (uint16_t)((argBuffer[2] << 8) | argBuffer[3]); + pendingCmd.sustain = argBuffer[4]; + pendingCmd.release = (uint16_t)((argBuffer[5] << 8) | argBuffer[6]); + } state = State::IDLE; cmdReady = true; return true; diff --git a/src/recorder.cpp b/src/recorder.cpp index 892c899..a085581 100644 --- a/src/recorder.cpp +++ b/src/recorder.cpp @@ -1,13 +1,17 @@ /** * @file recorder.cpp - * @brief Recorder implementation with non-blocking EEPROM storage + * @brief Recorder implementation with non-blocking EEPROM storage (tick-based timing - Optimization 1) */ #include "recorder.h" - #include "protocol.h" #include "sound.h" +// All timing is handled via the Sound namespace (centralized O(1) tick counters) +using Sound::controlTicks; +using Sound::ticksToMs; +using Sound::msToTicks; + // ============================================================================ // INTERNAL STATE // ============================================================================ @@ -31,22 +35,22 @@ bool flushingNode = false; // True if currently writing a note from buffer // Recording State bool recording = false; bool recordingStopping = false; // New flag for deferred stop -unsigned long lastNoteTime = 0; +uint16_t lastNoteTick = 0; // Optimization 1: Use tick counter instead of millis() // Playback State bool playing = false; uint16_t playIndex = 0; -unsigned long playNoteStart = 0; // Acts as "Last Event End Time" -uint16_t nextNoteDelay = - 0; // Unused in new logic but kept for struct alignment if needed +uint32_t playNoteStartMs = 0; // Optimization 1: Acts as "Last Event End Time" in MS +uint16_t nextNoteDelay = 0; // Unused in new logic but kept for struct alignment if needed // Playback note loading state (moved from static in update()) RecordedNote nextNote; bool noteLoaded = false; // Current Note State (moved here for visibility) -uint16_t currentNoteFreq = 0; -unsigned long currentNoteStartTime = 0; +uint8_t currentNoteNum = 0; +uint8_t currentNoteVel = 0; +uint16_t currentNoteStartTick = 0; // Optimization 1: Use tick counter instead of millis() // Helper: Push note to ring buffer bool bufferNote(const RecordedNote ¬e) { @@ -158,12 +162,12 @@ void Recorder::update() { } // --------------------------------------------------------- - // 2. PLAYBACK LOGIC + // 2. PLAYBACK LOGIC (Optimization 1: Tick-based timing) // --------------------------------------------------------- if (!playing) return; - unsigned long now = millis(); + uint32_t nowMs = ticksToMs(controlTicks); // Optimization 1: Use tick counter mathematically mapped to MS // If we need to load the next note to check timing if (nextNoteDelay == 0) { @@ -182,7 +186,7 @@ void Recorder::update() { } // Let's rewrite the state machine for clarity: - // We have 'playNoteStart' acting as 'lastEventEnd'. + // We have 'playNoteStartTick' acting as 'lastEventEnd'. // We need to wait 'delta' from 'lastEventEnd'. if (!noteLoaded) { @@ -199,36 +203,31 @@ void Recorder::update() { noteLoaded = true; } - // Calculate when this note should start - // playNoteStart = time the PREVIOUS note ended (or start of playback) - unsigned long targetStart = playNoteStart + nextNote.deltaMs; - - if (now >= targetStart) { - // Play the note - if (nextNote.frequency > 0) { - if (nextNote.duration > 0) { - Sound::playTimed(nextNote.frequency, nextNote.duration); - Protocol::respond(F("NOTE:ON"), nextNote.frequency); - } else { - // Should not happen in new format, but safety: - Sound::noteOn(nextNote.frequency); - Protocol::respond(F("NOTE:ON"), nextNote.frequency); - } + // Calculate when this note should start (Optimization 1: MS-based timing using Tick Clock) + // playNoteStartMs = time the PREVIOUS note ended (or start of playback) + // Use ticksToMs(controlTicks) to bypass integer drift! + uint32_t targetStartMs = playNoteStartMs + nextNote.deltaMs; + + if (nowMs >= targetStartMs) { + // Play the note (Optimization 11: MIDI note based playback) + if (nextNote.duration > 0) { + Sound::playTimed(nextNote.note, nextNote.duration, nextNote.velocity); + Protocol::respond(F("NOTE:ON"), Sound::mtof(nextNote.note)); } else { - Sound::noteOff(); - Protocol::respond(F("NOTE:OFF")); + Sound::noteOn(nextNote.note, nextNote.velocity); + Protocol::respond(F("NOTE:ON"), Sound::mtof(nextNote.note)); } + // Advance state // The "End" of this event is Start + Duration - playNoteStart = targetStart + nextNote.duration; + playNoteStartMs = targetStartMs + nextNote.duration; - // If we are lagging significantly, we might want to snap to 'now', - // but keeping strict time (virtual time) preserves rhythm better unless we - // drift too far. - if (now > playNoteStart + 100) { + // If we are lagging significantly, snap to nowMs + // Check for >100ms lag + if (nowMs > playNoteStartMs + 100) { // We are lagging behind (processing took too long?) -> resync - playNoteStart = now; + playNoteStartMs = nowMs; } playIndex++; @@ -245,8 +244,8 @@ void Recorder::startPlayback() { playing = true; playIndex = 0; noteLoaded = false; // Reset note loading state - // Initialize 'lastEventEnd' to now - playNoteStart = millis(); + // Initialize 'lastEventEnd' to now (Optimization 1: Track in MS to avoid drift) + playNoteStartMs = ticksToMs(controlTicks); } void Recorder::startRecording() { @@ -261,13 +260,13 @@ void Recorder::startRecording() { writeTail = 0; flushingNode = false; - lastNoteTime = millis(); + lastNoteTick = controlTicks; // Optimization 1: Use tick counter instead of millis() } void Recorder::stop() { if (recording && !recordingStopping) { // If a note is currently active, close it - if (currentNoteFreq > 0) { + if (currentNoteNum > 0) { stopNote(); } @@ -293,73 +292,45 @@ void Recorder::stopRecording() { stop(); } // New API: Write-on-Release -void Recorder::startNote(uint16_t freq) { +void Recorder::startNote(uint8_t note, uint8_t velocity) { if (!recording || recordingStopping) return; // EEPROM Overflow Protection - // Max notes = (1024 - 2) / 6 = 170 - // If we are approaching limit, stop. uint16_t maxNotes = (EEPROM_SIZE - EEPROM_START_ADDR) / sizeof(RecordedNote); if (recordedCount >= maxNotes) { stopRecording(); - // Visual feedback (Flash Rec LED 3 times quickly) - // We can't block here easily, but we can signal main loop? - // For now, safety stop is priority. return; } - // If a note was already playing (legato), close it first? - // Ideally we are monophonic, so yes. - if (currentNoteFreq > 0) { + // If a note was already playing (legato), close it first + if (currentNoteNum != 0) { stopNote(); } - currentNoteFreq = freq; - currentNoteStartTime = millis(); + currentNoteNum = note; + currentNoteVel = velocity; + currentNoteStartTick = controlTicks; } void Recorder::stopNote() { - if (!recording || currentNoteFreq == 0) + if (!recording || currentNoteNum == 0) return; - // Don't record if memory full - if (eepromWriteAddr >= - (EEPROM_START_ADDR + EEPROM_SIZE - sizeof(RecordedNote) * 2)) { - return; - } + uint32_t now = controlTicks; + uint16_t deltaMs = ticksToMs(currentNoteStartTick - lastNoteTick); + uint16_t durationMs = ticksToMs(now - currentNoteStartTick); - unsigned long now = millis(); + lastNoteTick = now; - // 1. Calculate Delta: Time from Previous Note End to This Note Start - // Wait, if we write on release, 'lastNoteTime' should track when the - // *previous* note ended. Actually, 'delta' in our format normally means "Time - // since last event". If we store {Delta, Freq, Dur}, then Delta is "Silence - // before this note". + RecordedNote newNote; + newNote.deltaMs = deltaMs; + newNote.note = currentNoteNum; + newNote.velocity = currentNoteVel; + newNote.duration = durationMs; - // Example: - // T0: Start A (Delta=0) - // T100: Stop A (Dur=100). lastNoteTime = 100. - // T2100: Start B. Delta = 2100 - 100 = 2000. - // T2200: Stop B (Dur=100). lastNoteTime = 2200. - - // So we need to calculate delta based on when startNote() WAS called vs - // lastNoteTime. But startNote() happened in the past. We need to have - // captured it then? No, we can calculate it now if we know when it started. - - uint16_t delta = (uint16_t)(currentNoteStartTime - lastNoteTime); - uint16_t duration = (uint16_t)(now - currentNoteStartTime); - - // Update lastNoteTime to NOW (end of this note) - lastNoteTime = now; - - RecordedNote newNote = {delta, currentNoteFreq, duration}; - - // Push to buffer bufferNote(newNote); - - // Reset current note - currentNoteFreq = 0; + currentNoteNum = 0; } void Recorder::stopPlayback() { diff --git a/src/sound.cpp b/src/sound.cpp index 672755b..cf5b132 100644 --- a/src/sound.cpp +++ b/src/sound.cpp @@ -1,568 +1,115 @@ /** * @file sound.cpp - * @brief Sound module using Mozzi wavetable synthesis - * - * Uses Mozzi's Oscil for waveform generation and ADSR for envelope shaping. - * Supports up to MAX_VOICES polyphonic voices. + * @brief Sound module -- thin wrapper around FM:: namespace (MeeBleeps-style FM) */ #include "sound.h" +#include "sound/fm_synth.h" extern int getPotValue(uint8_t pin); - -// Only include individual Mozzi components here β€” is in main.cpp -#include "tables/akwf_aguitarr.h" -#include "tables/akwf_cello.h" -#include "tables/akwf_clarinett.h" -#include "tables/akwf_epiano.h" -#include "tables/akwf_flute.h" -#include "tables/akwf_fmsynth.h" -#include "tables/akwf_nes_pulse.h" -#include "tables/akwf_oboe.h" -#include "tables/akwf_oscchip.h" -#include "tables/akwf_piano.h" -#include "tables/akwf_violin.h" -#include "tables/saw512_int8.h" -#include "tables/sin512_int8.h" -#include "tables/square_no_alias512_int8.h" -#include "tables/triangle512_int8.h" -#include -#include -#include - -// ============================================================================ -// VOICE STRUCTURE -// ============================================================================ - -struct Voice { - Oscil<512, MOZZI_AUDIO_RATE> osc; // Standardized to 512, since all the tables are 512 samples - ADSR env; - uint16_t freq; - uint16_t timedDuration; // ms remaining for timed notes, 0 = sustained - unsigned long timedStart; // millis() when timed note started - unsigned long noteOnTime; // millis() when note started (for stealing) - bool active; - volatile uint8_t currentLevel; // Shared with main loop for visualizer - - Voice() - : osc(AKWF_EPIANO_DATA), freq(0), timedDuration(0), timedStart(0), - noteOnTime(0), active(false), currentLevel(0) {} -}; - -// ... (skipping lines) - -// ============================================================================ -// INTERNAL STATE -// ============================================================================ +extern uint32_t getNoiseSeed(); namespace { -Voice voices[MAX_VOICES]; - -// Volume and instrument -uint8_t masterVolume = DEFAULT_VOLUME; -uint8_t currentInstrument = 0; // Piano -int lastPotReading = -1; - -// Track primary note (for getFrequency) -uint16_t primaryFreq = 0; - -// Cached envelope profile (read from PROGMEM once on instrument change) -EnvelopeProfile currentProfile; -EnvelopeProfile customProfile = {10, 100, 128, 5000, 100, 0}; // Default custom - -// Base frequencies for octave 4 -const uint16_t BASE_FREQ[] = {262, 277, 294, 311, 330, 349, - 370, 392, 415, 440, 466, 494}; - -// Instrument names in PROGMEM -const char INST_PIANO[] PROGMEM = "Piano"; -const char INST_ORGAN[] PROGMEM = "Organ"; -const char INST_STACCATO[] PROGMEM = "Staccato"; -const char INST_PAD[] PROGMEM = "Pad"; -const char INST_CUSTOM[] PROGMEM = "Custom"; -const char *const INST_NAMES[] PROGMEM = {INST_PIANO, INST_ORGAN, INST_STACCATO, - INST_PAD, INST_CUSTOM}; - -// Name buffer for PROGMEM reads -char nameBuffer[12]; - -// Load envelope profile from PROGMEM -void loadProfile(uint8_t inst) { - if (inst == INSTRUMENT_CUSTOM) { - currentProfile = customProfile; - } else { - if (inst >= NUM_INSTRUMENTS) - inst = 0; // Safety - memcpy_P(¤tProfile, &INSTRUMENTS[inst], sizeof(EnvelopeProfile)); - } -} - -// Apply envelope profile to a voice -void applyEnvelope(Voice &v) { - v.env.setADLevels(255, currentProfile.sustainLevel); - v.env.setTimes(currentProfile.attackMs, currentProfile.decayMs, currentProfile.sustainLenght, - currentProfile.releaseMs); -} - -// Find a free voice, or steal the oldest/quietest one -int8_t allocateVoice() { - // First: find an inactive voice - for (uint8_t i = 0; i < MAX_VOICES; i++) { - if (!voices[i].active) - return i; - } - // All voices active β€” steal the oldest one - // TODO! Consider stealing the quietest voice (lowest currentLevel) if that would be more musical β€” IGNORE for now, as it adds complexity and currentLevel is volatile/shared with main loop - int8_t oldestVoice = 0; - unsigned long maxDuration = 0; - unsigned long now = millis(); - - for (uint8_t i = 0; i < MAX_VOICES; i++) { - unsigned long duration = now - voices[i].noteOnTime; - if (duration > maxDuration) { - maxDuration = duration; - oldestVoice = i; - } - } - return oldestVoice; -} -} // namespace - -// Helper to get table data from ID -const int8_t *getTableData(uint8_t waveId) { - switch (waveId) { - case 0: - return TRIANGLE512_DATA; - case 1: - return SAW512_DATA; - case 2: - return SIN512_DATA; - case 3: - return SQUARE_NO_ALIAS512_DATA; - case 4: - return AKWF_EPIANO_DATA; - case 5: - return AKWF_clarinett_0001_512_DATA; - case 6: - return AKWF_violin_0001_512_DATA; - case 7: - return AKWF_fmsynth_0001_512_DATA; - case 8: - return AKWF_aguitar_0001_512_DATA; - case 9: - return AKWF_cello_0001_512_DATA; - case 10: - return AKWF_flute_0001_512_DATA; - case 11: - return AKWF_nes_pulse_12_5_512_DATA; - case 12: - return AKWF_oboe_0001_512_DATA; - case 13: - return AKWF_oscchip_0001_512_DATA; - case 14: - return AKWF_piano_0001_512_DATA; - default: - return TRIANGLE512_DATA; - } -} - -// ============================================================================ -// INITIALIZATION -// ============================================================================ - -void Sound::init() { - // Load default instrument profile - loadProfile(currentInstrument); - - // Initialize all voices with Piano wavetable - for (uint8_t i = 0; i < MAX_VOICES; i++) { - voices[i].osc.setTable(AKWF_EPIANO_DATA); - voices[i].osc.setFreq(0); - voices[i].active = false; - applyEnvelope(voices[i]); - } - - // Initialize pot pin for Mozzi analog reads - pinMode(PIN_POT, INPUT); - - // NOTE: startMozzi() must be called from main.cpp where is included + int8_t pitchTranspose = 0; + uint16_t latestFreq = 0; + uint8_t latestMidiNote = 0; + uint16_t timedDuration = 0; } -// ============================================================================ -// VOLUME & INSTRUMENT -// ============================================================================ - -void Sound::setWaveform(uint8_t waveId) { - if (waveId > 8) - waveId = 8; - // currentWaveform = waveId; - - const int8_t *table = getTableData(waveId); +namespace Sound { - for (uint8_t i = 0; i < MAX_VOICES; i++) { - voices[i].osc.setTable(table); - } -} +volatile uint16_t controlTicks = 0; -void Sound::setVolume(uint8_t vol) { - if (vol > MAX_VOLUME) - vol = MAX_VOLUME; - masterVolume = vol; +void init() { + FM::init(getNoiseSeed()); } -uint8_t Sound::getVolume() { return masterVolume; } +void setVolume(uint8_t) { /* hw pot β€” no software volume needed */ } +uint8_t getVolume() { return 100; } -void Sound::setInstrument(uint8_t inst) { - if (inst >= NUM_INSTRUMENTS) - inst = NUM_INSTRUMENTS - 1; - currentInstrument = inst; - loadProfile(inst); +void setPitchTranspose(int8_t semitones) { pitchTranspose = semitones; } +int8_t getPitchTranspose() { return pitchTranspose; } - // Apply new envelope to all voices - for (uint8_t i = 0; i < MAX_VOICES; i++) { - applyEnvelope(voices[i]); - } +void noteOn(uint8_t midiNote, uint8_t velocity) { + int16_t transposed = (int16_t)midiNote + pitchTranspose; + if (transposed < 0) transposed = 0; + if (transposed > 127) transposed = 127; + uint8_t note = (uint8_t)transposed; + latestMidiNote = note; + latestFreq = FM::midiToFreqHz(note); + FM::noteOn(note, velocity); + timedDuration = 0; } -uint8_t Sound::getInstrument() { return currentInstrument; } - -void Sound::setCustomEnvelope(uint16_t a, uint16_t d, uint8_t s, uint16_t sl, uint16_t r, - int8_t o) { - customProfile.attackMs = a; - customProfile.decayMs = d; - customProfile.sustainLevel = s; - customProfile.sustainLenght = sl; - customProfile.releaseMs = r; - customProfile.octaveShift = o; - - // If currently using custom instrument, reload immediately - if (currentInstrument == INSTRUMENT_CUSTOM) { - currentProfile = customProfile; - // Update active voices with the new envelope - for (uint8_t i = 0; i < MAX_VOICES; i++) { - voices[i].env.setADLevels(255, currentProfile.sustainLevel); - voices[i].env.setTimes(currentProfile.attackMs, currentProfile.decayMs, - currentProfile.sustainLenght, currentProfile.releaseMs); - } - } +void noteOff(uint16_t) { FM::allNotesOff(); } +void noteOffMidi(uint8_t midiNote) { + int16_t transposed = (int16_t)midiNote + pitchTranspose; + if (transposed < 0) transposed = 0; + if (transposed > 127) transposed = 127; + FM::noteOff((uint8_t)transposed); } +void noteOff() { FM::allNotesOff(); } -const char *Sound::getInstrumentName() { - strcpy_P(nameBuffer, (char *)pgm_read_word(&(INST_NAMES[currentInstrument]))); - return nameBuffer; +void noteOnFreq(uint16_t freqHz, uint8_t velocity) { + latestFreq = freqHz; + FM::noteOnFreq(freqHz, velocity); + timedDuration = 0; } -bool Sound::updateVolumeFromPot() { - // NOTE: Called from controlUpdate() at ~8Hz (every 16th control cycle) - // This is non-blocking and safer for audio - int potValue = getPotValue(PIN_POT); - - // Add hysteresis to prevent jitter - if (abs(potValue - lastPotReading) < 20) { - return false; - } - lastPotReading = potValue; - - uint8_t newVol; - if (potValue < 50) { - newVol = 0; - } else { - newVol = map(potValue, 50, 1023, 1, MAX_VOLUME); - } - - if (newVol != masterVolume) { - setVolume(newVol); - return true; - } - return false; +void playTimed(uint8_t midiNote, uint16_t durationMs, uint8_t velocity) { + noteOn(midiNote, velocity); + timedDuration = msToTicks(durationMs); } -// ============================================================================ -// NOTE PARSING -// ============================================================================ - -// TODO! fix warning -uint16_t Sound::parseNote(const char *noteStr) { - if (noteStr == nullptr || noteStr[0] == '\0') - return 0; - - // Check if raw frequency - if (noteStr[0] >= '0' && noteStr[0] <= '9') { - return atoi(noteStr); - } - - // Parse note letter - char letter = toupper(noteStr[0]); - int8_t noteIndex = -1; - - switch (letter) { - case 'C': - noteIndex = 0; - break; - case 'D': - noteIndex = 2; - break; - case 'E': - noteIndex = 4; - break; - case 'F': - noteIndex = 5; - break; - case 'G': - noteIndex = 7; - break; - case 'A': - noteIndex = 9; - break; - case 'B': - noteIndex = 11; - break; - default: - return 0; - } - - uint8_t pos = 1; +void stop() { FM::stop(); timedDuration = 0; } - // Sharp or flat - if (noteStr[pos] == '#') { - noteIndex++; - pos++; - } else if (noteStr[pos] == 'b' || noteStr[pos] == 'B') { - noteIndex--; - pos++; - } +bool isPlaying() { return FM::isPlaying(); } +uint16_t getLatestNoteFreq() { return latestFreq; } +uint8_t getLatestMidiNote() { return latestMidiNote; } +uint16_t mtof(uint8_t note) { return FM::midiToFreqHz(note); } +uint8_t getEnvelopeLevel() { return FM::isPlaying() ? 128 : 0; } - // Wraparound, TODO make sure this makes sense - if (noteIndex < 0) - noteIndex = 0; - if (noteIndex > 11) - noteIndex = 11; - - // Parse octave - if (noteStr[pos] < '0' || noteStr[pos] > '8') - return 0; - int8_t octave = noteStr[pos] - '0'; - - // Calculate frequency - uint16_t freq = BASE_FREQ[noteIndex]; - int8_t octaveShift = octave - 4; - - // Apply octave shift - while (octaveShift > 0) { - freq = freq << 1; - octaveShift--; - } - while (octaveShift < 0) { - freq = freq >> 1; - octaveShift++; - } - - // Apply instrument offset - return applyInstrument(freq); -} +// Modulation +void setModAmount(uint16_t v) { FM::setModAmount(v); } +void setModRatio(uint16_t v) { FM::setModRatio(v); } +void setFMMode(uint8_t m) { FM::setFMMode(m); } +void setModEnvAttack(uint16_t ms) { FM::setModEnvAttack(ms); } +void setModEnvDecay(uint16_t ms) { FM::setModEnvDecay(ms); } +void setModEnvSustain(uint8_t l) { FM::setModEnvSustain(l); } +void setModShape(uint16_t v) { FM::setModShape(v); } -uint16_t Sound::applyInstrument(uint16_t freq) { - if (freq == 0) - return 0; +// LFO +void setLFODepth(uint8_t d) { FM::setLFODepth(d); } +void setLFORate(uint16_t r) { FM::setLFORate(r); } +void setLFOWaveform(uint8_t id) { FM::setLFOWaveform(id); } - int8_t instOffset = currentProfile.octaveShift; - int8_t octaveOffset = instOffset / 12; // todo check assembly output, if we can optimize this +// Amp ADSR +void setAmpADSR(uint16_t a, uint16_t d, uint8_t s, uint16_t r) { FM::setAmpADSR(a, d, s, r); } - while (octaveOffset > 0) { - freq = freq << 1; - octaveOffset--; - } - while (octaveOffset < 0) { - freq = freq >> 1; - octaveOffset++; - } +// Performance +void setVibratoDepth(uint8_t d) { FM::setVibratoDepth(d); } +void setVibratoSpeed(uint8_t s) { FM::setVibratoSpeed(s); } +void setPortamento(uint8_t s) { FM::setPortamento(s); } - // clamp to human hearing range if needed, - /* - if (freq < 100) - freq = 100; +// Presets & wavetables +void loadFMPreset(uint8_t id) { FM::loadPreset(id); } +void setCarrierWave(uint8_t id) { FM::setCarrierWave(id); } +void setModulatorWave(uint8_t id) { FM::setModulatorWave(id); } +uint8_t nextCarrierWave() { return FM::nextCarrierWave(); } +uint8_t nextFMMode() { return FM::nextFMMode(); } +const FM::FMParams& getFMParams() { return FM::getParams(); } - if (freq > 20000) - freq = 20000; - */ - - return freq; -} - -// ============================================================================ -// NOTE CONTROL -// ============================================================================ - -void Sound::noteOn(uint16_t freq) { - if (freq == 0) - return; - - // Apply instrument-specific octave shift - freq = applyInstrument(freq); - - int8_t vi = allocateVoice(); - if (vi < 0) - return; - - Voice &v = voices[vi]; - v.osc.setFreq((int)freq); - v.freq = freq; - v.timedDuration = 0; - v.active = true; - applyEnvelope(v); - v.env.noteOn(); - - primaryFreq = freq; -} - -void Sound::noteOff(uint16_t freq) { - // Apply instrument-specific octave shift to match noteOn - freq = applyInstrument(freq); - - // Release voice matching frequency - for (uint8_t i = 0; i < MAX_VOICES; i++) { - if (voices[i].active && voices[i].timedDuration == 0) { - if (voices[i].freq == freq) { - voices[i].env.noteOff(); - // don't break, in case multiple voices are playing the same note (e.g., unison or overlapping timed/sustained) - // TODO! investigate this if this even can happen - } +void controlUpdate() { + controlTicks++; + FM::updateControl(); + if (timedDuration > 0) { + timedDuration--; + if (timedDuration == 0) FM::allNotesOff(); } - } } -void Sound::noteOff() { - // Release all active sustained (non-timed) voices - for (uint8_t i = 0; i < MAX_VOICES; i++) { - if (voices[i].active && voices[i].timedDuration == 0) { - voices[i].env.noteOff(); - } - } -} - -void Sound::playTimed(uint16_t freq, uint16_t durationMs) { - if (freq == 0 || durationMs == 0) - return; - - // Apply instrument-specific octave shift - freq = applyInstrument(freq); - - int8_t vi = allocateVoice(); - if (vi < 0) - return; - - Voice &v = voices[vi]; - v.osc.setFreq((int)freq); - v.freq = freq; - v.timedDuration = durationMs; - v.timedStart = millis(); // TODO make sure millis isnt a performance issue - v.noteOnTime = v.timedStart; - v.noteOnTime = v.timedStart; - v.active = true; - applyEnvelope(v); - v.env.noteOn(); - - primaryFreq = freq; -} - -void Sound::stop() { - for (uint8_t i = 0; i < MAX_VOICES; i++) { - voices[i].env.noteOff(); - voices[i].active = false; - voices[i].freq = 0; - } - primaryFreq = 0; -} - -bool Sound::isPlaying() { - for (uint8_t i = 0; i < MAX_VOICES; i++) { - if (voices[i].active) - return true; - } - return false; -} - -// TODO! fix warning -uint16_t Sound::getPrimaryFreq() { return primaryFreq; } - -uint8_t Sound::getEnvelopeLevel() { - // Find the voice playing the primaryFreq - for (uint8_t i = 0; i < MAX_VOICES; i++) { - if (voices[i].active && voices[i].freq == primaryFreq) { - return voices[i].currentLevel; - } - } - return 0; -} - -// ============================================================================ -// MOZZI CALLBACKS -// ============================================================================ - -void Sound::controlUpdate() { - unsigned long now = millis(); - - // Read pot every 16th control cycle (~8Hz) to avoid ADC overhead - static uint8_t potCounter = 0; - if (++potCounter >= 16) { - potCounter = 0; - updateVolumeFromPot(); - } - - for (uint8_t i = 0; i < MAX_VOICES; i++) { - if (!voices[i].active) - continue; - - // Update ADSR envelope - voices[i].env.update(); - - // Check timed notes for auto-release - if (voices[i].timedDuration > 0) { - if (now - voices[i].timedStart >= voices[i].timedDuration) { - voices[i].env.noteOff(); - voices[i].timedDuration = 0; // Don't re-trigger - } - } - - // Check if envelope has finished (released to silence) - if (!voices[i].env.playing()) { - voices[i].active = false; - voices[i].freq = 0; - } - } -} - -int Sound::audioUpdate() { - // Mix all active voices - int32_t mix = 0; - uint8_t activeCount = 0; - - for (uint8_t i = 0; i < MAX_VOICES; i++) { - if (!voices[i].active) - continue; - - // Oscillator sample * envelope * master volume - int16_t sample = voices[i].osc.next(); // -128 to 127 - uint16_t envVal = voices[i].env.next(); // 0 to 255 - voices[i].currentLevel = (uint8_t)envVal; // Store for visualizer - - // Scale: sample * envelope = -32768 to 32512 - // Multiply by volume (0-10) -> range approx +/- 320k - - int32_t voiced = (int32_t)sample * envVal * masterVolume; - mix += voiced; - activeCount++; - } - - // Mozzi expects int for audio output (~9-bit: -244 to 243). - // Current mix max potential per voice: 127 (sample) * 255 (env) * 100 (vol) - // = 3.2M Shift 15 -> ~98. (Wait, 3,200,000 / 32768 = 97). So >>= 15 is - // correct for ~5-7 active voices to sum to 512 (9-bit). - - mix >>= 15; - - return (int)mix; +int audioUpdate() { + return FM::updateAudio(); } -// NOTE: Mozzi hooks (updateControl, updateAudio) are defined in main.cpp -// because can only be included in one compilation unit. +} // namespace Sound diff --git a/src/sound/fm_synth.cpp b/src/sound/fm_synth.cpp new file mode 100644 index 0000000..328c6f9 --- /dev/null +++ b/src/sound/fm_synth.cpp @@ -0,0 +1,641 @@ +/** + * @file fm_synth.cpp + * @brief Polyphonic 2-op FM synthesis β€” MeeBleeps-style engine + * + * Algorithm (per voice): + * modEnvVal = modEnvelope.next() // mod envelope (control rate) + * lfoVal = lfo.next() + 128 // LFO 0-255 + * modulatorAmt = (modAmount * modEnvVal) + (lfoDepth * lfoVal) + * output = ampEnvelope.next() * carrier.phMod(modulatorAmt * modulator.next() >> 8) >> 8 + * + * Features: + * - 4 FM ratio modes: Exponential (quantized harmonics), Linear High/Low, Free + * - Independent modulation envelope (ADSR) controlling FM depth over time + * - LFO with depth and selectable waveform controlling FM depth + * - 1-knob modulation shape macro (controls mod env A/D/S together) + * - 6 wavetables: SIN, SAW, REVSAW, SQUARE, PSEUDORANDOM, NULL + * - Voice allocation with quietest-note stealing + */ + +#include "sound/fm_synth.h" +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "sound/tables/revsaw2048_int8.h" +#include "sound/tables/square2048_int8.h" +#include "sound/tables/pseudorandom2048_int8.h" +#include "sound/tables/nullwaveform2048_int8.h" + +extern int mtof(uint8_t note); + +namespace FM { + +// ============================================================================ +// 12 PRESETS (PROGMEM) β€” Synthwave-themed, FM-theory-informed +// +// Exponential mode ratios: idx = modRatio/61 β†’ FREQ_SHIFT[idx] +// 396β†’1.0Γ— 457β†’1.5Γ— 518β†’2.0Γ— 640β†’3.0Γ— 701β†’3.5Γ— 823β†’5.0Γ— +// ============================================================================ + +const FMPreset FM_PRESETS[FM_NUM_PRESETS] PROGMEM = { + // name modAmt ratio mode mA mD mS lfoD lfoR lfoW cW mW aA aD aS aR vibD vibS port + { "DX E.Piano", 850, 396, 0, 0, 900, 0, 0, 0, 0, WAVE_SIN, WAVE_SIN, 2, 900, 8, 600, 0, 0, 0 }, + { "SW Lead", 550, 518, 0, 0, 350, 130, 12, 180, 0, WAVE_SAW, WAVE_SIN, 8, 180, 220, 350, 35, 85, 0 }, + { "Lush Pad", 350, 518, 0, 400, 1800, 180, 25, 100, 0, WAVE_SIN, WAVE_SIN, 900,1500, 210,3500, 16, 40, 0 }, + { "FM Bass", 900, 396, 0, 0, 100, 80, 0, 0, 0, WAVE_SAW, WAVE_SIN, 2, 120, 200, 50, 0, 0, 0 }, + { "FM Bell", 650, 701, 0, 0, 2500, 0, 0, 0, 0, WAVE_SIN, WAVE_SIN, 2,2500, 0,3000, 0, 0, 0 }, + { "SynBrass", 750, 396, 0, 40, 250, 180, 18, 220, 0, WAVE_SAW, WAVE_SIN, 35, 200, 235, 200, 22, 60, 0 }, + { "Wobble", 700, 518, 0, 0, 400, 100, 200, 500, 1, WAVE_SIN, WAVE_SAW, 40, 200, 200, 200, 0, 0, 0 }, + { "Neon Arp", 720, 640, 0, 0, 120, 25, 8, 200, 0, WAVE_SQUARE,WAVE_SIN, 2, 80, 50, 100, 0, 0, 0 }, + { "Vapor Pad", 280, 457, 0, 600, 2500, 200, 45, 80, 0, WAVE_SAW, WAVE_SIN, 1000,2000, 230,4000, 18, 35, 0 }, + { "Acid Bass", 1000, 150, 1, 0, 180, 40, 35, 350, 1, WAVE_SAW, WAVE_SQUARE, 2, 100, 170, 35, 0, 0, 90 }, + { "Retro Stab", 900, 396, 0, 0, 50, 0, 0, 0, 0, WAVE_SQUARE,WAVE_SIN, 1, 40, 0, 60, 0, 0, 0 }, + { "Crystal", 380, 823, 0, 0, 3000, 0, 6, 80, 0, WAVE_SIN, WAVE_SIN, 2,2000, 0,3500, 10, 25, 0 }, +}; + +// ============================================================================ +// STATE +// ============================================================================ + +namespace { + +// Exponential FM mode frequency shift table (from MeeBleeps) +// Index 0 = sub-harmonic (1/x), 1-16 = harmonic multiples +const float FREQ_SHIFT[] PROGMEM = { + 0.0f, 0.03125f, 0.0625f, 0.125f, 0.25f, 0.5f, 1.0f, 1.5f, + 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f +}; + +// Per-voice state +struct Voice { + Oscil<2048, MOZZI_AUDIO_RATE> carrier; + Oscil<2048, MOZZI_AUDIO_RATE> modulator; + ADSR ampEnv; + ADSR modEnv; + Q16n16 carrierFreqQ16; + Q16n16 targetFreqQ16; // portamento target + uint16_t carrierFreqHz; + uint16_t vibratoPhase; // 0-65535 phase accumulator + uint8_t currentGain; // amp envelope output (0-255) + uint8_t midiNote; + uint8_t lastVelocity; + bool playing; +}; + +Voice voices[NUM_VOICES]; + +// Shared state +FMParams params; + +// Shared LFO (one for all voices β€” saves RAM vs per-voice) +Oscil<2048, MOZZI_CONTROL_RATE> sharedLfo(SIN2048_DATA); +uint16_t cachedLfoRate = 0xFFFF; +uint8_t lastLfoValue = 128; // LFO output shifted to 0-255 + +// Dynamic mix attenuation +uint8_t activeMixShift = 0; + +// PROGMEM wavetable pointer lookup +const int8_t* const WAVE_TABLES[] PROGMEM = { + SIN2048_DATA, + SAW2048_DATA, + REVSAW2048_DATA, + SQUARE2048_DATA, + PSEUDORANDOM2048_DATA, + NULLWAVEFORM2048_DATA, +}; + +const int8_t* getWaveTable(uint8_t id) { + if (id >= WAVE_COUNT) id = 0; + return (const int8_t*)pgm_read_ptr(&WAVE_TABLES[id]); +} + +// ------------------------------------------------------------------------- +// Set modulator frequency based on FM mode and ratio +void applyModFreq(Voice& v) { + Q16n16 modFreq; + switch (params.fmMode) { + case FM_MODE_EXPONENTIAL: { + uint8_t idx = params.modRatio / 61; // 0-1023 β†’ 0-16 + if (idx > 16) idx = 16; + float shift; + memcpy_P(&shift, &FREQ_SHIFT[idx], sizeof(float)); + if (idx > 0) { + modFreq = (Q16n16)((float)v.carrierFreqQ16 * shift); + } else { + // Sub-harmonic: carrier / (512 - ratio + 1) + modFreq = (Q16n16)((float)v.carrierFreqQ16 / (float)((512 - params.modRatio) + 1)); + } + break; + } + case FM_MODE_LINEAR_HIGH: + modFreq = (Q16n16)((float)v.carrierFreqQ16 * (float)params.modRatio / 100.0f); + break; + case FM_MODE_LINEAR_LOW: + modFreq = (Q16n16)((float)v.carrierFreqQ16 * (float)params.modRatio / 10000.0f); + break; + case FM_MODE_FREE: + modFreq = (uint32_t)params.modRatio << 17; // 0-1023 β†’ 0-2000 Hz in Q16n16 + break; + default: + modFreq = v.carrierFreqQ16; + break; + } + v.modulator.setFreq_Q16n16(modFreq); +} + +void applyAmpEnvelope(Voice& v) { + v.ampEnv.setADLevels(255, params.ampSustain); + v.ampEnv.setTimes(params.ampAttack, params.ampDecay, 60000UL, params.ampRelease); +} + +void applyModEnvelope(Voice& v) { + v.modEnv.setADLevels(params.modAmount >> 2, 0); // peak = modAmount/4 (0-255) + uint16_t mAttack = params.modEnvAttack < MIN_MOD_ENV_TIME ? 0 : params.modEnvAttack; + v.modEnv.setTimes(mAttack, params.modEnvDecay + MIN_MOD_ENV_TIME, 60000UL, 50); + v.modEnv.setSustainLevel(params.modEnvSustain >> 2); // 0-255 β†’ 0-63 (Mozzi ADSR range) +} + +// Voice allocator β€” quietest-note stealing +uint8_t allocateVoice() { + for (uint8_t i = 0; i < NUM_VOICES; i++) { + if (!voices[i].playing) return i; + } + uint8_t quietest = 0; + uint8_t minLevel = voices[0].currentGain; + for (uint8_t i = 1; i < NUM_VOICES; i++) { + if (voices[i].currentGain < minLevel) { + minLevel = voices[i].currentGain; + quietest = i; + } + } + return quietest; +} + +uint8_t findVoiceByNote(uint8_t note) { + for (uint8_t i = 0; i < NUM_VOICES; i++) { + if (voices[i].playing && voices[i].midiNote == note) return i; + } + return 0xFF; +} + +} // anonymous namespace + +// ============================================================================ +// API IMPLEMENTATION +// ============================================================================ + +void init(uint32_t noiseSeed) { + randSeed(noiseSeed); + params.modAmount = 500; + params.modRatio = 350; + params.fmMode = FM_MODE_EXPONENTIAL; + params.modEnvAttack = 0; + params.modEnvDecay = 400; + params.modEnvSustain = 100; + params.modShape = 400; + params.lfoDepth = 0; + params.lfoRate = 0; + params.lfoWaveform = WAVE_SIN; + params.carrierWave = WAVE_SIN; + params.modWave = WAVE_SIN; + params.ampAttack = 5; + params.ampDecay = 150; + params.ampSustain = 200; + params.ampRelease = 300; + params.vibratoDepth = 0; + params.vibratoSpeed = 0; + params.portamento = 0; + params.presetId = 0; + + for (uint8_t i = 0; i < NUM_VOICES; i++) { + Voice& v = voices[i]; + v.carrier.setTable(SIN2048_DATA); + v.modulator.setTable(SIN2048_DATA); + v.carrierFreqQ16 = 0; + v.targetFreqQ16 = 0; + v.carrierFreqHz = 0; + v.vibratoPhase = 0; + v.currentGain = 0; + v.midiNote = 0; + v.lastVelocity = 127; + v.playing = false; + v.ampEnv.noteOff(); + v.modEnv.noteOff(); + applyAmpEnvelope(v); + applyModEnvelope(v); + } + loadPreset(0); +} + +void noteOn(uint8_t midiNote, uint8_t velocity) { + uint16_t freq = (uint16_t)::mtof(midiNote); + if (freq == 0) return; + + uint8_t idx = findVoiceByNote(midiNote); + if (idx == 0xFF) idx = allocateVoice(); + + Voice& v = voices[idx]; + v.midiNote = midiNote; + v.lastVelocity = velocity; + v.carrierFreqHz = freq; + v.targetFreqQ16 = Q16n16_mtof(Q8n0_to_Q16n16(midiNote)); + + // Portamento: if already playing, glide from current freq + if (v.playing && params.portamento > 0) { + // carrierFreqQ16 stays at current value; updateControl will glide + } else { + v.carrierFreqQ16 = v.targetFreqQ16; + } + + v.carrier.setTable(getWaveTable(params.carrierWave)); + v.modulator.setTable(getWaveTable(params.modWave)); + v.carrier.setFreq_Q16n16(v.carrierFreqQ16); + applyModFreq(v); + applyAmpEnvelope(v); + applyModEnvelope(v); + + v.ampEnv.noteOn(); + v.modEnv.noteOn(true); // retrigger mod envelope + v.playing = true; +} + +void noteOnFreq(uint16_t freqHz, uint8_t velocity) { + if (freqHz == 0) return; + uint8_t idx = allocateVoice(); + Voice& v = voices[idx]; + v.midiNote = 0; + v.lastVelocity = velocity; + v.carrierFreqHz = freqHz; + v.targetFreqQ16 = (Q16n16)freqHz << 16; + v.carrierFreqQ16 = v.targetFreqQ16; + + v.carrier.setTable(getWaveTable(params.carrierWave)); + v.modulator.setTable(getWaveTable(params.modWave)); + v.carrier.setFreq_Q16n16(v.carrierFreqQ16); + applyModFreq(v); + applyAmpEnvelope(v); + applyModEnvelope(v); + + v.ampEnv.noteOn(); + v.modEnv.noteOn(true); + v.playing = true; +} + +void noteOff(uint8_t midiNote) { + uint8_t idx = findVoiceByNote(midiNote); + if (idx != 0xFF) { + voices[idx].ampEnv.noteOff(); + } +} + +void allNotesOff() { + for (uint8_t i = 0; i < NUM_VOICES; i++) { + voices[i].ampEnv.noteOff(); + } +} + +void stop() { + for (uint8_t i = 0; i < NUM_VOICES; i++) { + voices[i].ampEnv.noteOff(); + voices[i].modEnv.noteOff(); + voices[i].playing = false; + } +} + +bool isPlaying() { + for (uint8_t i = 0; i < NUM_VOICES; i++) { + if (voices[i].playing) return true; + } + return false; +} + +uint8_t getActiveVoiceCount() { + uint8_t count = 0; + for (uint8_t i = 0; i < NUM_VOICES; i++) { + if (voices[i].playing) count++; + } + return count; +} + +// ============================================================================ +// CONTROL UPDATE (runs at MOZZI_CONTROL_RATE) +// ============================================================================ + +void updateControl() { + // --- Shared LFO --- + if (params.lfoRate > 0) { + if (params.lfoRate != cachedLfoRate) { + sharedLfo.setFreq((float)params.lfoRate * 0.0078f); // ~0-8 Hz + cachedLfoRate = params.lfoRate; + } + lastLfoValue = (uint8_t)(sharedLfo.next() + 128); // -128..127 β†’ 0..255 + } else { + lastLfoValue = 128; + } + + // --- Per-voice update --- + uint8_t activeCount = 0; + for (uint8_t i = 0; i < NUM_VOICES; i++) { + Voice& v = voices[i]; + if (!v.playing) { + v.currentGain = 0; + continue; + } + + v.ampEnv.update(); + v.modEnv.update(); + + if (!v.ampEnv.playing()) { + v.playing = false; + v.currentGain = 0; + continue; + } + activeCount++; + + // --- Portamento (frequency glide) --- + if (params.portamento > 0 && v.carrierFreqQ16 != v.targetFreqQ16) { + // Glide speed: higher portamento = slower glide + // Shift amount: 1 (fast) to 8 (slow) + uint8_t shift = 1 + (params.portamento >> 5); // 0-255 β†’ 1-8 + Q16n16 diff = v.targetFreqQ16 - v.carrierFreqQ16; + Q16n16 step = diff >> shift; + if (step == 0) { + v.carrierFreqQ16 = v.targetFreqQ16; + } else { + v.carrierFreqQ16 += step; + } + v.carrier.setFreq_Q16n16(v.carrierFreqQ16); + applyModFreq(v); + } + + // --- Vibrato (pitch modulation) --- + if (params.vibratoDepth > 0 && params.vibratoSpeed > 0) { + // Phase accumulator: vibratoSpeed maps to ~0.5-12 Hz + v.vibratoPhase += (uint16_t)params.vibratoSpeed << 4; + // Sine approximation from phase: sin lookup via top 8 bits + // Using a simple triangle: phase 0-32767 β†’ 0..+depth, 32768-65535 β†’ 0..-depth + int16_t vibVal; + if (v.vibratoPhase < 32768U) { + vibVal = (int16_t)((uint32_t)v.vibratoPhase * params.vibratoDepth >> 15); + } else { + vibVal = -(int16_t)((uint32_t)(v.vibratoPhase - 32768U) * params.vibratoDepth >> 15); + } + // Apply as pitch offset (cents-like: scale relative to carrier freq) + // vibVal range: -vibratoDepth..+vibratoDepth + // Map to frequency offset: Β±(carrierFreq * vibVal / 2048) + Q16n16 vibOffset = ((int32_t)v.carrierFreqQ16 >> 11) * vibVal; + v.carrier.setFreq_Q16n16(v.carrierFreqQ16 + vibOffset); + } + + v.currentGain = v.ampEnv.next(); + } + + // Compute shared modulator amount: + // modulatorAmount = (modAmount * modEnvValue) + (lfoDepth * lfoValue) + // Note: modEnv.next() is called per-voice in updateAudio for per-voice envelope tracking, + // but the shared modulatorAmount uses a representative value computed here. + // For shared LFO approach: LFO component is the same for all voices. + // The per-voice mod envelope is handled in updateAudio(). + + // Dynamic mix attenuation + activeMixShift = (activeCount <= 1) ? 0 : 1; +} + +// ============================================================================ +// AUDIO UPDATE (runs at MOZZI_AUDIO_RATE) +// ============================================================================ + +int updateAudio() { + int32_t mix = 0; + + for (uint8_t i = 0; i < NUM_VOICES; i++) { + Voice& v = voices[i]; + if (!v.playing) continue; + + // Per-voice mod envelope value (0-255) + uint8_t modEnvVal = v.modEnv.next(); + + // MeeBleeps formula: modulatorAmount = (modAmount * modEnvVal) + (lfoDepth * lfoVal) + uint32_t modAmt = ((uint32_t)params.modAmount * (uint32_t)modEnvVal) + + ((uint32_t)params.lfoDepth * (uint32_t)lastLfoValue); + + // FM synthesis: carrier.phMod(modulatorAmount * modulator.next() >> 8) * gain >> 8 + int8_t modSample = v.modulator.next(); + int32_t modulation = (int32_t)modAmt * modSample >> 8; + int8_t carrSample = v.carrier.phMod(modulation); + int16_t out = (int16_t)v.currentGain * carrSample; + mix += out; + } + + // Dynamic attenuation + mix >>= activeMixShift; + + if (mix > 32767) mix = 32767; + if (mix < -32768) mix = -32768; + return (int)mix; +} + +// ============================================================================ +// PARAMETER SETTERS +// ============================================================================ + +void setModAmount(uint16_t val) { + if (val > MOD_AMOUNT_MAX) val = MOD_AMOUNT_MAX; + params.modAmount = val; + params.presetId = 255; + for (uint8_t i = 0; i < NUM_VOICES; i++) { + applyModEnvelope(voices[i]); + } +} + +void setModRatio(uint16_t val) { + if (val > MOD_RATIO_MAX) val = MOD_RATIO_MAX; + params.modRatio = val; + params.presetId = 255; + for (uint8_t i = 0; i < NUM_VOICES; i++) { + if (voices[i].playing) applyModFreq(voices[i]); + } +} + +void setFMMode(uint8_t mode) { + if (mode >= FM_MODE_COUNT) mode = FM_MODE_EXPONENTIAL; + params.fmMode = mode; + params.presetId = 255; + for (uint8_t i = 0; i < NUM_VOICES; i++) { + if (voices[i].playing) applyModFreq(voices[i]); + } +} + +void setModEnvAttack(uint16_t ms) { + params.modEnvAttack = ms; + params.presetId = 255; + for (uint8_t i = 0; i < NUM_VOICES; i++) applyModEnvelope(voices[i]); +} + +void setModEnvDecay(uint16_t ms) { + params.modEnvDecay = ms; + params.presetId = 255; + for (uint8_t i = 0; i < NUM_VOICES; i++) applyModEnvelope(voices[i]); +} + +void setModEnvSustain(uint8_t level) { + params.modEnvSustain = level; + params.presetId = 255; + for (uint8_t i = 0; i < NUM_VOICES; i++) applyModEnvelope(voices[i]); +} + +void setModShape(uint16_t val) { + if (val > MAX_MOD_SHAPE) val = MAX_MOD_SHAPE; + params.modShape = val; + params.presetId = 255; + + // 1-knob macro (ported from MeeBleeps setModulationShape): + // Attack: 0 for first 60%, then linear + if (val < 600) { + params.modEnvAttack = 0; + } else { + params.modEnvAttack = (val - 600) * 2; + } + + // Decay: direct mapping + params.modEnvDecay = val; + + // Sustain: cubic for first half, max for middle, decreasing for last quarter + if (val < 512) { + uint32_t p = (uint32_t)val * val; + p = (p * val) / 262144UL; + params.modEnvSustain = (uint8_t)(p > 255 ? 255 : p); + } else if (val < 768) { + params.modEnvSustain = 255; + } else { + params.modEnvSustain = (uint8_t)(255 + ((768 - (int16_t)val) / 2)); + } + + for (uint8_t i = 0; i < NUM_VOICES; i++) applyModEnvelope(voices[i]); +} + +void setLFODepth(uint8_t depth) { + params.lfoDepth = depth; + params.presetId = 255; +} + +void setLFORate(uint16_t rate) { + if (rate > LFO_RATE_MAX) rate = LFO_RATE_MAX; + params.lfoRate = rate; + params.presetId = 255; +} + +void setLFOWaveform(uint8_t id) { + if (id >= WAVE_COUNT) id = 0; + params.lfoWaveform = id; + params.presetId = 255; + sharedLfo.setTable(getWaveTable(id)); +} + +void setAmpADSR(uint16_t attack, uint16_t decay, uint8_t sustain, uint16_t release) { + params.ampAttack = attack; + params.ampDecay = decay; + params.ampSustain = sustain; + params.ampRelease = release; + params.presetId = 255; + for (uint8_t i = 0; i < NUM_VOICES; i++) applyAmpEnvelope(voices[i]); +} + +void setVibratoDepth(uint8_t depth) { + params.vibratoDepth = depth; + params.presetId = 255; +} + +void setVibratoSpeed(uint8_t speed) { + params.vibratoSpeed = speed; + params.presetId = 255; +} + +void setPortamento(uint8_t speed) { + params.portamento = speed; + params.presetId = 255; +} + +void setCarrierWave(uint8_t id) { + if (id >= WAVE_COUNT) id = 0; + params.carrierWave = id; + const int8_t* table = getWaveTable(id); + for (uint8_t i = 0; i < NUM_VOICES; i++) voices[i].carrier.setTable(table); +} + +void setModulatorWave(uint8_t id) { + if (id >= WAVE_COUNT) id = 0; + params.modWave = id; + const int8_t* table = getWaveTable(id); + for (uint8_t i = 0; i < NUM_VOICES; i++) voices[i].modulator.setTable(table); +} + +uint8_t nextCarrierWave() { + uint8_t next = (params.carrierWave + 1) % WAVE_COUNT; + setCarrierWave(next); + return next; +} + +uint8_t nextFMMode() { + uint8_t next = (params.fmMode + 1) % FM_MODE_COUNT; + setFMMode(next); + return next; +} + +void loadPreset(uint8_t id) { + if (id >= FM_NUM_PRESETS) return; + FMPreset p; + memcpy_P(&p, &FM_PRESETS[id], sizeof(FMPreset)); + + params.modAmount = p.modAmount; + params.modRatio = p.modRatio; + params.fmMode = p.fmMode; + params.modEnvAttack = p.modEnvAttack; + params.modEnvDecay = p.modEnvDecay; + params.modEnvSustain = p.modEnvSustain; + params.lfoDepth = p.lfoDepth; + params.lfoRate = p.lfoRate; + params.lfoWaveform = p.lfoWaveform; + params.ampAttack = p.ampAttack; + params.ampDecay = p.ampDecay; + params.ampSustain = p.ampSustain; + params.ampRelease = p.ampRelease; + params.vibratoDepth = p.vibratoDepth; + params.vibratoSpeed = p.vibratoSpeed; + params.portamento = p.portamento; + params.presetId = id; + + setCarrierWave(p.carrierWave); + setModulatorWave(p.modWave); + setLFOWaveform(p.lfoWaveform); + cachedLfoRate = 0xFFFF; // force LFO re-init + + for (uint8_t i = 0; i < NUM_VOICES; i++) { + applyAmpEnvelope(voices[i]); + applyModEnvelope(voices[i]); + } +} + +const FMParams& getParams() { + return params; +} + +uint16_t getCarrierFreq() { + for (uint8_t i = 0; i < NUM_VOICES; i++) { + if (voices[i].playing) return voices[i].carrierFreqHz; + } + return 0; +} + +uint16_t midiToFreqHz(uint8_t note) { + return (uint16_t)::mtof(note); +} + +} // namespace FM diff --git a/src/sound/wavetable.cpp b/src/sound/wavetable.cpp new file mode 100644 index 0000000..4c9cffc --- /dev/null +++ b/src/sound/wavetable.cpp @@ -0,0 +1,18 @@ +/** + * @file wavetable.cpp + * @brief FM oscillator table data β€” sine (carrier) and cosine (modulator/LFO) + */ + +#include "sound/wavetable.h" + +// Included ONCE here β€” never in any other .cpp +#include +#include + +const int8_t* getCos2048Data() { + return COS2048_DATA; +} + +const int8_t* getSin2048Data() { + return SIN2048_DATA; +} diff --git a/tools/gen_char_digits.py b/tools/gen_char_digits.py new file mode 100644 index 0000000..5a9192d --- /dev/null +++ b/tools/gen_char_digits.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +""" +Generate a 7-segment display digit lookup table for C/C++ header. +""" + +def gen_7seg_digit_table(): + # Segment bits: 0bgfedcba (a-g, dp) + digits = [ + 0x3f, # 0 + 0x06, # 1 + 0x5b, # 2 + 0x4f, # 3 + 0x66, # 4 + 0x6d, # 5 + 0x7d, # 6 + 0x07, # 7 + 0x7f, # 8 + 0x6f, # 9 + ] + header = [ + '#ifndef CHAR_DIGITS_H', + '#define CHAR_DIGITS_H', + '', + '#include ', + '', + '// 7-segment display digit lookup table (0-9)', + 'static const uint8_t CHAR_DIGITS[] = {', + ' ' + ', '.join(f'0x{d:02x}' for d in digits), + '};', + '', + '#endif // CHAR_DIGITS_H', + ] + return '\n'.join(header) + +if __name__ == "__main__": + print(gen_7seg_digit_table()) diff --git a/tools/gen_digit_tables.py b/tools/gen_digit_tables.py new file mode 100644 index 0000000..65183d0 --- /dev/null +++ b/tools/gen_digit_tables.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" +Generate lookup tables for 7-segment display digit extraction (hundreds, tens, ones) for C/C++ header. +""" + +def gen_digit_tables(): + header = [ + '#ifndef DIGIT_TABLES_H', + '#define DIGIT_TABLES_H', + '', + '#include ', + '#include ', + '', + '// Lookup tables for extracting hundreds, tens, ones digits from 0-255 without division/modulo', + 'static const uint8_t DIGIT_HUNDREDS[256] PROGMEM = {', + ' ' + ', '.join(str(v // 100) for v in range(256)), + '};', + '', + 'static const uint8_t DIGIT_TENS[256] PROGMEM = {', + ' ' + ', '.join(str((v // 10) % 10) for v in range(256)), + '};', + '', + 'static const uint8_t DIGIT_ONES[256] PROGMEM = {', + ' ' + ', '.join(str(v % 10) for v in range(256)), + '};', + '', + '#endif // DIGIT_TABLES_H', + ] + return '\n'.join(header) + +if __name__ == "__main__": + print(gen_digit_tables()) diff --git a/tools/gen_maps.py b/tools/gen_maps.py new file mode 100644 index 0000000..1c375ac --- /dev/null +++ b/tools/gen_maps.py @@ -0,0 +1,49 @@ +import math + +def generate_maps(): + size = 256 + + # Linear: y = x + linear = [i for i in range(size)] + + # Exponential: y = (i/255)^2 * 255 (Simple quadratic for natural sound) + # k = 2.0 + expo = [int(round(((i/255.0)**2.0) * 255)) for i in range(size)] + + # Logarithmic: Inverse of expo. note stays high longer. + # y = sqrt(i/255) * 255 + log = [int(round(math.sqrt(i/255.0) * 255)) for i in range(size)] + + # Format for C header + def format_table(name, data): + output = f"const uint8_t {name}[] PROGMEM = {{\n " + rows = [] + for i in range(0, len(data), 16): + row = ", ".join(map(str, data[i:i+16])) + rows.append(row) + output += ",\n ".join(rows) + output += "\n};\n\n" + return output + + header_content = """/** + * @file maps.h + * @brief Auto-generated Envelope Falloff Mapping Tables + * Generated by tools/gen_maps.py + */ + +#ifndef TABLES_MAPS_H +#define TABLES_MAPS_H + +#include + +""" + header_content += format_table("MAP_EXP256", expo) + header_content += format_table("MAP_LOG256", log) + header_content += "#endif // TABLES_MAPS_H\n" + + with open("include/tables/maps.h", "w") as f: + f.write(header_content) + print("Successfully generated include/tables/maps.h") + +if __name__ == "__main__": + generate_maps() diff --git a/tools/gui/app.py b/tools/gui/app.py index 1dc6525..ddd1ad2 100644 --- a/tools/gui/app.py +++ b/tools/gui/app.py @@ -6,7 +6,7 @@ from .config import ( WHITE_KEYS, BLACK_KEYS, KEYBOARD_TO_NOTE, NOTE_TO_KEYBOARD, - INSTRUMENTS, MELODIES, WAVES + FM_PRESETS, MELODIES, WAVE_NAMES, FM_MODE_NAMES ) from .serial_comm import ArduinoSerial from .components import PianoKey @@ -17,8 +17,8 @@ class PianoGUI: def __init__(self, root: tk.Tk, port: Optional[str] = None): self.root = root - self.root.title("Arduino Piano Player - Polyphony Edition") - self.root.geometry("1100x600") # Start wider for 61 keys + self.root.title("Arduino FM Synthwave") + self.root.geometry("1100x600") self.root.resizable(True, True) self.arduino = ArduinoSerial() @@ -29,6 +29,7 @@ def __init__(self, root: tk.Tk, port: Optional[str] = None): self.keys: Dict[str, PianoKey] = {} self.pressed_keys: Set[str] = set() self.current_note: Optional[str] = None + self._curve_map = {} # {id(IntVar): (slider_DoubleVar, max_val)} for quadratic sliders self.setup_ui() self.setup_keyboard_bindings() @@ -70,56 +71,51 @@ def setup_ui(self): # Bind resize event with debounce self.piano_frame.bind('', self.on_piano_resize) - # === Controls Frame === + # === Row 1: FM Synth / Performance / FM ADSR === + live_frame = ttk.Frame(main_frame) + live_frame.pack(fill=tk.X, pady=(0, 5)) + + # FM Synthesis (preset + sliders) + fm_frame = ttk.LabelFrame(live_frame, text="FM Synth", padding=5) + fm_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) + self.create_fm_controls(fm_frame) + + # Mod Envelope (Attack/Decay/Sustain + Shape macro) + mod_env_frame = ttk.LabelFrame(live_frame, text="Mod Env", padding=5) + mod_env_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) + self.create_mod_env_controls(mod_env_frame) + + # LFO (Rate / Depth / Waveform) + lfo_frame = ttk.LabelFrame(live_frame, text="LFO", padding=5) + lfo_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) + self.create_lfo_controls(lfo_frame) + + # Amp ADSR + adsr_frame = ttk.LabelFrame(live_frame, text="Amp ADSR", padding=5) + adsr_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) + self.create_envelope_controls(adsr_frame) + + # Performance (Vibrato / Portamento) + perf_frame = ttk.LabelFrame(live_frame, text="Performance", padding=5) + perf_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) + self.create_performance_controls(perf_frame) + + # === Row 2: Pitch / Recording === controls_frame = ttk.Frame(main_frame) controls_frame.pack(fill=tk.X, pady=(0, 10)) - - # Volume (Removed - Potentiometer Only) - # vol_frame = ttk.LabelFrame(controls_frame, text="Volume", padding=5) - # vol_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) - # ... (Removed) - - # Waveform Selector - wave_frame = ttk.LabelFrame(controls_frame, text="Waveform", padding=5) - wave_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) - - self.waveform_var = tk.StringVar(value="Piano") - - self.wave_combo = ttk.Combobox(wave_frame, textvariable=self.waveform_var, values=WAVES, width=10, state="readonly") - self.wave_combo.pack(side=tk.LEFT, pady=5) - self.wave_combo.bind("<>", self.on_waveform_change) - - # Envelopes (Instruments) - inst_frame = ttk.LabelFrame(controls_frame, text="Envelope Presets", padding=5) - inst_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) - - self.instrument_var = tk.IntVar(value=0) - - # Envelopes are now just ADSR presets - presets = [ - (0, "Piano"), (1, "Organ"), (2, "Staccato"), (3, "Pad") - ] - - for idx, name in presets: - ttk.Radiobutton(inst_frame, text=name, value=idx, - variable=self.instrument_var, - command=lambda i=idx: self.set_instrument(i)).pack(side=tk.LEFT, padx=2) - - # Custom Envelope - env_frame = ttk.LabelFrame(controls_frame, text="Custom ADSR", padding=5) - env_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) - self.create_envelope_controls(env_frame) - # Load default preset (Piano) values into sliders after creation - self.root.after(100, lambda: self.load_preset_to_sliders(0)) + # Global Pitch Transpose + pitch_frame = ttk.LabelFrame(controls_frame, text="Pitch", padding=5) + pitch_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) + self.env_pitch = tk.IntVar(value=0) + self.create_pitch_slider(pitch_frame, "Transpose", self.env_pitch, 24, -24, 100) # Recording - rec_frame = ttk.LabelFrame(controls_frame, text="EEPROM Recording", padding=5) + rec_frame = ttk.LabelFrame(controls_frame, text="EEPROM Rec", padding=5) rec_frame.pack(side=tk.LEFT, padx=(0, 10), fill=tk.Y) - - ttk.Button(rec_frame, text="⏺ Rec", width=6, command=self.start_recording).pack(side=tk.LEFT) + ttk.Button(rec_frame, text="⏺ Rec", width=6, command=self.start_recording).pack(side=tk.LEFT) ttk.Button(rec_frame, text="⏹ Stop", width=6, command=self.stop_recording).pack(side=tk.LEFT, padx=2) - ttk.Button(rec_frame, text="β–Ά Play", width=6, command=self.play_recording).pack(side=tk.LEFT, padx=2) + ttk.Button(rec_frame, text="β–Ά Play", width=6, command=self.play_recording).pack(side=tk.LEFT, padx=2) ttk.Button(rec_frame, text="πŸ—‘ Clear", width=6, command=self.clear_recording).pack(side=tk.LEFT) # === Melody Frame === @@ -379,7 +375,7 @@ def on_mouse_press(self, event): key = self.get_key_at_position(event.x_root, event.y_root) if key: self.mouse_note = key.note - self.on_key_press(key.note) + self.on_key_press(key.note, 100) # Mouse press at fixed velocity 100 def on_mouse_release(self, event): self.mouse_down = False @@ -411,38 +407,29 @@ def get_key_at_position(self, x, y): # --- Logic --- - def on_key_press(self, note: str): - # Ignore repeat key presses (OS auto-repeat) - if hasattr(self, 'pressed_keys_midi') and note in self.pressed_keys_midi: - return - - # Update visual state + def on_key_press(self, note: str, velocity: int = 127): + # Update visual state β€” also guards against duplicate press if note in self.keys: - if self.keys[note].pressed: return # Already pressed visually + if self.keys[note].pressed and velocity > 0: return # Already pressed visually self.keys[note].set_pressed(True) - # Binary Protocol: Note ON - # Map Note Name -> MIDI Note -> Byte - # C4 = 60. Protocol: 0x95 (149) = Note 21. - # So Byte = Note + 128. + # Proper MIDI Protocol: Note ON (0x90, Note, Velocity) midi = self.note_to_midi(note) if midi is not None: - self.arduino.send_bytes(bytes([midi + 128])) - # self.log(f"β†’ Note ON {note} ({midi})") # Create log spam? + # Command 0x90 = Note On, Channel 1 + self.arduino.send_bytes(bytes([0x90, midi, velocity])) self.log_msg_sent() - def on_key_release(self, note: str): + def on_key_release(self, note: str, velocity: int = 0): # Update visual state if note in self.keys: self.keys[note].set_pressed(False) - # Binary Protocol: Note OFF - # Protocol: 0x15 (21) = Note 21. - # So Byte = Note. + # Proper MIDI Protocol: Note OFF (0x80, Note, Velocity) midi = self.note_to_midi(note) if midi is not None: - self.arduino.send_bytes(bytes([midi])) - # self.log(f"β†’ Note OFF {note}") + # Command 0x80 = Note Off, Channel 1 + self.arduino.send_bytes(bytes([0x80, midi, velocity])) self.log_msg_sent() def note_to_midi(self, note_name: str) -> Optional[int]: @@ -462,47 +449,6 @@ def note_to_midi(self, note_name: str) -> Optional[int]: except: return None - def on_waveform_change(self, event): - wave_name = self.waveform_var.get() - try: - idx = WAVES.index(wave_name) - # CMD_SET_WAVEFORM (0x09) + ID - self.arduino.send_bytes(bytes([0x09, idx])) - self.log(f"Waveform: {wave_name}") - except ValueError: - pass - - def set_instrument(self, idx: int): - # CMD_SET_INST (0x07) + ID (Now sets Envelope only) - self.arduino.send_bytes(bytes([0x07, idx])) - # Fetch name from config if possible, or just log index - try: - name = INSTRUMENTS[idx]['name'] - except: - name = f"Preset {idx}" - - self.log(f"Envelope: {name}") - - # Load preset values into sliders (visual only, unless custom) - if idx != 4: - self.load_preset_to_sliders(idx) - - # If Custom, re-send immediately - if idx == 4: - self.send_custom_envelope() - - def load_preset_to_sliders(self, idx: int): - """Load preset ADSR values into the GUI sliders""" - if hasattr(self, 'env_attack'): - adsr = INSTRUMENTS[idx].get('adsr') - if adsr: - self.env_attack.set(adsr[0]) - self.env_decay.set(adsr[1]) - self.env_sustain.set(adsr[2]) - self.env_sustain_length.set(adsr[3] if len(adsr) > 3 else 5000) - self.env_release.set(adsr[4] if len(adsr) > 4 else 100) - self.env_octave.set(adsr[5] if len(adsr) > 5 else 0) - # --- Recording (Binary Commands) --- def start_recording(self): self.arduino.send_bytes(bytes([0x01])) def stop_recording(self): self.arduino.send_bytes(bytes([0x02])) @@ -513,77 +459,385 @@ def play_melody(self, idx): # CMD_PLAY_MELODY (0x08) + ID self.arduino.send_bytes(bytes([0x08, idx])) - # --- Envelope --- - def create_envelope_controls(self, parent): - # A, D, S, SL, R, Oct - self.env_attack = tk.IntVar(value=10) - self.create_slider(parent, "A", self.env_attack, 1000, 0, 100) - - self.env_decay = tk.IntVar(value=100) - self.create_slider(parent, "D", self.env_decay, 1000, 0, 100) - - self.env_sustain = tk.IntVar(value=128) - self.create_slider(parent, "S", self.env_sustain, 255, 0, 100) - - self.env_sustain_length = tk.IntVar(value=5000) - self.create_slider(parent, "SL", self.env_sustain_length, 10000, 0, 100) + # --- Quadratic slider curve helpers --- - self.env_release = tk.IntVar(value=100) - self.create_slider(parent, "R", self.env_release, 1000, 0, 100) + def _sync_slider_pos(self, variable): + """Update curved slider position to match current variable value.""" + key = id(variable) + if key in self._curve_map: + slider_var, max_val = self._curve_map[key] + v = variable.get() + slider_var.set(1000.0 * (v / max_val) ** 0.5 if max_val > 0 else 0) - self.env_octave = tk.IntVar(value=0) - self.create_slider(parent, "Oct", self.env_octave, 3, -3, 100) + def _sync_curved_var(self, variable, value): + """Set variable and update slider position if curved.""" + variable.set(value) + self._sync_slider_pos(variable) - ttk.Button(parent, text="Set", width=3, command=self.send_custom_envelope).pack(side=tk.BOTTOM, pady=2) + # --- FM ADSR --- + def create_envelope_controls(self, parent): + self.env_attack = tk.IntVar(value=5) + self.env_decay = tk.IntVar(value=150) + self.env_sustain = tk.IntVar(value=200) + self.env_release = tk.IntVar(value=300) + self._make_adsr_slider(parent, "A", self.env_attack, 0, 2000, curved=True) + self._make_adsr_slider(parent, "D", self.env_decay, 0, 2000, curved=True) + self._make_adsr_slider(parent, "S", self.env_sustain, 0, 255) + self._make_adsr_slider(parent, "R", self.env_release, 0, 2000, curved=True) + ttk.Button(parent, text="Set", width=3, command=self.send_fm_adsr).pack(side=tk.BOTTOM, pady=2) - def create_slider(self, parent, label, variable, max_val, min_val, length): + def _make_adsr_slider(self, parent, label, variable, from_, to, curved=False): frame = ttk.Frame(parent) frame.pack(side=tk.LEFT, padx=4, fill=tk.Y) - ttk.Label(frame, text=label, font=('Arial', 8, 'bold')).pack(side=tk.TOP) - - # Entry box for fine tuning / value display entry = ttk.Entry(frame, textvariable=variable, width=4) entry.pack(side=tk.TOP, pady=(0, 2)) - - # Bind Return key to update - entry.bind('', lambda e: self.send_custom_envelope()) - - # Force integer stepping - scale = ttk.Scale(frame, from_=max_val, to=min_val, orient=tk.VERTICAL, - variable=variable, length=length, - command=lambda v: variable.set(round(float(v)))) + entry.bind('', lambda e, v=variable: (self._sync_slider_pos(v), self.send_fm_adsr())) + if curved and to > 0: + slider_var = tk.DoubleVar() + self._curve_map[id(variable)] = (slider_var, to) + v = variable.get() + slider_var.set(1000.0 * (v / to) ** 0.5 if to > 0 else 0) + def on_slide(val, mx=to, var=variable): + norm = float(val) / 1000.0 + var.set(int(round(norm * norm * mx))) + scale = ttk.Scale(frame, from_=1000, to=0, orient=tk.VERTICAL, + variable=slider_var, length=100, command=on_slide) + else: + scale = ttk.Scale(frame, from_=to, to=from_, orient=tk.VERTICAL, + variable=variable, length=100, + command=lambda v: variable.set(round(float(v)))) scale.pack(side=tk.TOP, fill=tk.Y, expand=True) - - # Update on drag release (to avoid spamming serial), but Entry updates live via variable - scale.bind("", lambda e: self.send_custom_envelope()) + scale.bind("", lambda e: self.send_fm_adsr()) - def send_custom_envelope(self): - # Auto-switch to Custom instrument - if self.instrument_var.get() != 4: - self.instrument_var.set(4) - self.set_instrument(4) - - # CMD_SET_ADSR (0x05) + 10 bytes: [A_hi, A_lo, D_hi, D_lo, S, SL_hi, SL_lo, R_hi, R_lo, Oct] + def send_fm_adsr(self): a = self.env_attack.get() d = self.env_decay.get() s = self.env_sustain.get() - sl = self.env_sustain_length.get() r = self.env_release.get() - o = self.env_octave.get() * 12 # Convert octaves to semitones (Arduino expects semitones) - - # Pack - payload = [ + payload = bytes([ 0x05, (a >> 8) & 0xFF, a & 0xFF, (d >> 8) & 0xFF, d & 0xFF, s & 0xFF, - (sl >> 8) & 0xFF, sl & 0xFF, - (r >> 8) & 0xFF, r & 0xFF, - o & 0xFF # int8 cast to uint8 for byte packing - ] - self.arduino.send_bytes(bytes(payload)) - self.log(f"β†’ Custom Env Set") + (r >> 8) & 0xFF, r & 0xFF + ]) + self.arduino.send_bytes(payload) + self.log(f"FM ADSR: A={a} D={d} S={s} R={r}") + + def create_pitch_slider(self, parent, label, variable, max_val, min_val, length): + frame = ttk.Frame(parent) + frame.pack(side=tk.LEFT, padx=4, fill=tk.Y) + ttk.Label(frame, text=label, font=('Arial', 8, 'bold')).pack(side=tk.TOP) + entry = ttk.Entry(frame, textvariable=variable, width=4) + entry.pack(side=tk.TOP, pady=(0, 2)) + entry.bind('', lambda e: self.send_pitch_transpose()) + scale = ttk.Scale(frame, from_=max_val, to=min_val, orient=tk.VERTICAL, + variable=variable, length=length, + command=lambda v: variable.set(round(float(v)))) + scale.pack(side=tk.TOP, fill=tk.Y, expand=True) + scale.bind("", lambda e: self.send_pitch_transpose()) + + def send_pitch_transpose(self): + val = self.env_pitch.get() + # CMD_SET_PITCH (0x0A) + 1 byte (signed int8) + try: + payload = [0x0A, val & 0xFF] + self.arduino.send_bytes(bytes(payload)) + self.log(f"Pitch: {val} semitones") + except Exception as e: + self.log(f"Error: {e}") + + # --- FM Synthesis --- + def create_fm_controls(self, parent): + # Preset selector + ttk.Label(parent, text="Preset:", font=('Arial', 8, 'bold')).pack(anchor=tk.W) + self.fm_preset_var = tk.StringVar(value=FM_PRESETS[0]['name']) + preset_names = [p['name'] for p in FM_PRESETS] + preset_combo = ttk.Combobox(parent, textvariable=self.fm_preset_var, + values=preset_names, width=10, state="readonly") + preset_combo.pack(anchor=tk.W, pady=(0, 4)) + preset_combo.bind("<>", self._on_preset_change) + + # Mod Amount (0-1023) β€” quadratic for finer low-end control + self.mod_amount_var = tk.IntVar(value=650) + self._make_fm_slider(parent, "Mod Amt", self.mod_amount_var, 0, 1023, + self.send_mod_amount, curved=True) + + # Mod Ratio (0-1023) + self.mod_ratio_var = tk.IntVar(value=512) + self._make_fm_slider(parent, "Ratio", self.mod_ratio_var, 0, 1023, + self.send_mod_ratio) + + # FM Mode selector + mode_row = ttk.Frame(parent) + mode_row.pack(fill=tk.X, pady=1) + ttk.Label(mode_row, text="Mode:", font=('Arial', 8), width=7, anchor=tk.W).pack(side=tk.LEFT) + self.fm_mode_var = tk.StringVar(value=FM_MODE_NAMES[0]) + mode_combo = ttk.Combobox(mode_row, textvariable=self.fm_mode_var, + values=FM_MODE_NAMES, width=10, state="readonly") + mode_combo.pack(side=tk.LEFT, padx=2) + mode_combo.bind("<>", lambda e: self._send_fm_mode()) + + # Carrier wave selector + wave_row = ttk.Frame(parent) + wave_row.pack(fill=tk.X, pady=1) + ttk.Label(wave_row, text="Carrier:", font=('Arial', 8), width=7, anchor=tk.W).pack(side=tk.LEFT) + self.carrier_wave_var = tk.StringVar(value=WAVE_NAMES[0]) + cw_combo = ttk.Combobox(wave_row, textvariable=self.carrier_wave_var, + values=WAVE_NAMES, width=6, state="readonly") + cw_combo.pack(side=tk.LEFT, padx=2) + cw_combo.bind("<>", lambda e: self._send_carrier_wave()) + + # Modulator wave selector + mw_row = ttk.Frame(parent) + mw_row.pack(fill=tk.X, pady=1) + ttk.Label(mw_row, text="Mod:", font=('Arial', 8), width=7, anchor=tk.W).pack(side=tk.LEFT) + self.mod_wave_var = tk.StringVar(value=WAVE_NAMES[0]) + mw_combo = ttk.Combobox(mw_row, textvariable=self.mod_wave_var, + values=WAVE_NAMES, width=6, state="readonly") + mw_combo.pack(side=tk.LEFT, padx=2) + mw_combo.bind("<>", lambda e: self._send_mod_wave()) + + def _on_preset_change(self, event): + idx = next((p['id'] for p in FM_PRESETS if p['name'] == self.fm_preset_var.get()), 0) + self.load_fm_preset(idx) + + def load_fm_preset(self, preset_id: int): + self.arduino.send_bytes(bytes([0x18, preset_id & 0xFF])) + if preset_id < len(FM_PRESETS): + p = FM_PRESETS[preset_id] + if hasattr(self, 'mod_amount_var'): self._sync_curved_var(self.mod_amount_var, p['mod_amount']) + if hasattr(self, 'mod_ratio_var'): self.mod_ratio_var.set(p['mod_ratio']) + if hasattr(self, 'fm_mode_var'): self.fm_mode_var.set(FM_MODE_NAMES[p.get('fm_mode', 0)]) + if hasattr(self, 'carrier_wave_var'): self.carrier_wave_var.set(WAVE_NAMES[p.get('cwave', 0)]) + if hasattr(self, 'mod_wave_var'): self.mod_wave_var.set(WAVE_NAMES[p.get('mwave', 0)]) + # Mod envelope + mod_env = p.get('mod_env', (5, 120, 200)) + if hasattr(self, 'mod_env_attack_var'): self._sync_curved_var(self.mod_env_attack_var, mod_env[0]) + if hasattr(self, 'mod_env_decay_var'): self._sync_curved_var(self.mod_env_decay_var, mod_env[1]) + if hasattr(self, 'mod_env_sustain_var'): self.mod_env_sustain_var.set(mod_env[2]) + # LFO + if hasattr(self, 'lfo_depth_var'): self.lfo_depth_var.set(p.get('lfo_depth', 0)) + if hasattr(self, 'lfo_rate_var'): self._sync_curved_var(self.lfo_rate_var, p.get('lfo_rate', 0)) + if hasattr(self, 'lfo_wave_var'): self.lfo_wave_var.set(WAVE_NAMES[p.get('lfo_wave', 0)]) + # Amp ADSR + if hasattr(self, 'env_attack'): + adsr = p.get('adsr', (5, 150, 200, 300)) + self._sync_curved_var(self.env_attack, adsr[0]) + self._sync_curved_var(self.env_decay, adsr[1]) + self.env_sustain.set(adsr[2]) + self._sync_curved_var(self.env_release, adsr[3]) + # Performance + if hasattr(self, 'vibrato_depth_var'): self.vibrato_depth_var.set(p.get('vibrato_depth', 0)) + if hasattr(self, 'vibrato_speed_var'): self.vibrato_speed_var.set(p.get('vibrato_speed', 0)) + if hasattr(self, 'portamento_var'): self.portamento_var.set(p.get('portamento', 0)) + self.log(f"Preset: {p['name']}") + + def _make_fm_slider(self, parent, label, variable, from_, to, command, curved=False): + row = ttk.Frame(parent) + row.pack(fill=tk.X, pady=1) + ttk.Label(row, text=f"{label}:", font=('Arial', 8), width=7, anchor=tk.W).pack(side=tk.LEFT) + entry = ttk.Entry(row, textvariable=variable, width=5) + entry.pack(side=tk.LEFT, padx=2) + entry.bind('', lambda e, v=variable: (self._sync_slider_pos(v), command())) + if curved and to > 0: + slider_var = tk.DoubleVar() + self._curve_map[id(variable)] = (slider_var, to) + v = variable.get() + slider_var.set(1000.0 * (v / to) ** 0.5 if to > 0 else 0) + def on_slide(val, mx=to, var=variable): + norm = float(val) / 1000.0 + var.set(int(round(norm * norm * mx))) + scale = ttk.Scale(row, from_=0, to=1000, orient=tk.HORIZONTAL, + variable=slider_var, length=80, command=on_slide) + else: + scale = ttk.Scale(row, from_=from_, to=to, orient=tk.HORIZONTAL, + variable=variable, length=80, + command=lambda v: variable.set(round(float(v)))) + scale.pack(side=tk.LEFT) + scale.bind("", lambda e: command()) + + def send_mod_amount(self): + val = self.mod_amount_var.get() + self.arduino.send_bytes(bytes([0x0E, (val >> 8) & 0xFF, val & 0xFF])) + self.log(f"Mod amount: {val}") + + def send_mod_ratio(self): + val = self.mod_ratio_var.get() + self.arduino.send_bytes(bytes([0x10, (val >> 8) & 0xFF, val & 0xFF])) + self.log(f"Mod ratio: {val}") + + def _send_fm_mode(self): + name = self.fm_mode_var.get() + idx = FM_MODE_NAMES.index(name) if name in FM_MODE_NAMES else 0 + self.arduino.send_bytes(bytes([0x1B, idx & 0xFF])) + self.log(f"FM mode: {name}") + + def _send_carrier_wave(self): + name = self.carrier_wave_var.get() + idx = WAVE_NAMES.index(name) if name in WAVE_NAMES else 0 + self.arduino.send_bytes(bytes([0x19, idx & 0xFF])) + self.log(f"Carrier wave: {name}") + + def _send_mod_wave(self): + name = self.mod_wave_var.get() + idx = WAVE_NAMES.index(name) if name in WAVE_NAMES else 0 + self.arduino.send_bytes(bytes([0x1A, idx & 0xFF])) + self.log(f"Mod wave: {name}") + + def create_mod_env_controls(self, parent): + """Mod envelope: Attack/Decay/Sustain sliders + Shape macro.""" + self.mod_env_attack_var = tk.IntVar(value=5) + self._make_mod_env_slider(parent, "A", self.mod_env_attack_var, 0, 4096, + lambda: self._send_mod_env_param(0x1F, self.mod_env_attack_var), + curved=True) + self.mod_env_decay_var = tk.IntVar(value=120) + self._make_mod_env_slider(parent, "D", self.mod_env_decay_var, 0, 4096, + lambda: self._send_mod_env_param(0x20, self.mod_env_decay_var), + curved=True) + self.mod_env_sustain_var = tk.IntVar(value=210) + self._make_mod_env_slider(parent, "S", self.mod_env_sustain_var, 0, 255, + lambda: self._send_mod_env_1byte(0x21, self.mod_env_sustain_var)) + # Mod Shape macro (0-1023) + self.mod_shape_var = tk.IntVar(value=512) + self._make_mod_env_slider(parent, "Shape", self.mod_shape_var, 0, 1023, + self._send_mod_shape) + + def _make_mod_env_slider(self, parent, label, variable, from_, to, command, curved=False): + row = ttk.Frame(parent) + row.pack(fill=tk.X, pady=1) + ttk.Label(row, text=f"{label}:", font=('Arial', 8), width=5, anchor=tk.W).pack(side=tk.LEFT) + entry = ttk.Entry(row, textvariable=variable, width=4) + entry.pack(side=tk.LEFT, padx=2) + entry.bind('', lambda e, v=variable: (self._sync_slider_pos(v), command())) + if curved and to > 0: + slider_var = tk.DoubleVar() + self._curve_map[id(variable)] = (slider_var, to) + v = variable.get() + slider_var.set(1000.0 * (v / to) ** 0.5 if to > 0 else 0) + def on_slide(val, mx=to, var=variable): + norm = float(val) / 1000.0 + var.set(int(round(norm * norm * mx))) + scale = ttk.Scale(row, from_=0, to=1000, orient=tk.HORIZONTAL, + variable=slider_var, length=80, command=on_slide) + else: + scale = ttk.Scale(row, from_=from_, to=to, orient=tk.HORIZONTAL, + variable=variable, length=80, + command=lambda v: variable.set(round(float(v)))) + scale.pack(side=tk.LEFT, fill=tk.X, expand=True) + scale.bind("", lambda e: command()) + + def _send_mod_env_param(self, cmd_byte, variable): + val = variable.get() + self.arduino.send_bytes(bytes([cmd_byte, (val >> 8) & 0xFF, val & 0xFF])) + + def _send_mod_env_1byte(self, cmd_byte, variable): + val = variable.get() + self.arduino.send_bytes(bytes([cmd_byte, val & 0xFF])) + + def _send_mod_shape(self): + val = self.mod_shape_var.get() + self.arduino.send_bytes(bytes([0x1C, (val >> 8) & 0xFF, val & 0xFF])) + self.log(f"Mod shape: {val}") + + def create_lfo_controls(self, parent): + """LFO: Rate/Depth sliders + Waveform selector.""" + self.lfo_rate_var = tk.IntVar(value=180) + self._make_lfo_slider(parent, "Rate", self.lfo_rate_var, 0, 1023, + self._send_lfo_rate, curved=True) + self.lfo_depth_var = tk.IntVar(value=20) + self._make_lfo_slider(parent, "Depth", self.lfo_depth_var, 0, 255, + self._send_lfo_depth) + # LFO Waveform selector + wf_row = ttk.Frame(parent) + wf_row.pack(fill=tk.X, pady=1) + ttk.Label(wf_row, text="Wave:", font=('Arial', 8), width=5, anchor=tk.W).pack(side=tk.LEFT) + self.lfo_wave_var = tk.StringVar(value=WAVE_NAMES[0]) + lfo_combo = ttk.Combobox(wf_row, textvariable=self.lfo_wave_var, + values=WAVE_NAMES, width=6, state="readonly") + lfo_combo.pack(side=tk.LEFT, padx=2) + lfo_combo.bind("<>", lambda e: self._send_lfo_waveform()) + + def _make_lfo_slider(self, parent, label, variable, from_, to, command, curved=False): + row = ttk.Frame(parent) + row.pack(fill=tk.X, pady=1) + ttk.Label(row, text=f"{label}:", font=('Arial', 8), width=5, anchor=tk.W).pack(side=tk.LEFT) + entry = ttk.Entry(row, textvariable=variable, width=4) + entry.pack(side=tk.LEFT, padx=2) + entry.bind('', lambda e, v=variable: (self._sync_slider_pos(v), command())) + if curved and to > 0: + slider_var = tk.DoubleVar() + self._curve_map[id(variable)] = (slider_var, to) + v = variable.get() + slider_var.set(1000.0 * (v / to) ** 0.5 if to > 0 else 0) + def on_slide(val, mx=to, var=variable): + norm = float(val) / 1000.0 + var.set(int(round(norm * norm * mx))) + scale = ttk.Scale(row, from_=0, to=1000, orient=tk.HORIZONTAL, + variable=slider_var, length=80, command=on_slide) + else: + scale = ttk.Scale(row, from_=from_, to=to, orient=tk.HORIZONTAL, + variable=variable, length=80, + command=lambda v: variable.set(round(float(v)))) + scale.pack(side=tk.LEFT, fill=tk.X, expand=True) + scale.bind("", lambda e: command()) + + def _send_lfo_rate(self): + val = self.lfo_rate_var.get() + self.arduino.send_bytes(bytes([0x0F, (val >> 8) & 0xFF, val & 0xFF])) + self.log(f"LFO rate: {val}") + + def _send_lfo_depth(self): + val = self.lfo_depth_var.get() + self.arduino.send_bytes(bytes([0x1D, val & 0xFF])) + self.log(f"LFO depth: {val}") + + def _send_lfo_waveform(self): + name = self.lfo_wave_var.get() + idx = WAVE_NAMES.index(name) if name in WAVE_NAMES else 0 + self.arduino.send_bytes(bytes([0x1E, idx & 0xFF])) + self.log(f"LFO waveform: {name}") + + def create_performance_controls(self, parent): + """Performance: Vibrato Depth/Speed + Portamento sliders.""" + self.vibrato_depth_var = tk.IntVar(value=0) + self._make_perf_slider(parent, "Vib Dep", self.vibrato_depth_var, 0, 255, + self._send_vibrato_depth) + self.vibrato_speed_var = tk.IntVar(value=0) + self._make_perf_slider(parent, "Vib Spd", self.vibrato_speed_var, 0, 255, + self._send_vibrato_speed) + self.portamento_var = tk.IntVar(value=0) + self._make_perf_slider(parent, "Porta", self.portamento_var, 0, 255, + self._send_portamento) + + def _make_perf_slider(self, parent, label, variable, from_, to, command): + row = ttk.Frame(parent) + row.pack(fill=tk.X, pady=1) + ttk.Label(row, text=f"{label}:", font=('Arial', 8), width=7, anchor=tk.W).pack(side=tk.LEFT) + entry = ttk.Entry(row, textvariable=variable, width=4) + entry.pack(side=tk.LEFT, padx=2) + entry.bind('', lambda e: command()) + scale = ttk.Scale(row, from_=from_, to=to, orient=tk.HORIZONTAL, + variable=variable, length=80, + command=lambda v: variable.set(round(float(v)))) + scale.pack(side=tk.LEFT, fill=tk.X, expand=True) + scale.bind("", lambda e: command()) + + def _send_vibrato_depth(self): + val = self.vibrato_depth_var.get() + self.arduino.send_bytes(bytes([0x22, val & 0xFF])) + self.log(f"Vibrato depth: {val}") + + def _send_vibrato_speed(self): + val = self.vibrato_speed_var.get() + self.arduino.send_bytes(bytes([0x23, val & 0xFF])) + self.log(f"Vibrato speed: {val}") + + def _send_portamento(self): + val = self.portamento_var.get() + self.arduino.send_bytes(bytes([0x24, val & 0xFF])) + self.log(f"Portamento: {val}") # --- MIDI --- def refresh_midi_inputs(self): @@ -655,20 +909,35 @@ def connect(self): else: port = self.port_var.get() if not port: return - if self.arduino.connect(port, baud=2000000): # High speed - self.connect_btn.config(text="Disconnect") - self.status_label.config(text=f"Connected to {port}", foreground='green') - self.log(f"Connected to {port} @ 2Mbaud") - - # Start Msg/Sec Meter - self.msg_count = 0 - self.last_msg_check = time.time() - self.update_msg_meter() - - # Play "Notify" melody on startup - self.root.after(500, lambda: self.play_melody(11)) # 11 = Notify - else: - messagebox.showerror("Error", f"Could not connect to {port}") + self.connect_btn.config(text="Connecting…", state="disabled") + self.status_label.config(text="Connecting…", foreground='orange') + + def _on_done(success: bool, error: str): + # Callback from background thread β€” schedule onto Tk thread + self.root.after(0, lambda: self._finish_connect(port, success, error)) + + self.arduino.connect_async(port, baud=2000000, on_done=_on_done) + + def _finish_connect(self, port: str, success: bool, error: str): + self.connect_btn.config(state="normal") + if success: + self.connect_btn.config(text="Disconnect") + self.status_label.config(text=f"Connected to {port}", foreground='green') + self.log(f"Connected to {port} @ 2Mbaud") + + self.msg_count = 0 + self.last_msg_check = time.time() + self.update_msg_meter() + + # Play "Notify" melody on startup + self.root.after(500, lambda: self.play_melody(11)) + # Request current params from Arduino to sync GUI + self.root.after(800, lambda: self.arduino.send_bytes(bytes([0x11]))) + else: + self.connect_btn.config(text="Connect") + self.status_label.config(text="Disconnected", foreground='red') + self.log(f"Connection failed: {error}") + messagebox.showerror("Connection Error", f"Could not connect to {port}:\n{error}") def on_arduino_message(self, msg: str): # self.root.after(0, lambda: self.log(f"← {msg}")) # Disable log spam for notes @@ -684,11 +953,69 @@ def on_arduino_message(self, msg: str): pass elif msg.startswith("NOTE:OFF"): self.root.after(0, lambda: self.visualize_clear()) + elif msg.startswith("SYNC:"): + self._handle_sync(msg) # Log other messages - if not msg.startswith("NOTE:"): + if not msg.startswith("NOTE:") and not msg.startswith("SYNC:"): self.root.after(0, lambda: self.log(f"← {msg}")) + def _handle_sync(self, msg: str): + """Update GUI sliders when hardware pot changes a parameter.""" + try: + parts = msg.split() + key = parts[0] # e.g. "SYNC:MA" + val = int(parts[1]) + # FM params + if key == "SYNC:MA" and hasattr(self, 'mod_amount_var'): + self.root.after(0, lambda v=val: self._sync_curved_var(self.mod_amount_var, v)) + elif key == "SYNC:MR" and hasattr(self, 'mod_ratio_var'): + self.root.after(0, lambda v=val: self.mod_ratio_var.set(v)) + elif key == "SYNC:FM" and hasattr(self, 'fm_mode_var'): + self.root.after(0, lambda v=val: self.fm_mode_var.set(FM_MODE_NAMES[v] if v < len(FM_MODE_NAMES) else FM_MODE_NAMES[0])) + elif key == "SYNC:MS" and hasattr(self, 'mod_shape_var'): + self.root.after(0, lambda v=val: self.mod_shape_var.set(v)) + elif key == "SYNC:FP" and hasattr(self, 'fm_preset_var'): + if val < len(FM_PRESETS): + self.root.after(0, lambda n=FM_PRESETS[val]['name']: self.fm_preset_var.set(n)) + # Mod envelope + elif key == "SYNC:VA" and hasattr(self, 'mod_env_attack_var'): + self.root.after(0, lambda v=val: self._sync_curved_var(self.mod_env_attack_var, v)) + elif key == "SYNC:VD" and hasattr(self, 'mod_env_decay_var'): + self.root.after(0, lambda v=val: self._sync_curved_var(self.mod_env_decay_var, v)) + elif key == "SYNC:VS" and hasattr(self, 'mod_env_sustain_var'): + self.root.after(0, lambda v=val: self.mod_env_sustain_var.set(v)) + # LFO + elif key == "SYNC:LD" and hasattr(self, 'lfo_depth_var'): + self.root.after(0, lambda v=val: self.lfo_depth_var.set(v)) + elif key == "SYNC:LR" and hasattr(self, 'lfo_rate_var'): + self.root.after(0, lambda v=val: self._sync_curved_var(self.lfo_rate_var, v)) + elif key == "SYNC:LW" and hasattr(self, 'lfo_wave_var'): + self.root.after(0, lambda v=val: self.lfo_wave_var.set(WAVE_NAMES[v] if v < len(WAVE_NAMES) else 'SIN')) + # Amp ADSR + elif key == "SYNC:EA" and hasattr(self, 'env_attack'): + self.root.after(0, lambda v=val: self._sync_curved_var(self.env_attack, v)) + elif key == "SYNC:ED" and hasattr(self, 'env_decay'): + self.root.after(0, lambda v=val: self._sync_curved_var(self.env_decay, v)) + elif key == "SYNC:ES" and hasattr(self, 'env_sustain'): + self.root.after(0, lambda v=val: self.env_sustain.set(v)) + elif key == "SYNC:ER" and hasattr(self, 'env_release'): + self.root.after(0, lambda v=val: self._sync_curved_var(self.env_release, v)) + # Wavetables + elif key == "SYNC:CW" and hasattr(self, 'carrier_wave_var'): + self.root.after(0, lambda v=val: self.carrier_wave_var.set(WAVE_NAMES[v] if v < len(WAVE_NAMES) else 'SIN')) + elif key == "SYNC:MW" and hasattr(self, 'mod_wave_var'): + self.root.after(0, lambda v=val: self.mod_wave_var.set(WAVE_NAMES[v] if v < len(WAVE_NAMES) else 'SIN')) + # Performance + elif key == "SYNC:VB" and hasattr(self, 'vibrato_depth_var'): + self.root.after(0, lambda v=val: self.vibrato_depth_var.set(v)) + elif key == "SYNC:VP" and hasattr(self, 'vibrato_speed_var'): + self.root.after(0, lambda v=val: self.vibrato_speed_var.set(v)) + elif key == "SYNC:PT" and hasattr(self, 'portamento_var'): + self.root.after(0, lambda v=val: self.portamento_var.set(v)) + except (ValueError, IndexError): + pass + def visualize_press(self, note, pressed): if note in self.keys: # We use a special flag to indicate this is visual-only (from Arduino playback) diff --git a/tools/gui/config.py b/tools/gui/config.py index 280d5c6..b8701d4 100644 --- a/tools/gui/config.py +++ b/tools/gui/config.py @@ -19,8 +19,11 @@ 'i': 'C6' } -# Reverse mapping for display -NOTE_TO_KEYBOARD = {v: k for k, v in KEYBOARD_TO_NOTE.items()} +# Reverse mapping for display β€” prefer the first binding for each note +# (bottom-row keys, e.g. ',' takes priority over 'q' for C5) +NOTE_TO_KEYBOARD: dict = {} +for _k, _v in KEYBOARD_TO_NOTE.items(): + NOTE_TO_KEYBOARD.setdefault(_v, _k) # GUI Colors COLOR_WHITE_KEY_NORMAL = '#FFFFFF' @@ -30,14 +33,63 @@ COLOR_TEXT_WHITE = 'white' COLOR_TEXT_BLACK = 'black' -# Instrument Presets (ID, Name, Default ADSR) -# ADSR tuple: (Attack, Decay, Sustain, SustainLength, Release, Octave) -INSTRUMENTS = [ - {'id': 0, 'name': 'Piano', 'adsr': (5, 100, 20, 5000, 50, 0)}, - {'id': 1, 'name': 'Organ', 'adsr': (10, 50, 250, 10000, 100, 0)}, - {'id': 2, 'name': 'Staccato', 'adsr': (5, 20, 0, 500, 50, 0)}, - {'id': 3, 'name': 'Pad', 'adsr': (20, 50, 180, 8000, 100, 0)}, - {'id': 4, 'name': 'Custom', 'adsr': (10, 100, 128, 5000, 100, 0)} # Default custom +# Wavetable names (indexed by wave ID 0-5) +WAVE_NAMES = ['SIN', 'SAW', 'REVSAW', 'SQUARE', 'PRANDOM', 'NULL'] + +# FM Mode names (indexed by mode ID 0-3) +FM_MODE_NAMES = ['Exponential', 'Linear High', 'Linear Low', 'Free'] + +# FM Presets β€” must match order in fm_synth.cpp FM_PRESETS[] +# Ratios in Exponential mode: 396β†’1.0Γ— 457β†’1.5Γ— 518β†’2.0Γ— 640β†’3.0Γ— 701β†’3.5Γ— 823β†’5.0Γ— +FM_PRESETS = [ + {'id': 0, 'name': 'DX E.Piano','mod_amount': 850, 'mod_ratio': 396, 'fm_mode': 0, + 'mod_env': (0, 900, 0), 'lfo_depth': 0, 'lfo_rate': 0, 'lfo_wave': 0, + 'cwave': 0, 'mwave': 0, 'adsr': (2, 900, 8, 600), + 'vibrato_depth': 0, 'vibrato_speed': 0, 'portamento': 0}, + {'id': 1, 'name': 'SW Lead', 'mod_amount': 550, 'mod_ratio': 518, 'fm_mode': 0, + 'mod_env': (0, 350, 130), 'lfo_depth': 12, 'lfo_rate': 180, 'lfo_wave': 0, + 'cwave': 1, 'mwave': 0, 'adsr': (8, 180, 220, 350), + 'vibrato_depth': 35, 'vibrato_speed': 85, 'portamento': 0}, + {'id': 2, 'name': 'Lush Pad', 'mod_amount': 350, 'mod_ratio': 518, 'fm_mode': 0, + 'mod_env': (400, 1800, 180), 'lfo_depth': 25, 'lfo_rate': 100, 'lfo_wave': 0, + 'cwave': 0, 'mwave': 0, 'adsr': (900, 1500, 210, 3500), + 'vibrato_depth': 16, 'vibrato_speed': 40, 'portamento': 0}, + {'id': 3, 'name': 'FM Bass', 'mod_amount': 900, 'mod_ratio': 396, 'fm_mode': 0, + 'mod_env': (0, 100, 80), 'lfo_depth': 0, 'lfo_rate': 0, 'lfo_wave': 0, + 'cwave': 1, 'mwave': 0, 'adsr': (2, 120, 200, 50), + 'vibrato_depth': 0, 'vibrato_speed': 0, 'portamento': 0}, + {'id': 4, 'name': 'FM Bell', 'mod_amount': 650, 'mod_ratio': 701, 'fm_mode': 0, + 'mod_env': (0, 2500, 0), 'lfo_depth': 0, 'lfo_rate': 0, 'lfo_wave': 0, + 'cwave': 0, 'mwave': 0, 'adsr': (2, 2500, 0, 3000), + 'vibrato_depth': 0, 'vibrato_speed': 0, 'portamento': 0}, + {'id': 5, 'name': 'SynBrass', 'mod_amount': 750, 'mod_ratio': 396, 'fm_mode': 0, + 'mod_env': (40, 250, 180), 'lfo_depth': 18, 'lfo_rate': 220, 'lfo_wave': 0, + 'cwave': 1, 'mwave': 0, 'adsr': (35, 200, 235, 200), + 'vibrato_depth': 22, 'vibrato_speed': 60, 'portamento': 0}, + {'id': 6, 'name': 'Wobble', 'mod_amount': 700, 'mod_ratio': 518, 'fm_mode': 0, + 'mod_env': (0, 400, 100), 'lfo_depth': 200, 'lfo_rate': 500, 'lfo_wave': 1, + 'cwave': 0, 'mwave': 1, 'adsr': (40, 200, 200, 200), + 'vibrato_depth': 0, 'vibrato_speed': 0, 'portamento': 0}, + {'id': 7, 'name': 'Neon Arp', 'mod_amount': 720, 'mod_ratio': 640, 'fm_mode': 0, + 'mod_env': (0, 120, 25), 'lfo_depth': 8, 'lfo_rate': 200, 'lfo_wave': 0, + 'cwave': 3, 'mwave': 0, 'adsr': (2, 80, 50, 100), + 'vibrato_depth': 0, 'vibrato_speed': 0, 'portamento': 0}, + {'id': 8, 'name': 'Vapor Pad', 'mod_amount': 280, 'mod_ratio': 457, 'fm_mode': 0, + 'mod_env': (600, 2500, 200), 'lfo_depth': 45, 'lfo_rate': 80, 'lfo_wave': 0, + 'cwave': 1, 'mwave': 0, 'adsr': (1000, 2000, 230, 4000), + 'vibrato_depth': 18, 'vibrato_speed': 35, 'portamento': 0}, + {'id': 9, 'name': 'Acid Bass', 'mod_amount': 1000, 'mod_ratio': 150, 'fm_mode': 1, + 'mod_env': (0, 180, 40), 'lfo_depth': 35, 'lfo_rate': 350, 'lfo_wave': 1, + 'cwave': 1, 'mwave': 3, 'adsr': (2, 100, 170, 35), + 'vibrato_depth': 0, 'vibrato_speed': 0, 'portamento': 90}, + {'id': 10, 'name': 'Retro Stab','mod_amount': 900, 'mod_ratio': 396, 'fm_mode': 0, + 'mod_env': (0, 50, 0), 'lfo_depth': 0, 'lfo_rate': 0, 'lfo_wave': 0, + 'cwave': 3, 'mwave': 0, 'adsr': (1, 40, 0, 60), + 'vibrato_depth': 0, 'vibrato_speed': 0, 'portamento': 0}, + {'id': 11, 'name': 'Crystal', 'mod_amount': 380, 'mod_ratio': 823, 'fm_mode': 0, + 'mod_env': (0, 3000, 0), 'lfo_depth': 6, 'lfo_rate': 80, 'lfo_wave': 0, + 'cwave': 0, 'mwave': 0, 'adsr': (2, 2000, 0, 3500), + 'vibrato_depth': 10, 'vibrato_speed': 25, 'portamento': 0}, ] MELODIES = [ @@ -55,22 +107,3 @@ ("Notify", 11), ("Jingle Bells", 12) ] - -# These should match the order of waveforms in sound.cpp -WAVES = [ - "Triangle", - "Sawtooth", - "Sine", - "Square", - "EPiano", - "Clarinet", - "Violin", - "FM Synth", - "Guitar", - "Cello", - "Flute", - "NES Pulse", - "Oboe", - "Osc Chip", - "Piano" -] \ No newline at end of file diff --git a/tools/gui/midi_handler.py b/tools/gui/midi_handler.py index 048a00b..c9d81a2 100644 --- a/tools/gui/midi_handler.py +++ b/tools/gui/midi_handler.py @@ -12,8 +12,8 @@ class MidiHandler: """Handles MIDI input and file playback.""" - def __init__(self, note_on_callback: Callable[[str], None], - note_off_callback: Callable[[str], None], + def __init__(self, note_on_callback: Callable[[str, int], None], + note_off_callback: Callable[[str, int], None], log_callback: Callable[[str], None], progress_callback: Optional[Callable[[float, float], None]] = None): self.note_on_cb = note_on_callback @@ -65,10 +65,10 @@ def _on_message(self, msg): """Callback for incoming MIDI messages.""" if msg.type == 'note_on' and msg.velocity > 0: note = self.midi_to_note(msg.note) - self.note_on_cb(note) + self.note_on_cb(note, msg.velocity) elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0): note = self.midi_to_note(msg.note) - self.note_off_cb(note) + self.note_off_cb(note, msg.velocity) def load_file(self, filepath: str) -> bool: if not MIDI_AVAILABLE: return False diff --git a/tools/gui/serial_comm.py b/tools/gui/serial_comm.py index 3c7972d..44b4e2b 100644 --- a/tools/gui/serial_comm.py +++ b/tools/gui/serial_comm.py @@ -12,26 +12,35 @@ def __init__(self): self.connected = False self.reader_thread: Optional[threading.Thread] = None self.running = False - self.on_message: Optional[Callable[[str], None]] = None # Callback for received messages - - def connect(self, port: str, baud: int = 2000000) -> bool: - """Connect to Arduino.""" - try: - self.ser = serial.Serial(port, baud, timeout=0.1) - time.sleep(2) # Wait for Arduino reset - self.ser.reset_input_buffer() - self.connected = True - - # Start reader thread - self.running = True - self.reader_thread = threading.Thread(target=self._read_loop, daemon=True) - self.reader_thread.start() - - return True - except Exception as e: - print(f"Connection error: {e}") - return False - + self.on_message: Optional[Callable[[str], None]] = None + + def connect_async(self, port: str, baud: int = 2000000, + on_done: Optional[Callable[[bool, str], None]] = None): + """Connect to Arduino in a background thread. + Calls on_done(success: bool, error_msg: str) on the caller's thread + via the supplied callback (use root.after to keep it on the Tk thread). + """ + def _worker(): + try: + ser = serial.Serial(port, baud, timeout=0.1) + time.sleep(2) # Wait for Arduino bootloader + ser.reset_input_buffer() + self.ser = ser + self.connected = True + self.running = True + self.reader_thread = threading.Thread( + target=self._read_loop, daemon=True) + self.reader_thread.start() + if on_done: + on_done(True, "") + except Exception as exc: + self.ser = None + self.connected = False + if on_done: + on_done(False, str(exc)) + + threading.Thread(target=_worker, daemon=True).start() + def disconnect(self): """Disconnect from Arduino.""" self.running = False @@ -39,12 +48,12 @@ def disconnect(self): if self.ser: try: self.ser.close() - except: + except Exception: pass self.ser = None - + def _read_loop(self): - """Read responses from Arduino.""" + """Read responses from Arduino (background thread).""" while self.running and self.ser: try: if self.ser.in_waiting: @@ -54,22 +63,16 @@ def _read_loop(self): except Exception: pass time.sleep(0.01) - - def send(self, cmd: str): - """Send text command (legacy compatible).""" - if self.ser and self.connected: - try: - self.ser.write(f"{cmd}\n".encode()) - except Exception as e: - print(f"Send error: {e}") def send_bytes(self, data: bytes): """Send binary data.""" if self.ser and self.connected: try: self.ser.write(data) - except Exception as e: - print(f"Send bytes error: {e}") + except Exception as exc: + msg = f"Serial write error: {exc}" + if self.on_message: + self.on_message(msg) @staticmethod def list_ports():