{"id":30105846,"url":"https://github.com/croesnick/mood_bot","last_synced_at":"2026-06-14T21:32:41.574Z","repository":{"id":302083490,"uuid":"1011175167","full_name":"croesnick/mood_bot","owner":"croesnick","description":null,"archived":false,"fork":false,"pushed_at":"2025-12-05T11:09:49.000Z","size":106405,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-08T15:12:52.107Z","etag":null,"topics":["e-ink","elixir","embedded","nerves","raspberry-pi"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/croesnick.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-06-30T12:18:05.000Z","updated_at":"2025-12-05T11:09:53.000Z","dependencies_parsed_at":"2025-06-30T13:51:58.946Z","dependency_job_id":"ec0cca19-c9d0-4469-b530-882d77ddbc6e","html_url":"https://github.com/croesnick/mood_bot","commit_stats":null,"previous_names":["croesnick/mood_bot"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/croesnick/mood_bot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croesnick%2Fmood_bot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croesnick%2Fmood_bot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croesnick%2Fmood_bot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croesnick%2Fmood_bot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/croesnick","download_url":"https://codeload.github.com/croesnick/mood_bot/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croesnick%2Fmood_bot/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34339194,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["e-ink","elixir","embedded","nerves","raspberry-pi"],"created_at":"2025-08-10T00:20:06.690Z","updated_at":"2026-06-14T21:32:41.568Z","avatar_url":"https://github.com/croesnick.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MoodBot 🤖\n\nEver had your kid ask for a robot — and thought, \"Hey… maybe we could build one together\"?\n\nMoodBot is the result of that idea: a little robot built with Elixir, Nerves, a Raspberry Pi, and some help from AI. It talks using text-to-speech, shows its mood on an e-ink display, and grows over time — from basic GenServers to more playful, AI-powered behavior.\n\nIt's designed for software engineers who want to learn embedded systems without getting lost in C and hardware registers. You get to explore e-ink displays, SPI communication, and GPIO control while building something your kids can actually interact with.\n\nThe best part? You can start coding immediately without any hardware — the mock HAL lets you develop and test everything on your laptop.\n\n## What You'll Learn\n\nThis project will teach you practical embedded systems concepts:\n\n- **SPI Communication**: How microcontrollers talk to peripheral devices\n- **GPIO Control**: Managing hardware pins for reset, data/command selection\n- **Hardware Abstraction**: Writing code that works on both development machines and embedded hardware\n- **E-ink Display Technology**: Understanding refresh cycles, partial updates, and power management\n- **OTP/GenServer Patterns**: Applying Elixir's actor model to hardware control\n- **Embedded Development Workflow**: From laptop development to hardware deployment\n\nNo prior embedded experience required — we'll explain concepts as we go.\n\n## Quick Start (Development Mode)\n\n**Get started immediately without any hardware!** MoodBot includes a mock HAL that simulates the e-ink display for development.\n\n### Prerequisites\n\n- Elixir 1.18+ and Erlang/OTP 25+\n- Mix build tool\n\n### 1. Clone and Setup\n\n```bash\ngit clone \u003crepository-url\u003e\ncd mood_bot\nmix deps.get\n```\n\n### 2. Run in Development Mode\n\n```bash\n# Start the application with mock hardware\nmix run --no-halt\n\n# Or start an interactive shell\niex -S mix\n```\n\nThe application automatically uses the MockHAL when running on `:host` (your development machine), which logs all display operations to the console instead of sending them to actual hardware.\n\n### 3. Try the Display API\n\nIn the IEx shell, you can interact with the mock display:\n\n```elixir\n# Initialize the display\nMoodBot.Display.init_display()\n\n# Clear the display (logs to console)\nMoodBot.Display.clear()\n\n# Show different moods\nMoodBot.Display.show_mood(:happy)\nMoodBot.Display.show_mood(:sad)\nMoodBot.Display.show_mood(:neutral)\nMoodBot.Display.show_mood(:angry)\nMoodBot.Display.show_mood(:surprised)\n\n# Check display status\nMoodBot.Display.status()\n\n# Display raw image data (generates test pattern)\nalias MoodBot.DisplayTestHelper\ntest_image = DisplayTestHelper.test_image_data()\nMoodBot.Display.display_image(test_image)\n\n# Put display to sleep\nMoodBot.Display.sleep()\n```\n\n## Development Workflow\n\n### Running Tests\n\n```bash\n# Run all tests\nmix test\n\n# Run tests with coverage\nmix test --cover\n\n# Run specific test files\nmix test test/mood_bot/display_test.exs\n```\n\n### Code Quality\n\n```bash\n# Format code\nmix format\n\n# Check code quality\nmix credo --strict\n\n# Run static analysis\nmix dialyzer\n```\n\n## Architecture Overview\n\n**Learning objective**: After this section, you'll understand how MoodBot's layered architecture lets you develop on your laptop and deploy to hardware without changing your code.\n\nMoodBot uses a layered approach that lets you work with the same code whether you're developing on your laptop or running on a Raspberry Pi. This pattern is common in embedded development — it's called a Hardware Abstraction Layer (HAL).\n\n```mermaid\ngraph TD\n    A[Your Application Code] --\u003e B[MoodBot.Display GenServer]\n    B --\u003e C[MoodBot.Display.Driver]\n    C --\u003e D[HAL Interface]\n    D --\u003e E[MockHAL\u003cbr/\u003eDevelopment]\n    D --\u003e F[RpiHAL\u003cbr/\u003eHardware]\n    \n    style A fill:#e1f5fe\n    style B fill:#f3e5f5\n    style C fill:#e8f5e8\n    style D fill:#fff3e0\n    style E fill:#fce4ec\n    style F fill:#fce4ec\n```\n\nThe layers work like this:\n\n- **MoodBot.Display**: The main GenServer that handles mood changes and display updates\n- **MoodBot.Display.Driver**: Knows how to talk to the e-ink display (timing, refresh cycles)\n- **HAL Interface**: Switches between mock and real hardware without changing your code\n- **MockHAL**: Simulates everything for development — logs SPI writes, saves bitmap images\n- **RpiHAL**: Actually toggles GPIO pins and talks to the real display\n\nWhen you call `MoodBot.Display.show_mood(:happy)`, it works the same way in both modes. The only difference is whether it ends up as a log message or an actual display update.\n\nThis architecture is what lets you develop and test your robot's behavior entirely on your laptop, then deploy the exact same code to a Raspberry Pi.\n\n## How the E-Paper Display Works\n\n**Learning objective**: After this section, you'll understand why e-paper displays are perfect for embedded projects and how they communicate with microcontrollers.\n\nThe e-paper display is what makes MoodBot interesting. It's not just a screen — it behaves very differently from the displays you're used to.\n\n### E-Ink Technology\n\nE-paper displays have some unique characteristics:\n\n- **Retains images without power** — like a printed page that you can change\n- **Updates are slow but deliberate** — perfect for mood displays, not videos\n- **Three colors available** — black, white, and red (on our Waveshare 2.9\" display)\n- **Partial updates** — you can change just a portion of the screen\n\n### SPI Communication\n\nThe display uses SPI (Serial Peripheral Interface) to communicate with the Pi. Think of it as a one-way conversation from the Pi to the display:\n\n```plaintext\nRaspberry Pi                    E-Paper Display\n     │                               │\n     ├─ MOSI ──────────────────────→ │  (Data goes this way)\n     ├─ CLK  ──────────────────────→ │  (Clock/timing)\n     ├─ CS   ──────────────────────→ │  (Chip Select: \"Hey, I'm talking to you!\")\n     ├─ DC   ──────────────────────→ │  (Data vs Command mode)\n     ├─ RST  ──────────────────────→ │  (Reset the display)\n     └─ BUSY ←────────────────────── │  (Display says \"I'm busy updating\")\n```\n\nKey pins explained:\n\n- **CS (Chip Select)**: When low, the display listens. When high, it ignores you\n- **DC (Data/Command)**: High = \"here's image data\", Low = \"here's a command\"\n- **BUSY**: The display's way of saying \"give me a moment to update\"\n\n### Display Update Process\n\nHere's what happens every time MoodBot shows a new mood:\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e IdleAndReady : system starts\n\n    IdleAndReady --\u003e UpdatingDisplay : new content received\n    UpdatingDisplay --\u003e IdleAndReady : update complete\n\n    IdleAndReady --\u003e RefreshingScreen : over 3 min since last refresh\n    RefreshingScreen --\u003e IdleAndReady : full refresh complete\n\n    IdleAndReady --\u003e PowerSaving : no updates for a while\n    PowerSaving --\u003e UpdatingDisplay : new content received\n```\n\nThe two types of updates:\n\n1. **Partial Updates** (fast, ~2 seconds)\n   - Only updates changed pixels\n   - Perfect for mood changes\n   - Can cause \"ghosting\" over time\n\n2. **Full Refresh** (slower, ~15 seconds)\n   - Resets the entire display\n   - Eliminates ghosting\n   - Required every ~3 minutes\n\n### Power Management\n\nThe e-paper display is much more power-efficient than regular screens:\n\n- **During updates**: Uses power to rearrange the e-ink particles\n- **When idle**: Uses zero power but retains the image\n- **After 5 minutes**: Goes to sleep mode to save even more power\n\nThis is why MoodBot can run for days on a small battery.\n\nNow that you understand the architecture and e-paper display fundamentals, let's see how to put it all together.\n\n## Development with MockHAL\n\n**Learning objective**: After this section, you'll understand how to develop and debug embedded code without any hardware, and how to visualize what your code would display on the actual e-ink screen.\n\nThe MockHAL is what makes MoodBot development so smooth. It simulates all hardware operations and gives you visual feedback about what your code is doing.\n\n### What MockHAL Simulates\n\nThe MockHAL (`MoodBot.Display.MockHAL`) simulates all hardware operations:\n\n- **SPI writes**: Logged with data size and first 8 bytes\n- **GPIO operations**: Logs pin state changes with descriptions\n- **Busy pin**: Randomly simulates busy/ready states\n- **Sleep**: Actually sleeps to simulate timing\n- **Bitmap saving**: Automatically saves display frames as viewable images\n\nExample mock output:\n\n```plaintext\n[info] MockHAL: Initializing MockHAL for development mode with bitmap saving enabled (session: a1b2c3d4)\n[info] MockHAL: SPI write 5 bytes: \u003c\u003c1, 2, 3, 4, 5\u003e\u003e\n[info] MockHAL: Set DC pin to 1 (data mode)\n[info] MockHAL: Saved bitmap frame 0 (session: a1b2c3d4)\n[info] MockHAL: Read BUSY pin: 0 (ready)\n```\n\n### Visual Display Output\n\nWhen running in development mode, MoodBot automatically saves visual representations of what would be displayed on the e-ink screen. These bitmap files are saved to `priv/bitmaps/` with filenames like:\n\n```plaintext\nsession_a1b2c3d4_frame_000_1677123456789.pbm\nsession_a1b2c3d4_frame_001_1677123457890.pbm\n```\n\nEach file is a standard PBM (Portable Bitmap) image that can be opened in any image viewer to see exactly what was sent to the display. This makes it easy to:\n\n- Debug display output visually\n- Track the sequence of frames sent to the display\n- Verify mood indicators and custom images look correct\n- See the development history of display changes\n\nThe session ID changes each time you restart the application, and the frame counter increments for each image sent to the display.\n\n## API Reference\n\n### Display Control\n\n```elixir\n# Initialize hardware\n{:ok | :error, reason} = MoodBot.Display.init_display()\n\n# Clear display to white\n:ok = MoodBot.Display.clear()\n\n# Display moods (:happy, :sad, :neutral, :angry, :surprised)\n:ok = MoodBot.Display.show_mood(:happy)\n\n# Display raw image data (binary, 1 bit per pixel)\n:ok = MoodBot.Display.display_image(image_binary)\n\n# Load and process external images (PNG, BMP, TIFF, WebP, PBM)\n{:ok, display_data} = MoodBot.Images.Bitmap.load_for_display(\"image.png\")\n:ok = MoodBot.Display.display_image(display_data)\n\n# Sleep mode\n:ok = MoodBot.Display.sleep()\n\n# Get status\n%{initialized: boolean(), display_state: atom(), ...} = MoodBot.Display.status()\n```\n\n### Image Processing\n\nProcess images for e-ink display with conversion mode options:\n\n```bash\n# CLI script with mode options\n./scripts/process_image.exs input.png output.png --mode bw        # Binary (default)\n./scripts/process_image.exs input.png output.png --mode grayscale # Grayscale\n\n# Mix task with mode support  \nmix process_image input.png output.png --mode bw        # Binary for e-ink\nmix process_image input.png output.pbm --mode grayscale # Grayscale for preview\n```\n\n**Modes:**\n- **`bw` (default)**: True black/white conversion optimized for e-ink displays\n- **`grayscale`**: Grayscale conversion useful for preview/inspection\n\nFor detailed image processing documentation, see [`scripts/README.md`](scripts/README.md).\n\n### Image Format\n\nThe display expects binary data where:\n\n- 1 bit per pixel (0 = black, 1 = white)\n- Size: `(width / 8) * height` bytes\n- Default dimensions: 296x128 pixels = 4736 bytes\n\n## Hardware Setup (For Actual Deployment)\n\n**Learning objective**: After this section, you'll know how to connect the hardware and deploy your code to a real Raspberry Pi.\n\n### Supported Hardware\n\n- Raspberry Pi (Zero, 3, 3A+, 4, 5)\n- Waveshare 2.9\" e-ink display\n- MicroSD card (8GB+)\n\n### Pin Connections\n\n| Display Pin | RPi GPIO | Purpose      |\n|-------------|----------|--------------|\n| VCC         | 3.3V     | Power        |\n| GND         | GND      | Ground       |\n| DIN         | GPIO 19  | Data         |\n| CLK         | GPIO 23  | Clock        |\n| CS          | GPIO 24  | Chip Select  |\n| DC          | GPIO 22  | Data/Command |\n| RST         | GPIO 11  | Reset        |\n| BUSY        | GPIO 18  | Busy Signal  |\n\nSee: \u003chttps://www.waveshare.com/wiki/2.9inch_e-Paper_Module_Manual#Working_With_Raspberry_Pi\u003e\n\n### Building for Hardware\n\n```bash\n# Set target (rpi4, rpi5, etc.)\nexport MIX_TARGET=rpi4\n\n# Build firmware\nmix firmware\n\n# Burn to SD card (replace /dev/sdX with your card)\nmix burn\n\n# Upload to running device over network (OTA update)\nmix upload\n```\n\n### One-Time Firmware Flash Setup\n\n**Learning objective**: After this section, you'll know how to handle Erlang version compatibility and successfully flash firmware to your Raspberry Pi.\n\nThe first time you build firmware, you may encounter an Erlang version mismatch between your host system and the target system. Here's how to resolve it:\n\n#### Prerequisites\n\n- `asdf` version manager (install from [asdf-vm.com](https://asdf-vm.com))\n- A MicroSD card (8GB+)\n\n#### Step 1: Check for Version Mismatch\n\nWhen you run `mix firmware`, you might see this error:\n\n```plaintext\n** (Mix) Major version mismatch between host and target Erlang/OTP versions\n  Host version: 28\n  Target version: 27\n```\n\nThis happens because the Nerves system was built with a different Erlang version than your host system.\n\n#### Step 2: Install Compatible Erlang/Elixir Versions\n\n```bash\n# Install Erlang 27 (compatible with current Nerves systems)\nasdf install erlang 27.3.4.1\n\n# Install compatible Elixir version\nasdf install elixir 1.18.4-otp-27\n\n# Set local versions for this project\nasdf set erlang 27.3.4.1\nasdf set elixir 1.18.4-otp-27\n\n# Verify compatibility\nasdf current\nerl -eval \"io:format(\\\"~s~n\\\", [erlang:system_info(otp_release)]), halt().\"\nelixir --version\n```\n\n#### Step 3: Install Nerves Bootstrap\n\n```bash\n# Install the nerves_bootstrap archive\nmix archive.install hex nerves_bootstrap\n```\n\n#### Step 4: Build and Flash\n\n```bash\n# Set your target (rpi3, rpi4, rpi5, etc.)\nexport MIX_TARGET=rpi3\n\n# Get dependencies for target\nmix deps.get\n\n# Build firmware\nmix firmware\n\n# Burn to SD card (system will prompt for confirmation)\nmix burn\n```\n\n#### Step 5: Test Hardware\n\nOnce the SD card is ready:\n\n1. **Insert SD card** into your Raspberry Pi\n2. **Connect the e-ink display** (see pin connections above)\n3. **Power on** the Pi (wait 30-60 seconds for boot)\n4. **Connect via SSH**: `ssh nerves.local`\n\n**Note**: If you need WiFi, you can either:\n\n- Set `WIFI_SSID` and `WIFI_PSK` environment variables before building firmware\n- Connect via Ethernet first, then configure WiFi using the `wifi_connect()` command\n\n**Important**: The firmware defaults to Germany's regulatory domain (`DE`). If you're in a different country:\n\n1. Copy `.env.example` to `.env`\n2. Set `REGULATORY_DOMAIN` to your [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (e.g., `US`, `GB`, `FR`)\n3. Alternatively, export the environment variable: `export REGULATORY_DOMAIN=US`\n\nThis ensures your device can see all available WiFi networks in your region.\n\n#### Step 6: Verify Display Patterns\n\nIn the SSH session, test each display pattern using the built-in helper commands:\n\n```elixir\n# MoodBot helper commands are automatically available\n# Type help() to see all available commands\n\n# Initialize the display\ndisplay_init()\n\n# Test each mood pattern\ndisplay_mood(:happy)      # Checkerboard pattern\ndisplay_mood(:sad)        # Vertical lines  \ndisplay_mood(:neutral)    # Horizontal stripes\ndisplay_mood(:angry)      # Diagonal pattern\ndisplay_mood(:surprised)  # Border frame\n\n# Clear the display\ndisplay_clear()\n\n# Check system status\ndisplay_status()\n\n# Configure WiFi if needed\nwifi_scan()\nwifi_connect(\"YourNetwork\", \"YourPassword\")    # Persistent connection\nwifi_connect_temp(\"GuestNet\", \"TempPassword\")  # Temporary connection\nwifi_status()\n\n# Monitor network status\nnetwork_status()  # Shows all interfaces (eth0, wlan0, usb0)\n```\n\n#### Remote Debugging Commands\n\nFor non-interactive debugging via SSH MCP, use these direct function calls:\n\n```bash\n# Network overview\nVintageNet.info()\n\n# WiFi configuration and status\nVintageNet.get_configuration(\"wlan0\")\nVintageNet.all_interfaces()\nVintageNet.scan(\"wlan0\")\n\n# MoodBot WiFi status (when application is running)\nMoodBot.WiFiConfig.status()\n\n# System information\n:os.type()\nSystem.version()\nApplication.started_applications() |\u003e Enum.map(fn {name, _desc, _vsn} -\u003e name end)\n```\n\nEach command should display a distinct visual pattern on the e-ink display, confirming successful hardware integration.\n\n#### Troubleshooting First Flash\n\n**Archive not found error:**\n\n```bash\nmix archive.install hex nerves_bootstrap\n```\n\n**Dependency conflicts:**\n\n```bash\nmix deps.clean --all\nmix deps.get\n```\n\n**SD card not detected:**\n\n- Ensure the card is properly inserted\n- Check with `diskutil list` (macOS) or `lsblk` (Linux)\n- Try a different SD card if issues persist\n\n## Over-The-Air (OTA) Updates\n\n**Learning objective**: After this section, you'll understand how to update your deployed MoodBot remotely without physical access to the SD card.\n\nOnce you've deployed MoodBot to hardware, you can update it remotely using Nerves' built-in OTA capabilities. This is especially useful when your robot is deployed or hard to reach physically.\n\n### Prerequisites for OTA Updates\n\n1. **Network connectivity**: Your MoodBot must be connected to WiFi or Ethernet\n2. **SSH access**: The device runs an SSH server for secure updates\n3. **Initial deployment**: You need to burn the initial firmware to SD card first\n\n### How OTA Updates Work\n\nMoodBot uses SSH-based OTA updates through the `nerves_ssh` library:\n\n1. Your development machine connects to MoodBot over the network\n2. New firmware is transferred securely via SSH\n3. Nerves applies the update using A/B partition swapping\n4. The device reboots into the new firmware\n5. If something goes wrong, it can automatically rollback\n\n### Network Discovery\n\nMoodBot advertises itself on the local network as `nerves.local` via mDNS. You can also connect directly via IP address.\n\n```bash\n# Connect to the device\nssh nerves.local\n\n# Or find the IP and connect directly\nssh 192.168.1.100\n```\n\n### Updating via mix upload\n\nThe simplest way to update your deployed MoodBot:\n\n```bash\n# Set your target environment\nexport MIX_TARGET=rpi4\n\n# Build new firmware\nmix firmware\n\n# Upload to the running device\nmix upload\n\n# Specify device if multiple devices are present\nmix upload --target nerves.local\nmix upload --target 192.168.1.100\n```\n\nThe upload process:\n\n1. Transfers the new firmware file to the device\n2. Applies the update to the inactive partition\n3. Switches to the new partition on next reboot\n4. Automatically reboots the device\n\n### Manual OTA Updates\n\nFor more control, you can manually apply updates:\n\n```bash\n# Connect to the device\nssh nerves.local\n\n# Check current firmware info\niex\u003e Nerves.Runtime.KV.get_all()\n\n# Upload firmware file separately (via scp, sftp, etc.)\nscp mood_bot.fw nerves.local:/data/\n\n# Apply the update manually\niex\u003e cmd(\"fwup -i /data/mood_bot.fw --apply --task upgrade \" \u003c\u003e\n         \"--no-unmount -d #{Nerves.Runtime.KV.get(\"nerves_fw_devpath\")}\")\n\n# Reboot to new firmware\niex\u003e reboot()\n```\n\n### SSH Authentication\n\nMoodBot automatically discovers SSH public keys from your `~/.ssh` directory. If no keys are found, you'll need to add them:\n\n```bash\n# During development - keys are discovered automatically\nls ~/.ssh/id_*.pub\n\n# Add keys at runtime if needed\niex\u003e NervesSSH.add_authorized_key(\"ssh-rsa AAAAB3N...\")\n\n# Or set via environment before building\nexport NERVES_SSH_AUTHORIZED_KEYS=\"ssh-rsa AAAAB3N...your-key-here\"\nmix firmware\n```\n\n### Firmware Patches (Advanced)\n\nFor bandwidth-limited deployments, Nerves supports delta updates that only transfer changes:\n\n```bash\n# Generate a patch from old to new firmware  \nmix firmware.patch --source old_firmware.fw --target new_firmware.fw\n\n# Upload the much smaller patch file\nmix upload --firmware patch.fw\n```\n\nThis can reduce update sizes from ~20MB to ~4MB depending on changes.\n\n### Troubleshooting OTA Updates\n\n**Can't connect to device:**\n\n- Verify device is on network: `ping nerves.local`\n- Check SSH service: `ssh nerves.local` should prompt for authentication\n- Verify your SSH key is authorized\n\n**Upload fails:**\n\n- Ensure enough free space: `df -h` on device  \n- Check network stability during large transfers\n- Try manual upload via `scp` first\n\n**Device won't boot after update:**\n\n- Nerves automatically rolls back failed updates\n- Connect via serial console if available\n- Check logs: `dmesg` or `journalctl`\n\n**Multiple devices on network:**\n\n- Use specific IP instead of `nerves.local`\n- Each device has unique hostname: `nerves-\u003cserial\u003e.local`\n\n## Configuration\n\n### Environment Variables\n\nMoodBot uses environment variables for configuration to keep sensitive information secure and make deployment flexible:\n\n#### Setting Up Environment Variables\n\n1. **Copy the example file:**\n   ```bash\n   cp .env.example .env\n   ```\n\n2. **Edit `.env` for your configuration:**\n   ```bash\n   # Required: Set your country's regulatory domain\n   REGULATORY_DOMAIN=DE\n   \n   # Optional: Automatic WiFi connection on boot\n   WIFI_SSID=YourNetworkName\n   WIFI_PSK=YourPassword\n   ```\n\n3. **Available Environment Variables:**\n   - `REGULATORY_DOMAIN`: WiFi regulatory domain (required for proper operation)\n   - `WIFI_SSID`: Network name for automatic connection\n   - `WIFI_PSK`: Network password for automatic connection\n   - `NERVES_SSH_AUTHORIZED_KEYS`: SSH public keys for authentication\n\n#### Security Notes\n\n- The `.env` file is ignored by git to prevent committing sensitive information\n- Use `.env.example` as a template for team members\n- For production, set environment variables directly on the deployment system\n- Never commit actual WiFi passwords or SSH keys to version control\n\n### WiFi Configuration\n\n**Learning objective**: After this section, you'll understand multiple ways to configure WiFi on MoodBot without hardcoding credentials.\n\nMoodBot supports flexible WiFi configuration through several methods, prioritizing security and ease of use:\n\n#### Method 1: Environment Variables (Recommended for Development)\n\nCreate a `.env` file or set environment variables before building firmware:\n\n```bash\n# Copy the example file and customize\ncp .env.example .env\n\n# Edit .env to set your configuration:\n# REGULATORY_DOMAIN=US\n# WIFI_SSID=YourNetworkName  \n# WIFI_PSK=YourPassword\n\n# Or export variables directly\nexport MIX_TARGET=rpi3\nexport REGULATORY_DOMAIN=US\nexport WIFI_SSID=\"YourNetworkName\"\nexport WIFI_PSK=\"YourPassword\"\n\n# Build and burn firmware\nmix firmware\nmix burn\n```\n\nThe device will automatically connect to WiFi on startup if these environment variables are detected.\n\n#### Method 2: Interactive Configuration (Recommended for Production)\n\nConnect to your device via SSH and use the built-in helper commands:\n\n```bash\n# Connect to device\nssh nerves.local\n\n# Scan for networks\niex\u003e wifi_scan()\n\n# Connect to a network (persistent - survives reboots)\niex\u003e wifi_connect(\"YourNetworkName\", \"YourPassword\")\n\n# Connect temporarily (lost on reboot - useful for guest networks)\niex\u003e wifi_connect_temp(\"GuestNetwork\", \"TempPassword\")\n\n# Check WiFi connection status\niex\u003e wifi_status()\n\n# Check all network interfaces status\niex\u003e network_status()\n\n# Disconnect from WiFi\niex\u003e wifi_disconnect()\n```\n\n#### Method 3: Programmatic Configuration\n\nUse the WiFi configuration module directly:\n\n```elixir\n# Configure WiFi programmatically (persistent)\nMoodBot.WiFiConfig.configure_wifi(\"NetworkName\", \"Password\")\n\n# Configure WiFi temporarily (lost on reboot)\nMoodBot.WiFiConfig.configure_wifi_temporary(\"GuestNetwork\", \"TempPassword\")\n\n# Check current status\nMoodBot.WiFiConfig.status()\n\n# Scan for networks\nMoodBot.WiFiConfig.scan()\n\n# Disconnect\nMoodBot.WiFiConfig.disable_wifi()\n\n# Monitor network status across all interfaces\nMoodBot.NetworkMonitor.get_status()\nMoodBot.NetworkMonitor.has_internet?()\nMoodBot.NetworkMonitor.get_primary_interface()\n```\n\n#### WiFi Configuration Persistence\n\n- **Automatic persistence**: WiFi configurations are automatically saved and restored on reboot\n- **Temporary connections**: Use `wifi_connect_temp()` for connections that shouldn't persist\n- **Multiple networks**: You can configure multiple networks; the device will connect to the best available\n- **Modern security**: Uses WPA2/WPA3 compatible configuration that works with all modern routers\n- **Factory reset**: Clear all saved configurations by reflashing firmware\n\n#### Troubleshooting WiFi\n\n**Can't connect to WiFi:**\n\n- Check network name and password: `wifi_scan()` to verify SSID\n- Verify signal strength: Look for signal bars in scan results\n- Check frequency: Some networks use 5GHz which may not be supported on all Pi models\n- **Regulatory domain**: Ensure `REGULATORY_DOMAIN` environment variable matches your country (check your `.env` file)\n- Try different security modes: Modern WPA2/WPA3 configuration should work with most routers\n\n**Connection drops:**\n\n- Check `wifi_status()` and `network_status()` for connection state\n- Verify power supply is adequate (WiFi requires more power)\n- Check for interference from other 2.4GHz devices\n- Monitor network events: `MoodBot.NetworkMonitor.subscribe()` for real-time updates\n\n**Network monitoring:**\n\n- Use `network_status()` to see all interfaces (eth0, wlan0, usb0)\n- Check `MoodBot.NetworkMonitor.has_internet?()` for internet connectivity\n- Monitor connection quality with signal strength indicators\n- Subscribe to network events for real-time status updates\n\n### Network Monitoring\n\nMoodBot includes comprehensive network monitoring that tracks all interfaces in real-time:\n\n#### Real-time Network Events\n\nThe `MoodBot.NetworkMonitor` GenServer automatically monitors:\n\n- **Interface state changes**: configured, deconfigured, connecting, etc.\n- **Connection status**: internet, lan, disconnected\n- **IP address changes**: DHCP renewals, static IP changes\n- **WiFi signal strength**: real-time signal quality monitoring\n- **Network prioritization**: automatic primary interface selection (Ethernet \u003e WiFi \u003e Mobile)\n\n#### Subscribing to Network Events\n\n```elixir\n# Subscribe to network events in your application\nMoodBot.NetworkMonitor.subscribe()\n\n# You'll receive messages like:\n# {:network_event, :connection_change, \"wlan0\", %{connection: :internet}}\n# {:network_event, :signal_change, \"wlan0\", %{signal: 85}}\n# {:network_event, :ip_change, \"eth0\", %{ip: \"192.168.1.100\"}}\n```\n\n#### Integration with Display\n\nNetwork monitoring can be integrated with the mood display:\n\n```elixir\n# Example: Show network status as mood\ncase MoodBot.NetworkMonitor.has_internet?() do\n  true -\u003e MoodBot.Display.show_mood(:happy)    # Connected\n  false -\u003e MoodBot.Display.show_mood(:sad)     # Disconnected\nend\n\n# Show signal strength as mood intensity\ncase MoodBot.NetworkMonitor.get_status() do\n  %{\"wlan0\" =\u003e %{signal: signal}} when signal \u003e 70 -\u003e :happy\n  %{\"wlan0\" =\u003e %{signal: signal}} when signal \u003e 30 -\u003e :neutral\n  _ -\u003e :sad\nend\n```\n\n### Runtime Configuration\n\nOverride config when starting the display:\n\n```elixir\ncustom_config = %{dc_pin: 26, rst_pin: 18}\n{:ok, pid} = MoodBot.Display.start_link(config: custom_config, name: :my_display)\n```\n\n## Troubleshooting\n\n### Common Issues\n\n**Display not initializing:**\n\n- Check pin connections and power supply\n- Verify SPI is enabled: `sudo raspi-config` → Interface Options → SPI\n\n**Build failures:**\n\n- Ensure correct MIX_TARGET is set\n- Clean build: `mix deps.clean --all \u0026\u0026 mix deps.get`\n\n**Mock HAL not working:**\n\n- Ensure running on `:host` target (not hardware target)\n- Check logs for MockHAL initialization messages\n\n### Debug Mode\n\nEnable detailed logging:\n\n```elixir\n# In IEx\nLogger.configure(level: :debug)\n\n# Or in config\nconfig :logger, level: :debug\n```\n\n## Interactive Pipeline (MoodBot.Controller)\n\nMoodBot includes an integrated controller that orchestrates the complete interaction pipeline:\n\n**STT** → **Sentiment Analysis** → **Display Mood** → **LLM Response** → **TTS**\n\n### Pipeline Flow\n\n1. **Button press** → starts recording\n2. **Second press** (or 60s timeout) → stops recording \u0026 processes\n3. **Transcription** → German speech-to-text via Whisper\n4. **Sentiment Analysis** → determines mood from transcript\n5. **Mood Display** → shows robot face on e-ink screen\n6. **LLM Generation** → generates child-friendly German response\n7. **TTS Output** → speaks response via Azure TTS\n\n### API\n\n```elixir\n# Simulate button press (start recording)\nMoodBot.Controller.handle_button_press()\n\n# Second press (stop \u0026 process)\nMoodBot.Controller.handle_button_press()\n\n# Check status\nMoodBot.Controller.status()\n```\n\n### Configuration\n\n- **System Prompt**: Configured in `MoodBot.Controller` module attribute\n- **Target Audience**: Children aged 6-12\n- **Language**: German\n- **Recording Timeout**: 60 seconds\n\n### Future Enhancements\n\n- GPIO button integration (see [`.claude/plans/button-gpio.md`](.claude/plans/button-gpio.md))\n- Conversation history persistence\n- Interrupt handling during processing\n\n## Contributing\n\n1. Fork the repository\n2. Create your feature branch\n3. Make changes and add tests\n4. Run code quality checks: `mix format \u0026\u0026 mix credo --strict \u0026\u0026 mix dialyzer`\n5. Submit a pull request\n\n## License\n\nMIT License\n\n## Learn More\n\n- [Nerves Project](https://nerves-project.org/) - Embedded Elixir framework\n- [Circuits](https://github.com/elixir-circuits) - Hardware interface libraries\n- [E-ink Display Datasheet](https://www.waveshare.com/wiki/2.9inch_e-Paper_Module) - Hardware specifications\n- [Circuits.SPI](https://github.com/elixir-circuits/circuits_spi) - SPI communication library\n- [Circuits.GPIO](https://github.com/elixir-circuits/circuits_gpio) - GPIO control library\n- [Raspberry Pi pinout](https://pinout.xyz)\n- [Waveshare E-Paper 2.9\" Product Specifications](https://files.waveshare.com/upload/7/79/2.9inch-e-paper-v2-specification.pdf)\n\n[eink-frame]: https://www.printables.com/model/401112-frame-for-29-e-paper-waveshare-display-module/files\n[eink-rpi-case]: https://www.printables.com/model/159370-waveshare-29-inch-case-for-esphomehomeassistant-di\n[scenic]: https://hexdocs.pm/scenic/overview_general.html\n[nerves-underjord]: https://underjord.io/to-nerves-from-elixir.html\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcroesnick%2Fmood_bot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcroesnick%2Fmood_bot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcroesnick%2Fmood_bot/lists"}