diff --git a/.claude/agents/flashforge-docs-writer.md b/.claude/agents/flashforge-docs-writer.md deleted file mode 100644 index 47fe423..0000000 --- a/.claude/agents/flashforge-docs-writer.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: flashforge-docs-writer -description: Professional documentation writer for FlashForge API codebases. Use proactively when API documentation needs creation, updating, or refinement. -model: inherit ---- - -You are a professional documentation writer specializing in FlashForge API documentation. Your expertise encompasses both technical API documentation and user-facing documentation for FlashForge printer systems. - -## Your Expertise - -### FlashForge Product Knowledge -- **AD5X**: Special multi-color/multi-material printer with IFS (Interchangeable Filament System) for 4 slots. No built-in camera or factory-installed LEDs. No factory filtration support. -- **Adventurer 5M**: Base model with no camera, no factory LEDs, no filtration control. -- **Adventurer 5M Pro**: Only model with built-in camera and factory-installed LEDs. Features internal filtration and TVOC level reporting. -- All three models support aftermarket camera installation (OEM or third-party) and LED upgrades. - -### Documentation Standards -- Maintain a professional, formal, and informative tone -- Never use emojis in any documentation -- Follow consistent formatting and structure -- Ensure technical accuracy and clarity - -## When Invoked - -1. **Review existing documentation** in the codebase to understand current structure and style -2. **Identify documentation gaps** and areas needing updates -3. **Create or update documentation** including: - - API reference documentation - - Usage examples and tutorials - - Technical specifications - - Printer-specific documentation - - README files - - Change logs and release notes - -## Methodology - -### Documentation Assessment -- Scan the codebase for existing documentation files -- Analyze API endpoints and functionality -- Understand the target audience (developers, users, administrators) -- Identify documentation priorities and scope - -### Writing Process -- Maintain consistent terminology and naming conventions -- Use clear, concise language appropriate for technical documentation -- Structure content logically with proper headings and sections -- Include code examples where helpful for API documentation -- Ensure all printer-specific details are accurately represented - -### Quality Assurance -- Verify all technical specifications are correct -- Cross-reference with actual code implementation -- Ensure documentation is up-to-date with code changes -- Maintain documentation version consistency - -## Documentation Types - -### API Documentation -- Detailed endpoint descriptions -- Request/response schemas -- Authentication requirements -- Error handling and status codes -- Usage examples with sample code - -### Product Documentation -- Printer-specific features and capabilities -- Hardware specifications -- Installation and setup guides -- Troubleshooting information - -### Technical Guides -- Integration instructions -- Best practices -- Configuration options -- Performance optimization - -## Best Practices - -- Use markdown formatting for structure and readability -- Include version information for all documentation -- Provide clear navigation and cross-references -- Update documentation when code changes occur -- Maintain a documentation index or table of contents -- Ensure consistency across all documentation files - -Focus on delivering comprehensive, accurate documentation that serves both technical users and end-users of the FlashForge API systems. \ No newline at end of file diff --git a/.claude/skills/sub-agent-creator/SKILL.md b/.claude/skills/sub-agent-creator/SKILL.md deleted file mode 100644 index fda84e5..0000000 --- a/.claude/skills/sub-agent-creator/SKILL.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -name: sub-agent-creator -description: Interactive sub-agent creation skill for Claude Code. Use when user wants to create a custom subagent or mentions needing a specialized agent for specific tasks. This skill guides the entire subagent creation process including identifier design, system prompt generation, skill/context selection, and writing properly formatted agent files to .claude/agents. ---- - -# Sub-Agent Creator - -Interactive skill for creating Claude Code subagents. Guides the complete process: gathering requirements, designing the agent's purpose and persona, selecting helpful skills and documentation, and writing a properly formatted agent file to `.claude/agents/`. - -## Critical Formatting Rules - -Subagents MUST follow strict formatting or they will fail validation and not load: - -| Rule | Requirement | Consequence | -|------|-------------|-------------| -| **Single-line description** | `description` must be one line, no `\n` | Validation failure | -| **No literal `\n`** | Use actual newlines in body, not `\n` escapes | Validation failure | -| **Valid colors only** | If specified: red, blue, green, yellow, purple, orange, pink, cyan | Agent won't load | -| **Valid models only** | `sonnet`, `opus`, `haiku`, or `inherit` | Validation failure | -| **Name format** | Lowercase letters, numbers, hyphens only | Validation failure | - -### Examples of WRONG vs RIGHT - -```yaml -# WRONG - Multi-line YAML syntax -description: | - Expert code reviewer. - Use after code changes. - -# WRONG - Actual newlines in value -description: Expert code reviewer. - Use after code changes. - -# RIGHT - ONE continuous line -description: Expert code reviewer. Use proactively after code changes. -``` - -```yaml -# WRONG - Literal \n in body ---- -name: test-runner -description: Run tests ---- - -You are a test runner.\nWhen invoked:\n1. Run tests\n2. Report results - -# RIGHT - Actual newlines ---- -name: test-runner -description: Run tests ---- - -You are a test runner. -When invoked: -1. Run tests -2. Report results -``` - -## Workflow - -### 1. Understand Requirements - -When this skill triggers, the user has described what kind of agent they want. First, extract: - -- **Core purpose**: What should this agent do? -- **Trigger conditions**: When should Claude delegate to this agent? -- **Key capabilities**: What specific tasks will it handle? -- **Existing context**: Any relevant skills, docs, or project patterns? - -### 2. Use AskUserQuestion for Details - -Ask clarifying questions using the AskUserQuestion tool. Confirm: - -- **Identifier**: What should the agent be named? (lowercase-with-hyphens) -- **Proactive usage**: Should this agent be used proactively or only on explicit request? -- **Model**: Default to `inherit`. Only suggest `haiku` for simple, fast tasks. ALWAYS confirm before using non-inherit models. -- **Color**: Auto-select from valid options (red, blue, green, yellow, purple, orange, pink, cyan) OR let user choose. - -### 3. Identify Helpful Context - -Search the workspace for: - -**Relevant skills**: Check `.claude/skills/` and project skills that would help the agent. -```bash -ls .claude/skills/ -``` - -**Relevant documentation**: Look for references files, CLAUDE.md, API docs, etc. -```bash -find . -name "*.md" -type f | head -20 -``` - -### 4. Design the System Prompt - -Using the agent creation architect framework (see `references/agent-creation-prompt.md`): - -1. **Extract Core Intent** - Fundamental purpose and success criteria -2. **Design Expert Persona** - Compelling identity with domain knowledge -3. **Architect Instructions** - Behavioral boundaries, methodologies, edge cases -4. **Optimize for Performance** - Decision frameworks, quality control -5. **Create whenToUse Examples** - Concrete examples showing delegation - -The system prompt should be in second person ("You are...", "You will..."). - -### 5. Write the Agent File - -Write the agent file to `.claude/agents/.md`: - -```yaml ---- -name: -description: -model: inherit ---- - - -``` - -**Default settings:** -- `model`: Always `inherit` unless user confirms otherwise -- `tools`: Omit to allow all tools (user preference: never restrict) -- `skills`: Include if specific skills would help the agent - -### 6. Validate Before Completing - -Run the validation script: -```bash -python .claude/skills/sub-agent-creator/scripts/validate_agent.py .claude/agents/.md -``` - -Only proceed if validation passes. Fix any errors and re-validate. - -## Agent File Template - -```yaml ---- -name: agent-identifier -description: Brief single-line description starting with what it does. Use proactively when [trigger condition]. -model: inherit -skills: - - relevant-skill-1 - - relevant-skill-2 ---- - -You are an expert [domain] specialist. - -When invoked: -1. [First step] -2. [Second step] -3. [Continue as needed] - -Your approach: -- [Guideline 1] -- [Guideline 2] - -For each [task], provide: -- [Output format 1] -- [Output format 2] - -Focus on [core principle]. -``` - -## Description Best Practices - -The `description` field is Claude's primary signal for when to delegate. Include: - -- **What the agent does**: "Expert code reviewer specializing in..." -- **When to use**: "Use proactively after writing code" or "Use when analyzing..." -- **Triggers**: Specific situations that should trigger delegation - -Examples: -```yaml -description: Test execution specialist. Use proactively after writing tests or modifying test files. -description: Database query analyst. Use when needing to analyze data or generate reports from BigQuery. -description: Code archaeology expert. Use when exploring legacy codebases or understanding unfamiliar code. -``` - -## Resources - -- **scripts/validate_agent.py** - Validates agent files for formatting errors -- **references/sub-agents-docs.md** - Complete Claude Code subagents documentation -- **references/agent-creation-prompt.md** - Agent creation architect system prompt framework - -After creating an agent, verify it appears in `/agents` command output. diff --git a/.claude/skills/sub-agent-creator/references/agent-creation-prompt.md b/.claude/skills/sub-agent-creator/references/agent-creation-prompt.md deleted file mode 100644 index b53c453..0000000 --- a/.claude/skills/sub-agent-creator/references/agent-creation-prompt.md +++ /dev/null @@ -1,80 +0,0 @@ -# Agent Creation Architect System Prompt - -Source: https://github.com/Piebald-AI/claude-code-system-prompts/blob/main/system-prompts/agent-prompt-agent-creation-architect.md - -You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability. - -**Important Context**: You may have access to project-specific instructions from CLAUDE.md files and other context that may include coding standards, project structure, and custom requirements. Consider this context when creating agents to ensure they align with the project's established patterns and practices. - -When a user describes what they want an agent to do, you will: - -1. **Extract Core Intent**: Identify the fundamental purpose, key responsibilities, and success criteria for the agent. Look for both explicit requirements and implicit needs. Consider any project-specific context from CLAUDE.md files. For agents that are meant to review code, you should assume that the user is asking to review recently written code and not the whole codebase, unless the user has explicitly instructed you otherwise. - -2. **Design Expert Persona**: Create a compelling expert identity that embodies deep domain knowledge relevant to the task. The persona should inspire confidence and guide the agent's decision-making approach. - -3. **Architect Comprehensive Instructions**: Develop a system prompt that: - - Establishes clear behavioral boundaries and operational parameters - - Provides specific methodologies and best practices for task execution - - Anticipates edge cases and provides guidance for handling them - - Incorporates any specific requirements or preferences mentioned by the user - - Defines output format expectations when relevant - - Aligns with project-specific coding standards and patterns from CLAUDE.md - -4. **Optimize for Performance**: Include: - - Decision-making frameworks appropriate to the domain - - Quality control mechanisms and self-verification steps - - Efficient workflow patterns - - Clear escalation or fallback strategies - -5. **Create Identifier**: Design a concise, descriptive identifier that: - - Uses lowercase letters, numbers, and hyphens only - - Is typically 2-4 words joined by hyphens - - Clearly indicates the agent's primary function - - Is memorable and easy to type - - Avoids generic terms like "helper" or "assistant" - -6. **Example agent descriptions**: In the 'whenToUse' field of the JSON object, you should include examples of when this agent should be used. Examples should be of the form: - -``` -Context: The user is creating a test-runner agent that should be called after a logical chunk of code is written. -user: "Please write a function that checks if a number is prime" -assistant: "Here is the relevant function: " - -Since a significant piece of code was written, use the ${TASK_TOOL_NAME} tool to launch the test-runner agent to run the tests. -assistant: "Now let me use the test-runner agent to run the tests" -``` - -``` -Context: User is creating an agent to respond to the word "hello" with a friendly joke. -user: "Hello" -assistant: "I'm going to use the ${TASK_TOOL_NAME} tool to launch the greeting-responder agent to respond with a friendly joke" - -Since the user is greeting, use the greeting-responder agent to respond with a friendly joke. -``` - -If the user mentioned or implied that the agent should be used proactively, you should include examples of this. - -**NOTE**: Ensure that in the examples, you are making the assistant use the Agent tool and not simply respond directly to the task. - -## Output Format - -Your output must be a valid JSON object with exactly these fields: - -```json -{ -"identifier": "A unique, descriptive identifier using lowercase letters, numbers, and hyphens (e.g., 'test-runner', 'api-docs-writer', 'code-formatter')", -"whenToUse": "A precise, actionable description starting with 'Use this agent when...' that clearly defines the triggering conditions and use cases. Ensure you include examples as described above.", -"systemPrompt": "The complete system prompt that will govern the agent's behavior, written in second person ('You are...', 'You will...') and structured for maximum clarity and effectiveness" -} -``` - -## Key Principles - -- Be specific rather than generic - avoid vague instructions -- Include concrete examples when they would clarify behavior -- Balance comprehensiveness with clarity - every instruction should add value -- Ensure the agent has enough context to handle variations of the core task -- Make the agent proactive in seeking clarification when needed -- Build in quality assurance and self-correction mechanisms - -Remember: The agents you create should be autonomous experts capable of handling their designated tasks with minimal additional guidance. Your system prompts are their complete operational manual. diff --git a/.claude/skills/sub-agent-creator/references/sub-agents-docs.md b/.claude/skills/sub-agent-creator/references/sub-agents-docs.md deleted file mode 100644 index 99c0ac8..0000000 --- a/.claude/skills/sub-agent-creator/references/sub-agents-docs.md +++ /dev/null @@ -1,295 +0,0 @@ -# Subagents Documentation - -Source: https://code.claude.com/docs/en/sub-agents - -## Overview - -Subagents are specialized AI assistants that handle specific types of tasks. Each subagent runs in its own context window with a custom system prompt, specific tool access, and independent permissions. When Claude encounters a task that matches a subagent's description, it delegates to that subagent, which works independently and returns results. - -Subagents help you: -- Preserve context by keeping exploration and implementation out of your main conversation -- Enforce constraints by limiting which tools a subagent can use -- Reuse configurations across projects with user-level subagents -- Specialize behavior with focused system prompts for specific domains -- Control costs by routing tasks to faster, cheaper models like Haiku - -Claude uses each subagent's description to decide when to delegate tasks. When you create a subagent, write a clear description so Claude knows when to use it. - -## Built-in Subagents - -### Explore -A fast, read-only agent optimized for searching and analyzing codebases. -- **Model**: Haiku (fast, low-latency) -- **Tools**: Read-only tools (denied access to Write and Edit tools) -- **Purpose**: File discovery, code search, codebase exploration - -### Plan -A research agent used during plan mode to gather context before presenting a plan. -- **Model**: Inherits from main conversation -- **Tools**: Read-only tools (denied access to Write and Edit tools) -- **Purpose**: Codebase research for planning - -### General-purpose -A capable agent for complex, multi-step tasks that require both exploration and action. -- **Model**: Inherits from main conversation -- **Tools**: All tools -- **Purpose**: Complex research, multi-step operations, code modifications - -## Subagent File Structure - -Subagents are defined in Markdown files with YAML frontmatter. Store them in `.claude/agents/` for project-level agents. - -``` ---- -name: code-reviewer -description: Reviews code for quality and best practices -tools: Read, Glob, Grep -model: sonnet ---- - -You are a code reviewer. When invoked, analyze the code and provide -specific, actionable feedback on quality, security, and best practices. -``` - -## Frontmatter Fields - -| Field | Required | Description | -| --- | --- | --- | -| `name` | Yes | Unique identifier using lowercase letters and hyphens | -| `description` | Yes | When Claude should delegate to this subagent | -| `tools` | No | Tools the subagent can use. Inherits all tools if omitted | -| `disallowedTools` | No | Tools to deny, removed from inherited or specified list | -| `model` | No | Model to use: `sonnet`, `opus`, `haiku`, or `inherit`. Defaults to `inherit` | -| `permissionMode` | No | Permission mode: `default`, `acceptEdits`, `dontAsk`, `bypassPermissions`, or `plan` | -| `skills` | No | Skills to load into the subagent's context at startup | -| `hooks` | No | Lifecycle hooks scoped to this subagent | - -## CRITICAL: Formatting Requirements - -These formatting rules MUST be followed strictly or the subagent will fail to load: - -### 1. Description - MUST be single line -The `description` field MUST be a single line of text. Multi-line descriptions will cause validation failure. - -```yaml -# CORRECT - Single line -description: Expert code reviewer. Use proactively after code changes. - -# INCORRECT - Multi-line -description: | - Expert code reviewer. - Use proactively after code changes. -``` - -### 2. Prompt/Body - No literal \n characters -The system prompt (markdown body) must NOT contain literal `\n` escape sequences. Use actual newlines instead. - -```yaml -# CORRECT - Actual newlines ---- -name: test-runner -description: Run tests after code changes ---- - -You are a test runner. When invoked: -1. Run the test suite -2. Report failures - -# INCORRECT - Literal \n ---- -name: test-runner -description: Run tests after code changes ---- - -You are a test runner. When invoked:\n1. Run the test suite\n2. Report failures -``` - -### 3. Color field (if using) -If specifying a color for the subagent, ONLY these values are allowed: -- red -- blue -- green -- yellow -- purple -- orange -- pink -- cyan - -Any other color value will cause the subagent to fail validation and not be loaded. - -### 4. Model field -Allowed values: `sonnet`, `opus`, `haiku`, or `inherit`. Default is `inherit` if not specified. - -### 5. Tools field -Tools should be comma-separated on a single line: -```yaml -tools: Read, Write, Edit, Bash, Grep, Glob -``` - -## Tool Restrictions - -### Available Tools -Subagents can use any of Claude Code's internal tools. To restrict tools, use the `tools` field (allowlist) or `disallowedTools` field (denylist): - -```yaml ---- -name: safe-researcher -description: Research agent with restricted capabilities -tools: Read, Grep, Glob, Bash -disallowedTools: Write, Edit ---- -``` - -### Default Behavior -- If `tools` is omitted, subagent inherits ALL tools from the main conversation -- This is the recommended approach for most custom subagents - -## Model Selection - -- `inherit`: Use the same model as the main conversation (default) -- `sonnet`: Capable model for complex tasks -- `opus`: Most capable model for difficult reasoning -- `haiku`: Fast, low-latency model for simple tasks - -For most subagents, omit the `model` field to use `inherit`. - -## Permission Modes - -| Mode | Behavior | -| --- | --- | -| `default` | Standard permission checking with prompts | -| `acceptEdits` | Auto-accept file edits | -| `dontAsk` | Auto-deny permission prompts (explicitly allowed tools still work) | -| `bypassPermissions` | Skip all permission checks | -| `plan` | Plan mode (read-only exploration) | - -## Skills Field - -Use the `skills` field to inject skill content into a subagent's context at startup: - -```yaml ---- -name: api-developer -description: Implement API endpoints following team conventions -skills: - - api-conventions - - error-handling-patterns ---- -``` - -The full content of each skill is injected into the subagent's context. Subagents don't inherit skills from the parent conversation; you must list them explicitly. - -## Hooks - -Subagents can define hooks that run during the subagent's lifecycle: - -| Event | Matcher input | When it fires | -| --- | --- | --- | -| `PreToolUse` | Tool name | Before the subagent uses a tool | -| `PostToolUse` | Tool name | After the subagent uses a tool | -| `Stop` | (none) | When the subagent finishes | - -```yaml ---- -name: code-reviewer -description: Review code changes with automatic linting -hooks: - PreToolUse: - - matcher: "Bash" - hooks: - - type: command - command: "./scripts/validate-command.sh $TOOL_INPUT" - PostToolUse: - - matcher: "Edit|Write" - hooks: - - type: command - command: "./scripts/run-linter.sh" ---- -``` - -## Automatic Delegation - -Claude automatically delegates tasks based on: -- The task description in your request -- The `description` field in subagent configurations -- Current context - -To encourage proactive delegation, include phrases like "use proactively" in your subagent's `description` field. - -## Subagent Locations - -| Location | Scope | Priority | -| --- | --- | --- | -| `.claude/agents/` | Current project | 2 | -| `~/.claude/agents/` | All your projects | 3 | -| Plugin's `agents/` directory | Where plugin is enabled | 4 (lowest) | - -## Example Subagents - -### Code Reviewer -```yaml ---- -name: code-reviewer -description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code. -tools: Read, Grep, Glob, Bash -model: inherit ---- - -You are a senior code reviewer ensuring high standards of code quality and security. - -When invoked: -1. Run git diff to see recent changes -2. Focus on modified files -3. Begin review immediately - -Review checklist: -- Code is clear and readable -- Functions and variables are well-named -- No duplicated code -- Proper error handling -- No exposed secrets or API keys -- Input validation implemented -- Good test coverage -- Performance considerations addressed - -Provide feedback organized by priority: -- Critical issues (must fix) -- Warnings (should fix) -- Suggestions (consider improving) - -Include specific examples of how to fix issues. -``` - -### Debugger -```yaml ---- -name: debugger -description: Debugging specialist for errors, test failures, and unexpected behavior. Use proactively when encountering any issues. -tools: Read, Edit, Bash, Grep, Glob ---- - -You are an expert debugger specializing in root cause analysis. - -When invoked: -1. Capture error message and stack trace -2. Identify reproduction steps -3. Isolate the failure location -4. Implement minimal fix -5. Verify solution works - -Debugging process: -- Analyze error messages and logs -- Check recent code changes -- Form and test hypotheses -- Add strategic debug logging -- Inspect variable states - -For each issue, provide: -- Root cause explanation -- Evidence supporting the diagnosis -- Specific code fix -- Testing approach -- Prevention recommendations - -Focus on fixing the underlying issue, not the symptoms. -``` diff --git a/.claude/skills/sub-agent-creator/scripts/validate_agent.py b/.claude/skills/sub-agent-creator/scripts/validate_agent.py deleted file mode 100644 index f099f26..0000000 --- a/.claude/skills/sub-agent-creator/scripts/validate_agent.py +++ /dev/null @@ -1,202 +0,0 @@ -#!/usr/bin/env python3 -""" -Validate a Claude Code subagent markdown file. - -This script checks for common formatting errors that cause subagents -to fail validation and not be loaded by Claude Code. - -Run: python validate_agent.py -""" - -import sys -import re -from pathlib import Path - - -# Allowed color values -VALID_COLORS = {"red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"} - -# Allowed model values -VALID_MODELS = {"sonnet", "opus", "haiku", "inherit"} - -# Required frontmatter fields -REQUIRED_FIELDS = {"name", "description"} - - -def parse_frontmatter(content): - """Parse YAML frontmatter from markdown content.""" - if not content.startswith("---"): - return None, "File does not start with YAML frontmatter (---)" - - end_delimiter = content.find("\n---", 4) - if end_delimiter == -1: - return None, "No closing frontmatter delimiter (---) found" - - frontmatter_text = content[4:end_delimiter] - body = content[end_delimiter + 4:] # Skip past the closing --- - - # Parse simple YAML key-value pairs - frontmatter = {} - for line in frontmatter_text.strip().split("\n"): - if ":" in line and not line.strip().startswith("#"): - key, value = line.split(":", 1) - key = key.strip() - value = value.strip() - # Remove quotes if present - if value.startswith('"') and value.endswith('"'): - value = value[1:-1] - elif value.startswith("'") and value.endswith("'"): - value = value[1:-1] - frontmatter[key] = value - - return frontmatter, body - - -def check_literal_newlines(text, context=""): - """Check for literal \n escape sequences in the text.""" - errors = [] - # Look for literal \n not followed by another backslash (which would be \\n) - # We want to catch \n that would appear as actual escape sequences - pattern = r'(? multi-line YAML syntax.") - - # Check for literal \n in frontmatter values - for key, value in frontmatter.items(): - if isinstance(value, str): - if '\\n' in value and key not in ["description"]: # description already checked above - if not value.strip().endswith("\\n") or value.count('\\n') > 1: - errors.append(f"Field '{key}' contains literal \\n. Use actual line breaks") - - # Check color if present - if "color" in frontmatter: - color = frontmatter["color"].lower() - if color not in VALID_COLORS: - errors.append(f"Invalid color '{frontmatter['color']}'. Must be one of: {', '.join(sorted(VALID_COLORS))}") - - # Check model if present - if "model" in frontmatter: - model = frontmatter["model"].lower() - if model not in VALID_MODELS: - errors.append(f"Invalid model '{frontmatter['model']}'. Must be one of: {', '.join(sorted(VALID_MODELS))}") - - # Check for common multi-line frontmatter issues - frontmatter_section = content.split("\n---\n", 1)[0] - if "description:" in frontmatter_section: - desc_match = re.search(r'description:\s*[|>]', frontmatter_section) - if desc_match: - errors.append("Description field must be ONE continuous line with NO line breaks. Do not use | or > multi-line YAML syntax.") - - # Check body for literal \n - if body: - body_errors = check_literal_newlines(body) - errors.extend(body_errors) - - # Check that tools is comma-separated if present - if "tools" in frontmatter: - tools = frontmatter["tools"] - if "\n" in tools: - errors.append("Tools field must be on a single line, comma-separated") - - # Check for whenToUse field (should be in body, not frontmatter for agent creation architect) - if "whenToUse" in frontmatter or "whentouse" in frontmatter: - warnings.append("whenToUse in frontmatter may not be standard for subagent files") - - return frontmatter, errors, warnings - - -def main(): - if len(sys.argv) < 2: - print("Usage: python validate_agent.py ") - print("\nValidates a Claude Code subagent markdown file for:") - print(" - Required frontmatter fields (name, description)") - print(" - Single-line description (no multi-line)") - print(" - No literal \\n escape sequences") - print(" - Valid color values (if specified)") - print(" - Valid model values (if specified)") - sys.exit(1) - - filepath = sys.argv[1] - - result = validate_agent_file(filepath) - - if len(result) == 2: - # Error occurred - frontmatter, errors = result - print(f"[FAIL] Validation FAILED: {filepath}") - for error in errors: - print(f" - {error}") - sys.exit(1) - else: - frontmatter, errors, warnings = result - - if errors: - print(f"[FAIL] Validation FAILED: {filepath}") - for error in errors: - print(f" - {error}") - if warnings: - print("\n[WARN] Warnings:") - for warning in warnings: - print(f" - {warning}") - sys.exit(1) - else: - print(f"[PASS] Validation PASSED: {filepath}") - if frontmatter: - print(f"\n name: {frontmatter.get('name', 'N/A')}") - desc = frontmatter.get('description', 'N/A') - print(f" description: {desc[:60]}{'...' if len(desc) > 60 else ''}") - if warnings: - print("\n[WARN] Warnings:") - for warning in warnings: - print(f" - {warning}") - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/.gitignore b/.gitignore index 1f78bc3..dd03930 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/ai_reference -/docs-wiki +flashforge-dashboard/node_modules/ +*.log diff --git a/README.md b/README.md index 83f249c..f075c29 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,93 @@ -# FlashForge API Documentation +# FlashForge Home Assistant Add-on Repository -Unofficial, community-driven documentation for FlashForge 3D printer networking protocols and APIs. Covers the Adventurer 3, Adventurer 4, Adventurer 5M, and AD5X printer families. +Repository add-on per Home Assistant. +Contiene l’add-on **FlashForge Dashboard** pronto per essere aggiunto nello store di Home Assistant. -All documentation lives in the **[Wiki](../../wiki)**. +## Installazione ---- +1. In Home Assistant vai su **Impostazioni → Add-on → Store** +2. Clicca **⋮ → Repositories** +3. Aggiungi questo URL: -## Supported Printer Families +```text +https://github.com/MikManenti/flashforge-api-docs +``` -| Family | Primary API | Auth | Wiki Page | -|--------|-------------|------|-----------| -| Adventurer 5M / 5M Pro | HTTP REST (8898) + TCP (8899) | CheckCode | [Adventurer 5M Series](../../wiki/Adventurer-5M-Series) | -| AD5X | HTTP REST (8898) + TCP (8899) + IFS | CheckCode | [AD5X](../../wiki/AD5X) | -| Adventurer 4 Pro / Lite | TCP (8899) | None | [Adventurer 4 Series](../../wiki/Adventurer-4-Series) | -| Adventurer 3 Series | TCP (8899) | None | [Adventurer 3 Series](../../wiki/Adventurer-3-Series) | +4. Cerca e installa **FlashForge Dashboard** -## Documentation +## Add-on incluso -### Core Protocols +### FlashForge Dashboard +Dashboard locale per stampanti FlashForge AD5M / AD5X / 5M Pro con: +- camera live +- stato stampa +- temperature +- controlli pausa/riprendi/stop +- lista file e upload GCode +- integrazione MQTT Discovery per sensori e switch in Home Assistant -- **[HTTP REST API](../../wiki/HTTP-REST-API)** — JSON-based control interface on port 8898 (5M / AD5X) -- **[TCP Protocol](../../wiki/TCP-Protocol)** — Text-based G/M-code interface on port 8899 (all models) -- **[Discovery Protocol](../../wiki/Discovery-Protocol)** — UDP auto-discovery for finding printers on the network -- **[Authentication](../../wiki/Authentication)** — CheckCode auth for HTTP API, open access for TCP +## Struttura repository -### References +```text +. +├── repository.yaml +└── flashforge-dashboard/ + ├── config.yaml + ├── Dockerfile + ├── run.sh + ├── server.js + ├── package.json + └── frontend/public/ +``` -- **[G-Code Reference](../../wiki/G%E2%80%90Code-Reference)** — Supported G-code commands via TCP -- **[M-Code Reference](../../wiki/M-Code-Reference)** — Standard and proprietary FlashForge M-codes -- **[State Machines](../../wiki/State-Machines)** — Unified state model across modern and legacy firmware -- **[Capability Matrix](../../wiki/Capability-Matrix)** — Feature support matrix across all printer models -- **[Error Codes](../../wiki/Error-Codes)** — HTTP, TCP, and IFS error codes with recovery strategies +## Note -### Printer-Specific +Progetto non ufficiale, non affiliato con FlashForge o Home Assistant. -- **[Adventurer 5M Series](../../wiki/Adventurer-5M-Series)** — HTTP/TCP architecture, status polling, file operations - - **[5M Pro Features](../../wiki/Adventurer-5M-Pro-Features)** — Camera, air filtration, TVOC monitoring -- **[AD5X](../../wiki/AD5X)** — Material station commands, manual control, extended endpoints - - **[IFS Material Station](../../wiki/AD5X-IFS-Material-Station)** — Multi-material slot management, load/unload, color mapping -- **[Adventurer 4 Series](../../wiki/Adventurer-4-Series)** — TCP-only, 33 commands, Pro/Lite variant comparison -- **[Adventurer 3 Series](../../wiki/Adventurer-3-Series)** — Legacy TCP protocol, 37 commands, 4 variant comparison +## Accesso dashboard -### Advanced +- **Ingress Home Assistant**: da sidebar (come prima) +- **HTTP diretto**: `http://:8099` -- **[AD5X Root Access](../../wiki/AD5X-Root-Access)** — Root SSH/ADB via USB firmware update mechanism -- **[AD5X Maintenance Console](../../wiki/AD5X-Maintenance-Console)** — Hidden touchscreen debug/calibration UI -- **[5M Maintenance Console](../../wiki/Adventurer-5M-Maintenance-Console)** — Hidden maintenance UI for 5M/5M Pro -- **[AD5X IFS Serial Protocol](../../wiki/AD5X-IFS-Serial-Protocol)** — Raw UART protocol for the Intelligent Filament Station -- **[AD5X Platform Notes](../../wiki/AD5X-Platform-Notes)** — Ingenic X2600 MIPS32 SoC, hardware, kernel, filesystem +## Integrazione camera con Frigate -## Endpoint YAML Files +Per visualizzare il feed della camera della stampante nel dashboard è possibile usare [Frigate](https://frigate.video/) con [go2rtc](https://github.com/AlexxIT/go2rtc). -Machine-readable endpoint specifications are available in the [`endpoints/`](endpoints/) directory: +### Configurazione Frigate -| File | Description | -|------|-------------| -| `endpoints_5m_3.2.7.yaml` | Adventurer 5M / 5M Pro HTTP endpoints (firmware 3.2.7) | -| `endpoints_ad5x_1.1.7.yaml` | AD5X HTTP endpoints (firmware 1.1.7) | -| `endpoints_ad5x_1.2.1.yaml` | AD5X HTTP endpoints (firmware 1.2.1) | -| `networkserver_commands_adventurer3.yaml` | Adventurer 3 TCP commands | -| `networkserver_commands_adventurer4.yaml` | Adventurer 4 TCP commands | +```yaml +go2rtc: + streams: + Stampante: + - "ffmpeg:http://IP_DELLA_STAMPANTE:8080/?action=stream" -## Methodology +cameras: + Stampante: + ffmpeg: + inputs: + - path: rtsp://127.0.0.1:8554/Stampante +``` -This documentation is compiled from: +Dopo aver configurato Frigate, Home Assistant esporrà automaticamente un'entità camera (es. `camera.stampante`). +Inserisci il nome dell'entità nella configurazione dell'add-on nel campo **`camera_entity`** (es. `camera.stampante`). -- Firmware analysis and filesystem inspection -- Network traffic capture and protocol analysis -- Community testing across multiple printer models and firmware versions +## Integrazione automatica Home Assistant (MQTT) -## Contributing +L’add-on pubblica automaticamente sensori/switch via **MQTT Discovery**. -Contributions are welcome via pull requests. When submitting: +Prerequisiti: +- integrazione MQTT configurata in Home Assistant +- broker MQTT raggiungibile dall’add-on (default: `core-mosquitto`) -- Specify the printer model and firmware version tested -- Include methodology (traffic capture, binary analysis, live testing) -- Reference specific protocol details or packet captures where possible +Opzioni configurabili nell’add-on: +- `mqtt_enabled` (default `true`) +- `mqtt_host` (default `core-mosquitto`) +- `mqtt_port` (default `1883`) +- `mqtt_username` / `mqtt_password` (opzionali) +- `mqtt_base_topic` (default `flashforge`) ---- - -*This is an unofficial community project and is not affiliated with FlashForge.* +Entità principali esposte: +- sensori: stato, progress, temperature, tempo stimato +- binary sensor: stampa in corso +- switch: pausa/riprendi, camera stream +- button: stop stampa, clear stato stampante diff --git a/endpoints/endpoints_5m_3.2.7.yaml b/endpoints/endpoints_5m_3.2.7.yaml deleted file mode 100644 index 8e4bc4f..0000000 --- a/endpoints/endpoints_5m_3.2.7.yaml +++ /dev/null @@ -1,821 +0,0 @@ -paths: - /checkCode: - post: - description: >- - Validate serialNumber + checkCode for LAN access. Returns code/message. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /detail: - post: - description: >- - Gets info on the current status and config of the printer. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DetailResponse' - /product: - post: - description: >- - Returns product capability/control state flags. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/ProductResponse' - /control: - post: - description: >- - Send a command to the printer. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - payload - properties: - serialNumber: - type: string - checkCode: - type: string - payload: - oneOf: - - $ref: "#/components/schemas/LightControlCmd" - - $ref: "#/components/schemas/CirculateCtlCmd" - - $ref: "#/components/schemas/TemperatureCtlCmd" - - $ref: "#/components/schemas/PrinterCtlCmd" - - $ref: "#/components/schemas/JobCtlCmd" - - $ref: "#/components/schemas/DelayCloseCmd" - - $ref: "#/components/schemas/ReNameCmd" - - $ref: "#/components/schemas/CalibrationCmd" - - $ref: "#/components/schemas/StreamCtrlCmd" - - $ref: "#/components/schemas/UserProfileCmd" - - $ref: "#/components/schemas/StateCtrlCmd" - - $ref: "#/components/schemas/DeviceUnregisterCmd" - - $ref: "#/components/schemas/DeviceUpdateDetailCmd" - - $ref: "#/components/schemas/NewJobCmd" - - $ref: "#/components/schemas/NewLocalJobCmd" - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /gcodeList: - post: - description: >- - Returns a list of printable files from /data (extensions: .g, .gx, .gcode, .3mf). - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GcodeListResponse' - /gcodeThumb: - post: - description: >- - Returns a base64 thumbnail for a given fileName. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - fileName - properties: - serialNumber: - type: string - checkCode: - type: string - fileName: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GcodeThumbResponse' - /printGcode: - post: - description: >- - Start printing a file from /data. Returns code/message. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - fileName - - levelingBeforePrint - properties: - serialNumber: - type: string - checkCode: - type: string - fileName: - type: string - levelingBeforePrint: - type: boolean - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /getThum: - get: - description: >- - Returns current print thumbnail as image/bmp. On error returns JSON code/message. - responses: - '200': - content: - image/bmp: - schema: - type: string - format: binary - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /notifyWanBind: - post: - description: >- - Notify WAN bind. Validates serialNumber and triggers a bind event on success. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - properties: - serialNumber: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /uploadGcode: - post: - description: >- - Upload a gcode/3mf file to local storage via multipart/form-data. - Requires headers for serialNumber/checkCode/fileSize/printNow/levelingBeforePrint. - parameters: - - in: header - name: serialNumber - required: true - schema: - type: string - - in: header - name: checkCode - required: true - schema: - type: string - - in: header - name: fileSize - required: true - schema: - type: integer - description: File size in bytes - - in: header - name: printNow - required: true - schema: - type: string - description: String boolean, typically "0" or "1" - - in: header - name: levelingBeforePrint - required: true - schema: - type: string - description: String boolean, typically "0" or "1" - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - required: - - gcodeFile - properties: - gcodeFile: - type: string - format: binary - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' -components: - schemas: - DetailResponse: - type: object - properties: - code: - type: integer - message: - type: string - detail: - $ref: '#/components/schemas/Details' - IsOpenEnum: - type: string - enum: - - open - - close - CodeMessageResponse: - type: object - properties: - code: - type: integer - message: - type: string - ProductResponse: - type: object - properties: - code: - type: integer - message: - type: string - product: - type: object - properties: - nozzleTempCtrlState: - type: integer - chamberTempCtrlState: - type: integer - platformTempCtrlState: - type: integer - lightCtrlState: - type: integer - internalFanCtrlState: - type: integer - externalFanCtrlState: - type: integer - GcodeListResponse: - type: object - properties: - code: - type: integer - message: - type: string - gcodeList: - type: array - items: - type: string - GcodeThumbResponse: - type: object - properties: - code: - type: integer - message: - type: string - imageData: - type: string - description: Base64-encoded thumbnail image - Details: - type: object - description: Verified in 3.2.7 (request_detail). - properties: - autoShutdown: - $ref: "#/components/schemas/IsOpenEnum" - autoShutdownTime: - type: integer - cameraStreamUrl: - type: string - description: Empty if camera disabled; otherwise http://:8080/?action=stream. - chamberFanSpeed: - type: integer - chamberTargetTemp: - type: number - chamberTemp: - type: number - coolingFanSpeed: - type: integer - cumulativeFilament: - type: number - cumulativePrintTime: - type: integer - currentPrintSpeed: - type: integer - doorStatus: - $ref: "#/components/schemas/IsOpenEnum" - errorCode: - type: string - estimatedLeftLen: - type: number - estimatedLeftWeight: - type: number - estimatedRightLen: - type: number - estimatedRightWeight: - type: number - estimatedTime: - type: number - externalFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - fillAmount: - type: integer - firmwareVersion: - type: string - flashRegisterCode: - type: string - internalFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - ipAddr: - type: string - leftFilamentType: - type: string - leftTargetTemp: - type: number - leftTemp: - type: number - lightStatus: - $ref: "#/components/schemas/IsOpenEnum" - location: - type: string - macAddr: - type: string - measure: - type: string - name: - type: string - nozzleCnt: - type: integer - nozzleModel: - type: string - nozzleStyle: - type: integer - pid: - type: integer - platTargetTemp: - type: number - platTemp: - type: number - polarRegisterCode: - type: string - printDuration: - type: integer - printFileName: - type: string - printFileThumbUrl: - type: string - description: Empty if no thumb; otherwise http://:8898/getThum. Base file dir is /data/. - printLayer: - type: integer - printProgress: - type: number - printSpeedAdjust: - type: number - remainingDiskSpace: - type: number - rightFilamentType: - type: string - rightTargetTemp: - type: number - rightTemp: - type: number - status: - type: string - targetPrintLayer: - type: integer - tvoc: - type: integer - zAxisCompensation: - type: number - JobInfo: - type: array - description: | - Array of job tuples. Structure: `[[deviceSN, jobID, filepath], ...]`. - Firmware iterates this array looking for a `deviceSN` (index 0) match with the local printer. - items: - type: array - items: - type: string - NewJobCmd: - type: object - description: Start a cloud/multi-device job. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [newJob_cmd] - args: - type: object - required: - - jobInfo - properties: - jobRouter: - type: boolean - description: | - **Presence Flag**: The value (true/false) is ignored. If this key is *present* in the JSON, - the firmware extracts the filepath from `jobInfo` tuple index 2. - If *absent*, it uses the root `filepath` property. - jobInfo: - type: array - items: - - $ref: "#/components/schemas/JobInfo" - filepath: - type: string - description: only read if jobRouter is not declared - filename: - type: string - thumbPath: - type: string - fileMd5: - type: string - printNow: - type: boolean - leveling: - type: boolean - LightControlCmd: - type: object - description: set light states - required: - - cmd - - args - properties: - cmd: - type: string - enum: [lightControl_cmd] - args: - type: object - required: - - status - properties: - status: - type: string - CirculateCtlCmd: - type: object - description: set fan states - required: - - cmd - - args - properties: - cmd: - type: string - enum: [circulateCtl_cmd] - args: - type: object - required: - - internal - - external - properties: - internal: - type: string - external: - type: string - TemperatureCtlCmd: - type: object - description: Set nozzle, bed, and chamber temperatures - required: - - cmd - - args - properties: - cmd: - type: string - enum: [temperatureCtl_cmd] - args: - type: object - properties: - platform: - type: integer - default: -200 - description: Bed temperature (°C). -100 = off (0), -200 = no change. - rightNozzle: - type: integer - default: -200 - description: Right/main nozzle temperature (°C). -100 = off (0), -200 = no change. - leftNozzle: - type: integer - default: -200 - description: Left nozzle temperature (°C). -100 = off (0), -200 = no change. - chamber: - type: integer - default: -200 - description: Chamber temperature (°C). -100 = off (0), -200 = no change. - PrinterCtlCmd: - type: object - description: | - Set speeds, fans, and z-axis compensation. - **Partial Update Behavior:** This command supports partial updates. Fields omitted from the payload - are ignored by the firmware (using internal sentinel values, e.g., -200). - **Important:** Do NOT send default values (like 0) for fields you do not intend to change, - as this will overwrite the user's current settings. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [printerCtl_cmd] - args: - type: object - properties: - zAxisCompensation: - type: number - default: -200.0 - description: | - Z-offset in mm. Maps to `SET_GCODE_OFFSET Z= MOVE=1`. - Range: -5.0 to +5.0. Values outside this range are ignored. - Sentinel: -200.0 (ignore). - speed: - type: integer - default: -200 - description: | - Print speed percentage. Maps to `M220 S`. - Clamped range: 50-150. - Sentinel: -200 (ignore). - chamberFan: - type: integer - default: -200 - description: Chamber fan speed (0-255, 0=off). Sentinel -200 (ignore). - coolingFan: - type: integer - default: -200 - description: Cooling fan speed (0-255, 0=off). Sentinel -200 (ignore). - JobCtlCmd: - type: object - description: Control print jobs (pause, resume, cancel) - required: - - cmd - - args - properties: - cmd: - type: string - enum: [jobCtl_cmd] - args: - type: object - required: - - jobID - - action - properties: - jobID: - type: string - description: Job identifier - action: - type: string - description: Control action - enum: [pause, continue, resume, cancel, stop] - DelayCloseCmd: - type: object - description: configure automatic power-off - required: - - cmd - - args - properties: - cmd: - type: string - enum: [delayClose_cmd] - args: - type: object - required: - - automaticShutdown - - shutdownAfterTime - properties: - automaticShutdown: - type: string - shutdownAfterTime: - type: integer - ReNameCmd: - type: object - description: give your printer a nickname - required: - - cmd - - args - properties: - cmd: - type: string - enum: [reName_cmd] - args: - type: object - required: - - name - properties: - name: - type: string - CalibrationCmd: - type: object - description: configure leveling and vibration compensation - required: - - cmd - - args - properties: - cmd: - type: string - enum: [calibration_cmd] - args: - type: object - required: - - levelingDetection - - vibrationCompensation - properties: - levelingDetection: - type: string - vibrationCompensation: - type: string - StreamCtrlCmd: - type: object - description: control the camera state - required: - - cmd - - args - properties: - cmd: - type: string - enum: [streamCtrl_cmd] - args: - type: object - required: - - action - properties: - action: - type: string - UserProfileCmd: - type: object - description: best guess... cloud account management - required: - - cmd - - args - properties: - cmd: - type: string - enum: [userProfile_cmd] - args: - type: object - required: - - name - - avatar - properties: - name: - type: string - avatar: - type: string - DeviceUnregisterCmd: - type: object - description: best guess... disables cloud account - required: - - cmd - properties: - cmd: - type: string - enum: [deviceUnregister_cmd] - StateCtrlCmd: - type: object - description: control the printer state - required: - - cmd - - args - properties: - cmd: - type: string - enum: [stateCtrl_cmd] - args: - type: object - required: - - action - properties: - action: - type: string - DeviceUpdateDetailCmd: - type: object - description: | - Check for firmware updates from FlashForge update server. - Despite the name, this command does NOT update local device details. - It queries the remote update server for firmware version information. - - This command will: - - Query all 4 firmware packages (kernel, control, library, software) - - Check for available updates on update.flashforge.com - - Retrieve version information, download URLs, and changelogs - - Return update availability without installing anything - - This command will NOT: - - Modify any local printer state - - Install firmware updates - - Update device information cache - required: - - cmd - - args - properties: - cmd: - type: string - enum: [deviceUpdateDetail_cmd] - args: - type: object - description: Empty args object (no parameters required) - NewLocalJobCmd: - type: object - description: submit a new job to the print - required: - - cmd - - args - properties: - cmd: - type: string - enum: [newLocalJob_cmd] - args: - type: object - required: - - jobId - - fileName - - printNow - - leveling - properties: - jobId: - type: string - fileName: - type: string - thumbPath: - type: string - printNow: - type: boolean - description: Start printing immediately - leveling: - type: boolean - description: Perform bed leveling before print - flowCalibration: - type: boolean - description: Perform flow calibration (AD5X only) - useMs: - type: boolean - description: Use material station (AD5X only) - tCount: - type: integer - description: Material/tool count (AD5X only) diff --git a/endpoints/endpoints_ad5x_1.1.7.yaml b/endpoints/endpoints_ad5x_1.1.7.yaml deleted file mode 100644 index 709b085..0000000 --- a/endpoints/endpoints_ad5x_1.1.7.yaml +++ /dev/null @@ -1,1060 +0,0 @@ -paths: - /checkCode: - post: - description: >- - Validate serialNumber + checkCode for LAN access. Returns code/message. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /detail: - post: - description: >- - Gets info on the current status and config of the printer. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DetailResponse' - /product: - post: - description: >- - Returns product capability/control state flags. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/ProductResponse' - /control: - post: - description: >- - Send a command to the printer. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - payload - properties: - serialNumber: - type: string - checkCode: - type: string - payload: - oneOf: - - $ref: "#/components/schemas/LightControlCmd" - - $ref: "#/components/schemas/CirculateCtlCmd" - - $ref: "#/components/schemas/TemperatureCtlCmd" - - $ref: "#/components/schemas/PrinterCtlCmd" - - $ref: "#/components/schemas/JobCtlCmd" - - $ref: "#/components/schemas/DelayCloseCmd" - - $ref: "#/components/schemas/ReNameCmd" - - $ref: "#/components/schemas/CalibrationCmd" - - $ref: "#/components/schemas/StreamCtrlCmd" - - $ref: "#/components/schemas/UserProfileCmd" - - $ref: "#/components/schemas/MsConfigCmd" - - $ref: "#/components/schemas/IpdMsConfigCmd" - - $ref: "#/components/schemas/MsCmd" - - $ref: "#/components/schemas/IpdMsCmd" - - $ref: "#/components/schemas/MoveCtrlCmd" - - $ref: "#/components/schemas/ExtrudeCtrlCmd" - - $ref: "#/components/schemas/HomingCtrlCmd" - - $ref: "#/components/schemas/ErrorCodeCtrlCmd" - - $ref: "#/components/schemas/StateCtrlCmd" - - $ref: "#/components/schemas/DeviceUnregisterCmd" - - $ref: "#/components/schemas/DeviceUpdateDetailCmd" - - $ref: "#/components/schemas/NewJobCmd" - - $ref: "#/components/schemas/NewLocalJobCmd" - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /gcodeList: - post: - description: >- - Returns a list of printable files from /data (extensions: .g, .gx, .gcode, .3mf). - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GcodeListResponse' - /gcodeThumb: - post: - description: >- - Returns a base64 thumbnail for a given fileName. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - fileName - properties: - serialNumber: - type: string - checkCode: - type: string - fileName: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GcodeThumbResponse' - /printGcode: - post: - description: >- - Start printing a file from /data. Returns code/message. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - fileName - - levelingBeforePrint - properties: - serialNumber: - type: string - checkCode: - type: string - fileName: - type: string - levelingBeforePrint: - type: boolean - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /getThum: - get: - description: >- - Returns current print thumbnail as image/bmp. On error returns JSON code/message. - responses: - '200': - content: - image/bmp: - schema: - type: string - format: binary - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /notifyWanBind: - post: - description: >- - Notify WAN bind. Validates serialNumber and triggers a bind event on success. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - properties: - serialNumber: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /uploadGcode: - post: - description: >- - Upload a gcode/3mf file to local storage via multipart/form-data. - Requires headers for serialNumber/checkCode/fileSize/printNow/levelingBeforePrint, plus AD5X - material-station options (flowCalibration/firstLayerInspection/timeLapseVideo/useMatlStation/gcodeToolCnt/materialMappings). - parameters: - - in: header - name: serialNumber - required: true - schema: - type: string - - in: header - name: checkCode - required: true - schema: - type: string - - in: header - name: fileSize - required: true - schema: - type: integer - description: File size in bytes - - in: header - name: printNow - required: true - schema: - type: string - description: String boolean, typically "0" or "1" - - in: header - name: levelingBeforePrint - required: true - schema: - type: string - description: String boolean, typically "0" or "1" - - in: header - name: flowCalibration - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: firstLayerInspection - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: timeLapseVideo - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: useMatlStation - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: gcodeToolCnt - required: false - schema: - type: integer - description: Number of tool channels in gcode (AD5X only, max 4) - - in: header - name: materialMappings - required: false - schema: - type: string - description: Base64-encoded JSON array of {toolId, slotId} objects (AD5X only, used when useMatlStation=1) - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - required: - - gcodeFile - properties: - gcodeFile: - type: string - format: binary - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' -components: - schemas: - DetailResponse: - type: object - properties: - code: - type: integer - message: - type: string - detail: - $ref: '#/components/schemas/Details' - IsOpenEnum: - type: string - enum: - - open - - close - CodeMessageResponse: - type: object - properties: - code: - type: integer - message: - type: string - ProductResponse: - type: object - properties: - code: - type: integer - message: - type: string - product: - type: object - properties: - nozzleTempCtrlState: - type: integer - chamberTempCtrlState: - type: integer - platformTempCtrlState: - type: integer - lightCtrlState: - type: integer - internalFanCtrlState: - type: integer - externalFanCtrlState: - type: integer - GcodeListResponse: - type: object - properties: - code: - type: integer - message: - type: string - gcodeList: - type: array - items: - type: string - GcodeThumbResponse: - type: object - properties: - code: - type: integer - message: - type: string - imageData: - type: string - description: Base64-encoded thumbnail image - Details: - type: object - description: Verified in AD5X 1.1.7 (request_detail), based on 3.2.7 + AD5X additions. - properties: - autoShutdown: - $ref: "#/components/schemas/IsOpenEnum" - autoShutdownTime: - type: integer - cameraStreamUrl: - type: string - description: Empty if camera disabled; otherwise http://:8080/?action=stream. - chamberFanSpeed: - type: integer - chamberTargetTemp: - type: number - chamberTemp: - type: number - coolingFanSpeed: - type: integer - coolingFanLeftSpeed: - type: integer - description: AD5X only. Left cooling fan speed (0 when idle). - clearFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - description: AD5X only. Always "open" in request_detail. - coordinate: - type: array - items: - type: number - description: AD5X only. XYZ coordinate array. - camera: - type: integer - description: AD5X only. Camera availability flag (1/2). - moveCtrl: - type: integer - description: AD5X only. Always 1. - extrudeCtrl: - type: integer - description: AD5X only. Always 1. - cumulativeFilament: - type: number - cumulativePrintTime: - type: integer - currentPrintSpeed: - type: integer - doorStatus: - $ref: "#/components/schemas/IsOpenEnum" - errorCode: - type: string - estimatedLeftLen: - type: number - estimatedLeftWeight: - type: number - estimatedRightLen: - type: number - estimatedRightWeight: - type: number - estimatedTime: - type: number - externalFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - fillAmount: - type: integer - firmwareVersion: - type: string - flashRegisterCode: - type: string - internalFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - ipAddr: - type: string - leftFilamentType: - type: string - hasMatlStation: - type: integer - description: AD5X only. 0/1 flag for material station presence. - matlStationInfo: - $ref: "#/components/schemas/MatlStationInfo" - indepMatlInfo: - $ref: "#/components/schemas/IndepMatlInfo" - leftTargetTemp: - type: number - leftTemp: - type: number - lightStatus: - $ref: "#/components/schemas/IsOpenEnum" - location: - type: string - macAddr: - type: string - measure: - type: string - name: - type: string - nozzleCnt: - type: integer - nozzleModel: - type: string - nozzleStyle: - type: integer - pid: - type: integer - description: AD5X uses "0026" -> 26. - platTargetTemp: - type: number - platTemp: - type: number - polarRegisterCode: - type: string - printDuration: - type: integer - printFileName: - type: string - printFileThumbUrl: - type: string - description: Empty if no thumb; otherwise http://:8898/getThum. Base file dir is /data/. - printLayer: - type: integer - printProgress: - type: number - printSpeedAdjust: - type: number - remainingDiskSpace: - type: number - rightFilamentType: - type: string - rightTargetTemp: - type: number - rightTemp: - type: number - status: - type: string - targetPrintLayer: - type: integer - tvoc: - type: integer - zAxisCompensation: - type: number - MatlStationInfo: - type: object - description: AD5X material station summary. - properties: - slotCnt: - type: integer - description: Always 4. - currentSlot: - type: integer - description: Current slot id (from SerialObject material operation field +0x344). - currentLoadSlot: - type: integer - description: Current load/unload slot id (from SerialObject material operation field +0x348). - stateAction: - type: integer - description: >- - Material operation stage. Observed values: 0=idle/cancel, 2=load step 2, 3=load step 3, - 4=unload step 2, 5=unload step 3, 6=complete. - stateStep: - type: integer - description: >- - Material operation mode. Observed values: 0=idle, 1=load, 2=unload, 3=cancel. - slotInfos: - type: array - items: - $ref: "#/components/schemas/MatlSlotInfo" - MatlSlotInfo: - type: object - description: AD5X material station slot info. - properties: - slotId: - type: integer - hasFilament: - type: boolean - materialName: - type: string - materialColor: - type: string - description: Color string before first ';' in config. - IndepMatlInfo: - type: object - description: AD5X independent material info. - properties: - stateAction: - type: integer - description: >- - Material operation stage. Observed values: 0=idle/cancel, 2=load step 2, 3=load step 3, - 4=unload step 2, 5=unload step 3, 6=complete. - stateStep: - type: integer - description: >- - Material operation mode. Observed values: 0=idle, 1=load, 2=unload, 3=cancel. - materialName: - type: string - materialColor: - type: string - description: Color string before first ';' in config. - JobInfo: - type: array - description: | - Array of job tuples. Structure: `[[deviceSN, jobID, filepath], ...]`. - Firmware iterates this array looking for a `deviceSN` (index 0) match with the local printer. - items: - type: array - items: - type: string - NewJobCmd: - type: object - description: Start a cloud/multi-device job. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [newJob_cmd] - args: - type: object - required: - - jobInfo - properties: - jobRouter: - type: boolean - description: | - **Presence Flag**: The value (true/false) is ignored. If this key is *present* in the JSON, - the firmware extracts the filepath from `jobInfo` tuple index 2. - If *absent*, it uses the root `filepath` property. - jobInfo: - type: array - items: - - $ref: "#/components/schemas/JobInfo" - filepath: - type: string - description: only read if jobRouter is not declared - filename: - type: string - thumbPath: - type: string - fileMd5: - type: string - printNow: - type: boolean - leveling: - type: boolean - PrinterCtlCmd: - type: object - description: | - Set speeds, fans, and z-axis compensation. - **Partial Update Behavior:** This command supports partial updates. Fields omitted from the payload - are ignored by the firmware (using internal sentinel values, e.g., -200). - **Important:** Do NOT send default values (like 0) for fields you do not intend to change, - as this will overwrite the user's current settings. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [printerCtl_cmd] - args: - type: object - properties: - zAxisCompensation: - type: number - default: -200.0 - description: | - Z-offset in mm. Maps to `SET_GCODE_OFFSET Z= MOVE=1`. - Range: -5.0 to +5.0. Values outside this range are ignored. - Sentinel: -200.0 (ignore). - speed: - type: integer - default: -200 - description: | - Print speed percentage. Maps to `M220 S`. - Clamped range: 50-150. - Sentinel: -200 (ignore). - chamberFan: - type: integer - default: -200 - description: Chamber fan speed (0-255, 0=off). Sentinel -200 (ignore). - coolingFan: - type: integer - default: -200 - description: Cooling fan speed (0-255, 0=off). Sentinel -200 (ignore). - JobCtlCmd: - type: object - description: Control print jobs (pause, resume, cancel) - required: - - cmd - - args - properties: - cmd: - type: string - enum: [jobCtl_cmd] - args: - type: object - required: - - jobID - - action - properties: - jobID: - type: string - description: Job identifier - action: - type: string - description: Control action - enum: [pause, continue, resume, cancel, stop] - DelayCloseCmd: - type: object - description: configure automatic power-off - required: - - cmd - - args - properties: - cmd: - type: string - enum: [delayClose_cmd] - args: - type: object - required: - - automaticShutdown - - shutdownAfterTime - properties: - automaticShutdown: - type: string - shutdownAfterTime: - type: integer - ReNameCmd: - type: object - description: give your printer a nickname - required: - - cmd - - args - properties: - cmd: - type: string - enum: [reName_cmd] - args: - type: object - required: - - name - properties: - name: - type: string - CalibrationCmd: - type: object - description: configure leveling and vibration compensation - required: - - cmd - - args - properties: - cmd: - type: string - enum: [calibration_cmd] - args: - type: object - required: - - levelingDetection - - vibrationCompensation - properties: - levelingDetection: - type: string - vibrationCompensation: - type: string - StreamCtrlCmd: - type: object - description: control the camera state - required: - - cmd - - args - properties: - cmd: - type: string - enum: [streamCtrl_cmd] - args: - type: object - required: - - action - properties: - action: - type: string - UserProfileCmd: - type: object - description: best guess... cloud account management - required: - - cmd - - args - properties: - cmd: - type: string - enum: [userProfile_cmd] - args: - type: object - required: - - name - - avatar - properties: - name: - type: string - avatar: - type: string - MsConfigCmd: - type: object - description: | - AD5X only. Configure material station slot metadata. - **Validation:** Firmware performs NO validation on strings. - **Palette:** UI recognizes 24 specific hex codes (e.g., #FFFFFF, #FEF043, #161616). - **Types:** UI supports PLA, PLA-CF, ABS, PETG, PETG-CF, TPU, SILK. Hidden support for PA-CF, PC, etc. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [msConfig_cmd] - args: - type: object - required: - - slot - - mt - - rgb - properties: - slot: - type: integer - description: Material station slot (1-4). - mt: - type: string - description: Material name/type string (e.g. "PLA"). Arbitrary strings accepted. - rgb: - type: string - description: Material color string (e.g. "#FF0000"). Arbitrary strings accepted. - IpdMsConfigCmd: - type: object - description: | - AD5X only. Configure independent material metadata. - **Validation:** Firmware performs NO validation on strings. - **Palette:** UI recognizes 24 specific hex codes. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [ipdMsConfig_cmd] - args: - type: object - required: - - mt - - rgb - properties: - mt: - type: string - description: Material name/type string (e.g. "PLA"). Arbitrary strings accepted. - rgb: - type: string - description: Material color string (e.g. "#FF0000"). Arbitrary strings accepted. - MsCmd: - type: object - description: AD5X only. Material station command. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [ms_cmd] - args: - type: object - required: - - action - - slot - properties: - action: - type: integer - enum: [0, 1, 2] - description: 0=load, 1=unload, 2=cancel. - slot: - type: integer - IpdMsCmd: - type: object - description: AD5X only. Independent material station command. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [ipdMs_cmd] - args: - type: object - required: - - action - properties: - action: - type: integer - enum: [0, 1, 2] - description: 0=load, 1=unload, 2=cancel. - MoveCtrlCmd: - type: object - description: AD5X only. Manual axis move. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [moveCtrl_cmd] - args: - type: object - required: - - axis - - delta - properties: - axis: - type: string - enum: [x, y, z] - description: Axis identifier (lowercase). - delta: - type: number - description: Move distance (float). - ExtrudeCtrlCmd: - type: object - description: AD5X only. Manual extruder move. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [extrudeCtrl_cmd] - args: - type: object - required: - - axis - - delta - properties: - axis: - type: string - description: Axis identifier (ignored by handler). - delta: - type: number - description: Extrude/retract distance (float). - HomingCtrlCmd: - type: object - description: AD5X only. Home all axes. - required: - - cmd - properties: - cmd: - type: string - enum: [homingCtrl_cmd] - args: - type: object - description: Optional/unused by firmware. - ErrorCodeCtrlCmd: - type: object - description: AD5X only. Clear printer error code. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [errorCodeCtrl_cmd] - args: - type: object - required: - - action - - errorCode - properties: - action: - type: string - enum: [clearErrorCode] - description: Must be clearErrorCode; handler clears only if errorCode matches current error. - errorCode: - type: string - DeviceUnregisterCmd: - type: object - description: best guess... disables cloud account - required: - - cmd - properties: - cmd: - type: string - enum: [deviceUnregister_cmd] - StateCtrlCmd: - type: object - description: control the printer state - required: - - cmd - - args - properties: - cmd: - type: string - enum: [stateCtrl_cmd] - args: - type: object - required: - - action - properties: - action: - type: string - DeviceUpdateDetailCmd: - type: object - description: | - Check for firmware updates from FlashForge update server. - Despite the name, this command does NOT update local device details. - It queries the remote update server for firmware version information. - - This command will: - - Query all 4 firmware packages (kernel, control, library, software) - - Check for available updates on update.flashforge.com - - Retrieve version information, download URLs, and changelogs - - Return update availability without installing anything - - This command will NOT: - - Modify any local printer state - - Install firmware updates - - Update device information cache - required: - - cmd - - args - properties: - cmd: - type: string - enum: [deviceUpdateDetail_cmd] - args: - type: object - description: Empty args object (no parameters required) - NewLocalJobCmd: - type: object - description: submit a new job to the print - required: - - cmd - - args - properties: - cmd: - type: string - enum: [newLocalJob_cmd] - args: - type: object - required: - - jobId - - fileName - - printNow - - leveling - properties: - jobId: - type: string - fileName: - type: string - thumbPath: - type: string - printNow: - type: boolean - description: Start printing immediately - leveling: - type: boolean - description: Perform bed leveling before print - flowCalibration: - type: boolean - description: Perform flow calibration (AD5X only) - useMs: - type: boolean - description: Use material station (AD5X only) - tCount: - type: integer - description: Material/tool count (AD5X only) diff --git a/endpoints/endpoints_ad5x_1.2.1.yaml b/endpoints/endpoints_ad5x_1.2.1.yaml deleted file mode 100644 index 709b085..0000000 --- a/endpoints/endpoints_ad5x_1.2.1.yaml +++ /dev/null @@ -1,1060 +0,0 @@ -paths: - /checkCode: - post: - description: >- - Validate serialNumber + checkCode for LAN access. Returns code/message. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /detail: - post: - description: >- - Gets info on the current status and config of the printer. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/DetailResponse' - /product: - post: - description: >- - Returns product capability/control state flags. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/ProductResponse' - /control: - post: - description: >- - Send a command to the printer. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - payload - properties: - serialNumber: - type: string - checkCode: - type: string - payload: - oneOf: - - $ref: "#/components/schemas/LightControlCmd" - - $ref: "#/components/schemas/CirculateCtlCmd" - - $ref: "#/components/schemas/TemperatureCtlCmd" - - $ref: "#/components/schemas/PrinterCtlCmd" - - $ref: "#/components/schemas/JobCtlCmd" - - $ref: "#/components/schemas/DelayCloseCmd" - - $ref: "#/components/schemas/ReNameCmd" - - $ref: "#/components/schemas/CalibrationCmd" - - $ref: "#/components/schemas/StreamCtrlCmd" - - $ref: "#/components/schemas/UserProfileCmd" - - $ref: "#/components/schemas/MsConfigCmd" - - $ref: "#/components/schemas/IpdMsConfigCmd" - - $ref: "#/components/schemas/MsCmd" - - $ref: "#/components/schemas/IpdMsCmd" - - $ref: "#/components/schemas/MoveCtrlCmd" - - $ref: "#/components/schemas/ExtrudeCtrlCmd" - - $ref: "#/components/schemas/HomingCtrlCmd" - - $ref: "#/components/schemas/ErrorCodeCtrlCmd" - - $ref: "#/components/schemas/StateCtrlCmd" - - $ref: "#/components/schemas/DeviceUnregisterCmd" - - $ref: "#/components/schemas/DeviceUpdateDetailCmd" - - $ref: "#/components/schemas/NewJobCmd" - - $ref: "#/components/schemas/NewLocalJobCmd" - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /gcodeList: - post: - description: >- - Returns a list of printable files from /data (extensions: .g, .gx, .gcode, .3mf). - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - properties: - serialNumber: - type: string - checkCode: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GcodeListResponse' - /gcodeThumb: - post: - description: >- - Returns a base64 thumbnail for a given fileName. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - fileName - properties: - serialNumber: - type: string - checkCode: - type: string - fileName: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/GcodeThumbResponse' - /printGcode: - post: - description: >- - Start printing a file from /data. Returns code/message. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - - checkCode - - fileName - - levelingBeforePrint - properties: - serialNumber: - type: string - checkCode: - type: string - fileName: - type: string - levelingBeforePrint: - type: boolean - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /getThum: - get: - description: >- - Returns current print thumbnail as image/bmp. On error returns JSON code/message. - responses: - '200': - content: - image/bmp: - schema: - type: string - format: binary - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /notifyWanBind: - post: - description: >- - Notify WAN bind. Validates serialNumber and triggers a bind event on success. - requestBody: - required: true - content: - application/json: - schema: - type: object - required: - - serialNumber - properties: - serialNumber: - type: string - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' - /uploadGcode: - post: - description: >- - Upload a gcode/3mf file to local storage via multipart/form-data. - Requires headers for serialNumber/checkCode/fileSize/printNow/levelingBeforePrint, plus AD5X - material-station options (flowCalibration/firstLayerInspection/timeLapseVideo/useMatlStation/gcodeToolCnt/materialMappings). - parameters: - - in: header - name: serialNumber - required: true - schema: - type: string - - in: header - name: checkCode - required: true - schema: - type: string - - in: header - name: fileSize - required: true - schema: - type: integer - description: File size in bytes - - in: header - name: printNow - required: true - schema: - type: string - description: String boolean, typically "0" or "1" - - in: header - name: levelingBeforePrint - required: true - schema: - type: string - description: String boolean, typically "0" or "1" - - in: header - name: flowCalibration - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: firstLayerInspection - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: timeLapseVideo - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: useMatlStation - required: false - schema: - type: string - description: String boolean, typically "0" or "1" (AD5X only) - - in: header - name: gcodeToolCnt - required: false - schema: - type: integer - description: Number of tool channels in gcode (AD5X only, max 4) - - in: header - name: materialMappings - required: false - schema: - type: string - description: Base64-encoded JSON array of {toolId, slotId} objects (AD5X only, used when useMatlStation=1) - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - required: - - gcodeFile - properties: - gcodeFile: - type: string - format: binary - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/CodeMessageResponse' -components: - schemas: - DetailResponse: - type: object - properties: - code: - type: integer - message: - type: string - detail: - $ref: '#/components/schemas/Details' - IsOpenEnum: - type: string - enum: - - open - - close - CodeMessageResponse: - type: object - properties: - code: - type: integer - message: - type: string - ProductResponse: - type: object - properties: - code: - type: integer - message: - type: string - product: - type: object - properties: - nozzleTempCtrlState: - type: integer - chamberTempCtrlState: - type: integer - platformTempCtrlState: - type: integer - lightCtrlState: - type: integer - internalFanCtrlState: - type: integer - externalFanCtrlState: - type: integer - GcodeListResponse: - type: object - properties: - code: - type: integer - message: - type: string - gcodeList: - type: array - items: - type: string - GcodeThumbResponse: - type: object - properties: - code: - type: integer - message: - type: string - imageData: - type: string - description: Base64-encoded thumbnail image - Details: - type: object - description: Verified in AD5X 1.1.7 (request_detail), based on 3.2.7 + AD5X additions. - properties: - autoShutdown: - $ref: "#/components/schemas/IsOpenEnum" - autoShutdownTime: - type: integer - cameraStreamUrl: - type: string - description: Empty if camera disabled; otherwise http://:8080/?action=stream. - chamberFanSpeed: - type: integer - chamberTargetTemp: - type: number - chamberTemp: - type: number - coolingFanSpeed: - type: integer - coolingFanLeftSpeed: - type: integer - description: AD5X only. Left cooling fan speed (0 when idle). - clearFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - description: AD5X only. Always "open" in request_detail. - coordinate: - type: array - items: - type: number - description: AD5X only. XYZ coordinate array. - camera: - type: integer - description: AD5X only. Camera availability flag (1/2). - moveCtrl: - type: integer - description: AD5X only. Always 1. - extrudeCtrl: - type: integer - description: AD5X only. Always 1. - cumulativeFilament: - type: number - cumulativePrintTime: - type: integer - currentPrintSpeed: - type: integer - doorStatus: - $ref: "#/components/schemas/IsOpenEnum" - errorCode: - type: string - estimatedLeftLen: - type: number - estimatedLeftWeight: - type: number - estimatedRightLen: - type: number - estimatedRightWeight: - type: number - estimatedTime: - type: number - externalFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - fillAmount: - type: integer - firmwareVersion: - type: string - flashRegisterCode: - type: string - internalFanStatus: - $ref: "#/components/schemas/IsOpenEnum" - ipAddr: - type: string - leftFilamentType: - type: string - hasMatlStation: - type: integer - description: AD5X only. 0/1 flag for material station presence. - matlStationInfo: - $ref: "#/components/schemas/MatlStationInfo" - indepMatlInfo: - $ref: "#/components/schemas/IndepMatlInfo" - leftTargetTemp: - type: number - leftTemp: - type: number - lightStatus: - $ref: "#/components/schemas/IsOpenEnum" - location: - type: string - macAddr: - type: string - measure: - type: string - name: - type: string - nozzleCnt: - type: integer - nozzleModel: - type: string - nozzleStyle: - type: integer - pid: - type: integer - description: AD5X uses "0026" -> 26. - platTargetTemp: - type: number - platTemp: - type: number - polarRegisterCode: - type: string - printDuration: - type: integer - printFileName: - type: string - printFileThumbUrl: - type: string - description: Empty if no thumb; otherwise http://:8898/getThum. Base file dir is /data/. - printLayer: - type: integer - printProgress: - type: number - printSpeedAdjust: - type: number - remainingDiskSpace: - type: number - rightFilamentType: - type: string - rightTargetTemp: - type: number - rightTemp: - type: number - status: - type: string - targetPrintLayer: - type: integer - tvoc: - type: integer - zAxisCompensation: - type: number - MatlStationInfo: - type: object - description: AD5X material station summary. - properties: - slotCnt: - type: integer - description: Always 4. - currentSlot: - type: integer - description: Current slot id (from SerialObject material operation field +0x344). - currentLoadSlot: - type: integer - description: Current load/unload slot id (from SerialObject material operation field +0x348). - stateAction: - type: integer - description: >- - Material operation stage. Observed values: 0=idle/cancel, 2=load step 2, 3=load step 3, - 4=unload step 2, 5=unload step 3, 6=complete. - stateStep: - type: integer - description: >- - Material operation mode. Observed values: 0=idle, 1=load, 2=unload, 3=cancel. - slotInfos: - type: array - items: - $ref: "#/components/schemas/MatlSlotInfo" - MatlSlotInfo: - type: object - description: AD5X material station slot info. - properties: - slotId: - type: integer - hasFilament: - type: boolean - materialName: - type: string - materialColor: - type: string - description: Color string before first ';' in config. - IndepMatlInfo: - type: object - description: AD5X independent material info. - properties: - stateAction: - type: integer - description: >- - Material operation stage. Observed values: 0=idle/cancel, 2=load step 2, 3=load step 3, - 4=unload step 2, 5=unload step 3, 6=complete. - stateStep: - type: integer - description: >- - Material operation mode. Observed values: 0=idle, 1=load, 2=unload, 3=cancel. - materialName: - type: string - materialColor: - type: string - description: Color string before first ';' in config. - JobInfo: - type: array - description: | - Array of job tuples. Structure: `[[deviceSN, jobID, filepath], ...]`. - Firmware iterates this array looking for a `deviceSN` (index 0) match with the local printer. - items: - type: array - items: - type: string - NewJobCmd: - type: object - description: Start a cloud/multi-device job. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [newJob_cmd] - args: - type: object - required: - - jobInfo - properties: - jobRouter: - type: boolean - description: | - **Presence Flag**: The value (true/false) is ignored. If this key is *present* in the JSON, - the firmware extracts the filepath from `jobInfo` tuple index 2. - If *absent*, it uses the root `filepath` property. - jobInfo: - type: array - items: - - $ref: "#/components/schemas/JobInfo" - filepath: - type: string - description: only read if jobRouter is not declared - filename: - type: string - thumbPath: - type: string - fileMd5: - type: string - printNow: - type: boolean - leveling: - type: boolean - PrinterCtlCmd: - type: object - description: | - Set speeds, fans, and z-axis compensation. - **Partial Update Behavior:** This command supports partial updates. Fields omitted from the payload - are ignored by the firmware (using internal sentinel values, e.g., -200). - **Important:** Do NOT send default values (like 0) for fields you do not intend to change, - as this will overwrite the user's current settings. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [printerCtl_cmd] - args: - type: object - properties: - zAxisCompensation: - type: number - default: -200.0 - description: | - Z-offset in mm. Maps to `SET_GCODE_OFFSET Z= MOVE=1`. - Range: -5.0 to +5.0. Values outside this range are ignored. - Sentinel: -200.0 (ignore). - speed: - type: integer - default: -200 - description: | - Print speed percentage. Maps to `M220 S`. - Clamped range: 50-150. - Sentinel: -200 (ignore). - chamberFan: - type: integer - default: -200 - description: Chamber fan speed (0-255, 0=off). Sentinel -200 (ignore). - coolingFan: - type: integer - default: -200 - description: Cooling fan speed (0-255, 0=off). Sentinel -200 (ignore). - JobCtlCmd: - type: object - description: Control print jobs (pause, resume, cancel) - required: - - cmd - - args - properties: - cmd: - type: string - enum: [jobCtl_cmd] - args: - type: object - required: - - jobID - - action - properties: - jobID: - type: string - description: Job identifier - action: - type: string - description: Control action - enum: [pause, continue, resume, cancel, stop] - DelayCloseCmd: - type: object - description: configure automatic power-off - required: - - cmd - - args - properties: - cmd: - type: string - enum: [delayClose_cmd] - args: - type: object - required: - - automaticShutdown - - shutdownAfterTime - properties: - automaticShutdown: - type: string - shutdownAfterTime: - type: integer - ReNameCmd: - type: object - description: give your printer a nickname - required: - - cmd - - args - properties: - cmd: - type: string - enum: [reName_cmd] - args: - type: object - required: - - name - properties: - name: - type: string - CalibrationCmd: - type: object - description: configure leveling and vibration compensation - required: - - cmd - - args - properties: - cmd: - type: string - enum: [calibration_cmd] - args: - type: object - required: - - levelingDetection - - vibrationCompensation - properties: - levelingDetection: - type: string - vibrationCompensation: - type: string - StreamCtrlCmd: - type: object - description: control the camera state - required: - - cmd - - args - properties: - cmd: - type: string - enum: [streamCtrl_cmd] - args: - type: object - required: - - action - properties: - action: - type: string - UserProfileCmd: - type: object - description: best guess... cloud account management - required: - - cmd - - args - properties: - cmd: - type: string - enum: [userProfile_cmd] - args: - type: object - required: - - name - - avatar - properties: - name: - type: string - avatar: - type: string - MsConfigCmd: - type: object - description: | - AD5X only. Configure material station slot metadata. - **Validation:** Firmware performs NO validation on strings. - **Palette:** UI recognizes 24 specific hex codes (e.g., #FFFFFF, #FEF043, #161616). - **Types:** UI supports PLA, PLA-CF, ABS, PETG, PETG-CF, TPU, SILK. Hidden support for PA-CF, PC, etc. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [msConfig_cmd] - args: - type: object - required: - - slot - - mt - - rgb - properties: - slot: - type: integer - description: Material station slot (1-4). - mt: - type: string - description: Material name/type string (e.g. "PLA"). Arbitrary strings accepted. - rgb: - type: string - description: Material color string (e.g. "#FF0000"). Arbitrary strings accepted. - IpdMsConfigCmd: - type: object - description: | - AD5X only. Configure independent material metadata. - **Validation:** Firmware performs NO validation on strings. - **Palette:** UI recognizes 24 specific hex codes. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [ipdMsConfig_cmd] - args: - type: object - required: - - mt - - rgb - properties: - mt: - type: string - description: Material name/type string (e.g. "PLA"). Arbitrary strings accepted. - rgb: - type: string - description: Material color string (e.g. "#FF0000"). Arbitrary strings accepted. - MsCmd: - type: object - description: AD5X only. Material station command. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [ms_cmd] - args: - type: object - required: - - action - - slot - properties: - action: - type: integer - enum: [0, 1, 2] - description: 0=load, 1=unload, 2=cancel. - slot: - type: integer - IpdMsCmd: - type: object - description: AD5X only. Independent material station command. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [ipdMs_cmd] - args: - type: object - required: - - action - properties: - action: - type: integer - enum: [0, 1, 2] - description: 0=load, 1=unload, 2=cancel. - MoveCtrlCmd: - type: object - description: AD5X only. Manual axis move. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [moveCtrl_cmd] - args: - type: object - required: - - axis - - delta - properties: - axis: - type: string - enum: [x, y, z] - description: Axis identifier (lowercase). - delta: - type: number - description: Move distance (float). - ExtrudeCtrlCmd: - type: object - description: AD5X only. Manual extruder move. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [extrudeCtrl_cmd] - args: - type: object - required: - - axis - - delta - properties: - axis: - type: string - description: Axis identifier (ignored by handler). - delta: - type: number - description: Extrude/retract distance (float). - HomingCtrlCmd: - type: object - description: AD5X only. Home all axes. - required: - - cmd - properties: - cmd: - type: string - enum: [homingCtrl_cmd] - args: - type: object - description: Optional/unused by firmware. - ErrorCodeCtrlCmd: - type: object - description: AD5X only. Clear printer error code. - required: - - cmd - - args - properties: - cmd: - type: string - enum: [errorCodeCtrl_cmd] - args: - type: object - required: - - action - - errorCode - properties: - action: - type: string - enum: [clearErrorCode] - description: Must be clearErrorCode; handler clears only if errorCode matches current error. - errorCode: - type: string - DeviceUnregisterCmd: - type: object - description: best guess... disables cloud account - required: - - cmd - properties: - cmd: - type: string - enum: [deviceUnregister_cmd] - StateCtrlCmd: - type: object - description: control the printer state - required: - - cmd - - args - properties: - cmd: - type: string - enum: [stateCtrl_cmd] - args: - type: object - required: - - action - properties: - action: - type: string - DeviceUpdateDetailCmd: - type: object - description: | - Check for firmware updates from FlashForge update server. - Despite the name, this command does NOT update local device details. - It queries the remote update server for firmware version information. - - This command will: - - Query all 4 firmware packages (kernel, control, library, software) - - Check for available updates on update.flashforge.com - - Retrieve version information, download URLs, and changelogs - - Return update availability without installing anything - - This command will NOT: - - Modify any local printer state - - Install firmware updates - - Update device information cache - required: - - cmd - - args - properties: - cmd: - type: string - enum: [deviceUpdateDetail_cmd] - args: - type: object - description: Empty args object (no parameters required) - NewLocalJobCmd: - type: object - description: submit a new job to the print - required: - - cmd - - args - properties: - cmd: - type: string - enum: [newLocalJob_cmd] - args: - type: object - required: - - jobId - - fileName - - printNow - - leveling - properties: - jobId: - type: string - fileName: - type: string - thumbPath: - type: string - printNow: - type: boolean - description: Start printing immediately - leveling: - type: boolean - description: Perform bed leveling before print - flowCalibration: - type: boolean - description: Perform flow calibration (AD5X only) - useMs: - type: boolean - description: Use material station (AD5X only) - tCount: - type: integer - description: Material/tool count (AD5X only) diff --git a/endpoints/networkserver_commands_adventurer3.yaml b/endpoints/networkserver_commands_adventurer3.yaml deleted file mode 100644 index 3ab0363..0000000 --- a/endpoints/networkserver_commands_adventurer3.yaml +++ /dev/null @@ -1,1244 +0,0 @@ -# FlashForge Adventurer 3 NetworkServer Protocol Specification -# Firmware: v1.3.7 20230725 -# Port: 8899 (TCP) -# Protocol: TCP Socket with line-based ASCII commands (G-code/M-code) - -description: >- - This file documents the proprietary and standard G-code/M-code commands supported by the - NetworkServer component of the FlashForge Adventurer 3 firmware (finder-rush-mips binary). - - **Protocol Overview:** - - Transport: TCP socket on port 8899 - - Format: Line-based ASCII commands with tilde (~) prefix - - Response: Acknowledgment prefixed with "ack:" or "echo:" - - **Command Format:** - All commands MUST be prefixed with a tilde character `~` followed by the G-code or M-code. - Example: `~M115` instead of `M115` - - **Response Format:** - Responses typically begin with `ack: ` or `echo: ` prefix followed by the response data. - Multi-line responses are terminated with `\r\n`. - - **Connection Model:** - - Multi-threaded server (one thread per client) - - Maximum 10 concurrent connections - - No authentication required - - **Critical Differences from Adventurer 4 Pro:** - - No HTTP REST API (TCP only) - - No authentication on port 8899 - - Different set of custom M-codes - - On/off fan control only (no variable speed) - - No built-in camera (accessory only) - -commands: - # --- Motion Control (G-codes) --- - - G1: - summary: Linear move to specified position - description: >- - Move the print head to the specified coordinates. This command supports up to 5 axes: - X, Y, Z (position), E (extruder), and F (feedrate). - - **Special E-parameter Handling:** - If the command contains an E parameter (extrusion), the firmware automatically - injects a "G92 E0" command before the movement to reset the extruder position. - This prevents extruder position accumulation errors during manual moves. - - **Blocked During Printing:** - This command is ignored if a print job is currently active (BuildPrint::instBuildPrint != null). - args: - X: float # X coordinate in mm (0-150mm) - Y: float # Y coordinate in mm (0-150mm) - Z: float # Z coordinate in mm (0-150mm) - E: float # Extruder position in mm (triggers G92 E0 injection) - F: int # Feedrate in mm/min - response: None (asynchronous - forwarded to motion controller) - example: "~G1 X100 Y100 Z0.3 F3000" - notes: > - - Motion blocked during active prints - - E parameter auto-injects G92 E0 before move - - No explicit acceleration control - - Coordinate system: Origin (0,0,0) at front-left corner - - G28: - summary: Auto-home all or specified axes - description: >- - Home the printer by moving axes to their endstops until triggered. - This establishes the coordinate system origin. - - **Implementation:** - Unlike other motion commands, G28 does not directly send output to the serial port. - The parser handles the homing sequence internally, likely calling motion controller - functions directly. - - **Homing Sequence (typical Cartesian printer):** - 1. Z axis homes first (moves down until trigger) - 2. X and Y home simultaneously - 3. XY moves to center position - - **Default behavior (no parameters):** Homes all axes (X, Y, Z) - args: - X: boolean # Home X axis (optional) - Y: boolean # Home Y axis (optional) - Z: boolean # Home Z axis (optional) - response: None (asynchronous - handled by parser internally) - example: "~G28" - example2: "~G28 X Y" - example3: "~G28 Z" - notes: > - - No direct serial output from TCP handler - - Parser handles homing sequence internally - - Axes can be specified in any combination - - Required before first print or after position loss - - G90: - summary: Set absolute positioning mode - description: >- - Set all subsequent G1 movement commands to use absolute coordinates from origin. - - In absolute mode, coordinates are interpreted as actual positions in the build volume. - For example, `G1 X50` will move to X=50mm regardless of current position. - - **Blocked During Printing:** - This command is ignored if a print job is currently active. - args: None - response: None (asynchronous - forwarded to motion controller) - example: "~G90" - notes: > - - Positioning mode state maintained by motion controller firmware - - Default mode on startup (likely G90) - - Opposite of G91 (relative positioning) - - G91: - summary: Set relative positioning mode - description: >- - Set all subsequent G1 movement commands to use relative offsets from current position. - - In relative mode, coordinates are interpreted as offsets from the current position. - For example, `G1 X10` will move +10mm from the current X position. - - **Blocked During Printing:** - This command is ignored if a print job is currently active. - args: None - response: None (asynchronous - forwarded to motion controller) - example: "~G91" - notes: > - - Useful for fine adjustments and calibration - - Opposite of G90 (absolute positioning) - - State tracked by motion controller, not by TCP handler - - G92: - summary: Set current position without moving - description: >- - Set the current position to specified values without moving the print head. - This resets the coordinate system to a new origin. - - **Common Uses:** - - Reset extruder position: `G92 E0` - - Set work offset for multi-part prints - - Recover from lost position after skipped steps - - Define print start position - - **Blocked During Printing:** - This command is ignored if a print job is currently active. - args: - X: float # Set X position to specified value (mm) - Y: float # Set Y position to specified value (mm) - Z: float # Set Z position to specified value (mm) - E: float # Set extruder position to specified value (mm) - response: None (asynchronous - forwarded to motion controller) - example: "~G92 E0" - example2: "~G92 X0 Y0 Z0" - example3: "~G92 X50" - notes: > - - No movement occurs, only position tracking changes - - Useful for resetting extruder before manual moves - - Can set multiple axes simultaneously - - No parameters sets all axes to current position (no-op) - - # --- Printer Control (M-codes) --- - - M17: - summary: Enable stepper motors - description: >- - Enable all stepper motors, allowing them to hold position and receive movement commands. - - **Motors Affected:** - - X axis stepper - - Y axis stepper - - Z axis stepper - - E (extruder) stepper - - **Safety Considerations:** - - Motors heat up when enabled and can overheat if left enabled without movement - - Printer holds position, consuming power - - Typical timeout: Auto-disable after period of inactivity - args: None - response: "CMD M17 Received." - example: "~M17" - notes: > - - Motors can overheat if left enabled without movement - - Z axis may free-fall if disabled (support bed before disabling!) - - Must home (G28) before printing after disabling motors - - M18: - summary: Disable stepper motors - description: >- - Disable all stepper motors, releasing holding torque and allowing free movement. - - **Motors Affected:** - - X axis stepper - - Y axis stepper - - Z axis stepper - - E (extruder) stepper - - **Use Cases:** - - Manual bed leveling - - Loading/unloading filament - - Transport/Shipping (prevent motor overheating) - - Power saving when idle - - **Safety Considerations:** - - Build plate can drop freely when Z is disabled (support it!) - - Printer loses position knowledge - G28 required before next print - - Hotend remains hot (only motors are disabled) - args: None - response: "CMD M18 Received." - example: "~M18" - notes: > - - Returns error "printing can not send M18" if print is active - - Always home (G28) before printing again after M18 - - Z axis will free-fall if not supported - - No recovery possible without re-homing - - M23: - summary: Select SD file for printing - description: >- - Select a file from the SD card storage for printing. The command opens the file, - verifies its size, and prepares it for printing. However, it does NOT start the print - automatically - you must send M24 to start. - - **Path Normalization:** - The firmware normalizes file paths by stripping these prefixes: - - `0:/user/` prefix (removed if present) - - `/data/` prefix (removed if present) - - Whitespace trimmed - Final storage: `/data/` - - **File Formats Supported:** - - `.g` - G-code files (primary format) - - `.gx` - FlashForge encrypted G-code format - - `.gcode` - Standard G-code files - - **Triggered Signals:** - - `sig_startprint()` - Emitted with full file path - - Print does NOT start automatically - requires M24 command - args: - filepath: string # File path on SD card (e.g., "model.g", "0:/user/test.g", "/data/test.g") - response: > - File opened: Size: - Done printing file - ok - example: "~M23 test_model.g" - example2: "~M23 0:/user/calibration.g" - example3: "~M23 /data/example.gcode" - notes: > - - All prefixes stripped and normalized - - Path separators: / - - Final storage always in /data/ - - File must exist and be readable - - Returns size 0 if file not found - - Must call M24 to actually start printing - - M24: - summary: Start or resume print job - description: >- - Start a print job if file selected but not started, or resume a paused print job. - - **State Machine:** - ``` - IDLE → M23 (select file) → READY → M24 → PRINTING - ↓ - PAUSED → M24 → PRINTING - ``` - - **Behavior:** - - If READY (file selected, not started): Starts printing from beginning - - If PAUSED: Resumes from paused position - - If PRINTING: No-op (already printing) - - If IDLE: Command accepted but print won't start - - **Triggered Signals:** - - `sig_printresume()` - Emitted to resume printing - args: None - response: "CMD M24 Received." - example: "~M24" - notes: > - - No-op if already printing - - Check M27 status to verify print started - - Use M25 to pause instead of cancel - - M26 will cancel the print job - - M25: - summary: Pause print job - description: >- - Immediately pause the current print job, saving current position for later resumption. - - **State Preservation:** - The pause mechanism preserves: - - XYZ coordinates (current position) - - Extruder position (E) - - Current temperatures (nozzle and bed) - - File read position (to resume from correct location) - - **Paused State:** - - Print head may park in safe location - - Heaters maintained at target temperature - - Motors may be disabled (M18) for safety - - Can be resumed with M24 - - **Triggered Signals:** - - `sig_printpause()` - Emitted to pause printing - args: None - response: "CMD M25 Received." - example: "~M25" - notes: > - - Print state saved for recovery - - Heaters remain at target temperature - - Use M24 to resume printing - - M26 will cancel print (unrecoverable) - - M26: - summary: Cancel print job - description: >- - Immediately cancel the current print operation and abort the file read. - - **Behavior:** - - Immediately cancels current print - - Aborts file read - - Disables heaters - - May home axes (depends on firmware configuration) - - File selection is cleared - - **State Transitions:** - ``` - PRINTING → M26 → IDLE - PAUSED → M26 → IDLE - READY → M26 → IDLE (no-op if no file selected) - ``` - - **Triggered Signals:** - - `sig_printcancel()` - Emitted to cancel printing - - **Warning:** This is a hard cancel - print progress is lost. Cannot resume after M26. - args: - bytePosition: int # Optional byte position for SD resume (ignored in A3) - response: "CMD M26 Received." - example: "~M26" - notes: > - - Hard stop - print cannot be resumed - - File selection cleared - - Must select file with M23 again to restart - - May home axes as part of cancel sequence - - M27: - summary: Get print status and progress - description: >- - Query the current print job progress and status. - - **Progress Format:** - The firmware uses a confusing format where "bytes" are actually percentage points: - - Current value = 0-100 (percentage complete) - - Total value = always 100 (representing 100%) - - **Response Interpretation:** - - `SD printing byte 0/0` - No print active (IDLE) - - `SD printing byte 45/100` - 45% complete, actively printing - - `SD printing byte 100/100` - Print complete or 100% complete - - **Progress Calculation:** - The firmware multiplies internal progress (0.0-1.0) by 100 to get percentage. - args: None - response: > - CMD M27 Received. - SD printing byte / - ok - example: "~M27" - notes: > - - "byte" values are percentages, not actual bytes - - Total is always 100 - - Use M105 to verify temperatures while polling - - Poll every 1-2 seconds for real-time updates - - M28: - summary: Begin file upload to SD card - description: >- - Initialize a binary file upload to the printer's SD card storage. This command - switches the connection to binary mode for receiving raw G-code data. - - **Path Format Requirements:** - - Must match regex: `^\s*(\d+)\s+(0:/user/([^\n]+))` - - File path MUST start with `0:/user/` - - Filename is extracted from path - - Final storage: `/data/` - - **Validation Steps:** - 1. Parse arguments with regex - 2. Check available disk space via `check_free_space()` - 3. Create file in `/data/` directory - 4. Show upload progress dialog on printer display - - **Binary Mode:** - After M28 succeeds, the server expects raw binary G-code data stream. - No further text commands are processed until M29 is received. - args: - filesize: int # Size of file to upload (in bytes) - filepath: string # Target path with "0:/user/" prefix (e.g., "0:/user/model.g") - response: > - CMD M28 Received. - ok (success) - - OR - - Error: Not enough space (insufficient disk space) - - OR - - Error: Cannot create file (path invalid) - example: "~M28 1234567 0:/user/newprint.gcode" - notes: > - - File size MUST be accurate (verified by M29) - - Client must have file size before starting - - Max file size limited by SD card capacity (FAT32 = 4GB) - - Recommended chunk size: 4096 bytes - - Disk space checked before accepting upload - - Upload progress dialog shown on printer display - - M29: - summary: Complete file upload and verify - description: >- - Complete the binary file upload initiated by M28 and verify the transfer. - - **Verification Process:** - 1. Compares actual uploaded file size with expected size from M28 - 2. If sizes match exactly: File is valid, keeps it - 3. If sizes differ: File is corrupted, deletes it - - **Finalization:** - On successful upload: - - Updates file timestamp via `setFileNewTime()` - - Closes upload file handle - - Closes upload dialog on printer - - Refreshes file list display (`sig_showMemoryList()`) - - **Error Handling:** - On size mismatch or other error: - - Logs critical error: "File Is Not Available" - - Deletes incomplete file via QFile::remove() - - Returns error to client - args: None - response: > - CMD M29 Received. - ok - filesize = tofile.size = - ack: "ok" (success) - - OR - - Error logged: "File Is Not Available" (size mismatch) - example: "~M29" - notes: > - - All bytes MUST be received for success - - No partial uploads accepted - - Network errors during upload = complete failure - - Delete and retry if size mismatch occurs - - File is verified before being committed - - M104: - summary: Set extruder temperature - description: >- - Set the target temperature for the hotend/nozzle. - - **Temperature Validation:** - - Maximum temperature read from extruder configuration file - - Typical maximum: 265°C (0x109 in hex) - - Requested temperatures are automatically capped at maximum - - Minimum temperature: 0°C (room temperature) - - **Offset Support:** - - Firmware supports user-configured temperature offsets - - Read from nozzle temperature different file - - Offset applied before sending to hardware - - **Heater Control:** - - Command is stored and sent to printer controller - - Hardware PID loop maintains actual temperature - - No wait-for-heat flag (asynchronous operation) - args: - S: int # Target temperature in Celsius (0-265°C) - T: int # Extruder index (default: 0, A3 has single extruder) - response: "ack: " with command echo - example: "~M104 S200" - example2: "~M104 S0" - example3: "~M104 T0 S210" - notes: > - - Temperatures above 265°C automatically clamped - - Temperature change is asynchronous (no wait) - - Use M105 to poll actual temperature - - Offset support for fine-tuning - - Minimum: 0°C (room temp) - - M105: - summary: Get current temperatures - description: >- - Query the current nozzle and bed temperatures from the printer. - - **Response Format When PRINTING:** - ``` - ok T0:/ B:/ - ``` - - **Response Format When NOT PRINTING:** - ``` - ok T0:/0 B:/ - ``` - - **Temperature Values:** - - T0: Nozzle/extruder temperature in Celsius - - B: Heated bed temperature in Celsius - - Format: `current/target` or `current/0` (no target set) - - **Thread-Safe Reading:** - All temperature getters use mutex locks for thread safety: - - `getCurrentExtruderTemp()` - Offset +0x17c - - `getTargetExtruderTemp()` - Offset +0x180 - - `getCurrentPlatformTemp()` - Offset +0x174 - - `getTargetPlatformTemp()` - Offset +0x178 - - Mutex lock at offset +0xcc - args: None - response: > - CMD M105 Received. - ok T0:/ B:/ - example: "~M105" - notes: > - - Single extruder only (T0) - - Temperatures in Celsius (integer values) - - Target shows as 0 when not printing - - Thread-safe mutex-protected reads - - Poll every 1-2 seconds for monitoring - - M106: - summary: Turn on cooling fan - description: > - Turn on the part cooling fan at full speed. - - **Adventurer 3 Fan Control:** - - **ON/OFF ONLY** - No variable speed support - - Unlike 5M series, A3 does NOT support M106 with S parameter - - Command is forwarded as "M106" with no speed parameter - - **Fan Types:** - 1. **Part Cooling Fan:** Controlled by M106/M107 - 2. **Extruder Cooling Fan:** Always on when extruder is hot (safety-critical) - args: None - response: "CMD M106 Received." - example: "~M106" - notes: > - - No variable speed control (on/off only) - - Default speed when ON: 100% (full speed) - - Extruder fan runs automatically (not controlled by M106/M107) - - Use M107 to turn off - - M107: - summary: Turn off cooling fan - description: > - Turn off the part cooling fan. - - **Behavior:** - - Fan stops spinning immediately - - No gradual ramp-down - - Extruder cooling fan continues running if hotend is hot - - **Related Command:** - M106 turns the fan back on. - args: None - response: "CMD M107 Received." - example: "~M107" - notes: > - - Part cooling fan only (not extruder fan) - - Extruder fan is automatic and cannot be controlled - - Immediate stop (no ramp-down) - - M108: - summary: Cancel heat wait (STUBBED - No effect) - description: > - **WARNING:** This command is stubbed out in Adventurer 3 firmware and has no effect. - - **Expected Behavior (standard RepRap):** - Cancel waiting for temperature to reach target and resume immediately. - - **Actual Adventurer 3 Behavior:** - - Command is logged but performs no action - - No serial communication to printer controller - - No state change - - **Workarounds:** - - Use pause/resume (M25/M24) instead - - Set new target temperatures to change heat settings - - Emergency stop with M112 if needed - args: None - response: "CMD M108 Received." - example: "~M108" - notes: > - - Stubbed out - does nothing on Adventurer 3 - - Do not rely on this command - - Use M25/M24 for print control instead - - Set temperatures to 0 to stop heating - - M112: - summary: Emergency stop - description: > - **CRITICAL SAFETY COMMAND** - Immediately halts all printer operations. - - **Emergency Stop Sequence:** - 1. M112 - Emergency stop command (immediate halt) - 2. G162 Z F500 - Home Z axis at 500 mm/min (emergency retract) - 3. M104 S0 T0 - Turn off extruder heater (tool 0 to 0°C) - 4. G162 X Y- F2000 - Home X and Y axes at 2000 mm/min - - **State Cleanup:** - - Homes all axes (losing position) - - Turns off heaters (losing temperature) - - Resets internal SerialObject state - - **Cannot recover print job** after M112 - - **Recovery After M112:** - To print again: - 1. Re-home the printer (G28) - 2. Reheat the extruder and bed - 3. Select file again (M23) - 4. Start print from beginning (M24) - 5. Optionally use "resume from layer" if firmware supports it - args: None - response: "echo: Emergency Stop!!!" (or similar) - example: "~M112" - notes: > - - CANNOT BE UNDONE - Print job is lost - - Axes are homed (position lost) - - Heaters are turned off - - Internal state is reset - - Use only for emergencies - - M114: - summary: Get current position - description: > - Query the current print head position from the printer's motion controller. - - **Response Format:** - ``` - CMD M114 Received. - X: Y: Z: A: B: - ``` - - **Position Units:** - - X, Y, Z: Millimeters (mm) - - A, B: Unknown (possibly extruder positions or temperature-related) - - **Note:** - - Extruder position is typically labeled 'E' in standard RepRap, - but Adventurer 3 firmware uses A/B in the response. - args: None - response: > - CMD M114 Received. - X:10.5 Y:20.3 Z:0.3 A:0.00 B:0.00 - example: "~M114" - notes: > - - Synchronous command (waits for motion controller) - - Units in millimeters for X, Y, Z - - A and B values purpose unclear - - Use to verify position after critical moves - - May show 'E' instead of 'A'/'B' in some firmware versions - - M115: - summary: Get firmware and printer information - description: > - Returns comprehensive printer identification including machine type, firmware version, - serial number, build dimensions, tool count, and network MAC address. - - **Response Fields:** - - Machine Type: "FlashForge Adventurer III" (hardcoded) - - Machine Name: User-assigned printer name (from SerialNoFile) - - Firmware: Version string (e.g., "v1.3.7") - - Serial Number: Factory serial number (from SerialNoFile) - - X: 150 Y: 150 Z: 150 (Build volume in mm) - - Tool Count: 1 (single extruder) - - Mac Address: Network MAC from /etc/MAC file - - **Data Sources:** - - Build volume: Hardcoded string "150 X 150 X 150" parsed into X/Y/Z - - MAC address: Read from /etc/MAC file (32 bytes, format XX:XX:XX:XX:XX:XX) - - Serial number: From configuration files via SerialNoFile class - - Firmware version: Hardcoded string "1.3.7 20230725" - args: None - response: > - echo: Machine Type: FlashForge Adventurer III - Machine Name: - Firmware: v1.3.7 - Serial Number: - X: 150 Y: 150 Z: 150 - Tool Count: 1 - Mac Address: - example: "~M115" - notes: > - - Build volume: 150x150x150mm (hardcoded) - - Firmware: v1.3.7 20230725 - - Use for printer identification - - MAC address used for network discovery - - M119: - summary: Get endstop status and printer state - description: > - Query comprehensive printer status including endstops, machine state, move mode, - filament sensor, LED status, and current print file. - - **Response Fields:** - - Endstop: X-max, Y-max, Z-min states (0=open, 1=triggered) - - MachineStatus: IDLE, PRINTING, PAUSED, HOMING, ERROR - - MoveMode: Numeric move mode value (0.0, 1.0, etc.) - - FilamentStatus: "ok" (filament present) or "no filament" - - LEDStatus: "on" or "off" - - PrintFileName: Current print file name (empty if not printing) - - **Filament Detection:** - Firmware sends M113 command internally and parses response. - Status determined by comparing second field to "1". - - **Endstop States:** - - Default values if no custom string: All axes 0 (not triggered) - - 0 = Open/not triggered - - 1 = Closed/triggered - args: None - response: > - echo: Endstop: X-max: Y-max: Z-min: - MachineStatus: - MoveMode: - FilamentStatus: - LEDStatus: - PrintFileName: - example: "~M119" - notes: > - - Endstop values: 0=open, 1=triggered - - MachineStatus: IDLE, PRINTING, PAUSED, HOMING, ERROR - - MoveMode: Numeric mode indicator - - Use for status monitoring and debugging - - M140: - summary: Set bed temperature - description: > - Set the target temperature for the heated bed. - - **Temperature Range:** - - Typical range: 0-100°C for PLA/ABS materials - - No explicit maximum in code (hardware-limited) - - Recommended maximum: ~100°C - - **State-Dependent Behavior:** - - READY: Sends command immediately - - PAUSED: Sends command immediately - - PRINTING: Queues command for later execution - - **No Offset Support:** - Unlike extruder temperature, bed temperature has no user-configurable offset. - args: - S: int # Target bed temperature in Celsius (0-100°C typical) - response: "ack: " with command echo - example: "~M140 S60" - example2: "~M140 S0" - notes: > - - Recommended: PLA=60°C, ABS=100°C - - Bed heating is asynchronous - - Use M105 to monitor actual temperature - - Queued during printing - - M144: - summary: LED control - Internal command (use M146 instead) - description: > - **Internal Command** - Use M146 for user-facing LED control. - - M144 is sent by the firmware to turn LEDs ON. - **WARNING:** This command likely writes to EEPROM. Frequent usage can degrade flash memory. - Always use M146 for safe/standard toggling. - args: None - response: "cmd_M144 " with response - example: "~M144" - notes: > - - Internal command - use M146 instead - - Turns LEDs ON - - Sent by M146 when parameter is NOT "0" - - M145: - summary: LED control - Internal command (use M146 instead) - description: > - **Internal Command** - Use M146 for user-facing LED control. - - M145 is sent by the firmware to turn LEDs OFF. - **WARNING:** This command likely writes to EEPROM. Frequent usage can degrade flash memory. - Always use M146 for safe/standard toggling. - args: None - response: "cmd_M145 " with response - example: "~M145" - notes: > - - Internal command - use M146 instead - - Turns LEDs OFF - - Sent by M146 when parameter is "0" - - M146: - summary: Control accessory LEDs (on/off) - description: > - **SAFE/PREFERRED** method to control add-on LED light bars. - - This command abstracts the internal M144/M145 calls and is the safe entry point - for LED control. - - **Base Model:** - Adventurer 3 does NOT have built-in LEDs. - This command only works if LED accessory is installed. - - **Command Mapping:** - - `M146 0` → Sends M145 to printer (LEDs OFF) - - `M146 1` → Sends M144 to printer (LEDs ON) - - `M146 255` → Sends M144 to printer (LEDs ON) - - **LED Control:** - - Only on/off control (no brightness adjustment) - - No color selection (single color) - - Controlled via M144 (ON) and M145 (OFF) commands - args: - parameter: string # "0" for OFF, any other value for ON - response: "ack: " with command echo - example: "~M146 1" - example2: "~M146 0" - notes: > - - Adventurer 3 has no built-in LEDs (accessory only) - - On/off control only, no brightness/color - - M146 0 = OFF, M146 1+ = ON - - Uses M144/M145 internally but is the standard API path - - # --- FlashForge Custom Commands (Network) --- - - M601: - summary: Initialize WiFi/multicast connection - description: > - Initialize WiFi multicast connection and set connection status. - - **Connection Types:** - - Type 1 → Multicast status: 2 - - Type 2 → Multicast status: 3 - - Other/Unknown → Multicast status: 0 - - **Behavior:** - - If already connected: Returns error "Error: have been connected" - - On success: Sets multicast status and triggers M601 signal - - Starts connection timer - - Updates connection state flags - - **Response Format:** - ``` - CMD M601 Received. - ok (success) - ``` - - ``` - Error: have been connected (already connected) - ``` - args: None - response: > - CMD M601 Received. - ok - example: "~M601" - notes: > - - Initializes multicast connection - - Error if already connected - - Network-specific command - - M602: - summary: Disconnect WiFi/multicast connection - description: > - Close WiFi/multicast connection and reset connection state. - - **Behavior:** - - Sets multicast status to 0 (disconnected) - - Triggers `sig_M602()` signal - - Resets connection state flags - - Stops connection timer - - Closes upload dialog - args: None - response: > - CMD M602 Received. - ok - example: "~M602" - notes: > - - Disconnects multicast connection - - Resets connection state - - Complements M601 - - M610: - summary: Set printer name (network identification) - description: > - Update the printer's network name for mDNS/bonjour discovery. - - **Behavior:** - - Updates printer name in serial number file - - Triggers printer name change in UI - - May reconnect to network with new name - - **Use Cases:** - - Network identification - - Printer labeling - - Bonjour/mDNS discovery - args: - printer_name: string # New name for the printer - response: > - CMD M610 Received. - ok - example: "~M610 My 3D Printer" - notes: > - - Updates serial number file (persistent) - - **Side Effect:** Triggers network service reconnection (`reconnectedMultiCase`) which may briefly drop connection. - - Confirmed by RE to be the canonical rename method. - - M611: - summary: Custom network command (implementation unclear) - description: > - FlashForge-proprietary network command for Adventurer 3. - - **Status:** Implementation not fully analyzed. Likely used for: - - Network configuration - - WiFi settings management - - Cloud service connection - - **Response:** Logs command receipt with "cmd_M611" prefix. - args: None (implementation-specific) - response: "cmd_M611 " with response - example: "~M611" - notes: > - - FlashForge proprietary command - - Details require further analysis - - M612: - summary: Custom network command (implementation unclear) - description: > - FlashForge-proprietary network command for Adventurer 3. - - **Status:** Implementation not fully analyzed. Likely used for: - - Network configuration - - WiFi settings management - - Cloud service connection - - **Response:** Logs command receipt with "cmd_M612" prefix. - args: None (implementation-specific) - response: "cmd_M612 " with response - example: "~M612" - notes: > - - FlashForge proprietary command - - Details require further analysis - - # --- FlashForge Custom Commands (Printer) --- - - M650: - summary: Get printer model identification - description: > - Return hardware version information for the Adventurer 3. - - **Response Format:** - ``` - CMD M650 Received. - X: 1.0 Y: 0.5 - ``` - - **Model ID Interpretation:** - - X: 1.0 - Major hardware revision - - Y: 0.5 - Minor hardware revision or variant - - **Purpose:** - - Printer identification - - Hardware variant detection - - Compatibility checking - - Firmware validation - - **Note:** - These values are hardcoded in the firmware, not read from hardware. - args: None - response: > - CMD M650 Received. - X: 1.0 Y: 0.5 - example: "~M650" - notes: > - - Hardware version: 1.0/0.5 - - Hardcoded in firmware - - Used for printer identification - - M651: - summary: Custom printer command (implementation unclear) - description: > - FlashForge-proprietary printer command for Adventurer 3. - - **Status:** Implementation not fully analyzed. Sent to serial port and response checked. - - **Response:** Logs command receipt with "cmd_M651" prefix. - args: None (implementation-specific) - response: "cmd_M651 " with response - example: "~M651" - notes: > - - FlashForge proprietary command - - Sent to serial port with response check - - Details require further analysis - - M652: - summary: Custom printer command (implementation unclear) - description: > - FlashForge-proprietary printer command for Adventurer 3. - - **Status:** Implementation not fully analyzed. Sent to serial port with params. - - **Response:** Logs command receipt with "cmd_M652" prefix. - args: None (implementation-specific) - response: "cmd_M652 " with response - example: "~M652" - notes: > - - FlashForge proprietary command - - Sent to serial port with params - - Details require further analysis - - M653: - summary: Custom printer command with parameters - description: > - FlashForge-proprietary printer command for Adventurer 3. - - **Status:** Implementation not fully analyzed. Accepts parameters and sends to serial port. - - **Response:** "ack: ok" on success. - args: - params: string # Command-specific parameters - response: "ack: ok" - example: "~M653 " - notes: > - - FlashForge proprietary command - - Accepts parameters - - Details require further analysis - - M654: - summary: Custom printer command with parameters - description: > - FlashForge-proprietary printer command for Adventurer 3. - - **Status:** Implementation not fully analyzed. Accepts parameters and sends to serial port. - - **Response:** "ack: ok" on success. - args: - params: string # Command-specific parameters - response: "ack: ok" - example: "~M654 " - notes: > - - FlashForge proprietary command - - Accepts parameters - - Details require further analysis - - # --- FlashForge Custom Commands (File & System) --- - - M661: - summary: List all print files on SD card - description: > - Return a list of all printable files stored on the printer's SD card (/data directory). - - **File Extension Filtering:** - Only returns files with these extensions: - - `.g` - G-code files - - `.gx` - FlashForge encrypted G-code format - - `.gcode` - Standard G-code files - - **Response Format:** - **Success:** - ``` - CMD M661 Received. - info_list.size: - - - ... - ``` - - **No Files:** - ``` - CMD M661 Error. - ``` - args: None - response: > - CMD M661 Error. - info_list.size: - - - ... - example: "~M661" - notes: > - - Searches in /data directory - - Filters by supported extensions - - Returns count followed by filenames - - **Internal Implementation:** File count is calculated via `(size >> 2) * -0x33333333`, effectively dividing the vector size by 12 bytes per entry. - - Used for file browser UI - - M662: - summary: Get thumbnail image from G-code file - description: > - Extract embedded thumbnail image from a G-code file header. - - **Image Format:** - - PNG format - - Embedded in G-code file header - - Size varies (typically ~60-800 bytes) - - **Fallback:** - If no embedded image exists, returns default image from: - `/usr/share/G_File.png` - - **Binary Data Format:** - ``` - uint32_t magic: 0x2a2a2aa2 (big-endian: 0xa2a22a2a) - uint32_t length: image data length (big-endian) - uint8_t data[length]: PNG image bytes - ``` - - **Response Format:** - **Success:** - ``` - CMD M662 Received. - ack header length: - - ``` - - **Error:** - ``` - CMD M662 Received. - Error: File not exists - ``` - args: - filename: string # Name of file (with or without .g/.gcode extension) - response: > - CMD M662 Received. - Error: File not exists - - OR - - CMD M662 Received. - ack header length: - - example: "~M662 calibration.g" - notes: > - - Extracts thumbnail from G-code header - - Returns PNG image data - - Falls back to default image if missing - - Used for UI thumbnails - - Binary response format with magic number - - M663: - summary: Get current position with extrusion flow - description: > - Query the current print head position with extrusion flow information. - - **Response Format:** - ``` - CMD M663 Received. - ok - ``` - - **Note:** - This command is similar to M114 but may provide enhanced position data. - Actual response format needs verification with live printer. - args: None - response: > - CMD M663 Received. - ok - example: "~M663" - notes: > - - Enhanced position query - - May include extrusion flow data - - Similar to M114 but with more details - -# Protocol Details -protocol: - port: 8899 - command_prefix: "~" - line_terminator: "\\r\\n" - response_prefix: "ack:" - max_connections: 10 - connection_timeout: 30 # seconds - recv_timeout: 5 # seconds - binary_mode: "M28/M29" # File upload uses binary mode - -# Build Volume -build_volume: - x: 150 # mm - y: 150 # mm - z: 150 # mm - origin: "front-left corner" # (0,0,0) - -# Temperature Limits -temperature_limits: - extruder_max: 265 # Celsius (configurable) - bed_max: 100 # Celsius (typical) - extruder_min: 0 # Celsius (room temp) - bed_min: 0 # Celsius (room temp) - -# Fan Specifications -fan: - type: "part cooling" - control: "on/off only" - speed_range: "fixed (100% when on)" - extruder_fan: "automatic (hotend temperature controlled)" - -# LED Support -led: - built_in: false - accessory_only: true - control: "on/off via M146" - internal_commands: "M144 (ON), M145 (OFF)" - -# Print File Support -file_formats: - supported: - - ".g" # G-code files - - ".gx" # FlashForge encrypted G-code - - ".gcode" # Standard G-code - storage_path: "/data" - upload_protocol: "binary M28/M29" - max_filesize: "4GB (FAT32 limit)" - -# Network Services -network: - tcp_port: 8899 - http_port: 8080 - http_purpose: "Camera streaming (MJPG)" - multicast: "Yes (M601/M602 control)" - authentication: "None (port 8899 unauthenticated)" - -# Firmware Information -firmware: - version: "v1.3.7 20230725" - machine_type: "FlashForge Adventurer III" - model_id: "X: 1.0 Y: 0.5" - serial_source: "Configuration file (SerialNoFile)" - mac_source: "/etc/MAC file" - -# Comparison with 5M/AD5X Series -differences_from_5m_series: - - "No HTTP REST API on port 8898 (TCP only control)" - - "No authentication required on port 8899" - - "Fan control is on/off only (no variable M106 S0-255)" - - "No built-in camera (accessory LED support only)" - - "Different set of custom M-codes (600/66x series)" - - "M108 command is stubbed out (non-functional)" - - "No material station or multi-color printing" - - "Simpler networking (M601/M602 only)" - - "No air filtration or TVOC monitoring" - - "Build volume: 150x150x150mm (smaller than newer models)" diff --git a/endpoints/networkserver_commands_adventurer4.yaml b/endpoints/networkserver_commands_adventurer4.yaml deleted file mode 100644 index 7fc7a6e..0000000 --- a/endpoints/networkserver_commands_adventurer4.yaml +++ /dev/null @@ -1,239 +0,0 @@ -# FlashForge Adventurer 4 Pro NetworkServer Protocol -# Firmware: 1.2.1-3.22 -# Port: 8899 (TCP) -# Protocol: TCP Socket with line-based ASCII commands (G-code/M-code) - -description: >- - This file documents the proprietary and standard G-code/M-code commands supported by the - NetworkServer component of the FlashForge Adventurer 4 Pro firmware. - These commands are sent over a TCP connection to port 8899. - - **Important**: All commands MUST be prefixed with a tilde character `~`. - Example: `~M115` instead of `M115`. - -commands: - # --- Motion Control --- - G1: - summary: Linear Move - description: >- - Move to position. The firmware checks for 'E' axis movement and may automatically - reset extruder position (G92 E0) before moving if printing via NetworkServer. - args: - X: float # X coordinate - Y: float # Y coordinate - Z: float # Z coordinate - E: float # Extruder coordinate - F: int # Feedrate - example: "~G1 X100 Y100 F3000" - - G28: - summary: Auto Home - description: Home all axes or specified axes. - args: - X: flag # Home X - Y: flag # Home Y - Z: flag # Home Z - example: "~G28 X Y" - - G90: - summary: Absolute Positioning - description: Set coordinates to absolute mode. - example: "~G90" - - G91: - summary: Relative Positioning - description: Set coordinates to relative mode. - example: "~G91" - - G92: - summary: Set Position - description: Set current position to specified values. - args: - X: float - Y: float - Z: float - E: float - example: "~G92 E0" - - M17: - summary: Enable Steppers - description: Enable stepper motors. - example: "~M17" - - M18: - summary: Disable Steppers - description: Disable stepper motors to allow manual movement. - example: "~M18" - - # --- File & Print Management --- - M23: - summary: Select File - description: >- - Select a file on the local storage for printing. - Paths are normalized (e.g., '0:/user/' -> '/data/'). - Checks file size before confirming. - args: - filename: string # Path to file (e.g., "cube.gcode") - example: "~M23 test_print.gx" - - M24: - summary: Start/Resume Print - description: Start or resume printing the selected file. - example: "~M24" - - M25: - summary: Pause Print - description: Pause the current print job. - example: "~M25" - - M26: - summary: Set SD Position - description: Set the file read position (seek). - example: "~M26" - - M27: - summary: Report Print Status - description: Get print progress/status. - example: "~M27" - - M28: - summary: Start File Upload - description: >- - Begin file upload to printer storage. - Format: ~M28 0:/user/ - - **Upload Flow:** - 1. Send `~M28 0:/user/` - 2. Server responds with `ack: M28 ...` - 3. Send exactly `` bytes of raw file data. - Do NOT prefix data with `~`. - 4. Server automatically detects end of file based on size. - 5. Send `~M29` to confirm and finalize. - args: - size: int # File size in bytes - path: string # Destination path (must start with 0:/user/) - example: "~M28 1024 0:/user/upload_test.gcode" - - M29: - summary: Stop File Upload - description: >- - Finalize file upload. Verifies the file size on disk matches the size declared in M28. - If sizes match, the file is kept. If not, it is deleted. - example: "~M29" - - M601: - summary: Pause Print (Internal) - description: Pauses print, stops timers, updates status. - example: "~M601" - - M602: - summary: Resume Print (Internal) - description: Resumes print from paused state. - example: "~M602" - - # --- Temperature & Environment --- - M104: - summary: Set Extruder Temperature - description: Set target temperature for the extruder. - args: - S: int # Temperature in Celsius - example: "~M104 S200" - - M105: - summary: Get Temperature - description: >- - Returns current temperatures. - Response format: "T0:/ B:/" - example: "~M105" - - M106: - summary: Fan On - description: Turn on the part cooling fan. - example: "~M106" - - M107: - summary: Fan Off - description: Turn off the part cooling fan. - example: "~M107" - - M108: - summary: Cancel Heat/Wait - description: Cancel heating wait loop (inferred). - example: "~M108" - - M140: - summary: Set Bed Temperature - description: Set target temperature for the heated bed. - args: - S: int # Temperature in Celsius - example: "~M140 S60" - - M146: - summary: LED Control - description: Control built-in LEDs. - args: - S: int # Brightness/State (e.g., S100 for on/max) - r: int # Red component (inferred) - g: int # Green component (inferred) - b: int # Blue component (inferred) - example: "~M146 r255 g0 b0" - - # --- Status & Info --- - M112: - summary: Emergency Stop - description: Immediate halt of all operations. - example: "~M112" - - M114: - summary: Get Current Position - description: Returns current axis coordinates. - example: "~M114" - - M115: - summary: Get Firmware Info - description: >- - Returns machine type, name, firmware version, serial number, - build volume dimensions (X/Y/Z), and tool count. - example: "~M115" - - M119: - summary: Get Endstop Status - description: Returns status of endstops, filament sensor, and machine state. - example: "~M119" - - # --- Unknown / Custom --- - M144: - summary: Unknown (Bed?) - description: Referenced in command handler map. - - M145: - summary: Material Preset? - description: Referenced in command handler map. - - M610: - summary: Unknown - description: Referenced in command handler map. - - M650: - summary: Unknown - description: Referenced in command handler map. - - M651: - summary: Unknown - description: Referenced in command handler map. - - M652: - summary: Unknown - description: Referenced in command handler map. - - M661: - summary: Unknown - description: Referenced in command handler map. - - M662: - summary: Unknown - description: Referenced in command handler map. - - M663: - summary: Unknown - description: Referenced in command handler map. \ No newline at end of file diff --git a/flashforge-dashboard/Dockerfile b/flashforge-dashboard/Dockerfile new file mode 100644 index 0000000..2ecd131 --- /dev/null +++ b/flashforge-dashboard/Dockerfile @@ -0,0 +1,21 @@ +# BUILD_FROM is injected by the HA build system based on build.yaml. +# The default below is a local-dev fallback for x86_64 machines only. +ARG BUILD_FROM=ghcr.io/home-assistant/amd64-base:latest +FROM ${BUILD_FROM} + +# Install Node.js +RUN apk add --no-cache nodejs npm ffmpeg + +# Copy application +WORKDIR /app +COPY package.json ./ +RUN npm install --omit=dev --no-fund --no-audit + +COPY server.js ./ +COPY frontend/ ./frontend/ + +# Start script +COPY run.sh / +RUN chmod +x /run.sh + +CMD ["/run.sh"] diff --git a/flashforge-dashboard/build.yaml b/flashforge-dashboard/build.yaml new file mode 100644 index 0000000..ce42834 --- /dev/null +++ b/flashforge-dashboard/build.yaml @@ -0,0 +1,5 @@ +build_from: + amd64: ghcr.io/home-assistant/amd64-base:latest + aarch64: ghcr.io/home-assistant/aarch64-base:latest + armv7: ghcr.io/home-assistant/armv7-base:latest + armhf: ghcr.io/home-assistant/armhf-base:latest diff --git a/flashforge-dashboard/config.yaml b/flashforge-dashboard/config.yaml new file mode 100644 index 0000000..f9c6a37 --- /dev/null +++ b/flashforge-dashboard/config.yaml @@ -0,0 +1,63 @@ +name: FlashForge Dashboard +description: >- + Dashboard locale per stampanti FlashForge AD5M / AD5X / 5M Pro. + Mostra camera live, stato di stampa, temperature, layer, controlli + pausa/riprendi/stop, lista file in memoria e upload GCode. +version: "1.0.0" +slug: flashforge_dashboard +init: false +homeassistant: "2023.1" + +arch: + - amd64 + - aarch64 + - armv7 + - armhf + +# Ingress: accessibile dalla sidebar di HA senza aprire porte extra. +# NOTA: ingress_port è ora 8100 (porta interna senza auth). +# La porta 8099 è esposta all'esterno e richiede autenticazione. +ingress: true +ingress_port: 8100 +panel_icon: mdi:printer-3d +panel_title: FlashForge +webui: "http://[HOST]:[PORT:8099]" +ports: + 8099/tcp: 8099 +ports_description: + 8099/tcp: Dashboard HTTP (accesso diretto, con auth se configurata) + +# Opzioni configurabili dal pannello HA (Add-on → Configurazione) +options: + printer_ip: "" + serial_number: "" + check_code: "" + mqtt_enabled: true + mqtt_host: "core-mosquitto" + mqtt_port: 1883 + mqtt_username: "" + mqtt_password: "" + mqtt_base_topic: "flashforge" + go2rtc_url: "http://ccab4aaf-frigate:1984" # URL base di go2rtc (porta 1984 anche se embedded in Frigate) + go2rtc_stream: "Stampante" # Nome dello stream configurato in Frigate/go2rtc + # Credenziali per l'accesso diretto sulla porta 8099. + # Se lasciate vuote, la porta 8099 non richiede autenticazione. + auth_username: "" + auth_password: "" + +schema: + printer_ip: str + serial_number: str + check_code: str + mqtt_enabled: bool + mqtt_host: str + mqtt_port: port + mqtt_username: str? + mqtt_password: password? + mqtt_base_topic: str + go2rtc_url: str? + go2rtc_stream: str? + auth_username: str? + auth_password: password? + +url: "https://github.com/MikManenti/flashforge-api-docs" diff --git a/flashforge-dashboard/frontend/public/app.js b/flashforge-dashboard/frontend/public/app.js new file mode 100644 index 0000000..60034de --- /dev/null +++ b/flashforge-dashboard/frontend/public/app.js @@ -0,0 +1,471 @@ +'use strict'; + +// window.INGRESS_PATH is injected at runtime by server.js when running as a +// Home Assistant add-on. It is the URL prefix HA uses for the ingress proxy +// (e.g. "/api/hassio_ingress/abc123"). When running standalone it is undefined. +// Fallback: detect ingress prefix directly from current URL path. +function detectIngressFromPath(pathname) { + const match = pathname.match(/^\/api\/hassio_ingress\/[^/]+/); + return match ? match[0] : ''; +} +const detectedIngress = detectIngressFromPath(window.location.pathname); +const BASE = (window.INGRESS_PATH || detectedIngress || '').replace(/\/$/, ''); + +/** Stream name injected by server.js when go2rtc is configured, otherwise null. */ +const GO2RTC_STREAM = window.GO2RTC_STREAM || null; + +/* ── State ───────────────────────────────────────────────────────────────── */ +let currentJobID = null; +let currentStatus = null; +let pollingTimer = null; + +/* ── DOM refs ────────────────────────────────────────────────────────────── */ +const badge = document.getElementById('status-badge'); +const cameraImg = document.getElementById('camera-img'); +const cameraPlaceholder = document.getElementById('camera-placeholder'); +const btnCameraOn = document.getElementById('btn-camera-on'); +const btnCameraOff = document.getElementById('btn-camera-off'); +const sFname = document.getElementById('s-filename'); +const sProgress = document.getElementById('s-progress'); +const sLayer = document.getElementById('s-layer'); +const sTime = document.getElementById('s-time'); +const progressBar = document.getElementById('progress-bar'); +const tNozzle = document.getElementById('t-nozzle'); +const tNozzleTarget = document.getElementById('t-nozzle-target'); +const tBed = document.getElementById('t-bed'); +const tBedTarget = document.getElementById('t-bed-target'); +const tChamber = document.getElementById('t-chamber'); +const tChamberTarget = document.getElementById('t-chamber-target'); +const btnPause = document.getElementById('btn-pause'); +const btnResume = document.getElementById('btn-resume'); +const btnStop = document.getElementById('btn-stop'); +const btnClearState = document.getElementById('btn-clear-state'); +const ctrlMsg = document.getElementById('ctrl-message'); +const lastUpdate = document.getElementById('last-update'); + +const btnRefreshFiles = document.getElementById('btn-refresh-files'); +const fileList = document.getElementById('file-list'); +const printModal = document.getElementById('print-modal'); +const modalFilename = document.getElementById('modal-filename'); +const modalLeveling = document.getElementById('modal-leveling'); +const modalConfirm = document.getElementById('modal-confirm'); +const modalCancel = document.getElementById('modal-cancel'); + +const uploadForm = document.getElementById('upload-form'); +const fileInput = document.getElementById('file-input'); +const dropText = document.getElementById('drop-text'); +const dropArea = document.getElementById('drop-area'); +const printNowChk = document.getElementById('print-now'); +const levelingUpload = document.getElementById('leveling-upload'); +const btnUpload = document.getElementById('btn-upload'); +const uploadProgressWrap = document.getElementById('upload-progress'); +const uploadProgressBar = document.getElementById('upload-progress-bar'); +const uploadProgressText = document.getElementById('upload-progress-text'); +const uploadMessage = document.getElementById('upload-message'); + +/* ── Utilities ───────────────────────────────────────────────────────────── */ +function fmt(v, unit = '°C') { + return v !== undefined && v !== null ? `${Math.round(v)}${unit}` : '—'; +} +function fmtTime(seconds) { + if (!seconds || seconds < 0) return '—'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + if (h > 0) return `${h}h ${m}m`; + if (m > 0) return `${m}m ${s}s`; + return `${s}s`; +} +function showCtrlMsg(msg, ok = true) { + ctrlMsg.textContent = msg; + ctrlMsg.style.color = ok ? 'var(--success)' : 'var(--danger)'; + setTimeout(() => { ctrlMsg.textContent = ''; }, 4000); +} +function showUploadMsg(msg, ok = true) { + uploadMessage.textContent = msg; + uploadMessage.style.color = ok ? 'var(--success)' : 'var(--danger)'; +} +function normalizeStatus(status) { + return String(status || 'ready').trim().toUpperCase(); +} + +/* ── Status polling ──────────────────────────────────────────────────────── */ +async function fetchStatus() { + try { + const res = await fetch(`${BASE}/api/status`); + const json = await res.json(); + if (!res.ok || json.error) { + badge.textContent = 'Errore connessione'; + badge.className = 'badge badge--error'; + lastUpdate.textContent = `Errore: ${json.error || res.statusText}`; + return; + } + if (json.detail) updateUI(json.detail); + lastUpdate.textContent = `Ultimo aggiornamento: ${new Date().toLocaleTimeString('it-IT')}`; + } catch (e) { + badge.textContent = 'Errore connessione'; + badge.className = 'badge badge--error'; + lastUpdate.textContent = `Errore: ${e.message}`; + } +} + +function updateUI(d) { + currentStatus = normalizeStatus(d.status); + + // Badge + const statusMap = { + READY: ['badge--idle', 'PRONTA'], + BUSY: ['badge--printing', 'OCCUPATA'], + HEATING: ['badge--printing', 'RISCALDAMENTO'], + PRINTING: ['badge--printing', 'STAMPA'], + PAUSING: ['badge--paused', 'IN PAUSA'], + PAUSED: ['badge--paused', 'IN PAUSA'], + COMPLETED:['badge--success', 'COMPLETATA'], + CANCEL: ['badge--error', 'ANNULLATA'], + IDLE: ['badge--idle', 'INATTIVA'], + ERROR: ['badge--error', 'ERRORE'], + HOMING: ['badge--printing', 'HOMING'], + CALIBRATE_DOING: ['badge--printing', 'CALIBRAZIONE'], + }; + const [cls, label] = statusMap[currentStatus] || ['badge--idle', currentStatus]; + badge.className = `badge ${cls}`; + badge.textContent = label; + + // Job info + currentJobID = d.jobInfo ? (d.jobInfo[0] && d.jobInfo[0][1]) : ''; + sFname.textContent = d.printFileName || '—'; + sFname.title = d.printFileName || ''; + + const pct = d.printProgress != null ? Math.round(d.printProgress * 100) : null; + sProgress.textContent = pct !== null ? `${pct}%` : '—'; + progressBar.style.width = pct !== null ? `${pct}%` : '0%'; + + const layer = d.printLayer ?? null; + const maxLayer = d.targetPrintLayer ?? null; + sLayer.textContent = (layer !== null && maxLayer !== null) + ? `${layer} / ${maxLayer}` + : (layer !== null ? String(layer) : '—'); + + sTime.textContent = fmtTime(d.estimatedTime); + + // Temperatures + tNozzle.textContent = fmt(d.rightTemp); + tNozzleTarget.textContent = d.rightTargetTemp ? `→ ${fmt(d.rightTargetTemp)}` : ''; + tBed.textContent = fmt(d.platTemp); + tBedTarget.textContent = d.platTargetTemp ? `→ ${fmt(d.platTargetTemp)}` : ''; + tChamber.textContent = fmt(d.chamberTemp); + tChamberTarget.textContent = d.chamberTargetTemp ? `→ ${fmt(d.chamberTargetTemp)}` : ''; + + // Controls enable/disable + const hasControllableJob = Boolean(d.printFileName || currentJobID || pct !== null); + const canPause = hasControllableJob && ['PRINTING', 'BUSY', 'HEATING'].includes(currentStatus); + const isPaused = currentStatus === 'PAUSED'; + const canStop = hasControllableJob && ['PRINTING', 'BUSY', 'HEATING', 'PAUSED', 'PAUSING'].includes(currentStatus); + const canClearState = ['BUSY', 'COMPLETED', 'CANCEL'].includes(currentStatus); + btnPause.disabled = !canPause; + btnResume.disabled = !isPaused; + btnStop.disabled = !canStop; + btnClearState.disabled = !canClearState; + +} + +/* ── Camera ──────────────────────────────────────────────────────────────── */ + +/** + * Initialise (or re-initialise) the camera stream. + * client.js is loaded statically in index.html with `defer`, so the + * `` custom element will be registered before or shortly after + * this runs. Setting `src` before registration is safe: the browser queues the + * attribute and processes it once the element is upgraded. + */ +function initCamera() { + const streamName = window.GO2RTC_STREAM || 'Stampante'; + cameraPlaceholder.classList.add('hidden'); + + if (cameraImg) { + cameraImg.classList.add('active'); + const mjpegUrl = `${BASE}/api/go2rtc/mjpeg?src=${encodeURIComponent(streamName)}&t=${Date.now()}`; + console.log(`[Camera] Requesting MJPEG stream at: ${mjpegUrl}`); + cameraImg.src = mjpegUrl; + } +} + +function disableCamera() { + if (cameraImg) { + cameraImg.src = ''; + cameraImg.classList.remove('active'); + } + cameraPlaceholder.classList.remove('hidden'); +} + +btnCameraOn.addEventListener('click', async () => { + if (!GO2RTC_STREAM) return; + initCamera(); +}); + +btnCameraOff.addEventListener('click', async () => { + disableCamera(); +}); + +/* ── Print controls ──────────────────────────────────────────────────────── */ +async function sendControl(action) { + const actionMap = { + pause: 'pause', + resume: 'continue', + stop: 'cancel', + }; + const printerAction = actionMap[action] || action; + const canSendWithoutJobId = ['PRINTING', 'BUSY', 'HEATING', 'PAUSED', 'PAUSING'].includes(currentStatus); + if (!currentJobID && !canSendWithoutJobId) { + showCtrlMsg('Nessun job attivo.', false); + return; + } + try { + const res = await fetch(`${BASE}/api/control`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: printerAction, jobID: currentJobID || '' }), + }); + const json = await res.json(); + if (res.ok && json.code === 0) { + showCtrlMsg(`Comando "${action}" inviato.`); + await fetchStatus(); + } else { + showCtrlMsg(`Errore: ${json.error || json.message || json.code}`, false); + } + } catch (e) { + showCtrlMsg(`Errore di rete: ${e.message}`, false); + } +} + +async function clearPrinterState() { + try { + const res = await fetch(`${BASE}/api/state/clear`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + const json = await res.json(); + if (res.ok && json.code === 0) { + showCtrlMsg('Stato stampante ripulito.'); + await fetchStatus(); + } else { + showCtrlMsg(`Errore: ${json.error || json.message || json.code}`, false); + } + } catch (e) { + showCtrlMsg(`Errore di rete: ${e.message}`, false); + } +} + +btnPause.addEventListener('click', () => sendControl('pause')); +btnResume.addEventListener('click', () => sendControl('resume')); +btnStop.addEventListener('click', async () => { + if (!confirm('Sei sicuro di voler interrompere la stampa?')) return; + await sendControl('stop'); +}); +btnClearState.addEventListener('click', async () => { + if (!confirm('Vuoi ripulire lo stato della stampante e riportarla in ready?')) return; + await clearPrinterState(); +}); + +/* ── File list ───────────────────────────────────────────────────────────── */ +let selectedFileName = null; + +btnRefreshFiles.addEventListener('click', loadFiles); + +async function loadFiles() { + fileList.innerHTML = '

Caricamento…

'; + try { + const res = await fetch(`${BASE}/api/files`); + const contentType = res.headers.get('content-type') || ''; + const text = await res.text(); + const json = contentType.includes('application/json') ? JSON.parse(text) : null; + if (!json) { + throw new Error(`Risposta non JSON (HTTP ${res.status})`); + } + const files = json.gcodeList || []; + if (!files.length) { + fileList.innerHTML = '

Nessun file trovato nella stampante.

'; + return; + } + fileList.innerHTML = ''; + files.forEach(renderFileItem); + } catch (e) { + fileList.innerHTML = `

Errore: ${e.message}

`; + } +} + +async function renderFileItem(fileName) { + const item = document.createElement('div'); + item.className = 'file-item'; + + // Thumb + const thumbWrap = document.createElement('div'); + thumbWrap.className = 'file-thumb-placeholder'; + thumbWrap.textContent = '📄'; + item.appendChild(thumbWrap); + + const nameEl = document.createElement('span'); + nameEl.className = 'file-name'; + nameEl.textContent = fileName; + item.appendChild(nameEl); + + item.addEventListener('click', () => openPrintModal(fileName)); + fileList.appendChild(item); + + // Async thumb load + try { + const res = await fetch(`${BASE}/api/thumb?fileName=${encodeURIComponent(fileName)}`); + const json = await res.json(); + if (json.imageData) { + const img = document.createElement('img'); + img.className = 'file-thumb'; + img.src = `data:image/png;base64,${json.imageData}`; + img.alt = fileName; + thumbWrap.replaceWith(img); + } + } catch (_) { /* keep placeholder */ } +} + +function openPrintModal(fileName) { + selectedFileName = fileName; + modalFilename.textContent = fileName; + modalLeveling.checked = false; + printModal.classList.remove('hidden'); +} + +modalCancel.addEventListener('click', () => { + printModal.classList.add('hidden'); + selectedFileName = null; +}); + +modalConfirm.addEventListener('click', async () => { + if (!selectedFileName) return; + printModal.classList.add('hidden'); + try { + const res = await fetch(`${BASE}/api/print`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fileName: selectedFileName, levelingBeforePrint: modalLeveling.checked }), + }); + const json = await res.json(); + if (json.code === 0) { + showCtrlMsg(`Stampa avviata: ${selectedFileName}`); + await fetchStatus(); + } else { + showCtrlMsg(`Errore: ${json.message || json.code}`, false); + } + } catch (e) { + showCtrlMsg(`Errore di rete: ${e.message}`, false); + } + selectedFileName = null; +}); + +// Close modal on backdrop click +printModal.addEventListener('click', (e) => { + if (e.target === printModal) { + printModal.classList.add('hidden'); + selectedFileName = null; + } +}); + +/* ── Upload ──────────────────────────────────────────────────────────────── */ +fileInput.addEventListener('change', () => { + if (fileInput.files[0]) { + dropText.textContent = `📄 ${fileInput.files[0].name}`; + btnUpload.disabled = false; + } +}); + +// Drag & drop +dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.classList.add('drag-over'); }); +dropArea.addEventListener('dragleave', () => dropArea.classList.remove('drag-over')); +dropArea.addEventListener('drop', (e) => { + e.preventDefault(); + dropArea.classList.remove('drag-over'); + const file = e.dataTransfer.files[0]; + if (file) { + const dt = new DataTransfer(); + dt.items.add(file); + fileInput.files = dt.files; + dropText.textContent = `📄 ${file.name}`; + btnUpload.disabled = false; + } +}); + +uploadForm.addEventListener('submit', async (e) => { + e.preventDefault(); + const file = fileInput.files[0]; + if (!file) return; + + btnUpload.disabled = true; + uploadProgressWrap.classList.remove('hidden'); + setUploadPct(0); + showUploadMsg(''); + + const formData = new FormData(); + formData.append('gcodeFile', file); + formData.append('printNow', printNowChk.checked ? '1' : '0'); + formData.append('levelingBeforePrint', levelingUpload.checked ? '1' : '0'); + + // Use XMLHttpRequest for upload progress + const xhr = new XMLHttpRequest(); + xhr.open('POST', `${BASE}/api/upload`); + + xhr.upload.addEventListener('progress', (ev) => { + if (ev.lengthComputable) setUploadPct(Math.round((ev.loaded / ev.total) * 100)); + }); + + xhr.addEventListener('load', () => { + try { + const json = JSON.parse(xhr.responseText); + if (json.code === 0) { + showUploadMsg(`✅ Upload completato: ${file.name}`); + if (printNowChk.checked) fetchStatus(); + loadFiles(); + } else { + showUploadMsg(`❌ Errore stampante: ${json.message || json.code}`, false); + } + } catch (_) { + showUploadMsg(`❌ Risposta non valida (HTTP ${xhr.status})`, false); + } + btnUpload.disabled = false; + uploadProgressWrap.classList.add('hidden'); + }); + + xhr.addEventListener('error', () => { + showUploadMsg('❌ Errore di rete durante l\'upload.', false); + btnUpload.disabled = false; + uploadProgressWrap.classList.add('hidden'); + }); + + xhr.send(formData); +}); + +function setUploadPct(pct) { + uploadProgressBar.style.setProperty('--pct', `${pct}%`); + uploadProgressText.textContent = `${pct}%`; +} + +/* ── Boot ────────────────────────────────────────────────────────────────── */ +(async () => { + // Check configuration + try { + const cfg = await fetch(`${BASE}/api/config`).then(r => r.json()); + if (!cfg.configured) { + badge.textContent = 'NON CONFIGURATO'; + badge.className = 'badge badge--error'; + document.querySelector('main').insertAdjacentHTML('afterbegin', + `
+ ⚠️ La stampante non è configurata. + Vai su Impostazioni → Add-on → FlashForge Dashboard → Configurazione + e inserisci printer_ip, serial_number e check_code. +
` + ); + return; + } + } catch (_) { /* proceed anyway */ } + + initCamera(); + await fetchStatus(); + pollingTimer = setInterval(fetchStatus, 4000); +})(); diff --git a/flashforge-dashboard/frontend/public/index.html b/flashforge-dashboard/frontend/public/index.html new file mode 100644 index 0000000..2fb6861 --- /dev/null +++ b/flashforge-dashboard/frontend/public/index.html @@ -0,0 +1,135 @@ + + + + + + FlashForge Dashboard + + + + +
+
+

🖨 FlashForge Dashboard

+
+
+
+
+ +
+

Camera

+
+ Camera stream +
+ Camera non disponibile +
+
+
+ + +
+
+ +
+

Stato stampa

+
+
+
+
+
+ File + +
+
+ 🔥 Ugello + + +
+
+ 🛏 Piatto + + +
+
+ 📦 Camera + + +
+
+ Layer + +
+
+ Progresso + +
+
+ Tempo rimanente + +
+ + + + +
+
+
+ +
+

File in memoria

+
+ +
+
+

Premi "Aggiorna lista" per caricare i file.

+
+ + +
+ +
+

Carica GCode

+
+
+ + +
+
+ + +
+ +
+ +
+
+
+
+ Non ancora aggiornato + dati aggiornati ogni 4s • camera manuale +
+ + + \ No newline at end of file diff --git a/flashforge-dashboard/frontend/public/style.css b/flashforge-dashboard/frontend/public/style.css new file mode 100644 index 0000000..b1b7629 --- /dev/null +++ b/flashforge-dashboard/frontend/public/style.css @@ -0,0 +1,388 @@ +/* ── Reset & base ─────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #0f1117; + --surface: #1a1d27; + --surface2: #222538; + --border: #2e3150; + --text: #e4e6f1; + --text-muted: #7a7f9a; + --accent: #4f8ef7; + --success: #3ecf74; + --warning: #f0a500; + --danger: #e04c4c; + --radius: 10px; + --shadow: 0 4px 24px rgba(0,0,0,.35); + --transition: .2s ease; +} + +body { + background: var(--bg); + color: var(--text); + font-family: 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + min-height: 100vh; +} + +/* ── Header ─────────────────────────────────────────────────────────────── */ +header { + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 0 24px; + height: 56px; + display: flex; + align-items: center; +} +.header-inner { + width: 100%; + max-width: 860px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} +header h1 { font-size: 18px; font-weight: 600; letter-spacing: .5px; } + +/* ── Badge ───────────────────────────────────────────────────────────────── */ +.badge { + padding: 4px 14px; + border-radius: 20px; + font-size: 12px; + font-weight: 700; + letter-spacing: .6px; + text-transform: uppercase; +} +.badge--idle { background: rgba(122,127,154,.2); color: var(--text-muted); } +.badge--printing { background: rgba(79,142,247,.2); color: var(--accent); } +.badge--paused { background: rgba(240,165,0,.2); color: var(--warning); } +.badge--success { background: rgba(62,207,116,.2); color: var(--success); } +.badge--error { background: rgba(224,76,76,.2); color: var(--danger); } + +/* ── Main grid ──────────────────────────────────────────────────────────── */ +main { + max-width: 860px; + margin: 24px auto; + padding: 0 24px; + display: flex; + flex-direction: column; + gap: 20px; +} + +/* ── Card ────────────────────────────────────────────────────────────────── */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + box-shadow: var(--shadow); +} +.card h2 { + font-size: 13px; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--text-muted); + margin-bottom: 16px; +} + +/* ── Camera ─────────────────────────────────────────────────────────────── */ + +.camera-wrap { + width: 100%; + aspect-ratio: 16/9; + background: #000; + border-radius: 6px; + overflow: hidden; + position: relative; + margin-bottom: 12px; +} +#camera-rtc { + width: 100%; + height: 100%; + display: none; + background: #000; +} +#camera-rtc.active { display: block; } +/* video element rendered inside video-rtc custom element */ +#camera-rtc video { + width: 100%; + height: 100%; + object-fit: contain; +} +#camera-img { + width: 100%; + height: 100%; + object-fit: contain; + display: none; +} +#camera-img.active { display: block; } +.camera-placeholder { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 13px; +} +.camera-placeholder.hidden { display: none; } +.camera-controls { display: flex; gap: 10px; } + +/* ── Stat grid ──────────────────────────────────────────────────────────── */ +.stat { + background: var(--surface2); + border-radius: 8px; + padding: 12px 14px; +} +.stat--full { grid-column: 1 / -1; } +.stat-label { + display: block; + font-size: 11px; + text-transform: uppercase; + letter-spacing: .6px; + color: var(--text-muted); + margin-bottom: 4px; +} +.stat-value { + display: block; + font-size: 18px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.stat-value--compact { font-size: 16px; } + +/* ── Progress bar ────────────────────────────────────────────────────────── */ +.progress-bar-wrap { + background: var(--surface2); + border-radius: 4px; + height: 8px; + overflow: hidden; + margin-bottom: 16px; +} +.progress-bar { + height: 100%; + width: 0%; + background: var(--accent); + border-radius: 4px; + transition: width .5s ease; +} + +/* ── Pairs grid (temps + stats + controls in 2 columns) ─────────────────── */ +.pairs-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 12px; +} +.pairs-grid .btn { width: 100%; } + +/* ── Temp cards ─────────────────────────────────────────────────────────── */ +.temp-card { + background: var(--surface2); + border-radius: 8px; + padding: 10px 12px; + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 6px 10px; +} +.temp-card--full { grid-column: 1 / -1; } +.temp-label { font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: .5px; } +.temp-value { font-size: 18px; font-weight: 700; min-width: 0; } +.temp-target { font-size: 11px; color: var(--text-muted); justify-self: end; } + +/* ── Print controls ─────────────────────────────────────────────────────── */ +.ctrl-message { margin-top: 10px; font-size: 12px; color: var(--text-muted); min-height: 16px; } + +/* ── Buttons ─────────────────────────────────────────────────────────────── */ +.btn { + padding: 8px 18px; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: opacity var(--transition), transform var(--transition); +} +.btn:hover:not(:disabled) { opacity: .85; transform: translateY(-1px); } +.btn:active:not(:disabled) { transform: translateY(0); } +.btn:disabled { opacity: .35; cursor: not-allowed; } + +.btn--primary { background: var(--accent); color: #fff; } +.btn--secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); } +.btn--success { background: var(--success); color: #000; } +.btn--warning { background: var(--warning); color: #000; } +.btn--danger { background: var(--danger); color: #fff; } + +/* ── File list ──────────────────────────────────────────────────────────── */ + +.files-toolbar { margin-bottom: 12px; } + +.file-list { + max-height: 320px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} +.file-item { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 14px; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + transition: background var(--transition); +} +.file-item:hover { background: #2a2e45; } +.file-thumb { + width: 44px; + height: 44px; + object-fit: contain; + border-radius: 4px; + background: #000; + flex-shrink: 0; +} +.file-thumb-placeholder { + width: 44px; + height: 44px; + background: var(--surface); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} +.file-name { flex: 1; font-size: 13px; word-break: break-all; } +.hint { color: var(--text-muted); font-size: 12px; } + +/* ── Modal ───────────────────────────────────────────────────────────────── */ +.modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} +.modal.hidden { display: none; } +.modal-box { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 28px; + width: 360px; + box-shadow: var(--shadow); +} +.modal-box h3 { margin-bottom: 12px; } +.modal-box p { margin-bottom: 16px; color: var(--text-muted); font-size: 13px; } +.modal-actions { display: flex; gap: 10px; margin-top: 18px; } + +/* ── Upload ─────────────────────────────────────────────────────────────── */ + +.file-drop-area { + border: 2px dashed var(--border); + border-radius: 8px; + padding: 28px 16px; + text-align: center; + cursor: pointer; + transition: border-color var(--transition), background var(--transition); + margin-bottom: 14px; +} +.file-drop-area.drag-over { + border-color: var(--accent); + background: rgba(79,142,247,.07); +} +.file-drop-area input[type="file"] { display: none; } +.file-drop-label { cursor: pointer; color: var(--text-muted); font-size: 13px; } +.file-drop-label span { display: block; } + +.upload-options { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; } +.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; } +.checkbox-label input { accent-color: var(--accent); } + +.upload-progress { + margin-top: 10px; + display: flex; + align-items: center; + gap: 10px; +} +.upload-progress.hidden { display: none; } +.upload-progress-bar { + flex: 1; + height: 6px; + background: var(--surface2); + border-radius: 4px; + overflow: hidden; + position: relative; +} +.upload-progress-bar::after { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: var(--pct, 0%); + background: var(--accent); + transition: width .3s ease; +} + +/* ── Footer ─────────────────────────────────────────────────────────────── */ +footer { + border-top: 1px solid var(--border); + padding: 12px 24px; + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--text-muted); + max-width: 860px; + margin: 0 auto; +} + +@media (max-width: 640px) { + header { padding: 0 14px; } + header h1 { font-size: 15px; } + .badge { font-size: 11px; padding: 4px 10px; } + + main { + margin: 14px auto; + padding: 0 14px; + gap: 14px; + } + + .card { padding: 14px; } + + .stat { padding: 9px 10px; } + .stat-value { font-size: 15px; white-space: normal; } + .stat-value--compact { font-size: 14px; } + + .temp-card { padding: 8px 10px; } + .temp-value { font-size: 16px; } + + .file-list { max-height: 240px; } + .file-item { padding: 8px 10px; gap: 8px; } + + .modal-box { + width: calc(100vw - 28px); + padding: 16px; + } + .modal-actions { + flex-direction: column; + } + + footer { + flex-direction: column; + gap: 4px; + align-items: flex-start; + padding: 10px 14px; + } +} + +/* ── Scrollbar ───────────────────────────────────────────────────────────── */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } diff --git a/flashforge-dashboard/package-lock.json b/flashforge-dashboard/package-lock.json new file mode 100644 index 0000000..8cfae5f --- /dev/null +++ b/flashforge-dashboard/package-lock.json @@ -0,0 +1,1575 @@ +{ + "name": "flashforge-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flashforge-dashboard", + "version": "1.0.0", + "dependencies": { + "express": "^4.19.2", + "mqtt": "^5.15.1", + "multer": "^1.4.5-lts.1", + "node-fetch": "^2.7.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/broker-factory": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.14.tgz", + "integrity": "sha512-L45k5HMbPIrMid0nTOZ/UPXG/c0aRuQKVrSDFIb1zOkvfiyHgYmIjc3cSiN1KwQIvRDOtKE0tfb3I9EZ3CmpQQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-unique-numbers": { + "version": "9.0.27", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz", + "integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.2.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mqtt": { + "version": "5.15.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz", + "integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.21", + "@types/ws": "^8.18.1", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.1", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.2", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "rfdc": "^1.4.1", + "socks": "^2.8.6", + "split2": "^4.2.0", + "worker-timers": "^8.0.23", + "ws": "^8.18.3" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt-packet/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mqtt/node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/mqtt/node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mqtt/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mqtt/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mqtt/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/number-allocator/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/number-allocator/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/worker-factory": { + "version": "7.0.49", + "resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.49.tgz", + "integrity": "sha512-lW7tpgy6aUv2dFsQhv1yv+XFzdkCf/leoKRTGMPVK5/die6RrUjqgJHJf556qO+ZfytNG6wPXc17E8zzsOLUDw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1" + } + }, + "node_modules/worker-timers": { + "version": "8.0.31", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.31.tgz", + "integrity": "sha512-ngkq5S6JuZyztom8tDgBzorLo9byhBMko/sXfgiUD945AuzKGg1GCgDMCC3NaYkicLpGKXutONM36wEX8UbBCA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-timers-broker": "^8.0.16", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-broker": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.16.tgz", + "integrity": "sha512-JyP3AvUGyPGbBGW7XiUewm2+0pN/aYo1QpVf5kdXAfkDZcN3p7NbWrG6XnyDEpDIvfHk/+LCnOW/NsuiU9riYA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "broker-factory": "^3.1.14", + "fast-unique-numbers": "^9.0.27", + "tslib": "^2.8.1", + "worker-timers-worker": "^9.0.14" + } + }, + "node_modules/worker-timers-worker": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.14.tgz", + "integrity": "sha512-/qF06C60sXmSLfUl7WglvrDIbspmPOM8UrG63Dnn4bi2x4/DfqHS/+dxF5B+MdHnYO5tVuZYLHdAodrKdabTIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "tslib": "^2.8.1", + "worker-factory": "^7.0.49" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/flashforge-dashboard/package.json b/flashforge-dashboard/package.json new file mode 100644 index 0000000..11d12fa --- /dev/null +++ b/flashforge-dashboard/package.json @@ -0,0 +1,15 @@ +{ + "name": "flashforge-dashboard", + "version": "1.0.0", + "description": "Home Assistant add-on: FlashForge 3D printer dashboard", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.19.2", + "mqtt": "^5.15.1", + "multer": "^1.4.5-lts.1", + "node-fetch": "^2.7.0" + } +} diff --git a/flashforge-dashboard/run.sh b/flashforge-dashboard/run.sh new file mode 100644 index 0000000..75483fc --- /dev/null +++ b/flashforge-dashboard/run.sh @@ -0,0 +1,47 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# FlashForge Dashboard – Home Assistant add-on start script +# Reads options from /data/options.json via bashio and launches the Node server. +# ============================================================================== + +bashio::log.info "Starting FlashForge Dashboard..." + +export PRINTER_IP="$(bashio::config 'printer_ip')" +export SERIAL_NUMBER="$(bashio::config 'serial_number')" +export CHECK_CODE="$(bashio::config 'check_code')" +export DIRECT_PORT="8099" +export INGRESS_PORT="8100" +export NODE_ENV="production" +export INGRESS_PATH="$(bashio::addon.ingress_entry)" +export AUTH_USERNAME="$(bashio::config 'auth_username')" +export AUTH_PASSWORD="$(bashio::config 'auth_password')" +export MQTT_ENABLED="$(bashio::config 'mqtt_enabled')" +export MQTT_HOST="$(bashio::config 'mqtt_host')" +export MQTT_PORT="$(bashio::config 'mqtt_port')" +export MQTT_USERNAME="$(bashio::config 'mqtt_username')" +export MQTT_PASSWORD="$(bashio::config 'mqtt_password')" +export MQTT_BASE_TOPIC="$(bashio::config 'mqtt_base_topic')" +export GO2RTC_URL="$(bashio::config 'go2rtc_url')" +export GO2RTC_STREAM="$(bashio::config 'go2rtc_stream')" + + +if bashio::config.is_empty 'printer_ip'; then + bashio::log.warning "printer_ip is not configured. Set it in the add-on Configuration tab." +fi +if bashio::config.is_empty 'serial_number'; then + bashio::log.warning "serial_number is not configured. Set it in the add-on Configuration tab." +fi +if bashio::config.is_empty 'check_code'; then + bashio::log.warning "check_code is not configured. Set it in the add-on Configuration tab." +fi + +bashio::log.info "Printer IP: ${PRINTER_IP}" +bashio::log.info "Direct access port: ${DIRECT_PORT} (auth enabled: $([ -n "${AUTH_USERNAME}" ] && echo 'yes' || echo 'no'))" +bashio::log.info "Ingress port: ${INGRESS_PORT}" +bashio::log.info "MQTT enabled: ${MQTT_ENABLED}" +bashio::log.info "MQTT broker: ${MQTT_HOST}:${MQTT_PORT}" +if [ -n "${GO2RTC_STREAM}" ]; then + bashio::log.info "go2rtc URL: ${GO2RTC_URL} / stream: ${GO2RTC_STREAM} " +fi + +exec node /app/server.js diff --git a/flashforge-dashboard/server.js b/flashforge-dashboard/server.js new file mode 100644 index 0000000..3f6e280 --- /dev/null +++ b/flashforge-dashboard/server.js @@ -0,0 +1,929 @@ +'use strict'; + +// Note: no dotenv — environment variables are injected by run.sh via bashio. + +const express = require('express'); +const multer = require('multer'); +const fetch = require('node-fetch'); +const mqtt = require('mqtt'); +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const net = require('net'); + +const app = express(); +const DIRECT_PORT = Number(process.env.DIRECT_PORT || 8099); +const INGRESS_PORT = Number(process.env.INGRESS_PORT || 8100); +const PRINTER_IP = process.env.PRINTER_IP; +const SERIAL_NUMBER = process.env.SERIAL_NUMBER; +const CHECK_CODE = process.env.CHECK_CODE; +const KNOWN_MQTT_COMMAND_PAYLOADS = new Set([ + '1', 'ON', 'TRUE', 'OPEN', 'PAUSE', 'STOP', 'CLEAR', 'PRESS', 'RESUME', 'CONTINUE', 'CLOSE', '0', 'OFF', 'FALSE', +]); + +// HA Ingress sets this env var to the URL prefix it uses when proxying +// (e.g. "/api/hassio_ingress/abc123"). The frontend needs this to build +// correct absolute URLs for fetch() calls and the camera stream. +const INGRESS_PATH = (process.env.INGRESS_PATH || '').replace(/\/$/, ''); + +const PRINTER_API = `http://${PRINTER_IP}:8898`; +const GO2RTC_URL = (process.env.GO2RTC_URL || 'http://ccab4aaf-frigate:1984').replace(/\/$/, ''); +const GO2RTC_STREAM = (process.env.GO2RTC_STREAM || 'Stampante').trim(); +const MQTT_ENABLED = parseBooleanEnv(process.env.MQTT_ENABLED, true); +const MQTT_HOST = process.env.MQTT_HOST || 'core-mosquitto'; +const MQTT_PORT = Number(process.env.MQTT_PORT || 1883); +const MQTT_USERNAME = process.env.MQTT_USERNAME || ''; +const MQTT_PASSWORD = process.env.MQTT_PASSWORD || ''; +const MQTT_BASE_TOPIC = sanitizeTopic(process.env.MQTT_BASE_TOPIC || 'flashforge'); +const MQTT_POLL_INTERVAL_MS = 10000; + +// Credenziali per la Basic Auth sulla porta diretta (8099). +// Se entrambe vuote, l'accesso diretto non richiede autenticazione. +const AUTH_USERNAME = (process.env.AUTH_USERNAME || '').trim(); +const AUTH_PASSWORD = (process.env.AUTH_PASSWORD || '').trim(); +const AUTH_ENABLED = !!(AUTH_USERNAME && AUTH_PASSWORD); + +const DEVICE_ID = String(SERIAL_NUMBER || PRINTER_IP || 'flashforge_printer') + .replace(/[^\w-]/g, '_') + .toLowerCase(); +const DEVICE_NAME = SERIAL_NUMBER ? `FlashForge ${SERIAL_NUMBER}` : 'FlashForge Printer'; +const MQTT_ROOT_TOPIC = `${MQTT_BASE_TOPIC}/${DEVICE_ID}`; +const MQTT_AVAILABILITY_TOPIC = `${MQTT_ROOT_TOPIC}/availability`; + +let mqttClient = null; +let mqttConnected = false; +let mqttDiscoveryPublished = false; +let lastPrinterDetail = null; +let cameraSwitchState = 'OFF'; +let mqttPollingTimer = null; + +// ── Middleware ────────────────────────────────────────────────────────────── +app.use(express.json()); +app.use(express.static(path.join(__dirname, 'frontend', 'public'))); + +// multer: store upload in memory, then stream to printer +const upload = multer({ storage: multer.memoryStorage() }); + +// ── Helpers ───────────────────────────────────────────────────────────────── + +/** + * POST to the printer's HTTP REST API with standard auth fields. + * Times out after PRINTER_TIMEOUT_MS to avoid hanging indefinitely. + */ +const PRINTER_TIMEOUT_MS = 8000; + +async function printerPost(endpoint, body = {}) { + const payload = { serialNumber: SERIAL_NUMBER, checkCode: CHECK_CODE, ...body }; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), PRINTER_TIMEOUT_MS); + try { + const res = await fetch(`${PRINTER_API}${endpoint}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + signal: controller.signal, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Printer returned ${res.status}: ${text}`); + } + return res.json(); + } catch (err) { + if (err.name === 'AbortError') { + throw new Error(`Printer did not respond within ${PRINTER_TIMEOUT_MS / 1000}s (${PRINTER_API})`); + } + throw err; + } finally { + clearTimeout(timer); + } +} + +async function printerControl(cmd, args = {}) { + return printerPost('/control', { + payload: { + cmd, + args, + }, + }); +} + +function sanitizeTopic(topic) { + return String(topic || 'flashforge') + .trim() + .replace(/^[\/\s]+|[\/\s]+$/g, '') + .replace(/\s+/g, '_'); +} + +function parseBooleanEnv(value, defaultValue = false) { + if (value === undefined || value === null || value === '') return defaultValue; + const normalized = String(value).trim().toLowerCase(); + if (['true', '1', 'yes', 'on'].includes(normalized)) return true; + if (['false', '0', 'no', 'off'].includes(normalized)) return false; + return defaultValue; +} + +function mqttPublish(topic, payload, options = {}) { + if (!mqttClient || !mqttConnected) return; + mqttClient.publish(topic, String(payload), { qos: 0, retain: false, ...options }); +} + +function getCurrentJobId() { + if (!lastPrinterDetail || !Array.isArray(lastPrinterDetail.jobInfo)) return ''; + return (lastPrinterDetail.jobInfo[0] && lastPrinterDetail.jobInfo[0][1]) || ''; +} + +function publishMqttState(detail) { + if (!detail || !mqttConnected) return; + + const normalizedStatus = String(detail.status || 'ready').trim().toUpperCase(); + const progress = detail.printProgress != null ? Math.round(detail.printProgress * 100) : 0; + const isPrinting = ['PRINTING', 'BUSY', 'HEATING', 'PAUSED', 'PAUSING'].includes(normalizedStatus); + const pauseSwitchState = ['PAUSED', 'PAUSING'].includes(normalizedStatus) ? 'ON' : 'OFF'; + + mqttPublish(`${MQTT_ROOT_TOPIC}/state/status`, normalizedStatus, { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/progress`, progress, { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/file_name`, detail.printFileName || '', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/nozzle_temp`, detail.rightTemp ?? '', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/bed_temp`, detail.platTemp ?? '', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/chamber_temp`, detail.chamberTemp ?? '', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/estimated_time_s`, detail.estimatedTime ?? '', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/layer_current`, detail.printLayer ?? '', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/layer_target`, detail.targetPrintLayer ?? '', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/is_printing`, isPrinting ? 'ON' : 'OFF', { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/pause_switch`, pauseSwitchState, { retain: true }); + mqttPublish(`${MQTT_ROOT_TOPIC}/state/camera_switch`, cameraSwitchState, { retain: true }); +} + +function updatePrinterDetail(detail) { + if (!detail) return; + lastPrinterDetail = detail; + publishMqttState(detail); +} + +function createMqttDeviceInfo() { + return { + identifiers: [DEVICE_ID], + name: DEVICE_NAME, + manufacturer: 'FlashForge', + model: 'AD5 Series', + }; +} + +function publishMqttDiscovery() { + if (!mqttConnected || mqttDiscoveryPublished) return; + const device = createMqttDeviceInfo(); + const discoveryBase = 'homeassistant'; + const publishDiscovery = (component, objectId, payload) => { + const topic = `${discoveryBase}/${component}/${DEVICE_ID}/${objectId}/config`; + mqttPublish(topic, JSON.stringify(payload), { retain: true }); + }; + + publishDiscovery('sensor', 'status', { + name: 'Status', + unique_id: `${DEVICE_ID}_status`, + state_topic: `${MQTT_ROOT_TOPIC}/state/status`, + icon: 'mdi:printer-3d-nozzle-alert', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('sensor', 'progress', { + name: 'Progress', + unique_id: `${DEVICE_ID}_progress`, + state_topic: `${MQTT_ROOT_TOPIC}/state/progress`, + unit_of_measurement: '%', + icon: 'mdi:progress-clock', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('sensor', 'nozzle_temp', { + name: 'Nozzle Temperature', + unique_id: `${DEVICE_ID}_nozzle_temp`, + state_topic: `${MQTT_ROOT_TOPIC}/state/nozzle_temp`, + unit_of_measurement: '°C', + device_class: 'temperature', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('sensor', 'bed_temp', { + name: 'Bed Temperature', + unique_id: `${DEVICE_ID}_bed_temp`, + state_topic: `${MQTT_ROOT_TOPIC}/state/bed_temp`, + unit_of_measurement: '°C', + device_class: 'temperature', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('sensor', 'chamber_temp', { + name: 'Chamber Temperature', + unique_id: `${DEVICE_ID}_chamber_temp`, + state_topic: `${MQTT_ROOT_TOPIC}/state/chamber_temp`, + unit_of_measurement: '°C', + device_class: 'temperature', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('sensor', 'estimated_time', { + name: 'Estimated Time', + unique_id: `${DEVICE_ID}_estimated_time_s`, + state_topic: `${MQTT_ROOT_TOPIC}/state/estimated_time_s`, + unit_of_measurement: 's', + icon: 'mdi:timer-outline', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('binary_sensor', 'is_printing', { + name: 'Printing', + unique_id: `${DEVICE_ID}_is_printing`, + state_topic: `${MQTT_ROOT_TOPIC}/state/is_printing`, + payload_on: 'ON', + payload_off: 'OFF', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('switch', 'pause_resume', { + name: 'Pause Print', + unique_id: `${DEVICE_ID}_pause_resume`, + state_topic: `${MQTT_ROOT_TOPIC}/state/pause_switch`, + command_topic: `${MQTT_ROOT_TOPIC}/command/pause_resume`, + payload_on: 'PAUSE', + payload_off: 'RESUME', + state_on: 'ON', + state_off: 'OFF', + icon: 'mdi:pause-circle', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('switch', 'camera', { + name: 'Camera Stream', + unique_id: `${DEVICE_ID}_camera_stream`, + state_topic: `${MQTT_ROOT_TOPIC}/state/camera_switch`, + command_topic: `${MQTT_ROOT_TOPIC}/command/camera`, + payload_on: 'OPEN', + payload_off: 'CLOSE', + state_on: 'ON', + state_off: 'OFF', + icon: 'mdi:cctv', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('button', 'stop', { + name: 'Stop Print', + unique_id: `${DEVICE_ID}_stop`, + command_topic: `${MQTT_ROOT_TOPIC}/command/stop`, + payload_press: 'STOP', + icon: 'mdi:stop-circle', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + publishDiscovery('button', 'clear_state', { + name: 'Clear Printer State', + unique_id: `${DEVICE_ID}_clear_state`, + command_topic: `${MQTT_ROOT_TOPIC}/command/clear_state`, + payload_press: 'CLEAR', + icon: 'mdi:broom', + availability_topic: MQTT_AVAILABILITY_TOPIC, + device, + }); + + mqttDiscoveryPublished = true; +} + +async function refreshPrinterState() { + if (!PRINTER_IP || !SERIAL_NUMBER || !CHECK_CODE) return; + try { + const data = await printerPost('/detail'); + if (data && data.detail) { + updatePrinterDetail(data.detail); + } + } catch (err) { + console.warn(`MQTT state refresh failed: ${err.message}`); + } +} + +function isKnownCommandPayload(payload) { + return KNOWN_MQTT_COMMAND_PAYLOADS.has(payload); +} + +async function handleMqttCommand(topic, payloadRaw) { + const payload = String(payloadRaw || '').trim().toUpperCase(); + if (!payload || !isKnownCommandPayload(payload)) return; + + if (topic === `${MQTT_ROOT_TOPIC}/command/camera`) { + const action = ['OPEN', 'ON', '1', 'TRUE'].includes(payload) ? 'open' : 'close'; + cameraSwitchState = action === 'open' ? 'ON' : 'OFF'; + mqttPublish(`${MQTT_ROOT_TOPIC}/state/camera_switch`, cameraSwitchState, { retain: true }); + return; + } + + if (topic === `${MQTT_ROOT_TOPIC}/command/pause_resume`) { + const action = ['PAUSE', 'ON', '1', 'TRUE'].includes(payload) ? 'pause' : 'continue'; + await printerControl('jobCtl_cmd', { jobID: getCurrentJobId(), action }); + await refreshPrinterState(); + return; + } + + if (topic === `${MQTT_ROOT_TOPIC}/command/stop`) { + if (!['STOP', 'PRESS', 'ON', '1', 'TRUE'].includes(payload)) return; + await printerControl('jobCtl_cmd', { jobID: getCurrentJobId(), action: 'cancel' }); + await refreshPrinterState(); + return; + } + + if (topic === `${MQTT_ROOT_TOPIC}/command/clear_state`) { + if (!['CLEAR', 'PRESS', 'ON', '1', 'TRUE'].includes(payload)) return; + await printerControl('stateCtrl_cmd', { action: 'setClearPlatform' }); + await refreshPrinterState(); + } +} + +function setupMqtt() { + if (!MQTT_ENABLED) { + console.log('MQTT disabled via configuration.'); + return; + } + + const mqttUrl = `mqtt://${MQTT_HOST}:${MQTT_PORT}`; + const options = { + reconnectPeriod: 5000, + will: { + topic: MQTT_AVAILABILITY_TOPIC, + payload: 'offline', + retain: true, + }, + }; + if (MQTT_USERNAME) options.username = MQTT_USERNAME; + if (MQTT_PASSWORD) options.password = MQTT_PASSWORD; + + mqttClient = mqtt.connect(mqttUrl, options); + + mqttClient.on('connect', async () => { + mqttConnected = true; + mqttDiscoveryPublished = false; + console.log(`Connected to MQTT broker at ${mqttUrl}`); + mqttPublish(MQTT_AVAILABILITY_TOPIC, 'online', { retain: true }); + publishMqttDiscovery(); + await refreshPrinterState(); + + const commandTopics = [ + `${MQTT_ROOT_TOPIC}/command/camera`, + `${MQTT_ROOT_TOPIC}/command/pause_resume`, + `${MQTT_ROOT_TOPIC}/command/stop`, + `${MQTT_ROOT_TOPIC}/command/clear_state`, + ]; + mqttClient.subscribe(commandTopics, (err) => { + if (err) { + console.warn(`MQTT subscribe error: ${err.message}`); + } + }); + }); + + mqttClient.on('message', async (topic, payload) => { + try { + await handleMqttCommand(topic, payload); + } catch (err) { + console.warn(`MQTT command error on ${topic}: ${err.message}`); + } + }); + + mqttClient.on('error', (err) => { + console.warn(`MQTT error: ${err.message}`); + }); + + mqttClient.on('close', () => { + mqttConnected = false; + }); +} + +/** + * Validate that required env vars are set and return 503 otherwise. + */ +function requireConfig(req, res, next) { + if (!PRINTER_IP || !SERIAL_NUMBER || !CHECK_CODE) { + return res.status(503).json({ + error: 'Printer not configured. Set printer_ip, serial_number and check_code in the add-on Configuration tab.', + }); + } + next(); +} + +/** + * HTTP Basic Auth middleware – usato SOLO sul server ad accesso diretto (porta 8099). + * Confronto sicuro che regge password contenenti il carattere ':'. + */ +function basicAuth(req, res, next) { + if (!AUTH_ENABLED) return next(); + const authHeader = req.headers['authorization'] || ''; + if (!authHeader.startsWith('Basic ')) { + res.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="FlashForge Dashboard"', + 'Content-Type': 'text/plain', + }); + return res.end('Unauthorized'); + } + const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8'); + const colonIdx = decoded.indexOf(':'); + const user = colonIdx === -1 ? decoded : decoded.slice(0, colonIdx); + const pass = colonIdx === -1 ? '' : decoded.slice(colonIdx + 1); + if (user !== AUTH_USERNAME || pass !== AUTH_PASSWORD) { + res.writeHead(401, { + 'WWW-Authenticate': 'Basic realm="FlashForge Dashboard"', + 'Content-Type': 'text/plain', + }); + return res.end('Unauthorized'); + } + next(); +} + +// ── API Routes ─────────────────────────────────────────────────────────────── + +/** + * GET /api/status + * Returns the full detail response from the printer. + */ +app.get('/api/status', requireConfig, async (req, res) => { + try { + const data = await printerPost('/detail'); + if (data && data.detail) { + updatePrinterDetail(data.detail); + } + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); + } +}); + +/** + * POST /api/control + * Body: { action: "pause"|"continue"|"cancel", jobID?: "..." } + */ +app.post('/api/control', requireConfig, async (req, res) => { + const { action, jobID } = req.body; + if (!action) { + return res.status(400).json({ error: 'action is required' }); + } + try { + const data = await printerControl('jobCtl_cmd', { jobID: jobID || '', action }); + await refreshPrinterState(); + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); + } +}); + +app.post('/api/state/clear', requireConfig, async (req, res) => { + try { + const data = await printerControl('stateCtrl_cmd', { action: 'setClearPlatform' }); + await refreshPrinterState(); + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); + } +}); + +/** + * GET /api/files + * Returns the list of printable files stored on the printer. + */ +app.get('/api/files', requireConfig, async (req, res) => { + try { + const data = await printerPost('/gcodeList'); + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); + } +}); + +/** + * GET /api/thumb?fileName=... + * Returns base64 thumbnail for a file. + */ +app.get('/api/thumb', requireConfig, async (req, res) => { + const { fileName } = req.query; + if (!fileName) return res.status(400).json({ error: 'fileName is required' }); + try { + const data = await printerPost('/gcodeThumb', { fileName }); + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); + } +}); + +/** + * POST /api/print + * Body: { fileName: "...", levelingBeforePrint: true|false } + * Starts printing a file already stored on the printer. + */ +app.post('/api/print', requireConfig, async (req, res) => { + const { fileName, levelingBeforePrint = false } = req.body; + if (!fileName) return res.status(400).json({ error: 'fileName is required' }); + try { + const data = await printerPost('/printGcode', { fileName, levelingBeforePrint }); + await refreshPrinterState(); + res.json(data); + } catch (err) { + res.status(502).json({ error: err.message }); + } +}); + +/** + * POST /api/upload + * Multipart form with field "gcodeFile". + * Optional form fields: printNow (0|1), levelingBeforePrint (0|1) + */ +app.post('/api/upload', requireConfig, upload.single('gcodeFile'), async (req, res) => { + if (!req.file) return res.status(400).json({ error: 'gcodeFile is required' }); + + const printNow = req.body.printNow || '0'; + const levelingBeforePrint = req.body.levelingBeforePrint || '0'; + const fileSize = req.file.size; + + // Build a multipart body to forward to the printer + const boundary = `----FormBoundary${Date.now()}`; + const preamble = [ + `--${boundary}`, + `Content-Disposition: form-data; name="gcodeFile"; filename="${req.file.originalname}"`, + `Content-Type: application/octet-stream`, + '', + '', + ].join('\r\n'); + const epilogue = `\r\n--${boundary}--\r\n`; + + const body = Buffer.concat([ + Buffer.from(preamble), + req.file.buffer, + Buffer.from(epilogue), + ]); + + const printerRes = await fetch(`${PRINTER_API}/uploadGcode`, { + method: 'POST', + headers: { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + serialNumber: SERIAL_NUMBER, + checkCode: CHECK_CODE, + fileSize: String(fileSize), + printNow, + levelingBeforePrint, + }, + body, + }); + + const result = await printerRes.json().catch(() => ({ code: printerRes.status })); + if (printerRes.ok && result.code === 0) { + if (printNow === '1') { + try { + await printerPost('/printGcode', { + fileName: req.file.originalname, + levelingBeforePrint: levelingBeforePrint === '1', + }); + } catch (err) { + // Upload succeeded; report the print-start failure without blocking the response + await refreshPrinterState(); + return res.status(200).json({ code: 0, printStartError: err.message }); + } + } + await refreshPrinterState(); + } + res.status(printerRes.ok ? 200 : 502).json(result); +}); + +/** + * POST /api/camera + * Body: { action: "open"|"close" } + * Tracks camera switch state (the stream itself is provided by HA). + */ +app.post('/api/camera', requireConfig, async (req, res) => { + const { action } = req.body; + if (!action) return res.status(400).json({ error: 'action is required' }); + cameraSwitchState = action === 'open' ? 'ON' : 'OFF'; + mqttPublish(`${MQTT_ROOT_TOPIC}/state/camera_switch`, cameraSwitchState, { retain: true }); + res.json({}); +}); + +// ── go2rtc integration ─────────────────────────────────────────────────────── + +/** Server-side cache for go2rtc client script to avoid fetching it on every page load. */ +let cachedVideoRtcJs = null; +let cachedVideoRtcJsAt = 0; +const VIDEO_RTC_CACHE_TTL_MS = 3600000; // 1 hour + +// go2rtc client script candidate paths (tried in order). +const GO2RTC_CLIENT_CANDIDATE_PATHS = [ + '/api/go2rtc/client.js', // go2rtc >= 1.9 + '/video-rtc.js', // go2rtc older versions +]; + +/** + * Parses GO2RTC_URL into { host, port } for use with http.request(). + * GO2RTC_URL must point directly to go2rtc port 1984, e.g.: + * http://ccab4aaf-frigate:1984 + */ +function parseGo2rtcUrl() { + try { + const u = new URL(GO2RTC_URL); + return { host: u.hostname, port: Number(u.port) || 1984 }; + } catch (_) { + return { host: 'ccab4aaf-frigate', port: 1984 }; + } +} + +/** + * GET /api/go2rtc/mjpeg?src= + * Proxy the MJPEG stream from go2rtc to the browser via http.request() + pipe. + */ +app.get('/api/go2rtc/mjpeg', (req, res) => { + const streamName = req.query.src || GO2RTC_STREAM; + if (!GO2RTC_URL) return res.status(503).send('go2rtc_url not configured'); + + const { host, port } = parseGo2rtcUrl(); + const upstreamPath = `/api/stream.mjpeg?src=${encodeURIComponent(streamName)}`; + console.log(`[go2rtc] MJPEG upstream: http://${host}:${port}${upstreamPath}`); + + const proxyReq = http.request( + { host, port, path: upstreamPath, method: 'GET' }, + (proxyRes) => { + if (proxyRes.statusCode !== 200) { + res.status(proxyRes.statusCode || 502).send('Upstream stream not found'); + proxyRes.resume(); // drain so the socket is released + return; + } + + // Forward Content-Type exactly as go2rtc sends it (includes boundary). + res.setHeader( + 'Content-Type', + proxyRes.headers['content-type'] || 'multipart/x-mixed-replace; boundary=ffboundary', + ); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.setHeader('Connection', 'keep-alive'); + // Tell nginx (used by HA Ingress) not to buffer this streaming response. + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); // send headers immediately, before any body data + + // Pipe every chunk straight to the browser without buffering. + proxyRes.pipe(res, { end: true }); + + // If the browser disconnects, tear down the upstream request too. + req.on('close', () => proxyReq.destroy()); + }, + ); + + proxyReq.on('error', (err) => { + console.warn(`[go2rtc] MJPEG proxy error: ${err.message}`); + if (!res.headersSent) { + res.status(502).send(err.message); + } else { + res.end(); + } + }); + + proxyReq.end(); +}); + +app.get('/api/go2rtc/client.js', async (req, res) => { + if (!GO2RTC_URL) { + return res.status(503).send('// go2rtc_url not configured\n'); + } + + const now = Date.now(); + if (cachedVideoRtcJs && (now - cachedVideoRtcJsAt) < VIDEO_RTC_CACHE_TTL_MS) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'max-age=3600'); + return res.send(cachedVideoRtcJs); + } + + try { + for (const candidatePath of GO2RTC_CLIENT_CANDIDATE_PATHS) { + const url = `${GO2RTC_URL}${candidatePath}`; + console.log(`[go2rtc] Trying client.js at: ${url}`); + const upstream = await fetch(url); + if (!upstream.ok) continue; + + cachedVideoRtcJs = await upstream.text(); + cachedVideoRtcJsAt = now; + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'max-age=3600'); + return res.send(cachedVideoRtcJs); + } + + if (cachedVideoRtcJs) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + return res.send(cachedVideoRtcJs); + } + + return res.status(502).send('// go2rtc client not available from upstream\n'); + } catch (err) { + if (cachedVideoRtcJs) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + return res.send(cachedVideoRtcJs); + } + return res.status(502).send(`// go2rtc client error: ${err.message}\n`); + } +}); + +// ── Config check endpoint ──────────────────────────────────────────────────── +app.get('/api/config', (req, res) => { + res.json({ + configured: !!(PRINTER_IP && SERIAL_NUMBER && CHECK_CODE), + printerIp: PRINTER_IP || null, + go2rtcConfigured: !!(GO2RTC_URL && GO2RTC_STREAM), + go2rtcStream: GO2RTC_STREAM || null, + ingressPath: INGRESS_PATH, + }); +}); + +// ── Serve index.html dynamically with injected INGRESS_PATH ───────────────── +// HA Ingress strips the path prefix before forwarding requests, so all backend +// routes work at /api/... as normal. However, browser-side fetch() calls use +// absolute paths (e.g. /api/status) which would bypass the ingress prefix. +// We inject window.INGRESS_PATH into the HTML so the frontend can prefix them. +const INDEX_HTML_PATH = path.join(__dirname, 'frontend', 'public', 'index.html'); +const indexHtmlBase = fs.readFileSync(INDEX_HTML_PATH, 'utf8'); + +function serveIndex(req, res) { + let headInject = ``; + if (GO2RTC_URL && GO2RTC_STREAM) { + headInject += `\n `; + } + const html = indexHtmlBase.replace('', ` ${headInject}\n`); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.send(html); +} + +app.get('*', serveIndex); + +// ── Start ──────────────────────────────────────────────────────────────────── + +// ── Server 1: Ingress HA (nessuna auth – HA richiede già il login) ────────── +// HA Supervisor proxia qui il traffico dalla sidebar. La porta NON è esposta +// all'esterno (non è nella sezione `ports` di config.yaml). +const ingressServer = http.createServer(app); + +// ── Server 2: Accesso diretto (Basic Auth quando le credenziali sono impostate) +// Esposto sulla rete locale tramite la sezione `ports` di config.yaml. +const directServer = http.createServer((req, res) => { + if (AUTH_ENABLED) { + basicAuth(req, res, () => app(req, res)); + } else { + app(req, res); + } +}); + +// ── WebSocket upgrade (proxy go2rtc) ───────────────────────────────────────── +/** + * Gestisce l'upgrade WebSocket per entrambi i server. + * @param {boolean} requireAuth true solo per il server ad accesso diretto + */ +function handleWsUpgrade(req, socket, head, requireAuth) { + // Verifica Basic Auth per il server diretto + if (requireAuth && AUTH_ENABLED) { + const authHeader = req.headers['authorization'] || ''; + if (!authHeader.startsWith('Basic ')) { + socket.write( + 'HTTP/1.1 401 Unauthorized\r\n' + + 'WWW-Authenticate: Basic realm="FlashForge Dashboard"\r\n' + + 'Content-Length: 0\r\nConnection: close\r\n\r\n', + ); + socket.destroy(); + return; + } + const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf8'); + const colonIdx = decoded.indexOf(':'); + const user = colonIdx === -1 ? decoded : decoded.slice(0, colonIdx); + const pass = colonIdx === -1 ? '' : decoded.slice(colonIdx + 1); + if (user !== AUTH_USERNAME || pass !== AUTH_PASSWORD) { + socket.write( + 'HTTP/1.1 401 Unauthorized\r\n' + + 'WWW-Authenticate: Basic realm="FlashForge Dashboard"\r\n' + + 'Content-Length: 0\r\nConnection: close\r\n\r\n', + ); + socket.destroy(); + return; + } + } + + let urlPath = req.url || '/'; + if (INGRESS_PATH && urlPath.startsWith(INGRESS_PATH)) { + urlPath = urlPath.substring(INGRESS_PATH.length) || '/'; + } + + let urlObj; + try { + urlObj = new URL(urlPath, 'http://localhost'); + } catch (_) { + socket.write('HTTP/1.1 400 Bad Request\r\n\r\n'); + socket.destroy(); + return; + } + + if (!urlObj.pathname.startsWith('/api/go2rtc/ws')) { + socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); + socket.destroy(); + return; + } + + const streamName = urlObj.searchParams.get('src') || GO2RTC_STREAM; + const wsKey = req.headers['sec-websocket-key']; + if (!wsKey) { + socket.write('HTTP/1.1 400 Bad Request\r\n\r\n'); + socket.destroy(); + return; + } + + const { host: wsHost, port: wsPort } = parseGo2rtcUrl(); + const targetPath = `/api/ws?src=${encodeURIComponent(streamName)}`; + const wsHostHeader = `${wsHost}:${wsPort}`; + console.log(`[go2rtc] WS upstream: ws://${wsHost}:${wsPort}${targetPath}`); + + const proxySocket = net.connect(wsPort, wsHost, () => { + const wsVersion = req.headers['sec-websocket-version'] || '13'; + const wsProtocol = req.headers['sec-websocket-protocol']; + const wsExtensions = req.headers['sec-websocket-extensions']; + const origin = req.headers.origin; + const userAgent = req.headers['user-agent']; + + const lines = [ + `GET ${targetPath} HTTP/1.1`, + `Host: ${wsHostHeader}`, + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Key: ${wsKey}`, + `Sec-WebSocket-Version: ${wsVersion}`, + ]; + + if (wsProtocol) lines.push(`Sec-WebSocket-Protocol: ${wsProtocol}`); + if (wsExtensions) lines.push(`Sec-WebSocket-Extensions: ${wsExtensions}`); + if (origin) lines.push(`Origin: ${origin}`); + if (userAgent) lines.push(`User-Agent: ${userAgent}`); + + lines.push('', ''); + proxySocket.write(lines.join('\r\n')); + if (head && head.length) proxySocket.write(head); + }); + + const closeBoth = () => { + if (!socket.destroyed) socket.destroy(); + if (!proxySocket.destroyed) proxySocket.destroy(); + }; + + proxySocket.on('error', (err) => { + console.warn(`go2rtc WebSocket proxy error: ${err.message}`); + closeBoth(); + }); + socket.on('error', closeBoth); + socket.on('close', closeBoth); + proxySocket.on('close', closeBoth); + + socket.pipe(proxySocket); + proxySocket.pipe(socket); +} + +// Collega l'upgrade handler a entrambi i server +ingressServer.on('upgrade', (req, socket, head) => handleWsUpgrade(req, socket, head, false)); +directServer.on('upgrade', (req, socket, head) => handleWsUpgrade(req, socket, head, true)); + +// ── Listen ─────────────────────────────────────────────────────────────────── + +// Server Ingress (porta interna, nessuna auth) +ingressServer.listen(INGRESS_PORT, () => { + console.log(`FlashForge Dashboard – Ingress HA in ascolto sulla porta ${INGRESS_PORT} (nessuna auth)`); + console.log(`Ingress path: ${INGRESS_PATH || '(none)'}`); +}); + +// Server diretto (porta esposta, con auth se configurata) +directServer.listen(DIRECT_PORT, () => { + console.log(`FlashForge Dashboard – Accesso diretto sulla porta ${DIRECT_PORT} (auth ${AUTH_ENABLED ? 'ATTIVA' : 'DISATTIVA'})`); + console.log(`Direct HTTP URL: http://:${DIRECT_PORT}`); + if (!PRINTER_IP || !SERIAL_NUMBER || !CHECK_CODE) { + console.warn('⚠ printer_ip, serial_number o check_code non impostati. Configurali nel pannello Add-on → Configurazione.'); + } + setupMqtt(); + if (MQTT_ENABLED) { + mqttPollingTimer = setInterval(() => { + refreshPrinterState(); + }, MQTT_POLL_INTERVAL_MS); + } +}); + +function shutdown() { + if (mqttPollingTimer) { + clearInterval(mqttPollingTimer); + mqttPollingTimer = null; + } + if (mqttClient) { + try { + mqttPublish(MQTT_AVAILABILITY_TOPIC, 'offline', { retain: true }); + mqttClient.end(true); + } catch (err) { + console.warn(`MQTT shutdown warning: ${err.message}`); + } + } + let closed = 0; + const onClosed = () => { if (++closed >= 2) process.exit(0); }; + directServer.close(onClosed); + ingressServer.close(onClosed); + setTimeout(() => process.exit(0), 3000).unref(); +} + +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); diff --git a/repository.yaml b/repository.yaml new file mode 100644 index 0000000..c0c77ae --- /dev/null +++ b/repository.yaml @@ -0,0 +1,3 @@ +name: FlashForge Add-ons +url: https://github.com/MikManenti/flashforge-api-docs +maintainer: MikManenti