All files / server/services sketch-file-builder.ts

90.62% Statements 29/32
80% Branches 8/10
100% Functions 5/5
90.62% Lines 29/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139              14x 14x                       69x 69x   69x                   44x 44x 44x   44x 44x 44x             44x   44x 2x         44x 44x   44x   44x   44x       26x       61x             44x                             44x 42x             44x 42x                                     2x           44x                   44x      
/**
 * Sketch File Builder
 * 
 * Handles the construction of Arduino sketch files by wrapping user code
 * with the Arduino mock implementation and generating appropriate main() wrappers.
 */
 
import { join } from "node:path";
import { mkdir, writeFile } from "node:fs/promises";
import { ARDUINO_MOCK_CODE } from "./arduino-mock";
import { Logger } from "@shared/logger";
import { detectSketchEntrypoints } from "@shared/utils/sketch-validation";
 
interface SketchBuildResult {
  sketchDir: string;
  sketchFile: string;
  exeFile: string;
}
 
export class SketchFileBuilder {
  private readonly logger = new Logger("SketchFileBuilder");
  private readonly createdSketchDirs = new Set<string>();
 
  constructor(private readonly tempDir: string) {}
 
  /**
   * Builds a complete sketch file with Arduino mock and user code
   * 
   * @param code - User's Arduino code
   * @param sketchId - Unique identifier for this sketch
   * @returns Paths to sketch directory and files
   */
  async build(code: string, sketchId: string): Promise<SketchBuildResult> {
    const sketchDir = join(this.tempDir, sketchId);
    const sketchFile = join(sketchDir, "sketch.cpp");
    const exeFile = join(sketchDir, "sketch");
 
    try {
      await mkdir(sketchDir, { recursive: true });
      this.createdSketchDirs.add(sketchDir);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      this.logger.error(`Failed to create sketch directory: ${msg}`);
      throw err;
    }
 
    const { hasSetup, hasLoop } = detectSketchEntrypoints(code);
 
    if (!hasSetup && !hasLoop) {
      this.logger.warn(
        "Weder setup() noch loop() gefunden - Code wird nur als Bibliothek kompiliert",
      );
    }
 
    const footer = this.buildFooter(hasSetup, hasLoop);
    const cleanedCode = code.replaceAll(/#include\s*[<"]Arduino\.h[>"]/g, "");
 
    const combined = `${ARDUINO_MOCK_CODE}\n\n// --- User code follows ---\n${cleanedCode}\n\n// --- Footer ---\n${footer}`;
 
    await writeFile(sketchFile, combined);
 
    return { sketchDir, sketchFile, exeFile };
  }
 
  getCreatedSketchDirs(): string[] {
    return Array.from(this.createdSketchDirs);
  }
 
  clearCreatedSketchDir(dir: string): void {
    this.createdSketchDirs.delete(dir);
  }
 
  /**
   * Generates the main() wrapper based on presence of setup() and loop()
   */
  private buildFooter(hasSetup: boolean, hasLoop: boolean): string {
    let footer = `
#include <thread>
#include <atomic>
#include <cstring>
#include <chrono>
 
int main() {
    // Initialize IO registry for pin state tracking
    initIORegistry();
    
    // Start background thread for serial input
    std::thread readerThread(serialInputReader);
    readerThread.detach();
`;
 
    if (hasSetup) {
      footer += `
    // Call user's setup() function
    setup();
    Serial.flush();
`;
    }
 
    if (hasLoop) {
      footer += `
    // Run user's loop() function continuously
    bool __registry_sent = false;
    while (1) {
        Serial.flush();
        loop();
        
        // Send registry after first loop iteration
        if (!__registry_sent) {
            Serial.flush();
            outputIORegistry();
            __registry_sent = true;
        }
        
        // Sleep 1ms to prevent 100% CPU usage (Arduino runs at ~16MHz, so 1ms is reasonable throttle)
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
`;
    } else {
      footer += `
    // No loop() function, just output registry once
    outputIORegistry();
`;
    }
 
    footer += `
    Serial.flush();
    
    // Cleanup: stop serial input reader
    keepReading.store(false);
    
    return 0;
}
`;
 
    return footer;
  }
}