DeskHog Documentation

repository·main·Indexed 19 days ago

https://github.com/posthog/deskhog

Documentation for the DeskHog project, including 3D printing instructions, hardware assembly guides, and a developer framework for creating dynamic UI cards using the CardDefinition and CardConfig system. Includes details on the Card Management API, InputHandler interface, and PlatformIO library organization.

Tokens
16.6K
Snippets
49
Records
76
Agent score
66%

What's inside deskhog

  1. How card management works in DeskHog

    main

    DeskHog uses a system of Card Definitions and Card Configurations to manage the UI.

    1. Card Definitions (CardDefinition) represent the available types of cards (e.g., a PostHog Insight) that a user can choose to add. These are registered in the CardController and include a factory function to create the UI component.
    2. Card Configurations (CardConfig) represent the active instances of cards currently displayed on the screen. These are stored in persistent memory as a JSON array.

    When a user modifies their configuration via the Web UI or API, the CardController performs a reconciliation: it diffs the new configuration against the currently displayed cards, removes obsolete ones, creates new ones using their registered factory functions, and re-orders the CardNavigationStack to match the new order.

    // Example of the relationship between a definition and a config
    // A definition tells you WHAT can be added
    struct CardDefinition {
        CardType id;
        String name;
        bool allowMultiple;
        bool needsConfigInput;
        String configInputLabel;
        String uiDescription;
        std::function<lv_obj_t*(const String& configValue)> factory;
    };
    
    // A config tells you WHAT IS currently active
    struct CardConfig {
        CardType id;
        String config; // e.g., insight ID, animation speed
        int order;
    };
  2. How DeskHog OTA updates work via GitHub Releases

    main

    DeskHog uses GitHub Releases to host and distribute firmware updates. The update lifecycle follows these steps:

    1. Check for Updates: The ESP32 performs an HTTPS GET request to the GitHub API: https://api.github.com/repos/PostHog/DeskHog/releases/latest.
    2. Parse Response: Using ArduinoJson, the device extracts the tag_name (version) and the browser_download_url for the .bin asset.
    3. Version Comparison: The device compares the GitHub tag_name against the CURRENT_FIRMWARE_VERSION running on the device.
    4. Download & Apply: If a newer version is found, the device downloads the binary via HTTPS and streams it to the Arduino Update library using Update.writeStream().
    5. Reboot: Upon successful completion (Update.end()), the device calls ESP.restart() to boot the new firmware.
  3. How the new card architecture works

    main

    The DeskHog card architecture is an extensible system designed for dynamic card management via a web UI. Instead of direct integration in the main loop, cards are managed through a factory pattern and a standardized interface.

    Key components include:

    • CardType enum: Defines the available card types.
    • CardDefinition: Metadata describing a card (name, UI description, configuration needs).
    • Factory Pattern: Uses lambda functions to instantiate cards dynamically.
    • InputHandler interface: The base class that all cards must implement to handle user input and lifecycle events.
    • Dynamic Management: Cards can be added or removed at runtime through the web interface.
    // Conceptual relationship: CardDefinition uses a factory lambda to create InputHandler instances
    flappyDef.factory = [this](const String& configValue) -> lv_obj_t* {
        // ... returns the UI object (lv_obj_t*)
    };
  4. Create game-like cards using InputHandler

    main

    For cards requiring high-frequency updates (games, animations, or real-time visualizations), inherit from InputHandler. This allows you to hook into the UI system's update loop without creating new tasks or timers.

    1. Inherit from InputHandler: Implement handleButtonPress(uint8_t button_index), update(), and prepareForRemoval().
    2. The update() method: This is called approximately 60 times per second when the card is active/visible. Return true to continue receiving updates, or false to stop.
    3. Registration: In your CardDefinition factory, call cardStack->registerInputHandler(card_object, handler_instance) to link the LVGL object to your logic class.

    This approach maintains thread safety by working within the existing UI task and automatically stops updates when the card is no longer visible.

    // 1. Your card class should inherit from InputHandler
    class FlappyHogCard : public InputHandler {
    public:
        // Handle button presses (required by InputHandler)
        bool handleButtonPress(uint8_t button_index) override {
            // Return false to allow navigation, true if you handled it
            return false;
        }
        
        // Update method for game logic (called ~60 times per second)
        bool update() override {
            if (game) {
                game->loop();  // Update game state
                return true;   // Continue receiving updates
            }
            return false;
        }
        
        // Required for proper cleanup
        void prepareForRemoval() override {
            // Called before LVGL object deletion
        }
    };
    
    // 2. Register the card with an InputHandler
    helloDef.factory = [this](const String& configValue) -> lv_obj_t* {
        FlappyHogCard* newCard = new FlappyHogCard(screen);
        if (newCard && newCard->getCard()) {
            // Register as InputHandler to receive updates
            cardStack->registerInputHandler(newCard->getCard(), newCard);
            return newCard->getCard();
        }
        delete newCard;
        return nullptr;
    };
  5. How the DeskHog task and core architecture works

    main

    DeskHog uses a dual-core architecture with FreeRTOS tasks to ensure stability. A critical constraint is that all UI updates must occur on the UI thread (Core 1); attempting to update the UI from other tasks will cause the board to crash.

    Core 0 (Protocol CPU) handles background tasks:

    • WiFi
    • Web portal server
    • Insight parsing
    • NeoPixel control

    Core 1 (Application CPU) handles UI tasks:

    • LVGL tick (timing, animations)
    • UI drawing and input handling

    Communication between cores is managed via an EventQueue, which dispatches events (like Web UI changes or PostHog client responses) from Core 0 to the UI task on Core 1 safely.

  6. Run the Multi-Board Flash Utility

    main

    The utility automatically detects and flashes DeskHog firmware to multiple ESP32-S3 boards as they are connected. Run the script from the DeskHog project root directory.

    By default, the script monitors USB connections, detects ESP32-S3 boards (via VID/PID or description), and initiates parallel flashing using PlatformIO.

    python multi_flash.py
  7. Integrate OTA updates into the Web UI

    main

    The captive portal UI (portal.html and portal.js) must be extended to allow users to trigger and monitor updates.

    Required HTML Elements (portal.html)

    • #current-version: Displays current firmware version.
    • #available-version: Displays version found on GitHub.
    • #check-update-btn: Triggers the update check.
    • #install-update-btn: Triggers the installation (initially hidden).
    • #release-notes: Displays release notes from the GitHub API.
    • #update-status-container & #update-progress-bar: Visual feedback for the update process.
    • #update-error-message: Displays error details.

    Required JavaScript Logic (portal.js)

    • checkFirmwareUpdate(): Fetches /check-update to populate version info and show the install button.
    • startFirmwareUpdate(): POSTs to /start-update to begin the process.
    • pollUpdateStatus(): Periodically fetches /update-status to update the progress bar and status message.
  8. Port a hackathon branch to the new card architecture

    main

    To port an existing implementation (like a standalone game) to the new architecture, follow these steps:

    1. Create a Card Wrapper: Implement the InputHandler interface in a new class that wraps your existing logic.
    2. Update CardType: Add your new type to the CardType enum in src/config/CardConfig.h and update the associated cardTypeToString() and stringToCardType() functions.
    3. Register the Card: Use CardController::registerCardType() with a CardDefinition and a factory lambda.
    4. Handle Updates: If your card requires a game loop, ensure you implement the update() method (see 'Handle Game Updates' for implementation details).
    5. Cleanup: Remove all direct references to the old implementation from main.cpp and CardController.
  9. Convert PNG images to LVGL C arrays

    main

    DeskHog uses a png2c.py script to convert PNG images into LVGL-compatible ARGB8888 C arrays. This is useful for adding sprite-based animations.

    Workflow

    1. Organize Files: Place PNG files in subdirectories under raw-png/. Each subdirectory will become a named array in the generated code. Example structure:
      raw-png/
      ├── walking/
      │   ├── frame_01.png
      │   └── frame_02.png
      └── idle/
          └── idle_01.png
    2. Run Conversion: Execute the script from your terminal:
      python3 png2c.py
    3. Use in Code: Include include/sprites/sprites.h. The script generates an array for each directory and a corresponding _count variable.

    Requirements

    • Python 3
    • Pillow and numpy libraries (pip install Pillow numpy)

    Usage Example

    #include "sprites/sprites.h"
    
    // Access the walking animation array
    lv_obj_t* img = lv_img_create(parent);
    lv_img_set_src(img, &walking_sprites[0]);  // First frame
    
    // Animate through frames
    for (int i = 0; i < walking_sprites_count; i++) {
        lv_img_set_src(img, &walking_sprites[i]);
        // Add delay or use LVGL animation
    }
    python3 png2c.py
  10. Assemble the DeskHog hardware

    main

    DeskHog assembly follows a 'sandwich' stacking order. If you are using a kit or a 3D-printed case, stack the components in this specific sequence:

    1. Base: The largest plastic piece.
    2. Battery
    3. Board
    4. Buttons
    5. Top face: The piece featuring the PostHog logo.

    Battery Wire Finesse

    To ensure the battery fits correctly, follow these steps for the battery cable:

    • Bind wires: Use the small piece of tape provided with the battery to bind the wires together. For best results, place the tape at the midpoint of the wires.
    • Bend wires: Gently but firmly bend the wires into a V shape right before the tape, on the battery side.
    • Alignment: Align the point where the wires exit the battery with the small notch cut into the battery well.
    • Placement: Attach the battery to the board and then attempt to lay the board into its designated space.
  11. Add a new card type to the DeskHog UI

    main

    The DeskHog uses a dynamic card system managed by CardController. To add a new card type, follow these four steps:

    1. Update the Enum: Add your new type to the CardType enum in src/ui/CardController.h.
    2. Implement the Class: Create a new class that implements the card UI using the LVGL library.
    3. Register the Card: Add the card to the CardController::initializeCardTypes() method using a CardDefinition.
    4. Add Configuration (Optional): Implement support if your card requires user input via the Web UI.

    CardDefinition Properties

    When registering a card, you must provide a CardDefinition object with these properties:

    • type: Unique CardType enum value.
    • name: The display name shown in the Web UI.
    • allowMultiple: Boolean indicating if users can add multiple instances.
    • needsConfigInput: Boolean indicating if the card requires configuration.
    • configInputLabel: The label for the configuration input field (if needsConfigInput is true).
    • uiDescription: A description shown in the Web UI.
    • factory: A lambda function (const String& configValue) -> lv_obj_t* that instantiates the card.
    // 1. Add to CardType enum (src/ui/CardController.h)
    enum class CardType {
        INSIGHT = 0,
        FRIEND = 1,
        HELLO = 2  // Add your new type here
    };
    
    // 2. Create your card class (src/ui/HelloCard.h)
    class HelloCard {
    public;
        HelloCard(lv_obj_t* parent);
        lv_obj_t* getCard() const { return _card; }
    
    private:
        lv_obj_t* _card;
    };
    
    // 3. Register in CardController::initializeCardTypes()
    CardDefinition helloDef;
    helloDef.type = CardType::HELLO;
    helloDef.name = "Hello world";
    helloDef.allowMultiple = true;
    helloDef.needsConfigInput = false;
    helloDef.uiDescription = "A simple greeting card";
    helloDef.factory = [this](const String& configValue) -> lv_obj_t* {
        HelloCard* newCard = new HelloCard(screen);
        return newCard ? newCard->getCard() : nullptr;
    };
    registerCardType(helloDef);
  12. Install requirements for Multi-Board Flash Utility

    main

    To use the multi_flash.py utility, ensure you have the following dependencies installed on your system:

    • Python 3.6+
    • PlatformIO CLI: Install via pip install platformio
    • pyserial: Install via pip install pyserial
    • USB drivers: Ensure appropriate USB drivers for ESP32-S3 are installed.
    pip install platformio pyserial