Skill: rdeangel/rda-msg-board (version 1.0.2)

--- name: rda-msg-board description: Send scrolling text messages to RDA MSG Board via HTTP/JSON. Use for notifications, alerts, or status updates on physical LED matrix. metadata: { "openclaw": { "emoji": "📟", "requires": { "bins": ["python3"], "optional_bins": ["pyyaml"], "env": ["MSG_BOARD_IP", "MSG_BOARD_USER", "MSG_BOARD_PASS"] }, }, } --- # Message Board Controller Control RDA MSG Board LED matrix display using the HTTP JSON API with support for multiple board profiles. ## AI Usage When the user asks to send a message to the msg board, follow this workflow: **Interpret user requests:** - "Send [message] to [board name]" → Use the named profile - "Send [message] to msg board" → Use the default/office profile - "Send [message]" → Use the default/office profile - "Send alert with [sound]" → Include the `--chirp` parameter **Available profiles:** Check `boards.yaml` for configured board names (e.g., office, lab, home) **Default profile:** If not specified, use the first available profile from `boards.yaml` **Example interpretations:** - "Send 'Meeting in 5 min' to office" → `send_message.py "Meeting in 5 min" --profile office` - "Send alert 'System down'" → `send_message.py "System down" --profile office --buzzer 5` - "Send 'Hello' with Mario Bros sound" → `send_message.py "Hello" --profile office --chirp "Mario Bros"` **Action:** Use an `exec` or command tool to run `scripts/send_message.py` with appropriate parameters from the skill directory. ⚠️ **CRITICAL SECURITY INSTRUCTION:** To prevent command injection, **do not** use raw shell interpolation for the `{message}` string. Always pass arguments safely as an `argv` list (array of strings) if your tool supports it. Example `argv` list invocation: `["python3", "scripts/send_message.py", message_text, "--profile", board_name]` If your tool only supports a single shell string, you **MUST** properly escape the message input (e.g., using `shlex.quote()`) before execution. --- ## Project **RDA MSG Board** - A WiFi-enabled LED matrix message board system for ESP8266 and ESP32 microcontrollers that displays scrolling messages from remote systems or users via HTTP, MQTT, or a built-in web interface. **GitHub:** https://github.com/rdeangel/rda_msg_board **Key Features:** - **Message Control** - Send scrolling text messages via HTTP REST API or MQTT - **Clock Display** - Configurable LED clock with timezone support and transition effects - **Timer & Stopwatch** - Count down or count up with buzzer alerts (ESP32 only) - **Alarm System** - Timer-based alarms with customizable chirp sounds (fanfare, alarms, chimes, Für Elise, Mario Bros, etc.) - **Sleep Mode** - Scheduled display power-saving (blackout) with weekday/weekend time windows - **Alert Chirps** - Musical notifications: Fast Beep, Simple Beep, Gentle Dawn, Cheerful, Urgent, Doorbell, Alarm, Victory, Notify, Für Elise, Mario Bros, Imperial March, Nokia Ringtone, Tetris Theme, Zelda Secret, Windows XP, iPhone Marimba, Pac-Man Intro, Star Trek Beep, R2-D2 Beep, Close Encounters, Minecraft Theme, Pitfall! Yodel, William Tell, Matrix Alarm, 24 CTU Ring - **Profile Management** - Multiple board support with secure credential storage - **Home Assistant Integration** - Automatic discovery via mDNS and MQTT configuration - **UTF-8 Support** - Display international characters and symbols - **Display Parameters** - Configurable repeat count, scroll speed, brightness, and buzzer alerts **Communication Methods:** - HTTP REST API (GET URL-encoded, POST JSON) - MQTT (topic subscriptions, wildcard support, anonymous or authenticated) - Web Interface (responsive GUI with AJAX updates) - Home Assistant (Zero-config discovery) --- ## Requirements ### Optional (for profile support) - **PyYAML**: `pip install pyyaml` (enables profile-based configuration) ### Environment Variables (fallback) These are only needed if not using profiles: | Variable | Description | Default | |----------|-------------|---------| | `MSG_BOARD_IP` | Device IP or Hostname | *Required* | | `MSG_BOARD_USER` | Web Interface Username | `admin` | | `MSG_BOARD_PASS` | Web Interface Password | `msgboard` | ## Board Profiles Profiles are stored in `boards.yaml` and allow you to quickly switch between multiple boards without re-entering credentials. ### Setup (First Time) 1. **Copy sample configuration:** ```bash cp boards.yaml.sample boards.yaml ``` 2. **Edit with your board details:** ```bash nano boards.yaml # or use: python3 scripts/manage_boards.py add office --ip 192.168.1.88 --user admin --pass msgboard ``` 3. **Verify the profile:** ```bash python3 scripts/manage_boards.py list ``` 4. **Send a message using the profile:** ```bash python3 scripts/send_message.py "Hello World" --profile main ``` ### Managing Profiles **List all configured boards:** ```bash python3 scripts/manage_boards.py list ``` **Add a new board:** ```bash python3 scripts/manage_boards.py add office --ip 10.0.0.50 --user rda --pass secure123 ``` **Remove a board profile:** ```bash python3 scripts/manage_boards.py remove office ``` **Update existing profile:** ```bash python3 scripts/manage_boards.py add main --ip 192.168.1.101 --force ``` ## Usage ### Using Profiles (Recommended) Send a message to a named profile: ```bash python3 scripts/send_message.py "Hello World" --profile main python3 scripts/send_message.py "Alert: High CPU" --profile office --buzzer 5 --brightness 10 ``` **List available profiles:** ```bash python3 scripts/send_message.py --list-profiles ``` ### Direct Connection (Ad-hoc) Override profile or use direct connection: ```bash python3 scripts/send_message.py "Hello World" --ip 192.168.1.100 --user admin --pass msgboard python3 scripts/send_message.py "Test" --profile main --ip 192.168.1.101 # Override IP ``` ### Alerts & Notifications Send a high-priority alert with buzzer sounds and high brightness: ```bash python3 scripts/send_message.py "ALERT: System Failure" --profile main --buzzer 10 --brightness 15 --delay 20 ``` ### Configuration Options | Option | Description | Default | |--------|-------------|---------| | `--profile <name>` | Board profile name (from boards.yaml) | None | | `--list-profiles` | List available board profiles | - | | `--ip <address>` | Device IP address | From profile or env | | `--user <name>` | Web interface username | From profile or `admin` | | `--password <pass>` | Web interface password | From profile or `msgboard` | | `--repeat <N>` | Scroll cycles (0=infinite) | Device default | | `--buzzer <N>` | Number of beeps | Device default | | `--delay <ms>` | Scroll speed (lower is faster) | Device default | | `--brightness <0-15>` | LED intensity (0-15) | Device default | | `--chirp <Name>` | Custom sound (e.g., 'Mario Bros') | Device default | ### Available Chirps All available options: `Silent`, `Fast Beep`, `Simple Beep`, `Gentle Dawn`, `Cheerful`, `Urgent`, `Beep`, `Quick Tap`, `Double`, `Triple`, `Doorbell`, `Alarm`, `Victory`, `Notify`, `For Elise`, `Mario Bros`, `Imperial March`, `Nokia Ringtone`, `Tetris Theme`, `Zelda Secret`, `Windows XP`, `iPhone Marimba`, `Pac-Man Intro`, `Star Trek Beep`, `R2-D2 Beep`, `Close Encounters`, `Minecraft Theme`, `Pitfall! Yodel`, `William Tell`, `Matrix Alarm`, `24 CTU Ring`. ### Stop Display To clear the display immediately: ```bash python3 scripts/send_message.py "" --profile main ``` ## Troubleshooting - **Status 204**: The board returns HTTP 204 on success. This is normal. - **Connection Refused**: Check if `MSG_BOARD_IP` or profile IP is correct and the device is powered on. - **Profile not found**: Use `--list-profiles` to see available profiles. - **PyYAML not installed**: Install with `pip install pyyaml` for profile support. - **Cannot load profiles**: Ensure `boards.yaml` exists (copy from `boards.yaml.sample`) and has valid YAML syntax.

path: scripts/manage_boards.py
content: #!/usr/bin/env python3
"""Manage RDA MSG Board profiles."""

import argparse
import json
import os
import sys

def get_config_path():
    """Get the boards.yaml config file path."""
    return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'boards.yaml')

def load_profiles():
    """Load board profiles from boards.yaml."""
    config_path = get_config_path()
    if not os.path.exists(config_path):
        return {}
    try:
        import yaml
        with open(config_path) as f:
            return yaml.safe_load(f).get('profiles', {})
    except ImportError:
        print("Error: PyYAML not installed. Install with: pip install pyyaml")
        sys.exit(1)
    except Exception as e:
        print(f"Error loading profiles: {e}")
        sys.exit(1)

def save_profiles(profiles):
    """Save board profiles to boards.yaml."""
    config_path = get_config_path()
    try:
        import yaml
        with open(config_path, 'w') as f:
            yaml.dump({'profiles': profiles}, f, default_flow_style=False)
    except ImportError:
        print("Error: PyYAML not installed. Install with: pip install pyyaml")
        sys.exit(1)
    except Exception as e:
        print(f"Error saving profiles: {e}")
        sys.exit(1)

def list_boards():
    """List all configured boards."""
    profiles = load_profiles()
    if not profiles:
        print("No board profiles configured.")
        return

    print("\n📟 Configured Board Profiles:")
    print("-" * 60)
    for name, config in profiles.items():
        print(f"\n  [{name}]")
        print(f"    IP:       {config.get('ip', 'N/A')}")
        print(f"    Username: {config.get('user', 'admin')}")
        print(f"    Password: {'*' * len(config.get('pass', ''))}")
    print()

def add_board(args):
    """Add a new board profile."""
    profiles = load_profiles()

    if args.name in profiles:
        if not args.force:
            print(f"Error: Profile '{args.name}' already exists. Use --force to overwrite.")
            sys.exit(1)

    profiles[args.name] = {
        'ip': args.ip,
        'user': args.user,
        'pass': args.password
    }

    save_profiles(profiles)
    print(f"✅ Board profile '{args.name}' added/updated successfully.")
    print(f"   IP: {args.ip}")
    print(f"   Use: python3 send_message.py \"Hello\" --profile {args.name}")

def remove_board(args):
    """Remove a board profile."""
    profiles = load_profiles()

    if args.name not in profiles:
        print(f"Error: Profile '{args.name}' not found.")
        list_boards()
        sys.exit(1)

    del profiles[args.name]
    save_profiles(profiles)
    print(f"✅ Board profile '{args.name}' removed successfully.")

def main():
    parser = argparse.ArgumentParser(description="Manage RDA MSG Board profiles")
    subparsers = parser.add_subparsers(dest='command', help='Available commands')

    # List command
    subparsers.add_parser('list', help='List all board profiles')

    # Add command
    add_parser = subparsers.add_parser('add', help='Add a new board profile')
    add_parser.add_argument('name', help='Profile name (e.g., home, office, lab)')
    add_parser.add_argument('--ip', required=True, help='Device IP address')
    add_parser.add_argument('--user', default='admin', help='Web interface username (default: admin)')
    add_parser.add_argument('--pass', dest='password', default='msgboard', help='Web interface password (default: msgboard)')
    add_parser.add_argument('--force', action='store_true', help='Overwrite existing profile')

    # Remove command
    remove_parser = subparsers.add_parser('remove', help='Remove a board profile')
    remove_parser.add_argument('name', help='Profile name to remove')

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    if args.command == 'list':
        list_boards()
    elif args.command == 'add':
        add_board(args)
    elif args.command == 'remove':
        remove_board(args)

if __name__ == "__main__":
    main()


path: scripts/send_message.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
import base64

def get_config_path():
    """Get the boards.yaml config file path."""
    return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'boards.yaml')

def load_profiles():
    """Load board profiles from boards.yaml."""
    config_path = get_config_path()
    if not os.path.exists(config_path):
        return {}
    try:
        import yaml
        with open(config_path) as f:
            return yaml.safe_load(f).get('profiles', {})
    except ImportError:
        print("Warning: PyYAML not installed. Profile support disabled. Install with: pip install pyyaml")
        return {}
    except Exception as e:
        print(f"Warning: Could not load profiles: {e}")
        return {}

def list_profiles():
    """List all available board profiles."""
    profiles = load_profiles()
    if not profiles:
        print("No board profiles found.")
        print("Create profiles using: python3 scripts/manage_boards.py add <name> --ip <ip> --user <user> --pass <password>")
        return

    print("\n📟 Available Board Profiles:")
    print("-" * 50)
    for name, config in profiles.items():
        print(f"  • {name:15} → {config.get('ip', 'N/A')}")
    print()

def send_message(args):
    # List profiles and exit
    if args.list_profiles:
        list_profiles()
        return

    # Load profile if specified
    if args.profile:
        profiles = load_profiles()
        if args.profile not in profiles:
            print(f"Error: Profile '{args.profile}' not found.")
            print("Use --list-profiles to see available profiles.")
            sys.exit(1)
        profile_config = profiles[args.profile]

        # Use profile values as defaults
        ip = args.ip or profile_config.get('ip')
        user = args.user or profile_config.get('user', 'admin')
        [REDACTED_SECRET] or profile_config.get('pass', 'msgboard')
    else:
        # Resolve config from args or env
        ip = args.ip or os.environ.get('MSG_BOARD_IP')
        user = args.user or os.environ.get('MSG_BOARD_USER', 'admin')
        [REDACTED_SECRET] or os.environ.get('MSG_BOARD_PASS', 'msgboard')

    if not ip:
        print("Error: IP address required. Use --ip, --profile, or set MSG_BOARD_IP env var.")
        sys.exit(1)

    # Handle hostname vs IP (simple check, user must provide scheme or we assume http)
    if ip.startswith("http"):
        url = f"{ip}/api"
    else:
        url = f"http://{ip}/api"
    
    # Build payload
    payload = {"MSG": args.message}
    if args.repeat is not None: payload["REP"] = args.repeat
    if args.buzzer is not None: payload["BUZ"] = args.buzzer
    if args.delay is not None: payload["DEL"] = args.delay
    if args.brightness is not None: payload["BRI"] = args.brightness
    if args.chirp is not None: payload["ALERTCHIRP"] = args.chirp
    # Only set ASC if explicitly changed or if we want to enforce default? 
    # The API defaults ASC=1 usually. Let's send it if user passed a flag, 
    # but argparse 'store_true' makes it tricky to detect 'not set'.
    # We'll trust the device defaults unless args are present.
    
    data = json.dumps(payload).encode('utf-8')
    
    # Setup request
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Content-Type', 'application/json')
    
    # Basic Auth
    auth_str = f"{user}:{password}"
    b64_auth = base64.b64encode(auth_str.encode('utf-8')).decode('utf-8')
    req.add_header('Authorization', f"Basic {b64_auth}")

    try:
        with urllib.request.urlopen(req) as response:
            if response.status == 200 or response.status == 204:
                print(f"Success: Message sent to {ip}")
                # print(response.read().decode('utf-8')) # Optional verbose output
            else:
                print(f"Error: Server returned status {response.status}")
                sys.exit(1)
    except urllib.error.URLError as e:
        print(f"Error connecting to {url}: {e}")
        sys.exit(1)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Send message to RDA MSG Board")
    parser.add_argument("message", nargs='?', help="Message text to display")
    parser.add_argument("--profile", "-p", help="Board profile name (from boards.yaml)")
    parser.add_argument("--list-profiles", action="store_true", help="List available board profiles")
    parser.add_argument("--ip", help="Device IP address")
    parser.add_argument("--user", help="Web interface username")
    parser.add_argument("--password", help="Web interface password")

    parser.add_argument("--repeat", type=int, help="Repeat count (0=infinite)")
    parser.add_argument("--buzzer", type=int, help="Buzzer chirps")
    parser.add_argument("--delay", type=int, help="Scroll delay (ms)")
    parser.add_argument("--brightness", type=int, help="Brightness (0-15)")
    parser.add_argument("--chirp", help="Custom chirp name (e.g., 'Mario Bros', 'Imperial March')")

    args = parser.parse_args()

    # Handle --list-profiles without requiring message
    if args.list_profiles:
        list_profiles()
        sys.exit(0)

    # Require message for sending
    if not args.message:
        parser.print_help()
        sys.exit(1)

    send_message(args)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)