Skip to content

Commit 3fcc57e

Browse files
Release v1.24.3
1 parent 61dcc35 commit 3fcc57e

20 files changed

Lines changed: 463 additions & 30 deletions

cli-common/config.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface SerialConfig {
4747
camera: string | undefined;
4848
audio: string | undefined;
4949
runner: RunnerConfig;
50+
apiKeysForProject: { [projectId: string]: string } | undefined;
5051
}
5152

5253
export interface EdgeImpulseEndpoints {
@@ -174,6 +175,21 @@ export class Config {
174175
await this.store(config);
175176
}
176177

178+
async getApiKeyForProject(projectId: number): Promise<string | undefined> {
179+
let config = await this.load();
180+
if (!config) {
181+
return undefined;
182+
}
183+
return (config.apiKeysForProject || { })[projectId.toString()];
184+
}
185+
186+
async setApiKeyForProject(projectId: number, apiKey: string) {
187+
let config = await this.load();
188+
config.apiKeysForProject = config.apiKeysForProject || { };
189+
config.apiKeysForProject[projectId.toString()] = apiKey;
190+
await this.store(config);
191+
}
192+
177193
async getLastVersionCheck() {
178194
let config = await this.load();
179195
if (!config) {
@@ -310,12 +326,18 @@ export class Config {
310326

311327
// fetch user...
312328
if (config.jwtToken) {
313-
let user = await this._api.user.getCurrentUser();
314-
// check if has developer profile...
315-
if (!user.organizations.find(x => x.isDeveloperProfile)) {
316-
console.log(PREFIX, 'Creating developer profile...');
317-
await this._api.user.createDeveloperProfile();
318-
console.log(PREFIX, 'Creating developer profile OK');
329+
try {
330+
let user = await this._api.user.getCurrentUser();
331+
// check if has developer profile...
332+
if (!user.organizations.find(x => x.isDeveloperProfile)) {
333+
console.log(PREFIX, 'Creating developer profile...');
334+
await this._api.user.createDeveloperProfile();
335+
console.log(PREFIX, 'Creating developer profile OK');
336+
}
337+
}
338+
catch (ex) {
339+
if (!apiKey) throw ex;
340+
// Otherwise ok
319341
}
320342
}
321343

@@ -543,7 +565,8 @@ export class Config {
543565
modelVariantsForProjectId: undefined,
544566
storageMaxSizeMb: undefined,
545567
monitorSummaryIntervalMs: undefined,
546-
}
568+
},
569+
apiKeysForProject: undefined,
547570
};
548571
}
549572

cli-common/init-cli-app.ts

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import checkNewVersions from './check-new-version';
66
import { AWSSecretsManagerUtils } from "./aws-sm-utils";
77
import { AWSIoTCoreConnector } from "./aws-iotcore-connector";
88
import searchList from 'inquirer-search-list';
9+
import { EdgeImpulseApi } from '../sdk/studio';
10+
import os from 'node:os';
11+
12+
const PREFIX = '\x1b[33m[INT]\x1b[0m';
913

1014
inquirer.registerPrompt('search-list', searchList);
1115

@@ -164,6 +168,7 @@ export async function setupCliApp(configFactory: Config, config: EdgeImpulseConf
164168
apiKeyArgv: string | undefined,
165169
devArgv: boolean,
166170
hmacKeyArgv: string | undefined,
171+
verboseArgv: boolean,
167172
connectProjectMsg: string,
168173
getProjectFromConfig?: (deviceId: string | undefined) => Promise<{ projectId: number } | undefined>
169174
}, deviceId: string | undefined) {
@@ -228,24 +233,63 @@ export async function setupCliApp(configFactory: Config, config: EdgeImpulseConf
228233
apiKey: opts.apiKeyArgv || '',
229234
hmacKey: opts.hmacKeyArgv || '0'
230235
};
231-
if (!opts.apiKeyArgv) {
236+
237+
// get hmac dev key (if set)
238+
if (!opts.hmacKeyArgv) {
232239
try {
233-
let dk = (await config.api.projects.listDevkeys(projectId));
240+
let dk = (await config.api.projects.getHmacDevkey(projectId));
241+
if (dk.hmacKey) {
242+
devKeys.hmacKey = dk.hmacKey;
243+
}
244+
}
245+
catch (ex2) {
246+
// noop, e.g. key with not enough permissions
247+
if (opts.verboseArgv) {
248+
const ex = <Error>ex2;
249+
console.log(PREFIX, `Could not fetch development Hmac key: ` + (ex.message || ex.toString()));
250+
}
251+
}
252+
}
234253

235-
if (!dk.apiKey) {
236-
throw new Error('No API key set (via --api-key), and no development API keys configured for ' +
237-
'this project. Add a development API key from the Edge Impulse dashboard to continue.');
254+
if (!opts.apiKeyArgv) {
255+
// create API key
256+
let apiKey = await configFactory.getApiKeyForProject(projectId);
257+
if (apiKey) {
258+
// validate API key...
259+
const api = new EdgeImpulseApi({
260+
endpoint: config.endpoints.internal.api,
261+
extraHeaders: { 'User-Agent': 'EDGE_IMPULSE_CLI' },
262+
});
263+
api.authenticate({
264+
method: 'apiKey',
265+
apiKey: apiKey,
266+
});
267+
try {
268+
await api.projects.getProjectInfo(projectId);
238269
}
270+
catch (ex2) {
271+
// noop, e.g. key with not enough permissions
272+
if (opts.verboseArgv) {
273+
const ex = <Error>ex2;
274+
console.log(PREFIX, `Failed to do request with stored API key for project ${projectId} (ERR: ${ex.message || ex.toString()}). ` +
275+
`Creating new API key...`);
276+
}
239277

240-
devKeys.apiKey = dk.apiKey;
241-
if (!opts.hmacKeyArgv && dk.hmacKey) {
242-
devKeys.hmacKey = dk.hmacKey;
278+
apiKey = undefined;
243279
}
244280
}
245-
catch (ex2) {
246-
let ex = <Error>ex2;
247-
throw new Error('Failed to load development keys: ' + (ex.message || ex.toString()));
281+
282+
if (!apiKey) {
283+
const res = await config.api.projects.addProjectApiKey(projectId, {
284+
name: `Edge Impulse CLI (${os.hostname()})`,
285+
role: 'ingestion_deployment',
286+
isDevelopmentKey: false,
287+
});
288+
apiKey = res.apiKey;
289+
await configFactory.setApiKeyForProject(projectId, apiKey);
248290
}
291+
292+
devKeys.apiKey = apiKey;
249293
}
250294

251295
return {

cli/linux/linux.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import program from 'commander';
99
import Path from 'path';
1010
import fs from 'fs';
1111
import Websocket from 'ws';
12-
import { initCamera, initMicrophone, getCameraType } from '../../library/sensors/sensors-helper';
12+
import { initCamera, initMicrophone, getCameraType, CameraType } from '../../library/sensors/sensors-helper';
1313
import { LinuxDevice } from './linux-device';
1414

1515
const packageVersion = (<{ version: string }>JSON.parse(fs.readFileSync(
@@ -39,6 +39,8 @@ program
3939
.option('--verbose', 'Enable debug logs')
4040
.option('--greengrass', 'Enable AWS IoT greengrass integration mode')
4141
.option('--gst-source <args>', 'Defines the gstreamer source. E.g --gst-source \"tcpserversrc host=0.0.0.0 port=5050 ! jpegdec\"')
42+
.option('--fake-camera <path>', 'Create a fake camera instance')
43+
.option('--dont-prompt-for-device-name', `Don't prompt for a device name`)
4244
.allowUnknownOption(true)
4345
.parse(process.argv);
4446

@@ -60,6 +62,8 @@ const cameraArgv = <string | undefined>program.camera;
6062
const microphoneArgv = <string | undefined>program.microphone;
6163
const cameraColorFormatArgv = <string | undefined>program.cameraColorFormat;
6264
const gstSourceArgv = <string | undefined>program.gstSource;
65+
const fakeCameraArgv = <string | undefined>program.fakeCamera;
66+
const dontPromptForDeviceName = !!program.dontPromptForDeviceName;
6367

6468
if ((program.width && !program.height) || (!program.width && program.height)) {
6569
console.error('--width and --height need to either be both specified or both omitted');
@@ -90,6 +94,7 @@ const cliOptions = {
9094
devArgv: devArgv,
9195
hmacKeyArgv: hmacKeyArgv,
9296
silentArgv: silentArgv,
97+
verboseArgv: verboseArgv,
9398
connectProjectMsg: 'To which project do you want to connect this device?',
9499
getProjectFromConfig: async () => {
95100
let projectId = await configFactory.getLinuxProjectId();
@@ -107,7 +112,9 @@ let isExiting = false;
107112
// eslint-disable-next-line @typescript-eslint/no-floating-promises
108113
(async () => {
109114
try {
110-
const cameraType = getCameraType();
115+
const cameraType = fakeCameraArgv ?
116+
CameraType.Fake :
117+
getCameraType();
111118

112119
const init = await initCliApp(cliOptions);
113120
const config = init.config;
@@ -128,6 +135,8 @@ let isExiting = false;
128135
undefined, // model monitoring object
129136
url => new Websocket(url),
130137
async (currName) => {
138+
if (dontPromptForDeviceName) return currName;
139+
131140
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
132141
let nameDevice = <{ nameDevice: string }>await inquirer.prompt([{
133142
type: 'input',
@@ -200,6 +209,7 @@ let isExiting = false;
200209
dontOutputRgbBuffers: true,
201210
preferJpegCaps: false,
202211
gstSource: gstSourceArgv,
212+
fakeImageCameraPath: fakeCameraArgv,
203213
});
204214
camera = await initedCamera.start();
205215
const cameraOpts = camera.getLastOptions();

cli/linux/runner.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ program
114114
`Then you write to the server from another GStreamer pipeline via: ` +
115115
`"gst-launch-1.0 v4l2src device=/dev/video0 ! video/x-raw,width=640,height=480 ! jpegenc ! tcpclientsink host=localhost port=5050". ` +
116116
`Only "tcpserversrc" elements are currently supported.`)
117+
.option('--fake-camera <path>', 'Create a fake camera instance')
117118
.option('--enable-gpu', 'Enable GPU acceleration for VLM models (removes CPU-only mode) on certain Qualcomm targets (RB3 Gen 2, IQ-9)')
118119
.option('--verbose', 'Enable debug logs')
119120
.allowUnknownOption(true)
@@ -178,6 +179,7 @@ const microphoneArgv = <string | undefined>program.microphone;
178179
const cameraColorFormatArgv = <string | undefined>program.cameraColorFormat;
179180
const experimentalGstPreferJpegArgv = !!program.experimentalGstPreferJpeg;
180181
let modeArgv = <'streaming' | 'http-server' | undefined>program.mode;
182+
const fakeCameraArgv = <string | undefined>program.fakeCamera;
181183

182184
if (modeArgv === 'http-server' && typeof runHttpServerPort === 'undefined') {
183185
runHttpServerPort = 1337;
@@ -206,6 +208,7 @@ const cliOptions = {
206208
devArgv: devArgv,
207209
hmacKeyArgv: undefined,
208210
silentArgv: silentArgv,
211+
verboseArgv: verboseArgv,
209212
connectProjectMsg: 'From which project do you want to load the model?',
210213
getProjectFromConfig: async () => {
211214
if (!configFactory) return undefined;
@@ -349,7 +352,9 @@ async function ensureVlmServer(opts: { serverPath: string,
349352
let devKeys: { apiKey: string, hmacKey: string };
350353
let runner: LinuxImpulseRunner;
351354
let model: ModelInformation;
352-
const cameraType = getCameraType();
355+
const cameraType = fakeCameraArgv ?
356+
CameraType.Fake :
357+
getCameraType();
353358

354359
// AWS Support
355360
let awsSM: AWSSecretsManagerUtils | undefined;
@@ -1027,6 +1032,7 @@ async function ensureVlmServer(opts: { serverPath: string,
10271032
dontOutputRgbBuffers: dontOutputRgbBuffersArgv,
10281033
preferJpegCaps: experimentalGstPreferJpegArgv,
10291034
gstSource: gstSourceArgv,
1035+
fakeImageCameraPath: fakeCameraArgv,
10301036
});
10311037

10321038
console.log(RUNNER_PREFIX, `Using camera "${cameraInit.cameraDevice}" (because --enable-camera, run with --clean to select another one)`);
@@ -1191,6 +1197,7 @@ async function ensureVlmServer(opts: { serverPath: string,
11911197
dontOutputRgbBuffers: dontOutputRgbBuffersArgv,
11921198
preferJpegCaps: experimentalGstPreferJpegArgv,
11931199
gstSource: gstSourceArgv,
1200+
fakeImageCameraPath: fakeCameraArgv,
11941201
});
11951202

11961203
await configFactory.storeCamera(cameraInit.cameraDevice);

library/sensors/fake-camera.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import fs from 'node:fs';
2+
import { EventEmitter } from 'tsee';
3+
import { ICamera, ICameraProfilingInfoEvent, ICameraSnapshotForInferenceEvent, ICameraStartOptions } from './icamera';
4+
5+
export class FakeCamera extends EventEmitter<{
6+
snapshot: (buffer: Buffer, filename: string) => void,
7+
snapshotForInference: (ev: ICameraSnapshotForInferenceEvent) => void,
8+
error: (message: string) => void,
9+
profilingInfo: (ev: ICameraProfilingInfoEvent) => void,
10+
}> implements ICamera {
11+
private _imagePath: string;
12+
private _image: Buffer | undefined;
13+
private _timeout: NodeJS.Timeout | undefined;
14+
private _lastOptions: ICameraStartOptions | undefined;
15+
16+
/**
17+
* Instantiate the imagesnap backend (on macOS)
18+
*/
19+
constructor(opts: {
20+
imagePath: string,
21+
}) {
22+
super();
23+
24+
this._imagePath = opts.imagePath;
25+
}
26+
27+
/**
28+
* Verify that all dependencies are installed
29+
*/
30+
async init() {
31+
this._image = await fs.promises.readFile(this._imagePath);
32+
}
33+
34+
/**
35+
* List all available cameras
36+
*/
37+
async listDevices() {
38+
return [ `Fake camera` ];
39+
}
40+
41+
/**
42+
* Start the capture process
43+
* @param options Specify the camera, and the required interval between snapshots
44+
*/
45+
async start(options: ICameraStartOptions) {
46+
if (!this._image) {
47+
throw new Error(`Camera was not initialized`);
48+
}
49+
if (this._timeout) {
50+
clearInterval(this._timeout);
51+
}
52+
53+
this._lastOptions = options;
54+
55+
this._timeout = setInterval(() => {
56+
this.emit('snapshot', this._image!, `fake.jpg`);
57+
// snapshotForInference() sends out the resized image
58+
this.emit('snapshotForInference', {
59+
imageForInferenceJpg: this._image!,
60+
filename: 'fake.jpg',
61+
imageFromCameraJpg: this._image!,
62+
imageForInferenceRgb: undefined,
63+
});
64+
}, options.intervalMs === 0 ? 100 : options.intervalMs);
65+
}
66+
67+
async stop() {
68+
if (this._timeout) {
69+
clearInterval(this._timeout);
70+
}
71+
}
72+
73+
getLastOptions() {
74+
return this._lastOptions;
75+
}
76+
}

library/sensors/sensors-helper.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { Prophesee } from './prophesee';
44
import { GStreamer } from './gstreamer';
55
import { Imagesnap } from './imagesnap';
66
import { AudioRecorder } from './recorder';
7+
import { FakeCamera } from './fake-camera';
78

89
export enum CameraType {
910
PropheseeCamera = 'prophesee',
1011
ImagesnapCamera = 'imagesnap',
1112
GStreamerCamera = 'gstreamer',
13+
Fake = 'fake',
1214
UnknownCamera = 'unknown',
1315
}
1416

@@ -25,13 +27,20 @@ export async function initCamera(opts: {
2527
profiling: boolean,
2628
preferJpegCaps: boolean,
2729
gstSource: string | undefined,
30+
fakeImageCameraPath: string | undefined,
2831
}) {
2932
const { cameraType, cameraDeviceNameInConfig, dimensions, inferenceDimensions,
3033
gstLaunchArgs, verboseOutput, cameraColorFormat, gstSource } = opts;
3134
let { cameraNameArgv } = opts;
3235

3336
let camera: ICamera;
34-
if (cameraType === CameraType.PropheseeCamera) {
37+
if (cameraType === CameraType.Fake) {
38+
if (!opts.fakeImageCameraPath) {
39+
throw new Error(`opts.fakeImageCameraPath is required for FakeCamera`);
40+
}
41+
camera = new FakeCamera({ imagePath: opts.fakeImageCameraPath });
42+
}
43+
else if (cameraType === CameraType.PropheseeCamera) {
3544
camera = new Prophesee(verboseOutput);
3645
}
3746
else if (cameraType === CameraType.ImagesnapCamera) {

0 commit comments

Comments
 (0)