-
Notifications
You must be signed in to change notification settings - Fork 5
Open
Description
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
friendlyNamecontaining'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 COM16command-line argument - Support
BRUCE_PORTenvironment 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
Labels
No labels