Skip to content

Add port param to start #4

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions