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

90.9% Statements 30/33
80% Branches 8/10
100% Functions 5/5
90.9% Lines 30/33

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              12x 12x                     86x 86x   86x                   61x 61x 61x   61x 61x 61x             61x 61x   61x 2x         61x 61x   61x   61x   61x       71x       62x             61x                             61x 59x             61x 59x                                     2x           61x                   61x      
/**
 * 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 "path";
import { mkdir, writeFile } from "fs/promises";
import { ARDUINO_MOCK_CODE } from "../mocks/arduino-mock";
import { Logger } from "@shared/logger";
 
export interface SketchBuildResult {
  sketchDir: string;
  sketchFile: string;
  exeFile: string;
}
 
export class SketchFileBuilder {
  private logger = new Logger("SketchFileBuilder");
  private createdSketchDirs = new Set<string>();
 
  constructor(private 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 = /void\s+setup\s*\([^)]*\)/.test(code);
    const hasLoop = /void\s+loop\s*\([^)]*\)/.test(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.replace(/#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;
  }
}