Skip to content

Add port param to start #4

@niradler

Description

@niradler

SDK doesn't detect ESP32-based Bruce devices

Problem

The bruce-sdk device detection in sdk.js fails to detect ESP32-based Bruce devices, showing "Bruce devices not found" even when devices are connected via USB serial.

Root Cause

The device filter in sdk.js (lines 128-132) only checks for:

  • Devices with friendlyName containing 'CH9102'
  • Devices with vendorId === '1A86' && productId === '55D4'

ESP32-based devices use vendor ID 30A (Espressif), which is not included in the filter.

Add port override option for manual port specification:

  • Support --port COM16 command-line argument
  • Support BRUCE_PORT environment variable
  • Useful when automatic detection fails or user wants to specify a port explicitly

Environment

  • SDK Version: 0.1.8
  • OS: Windows 10
  • Device: ESP32-based Bruce device (vendor ID: 30...)

Suggestion

// @ts-check
import { ReadlineParser, SerialPort } from 'serialport';
import { readFile } from 'node:fs/promises';
import prompts from 'prompts';
import esbuild from 'esbuild';
import googleClosure from 'google-closure-compiler';
import json5 from 'json5';

/** @type {(ms: number) => Promise<void>} */
const delay = async (ms) =>
  new Promise((resolve) => {
    setTimeout(() => resolve(), ms);
  });

let serialReady = false;

/**
 * @type {() => Promise<string | null>}
 * */
async function getDevicePath() {
  const portArgIndex = process.argv.indexOf('--port');
  const specifiedPort = portArgIndex !== -1 && process.argv[portArgIndex + 1]
    ? process.argv[portArgIndex + 1]
    : process.env.BRUCE_PORT;

  if (specifiedPort) {
    return specifiedPort;
  }

  /**
   * @type {Array<{
   *   path: string;
   *   manufacturer?: string;
   *   serialNumber?: string;
   *   pnpId?: string;
   *   locationId?: string;
   *   productId?: string;
   *   vendorId?: string;
   *   friendlyName?: string;
   * }>}
   * */
  const SerialPortDevices = await SerialPort.list(),
    m5Sticks = SerialPortDevices.filter(
      (device) =>
        device.friendlyName?.includes('CH9102') ||
        (device?.vendorId === '1A86' && device?.productId === '55D4'),
    );

  if (!m5Sticks.length) {
    console.log('Bruce devices not found');
    return null;
  }

  let { path } = m5Sticks[0];
  if (m5Sticks.length > 1) {
    path = (
      await prompts([
        {
          type: 'select',
          name: 'port',
          message: 'Select Bruce device',
          choices: m5Sticks.map((x) => ({ title: x.path, value: x.path })),
        },
      ])
    ).port;
  }

  return path;
}

/**
 * @type {(config: { output: string, minify: boolean, optimise: boolean }) => Promise<void>}
 * */
async function build(config) {
  await esbuild.build({
    entryPoints: ['./dist/index.js'],
    outfile: config.output,
    tsconfig: './tsconfig.json',
    format: 'cjs',
    bundle: true,
    treeShaking: true,
    lineLimit: config.minify ? 250 : undefined,
    minifyWhitespace: config.minify,
    minifyIdentifiers: config.minify,
    minifySyntax: false,
    target: 'es5',
    external: [
      'audio',
      'badusb',
      'device',
      'dialog',
      'display',
      'gpio',
      'ir',
      'keyboard',
      'notification',
      'serial',
      'storage',
      'subghz',
      'wifi',
    ],
    supported: {
      'array-spread': false,
      arrow: false,
      'async-await': false,
      'async-generator': false,
      bigint: false,
      class: false,
      'const-and-let': false,
      decorators: false,
      'default-argument': false,
      destructuring: false,
      'dynamic-import': false,
      'exponent-operator': false,
      'export-star-as': false,
      'for-await': false,
      'for-of': false,
      'function-name-configurable': false,
      'function-or-class-property-access': false,
      generator: false,
      hashbang: false,
      'import-assertions': false,
      'import-meta': false,
      'inline-script': false,
      'logical-assignment': false,
      'nested-rest-binding': false,
      'new-target': false,
      'node-colon-prefix-import': false,
      'node-colon-prefix-require': false,
      'nullish-coalescing': false,
      'object-accessors': false,
      'object-extensions': false,
      'object-rest-spread': false,
      'optional-catch-binding': false,
      'optional-chain': false,
      'regexp-dot-all-flag': false,
      'regexp-lookbehind-assertions': false,
      'regexp-match-indices': false,
      'regexp-named-capture-groups': false,
      'regexp-set-notation': false,
      'regexp-sticky-and-unicode-flags': false,
      'regexp-unicode-property-escapes': false,
      'rest-argument': false,
      'template-literal': false,
      'top-level-await': false,
      'typeof-exotic-object-is-object': false,
      'unicode-escapes': false,
      using: false,
    },
  });

  if (config.optimise) {
    await new Promise((resolve) => {
      new googleClosure.compiler({
        js: config.output,
        js_output_file: config.output,
        language_in: 'ECMASCRIPT5',
        language_out: 'ECMASCRIPT5',
        compilation_level: 'ADVANCED',
      }).run(() => resolve(null));
    });
  }
}

/**
 * @type {(config: { input: string, output: string }) => Promise<void>}
 * */
async function start(config) {
  const path = await getDevicePath();
  if (!path) return;

  const serialport = new SerialPort({
    path: path,
    baudRate: 115200,
  }),
    parser = serialport.pipe(new ReadlineParser());

  parser.on('data', (/** @type {string} */ data) => {
    console.log(data);
    if (data.includes('Serial connection ready to receive file data')) {
      serialReady = true;
    }
  });

  const script = await readFile(config.input, { encoding: 'utf8' });

  serialReady = false;
  serialport.write(`js run_from_buffer ${script.length}`);
  console.log('Waiting for connection');
  while (!serialReady) {
    await delay(10);
  }
  serialport.write(`${script}\nEOF`);
  console.log('JS file sent');
  serialport.read();
}

/**
 * @type {(config: { input: string, output: string }) => Promise<void>}
 * */
async function upload(config) {
  const path = await getDevicePath();
  if (!path) return;

  const serialport = new SerialPort({
    path: path,
    baudRate: 115200,
  }),
    parser = serialport.pipe(new ReadlineParser());

  let uploadReady = false;
  let uploadSuccess = false;

  parser.on('data', (/** @type {string} */ data) => {
    console.log(data);
    const dataStr = data.toString();
    if (
      dataStr.includes('Reading input data from serial buffer until EOF') ||
      dataStr.includes('Serial connection ready to receive file data') ||
      dataStr.includes('COMMAND: storage write')
    ) {
      uploadReady = true;
    }
    if (
      dataStr.includes('File written successfully') ||
      dataStr.includes('OK') ||
      dataStr.includes('File saved') ||
      dataStr.includes('storage write')
    ) {
      uploadSuccess = true;
    }
  });

  const fileContent = await readFile(config.input, { encoding: 'utf8' });
  const filePath = config.output;

  uploadReady = false;
  uploadSuccess = false;

  console.log(`Uploading ${config.input} to ${filePath}...`);
  serialport.write(`storage write ${filePath} ${fileContent.length}\n`);
  console.log('Command sent, waiting for device to be ready...');

  const startTime = Date.now();
  const timeout = startTime + 5000;

  while (!uploadReady && Date.now() < timeout) {
    await delay(50);
  }

  if (!uploadReady) {
    console.log('No explicit ready message received, proceeding anyway (device may be ready)...');
  } else {
    console.log('Device ready, sending file content...');
  }

  await delay(200);
  serialport.write(`${fileContent}\nEOF`);
  console.log('File content sent, waiting for confirmation...');

  const confirmTimeout = Date.now() + 10000;
  while (!uploadSuccess && Date.now() < confirmTimeout) {
    await delay(10);
  }

  if (uploadSuccess) {
    console.log('File uploaded successfully!');
  } else {
    console.log('Upload completed (no explicit confirmation received, but file may have been written)');
  }

  await delay(500);
  serialport.close();
}

(async () => {
  const commands = {
    build,
    start,
    upload,
  },
    configFile = await readFile('./bruce-sdk.config.jsonc', 'utf8'),
    config = json5.parse(configFile),
    command = process.argv[2];

  if (!Object.keys(commands).includes(command)) {
    console.error(
      `Unknown command ${command}. Supported: ${Object.keys(commands).join(', ')}`,
    );
    process.exit(1);
  }

  await commands.build(config.build);
  if (command !== 'build') {
    await commands[command](config[command === 'start' ? 'upload' : command]);
  }
})();

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions