仿真平台内核初版 -tlib库 包含<sparc arm riscv powerPC>
This commit is contained in:
3
ws-api/ts/.gitignore
vendored
Normal file
3
ws-api/ts/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
*.tgz
|
||||
3
ws-api/ts/.prettierignore
Normal file
3
ws-api/ts/.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
pnpm-lock.yaml
|
||||
dist
|
||||
node_modules
|
||||
202
ws-api/ts/LICENSE
Normal file
202
ws-api/ts/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
38
ws-api/ts/esbuild.js
Normal file
38
ws-api/ts/esbuild.js
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const esbuild = require('esbuild');
|
||||
const { polyfillNode } = require('esbuild-plugin-polyfill-node');
|
||||
|
||||
const production = process.argv.includes('--production');
|
||||
|
||||
function esbuildContext(entryPoint, outfile, browser) {
|
||||
return esbuild.context({
|
||||
entryPoints: [entryPoint],
|
||||
bundle: true,
|
||||
format: browser ? 'esm' : 'cjs',
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
sourcesContent: false,
|
||||
platform: browser ? 'browser' : 'node',
|
||||
outfile,
|
||||
plugins: browser ? [polyfillNode({})] : [],
|
||||
packages: 'external',
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctxMain = await esbuildContext('src/index.ts', 'dist/index.js');
|
||||
const ctxWeb = await esbuildContext('src/index.ts', 'dist/web.js', true);
|
||||
|
||||
const ctxs = [ctxMain, ctxWeb];
|
||||
|
||||
await Promise.all(ctxs.map(ctx => ctx.rebuild()));
|
||||
await Promise.all(ctxs.map(ctx => ctx.dispose()));
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
32
ws-api/ts/eslint.config.mjs
Normal file
32
ws-api/ts/eslint.config.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const configs = tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
args: 'all',
|
||||
argsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
destructuredArrayIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export default configs.map(config => ({
|
||||
...config,
|
||||
files: ['src/**/*.ts'],
|
||||
}));
|
||||
56
ws-api/ts/package.json
Normal file
56
ws-api/ts/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "renode-ws-api",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"browser": "./dist/web.js",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"scripts": {
|
||||
"check:types": "tsc --noEmit",
|
||||
"check:lint": "eslint",
|
||||
"check:fmt": "prettier --check .",
|
||||
"check": "npm-run-all check:*",
|
||||
"fix:lint": "eslint --fix",
|
||||
"fix:fmt": "prettier --write .",
|
||||
"fix": "npm-run-all fix:*",
|
||||
"compile:js": "node esbuild.js --production",
|
||||
"compile:types": "dts-bundle-generator -o dist/index.d.ts src/index.ts",
|
||||
"precompile": "npm-run-all check",
|
||||
"compile": "npm-run-all --parallel compile:*",
|
||||
"prepare": "npm-run-all compile"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/ws": "^8.5.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.6.0",
|
||||
"@typescript-eslint/parser": "^8.6.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"dts-bundle-generator": "^9.5.1",
|
||||
"esbuild": "^0.25.0",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"eslint": "^9.11.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "~5.6.0",
|
||||
"typescript-eslint": "^8.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"semver": "^7.7.3",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
}
|
||||
2616
ws-api/ts/pnpm-lock.yaml
generated
Normal file
2616
ws-api/ts/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
ws-api/ts/src/events.ts
Normal file
32
ws-api/ts/src/events.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const UartOpened = 'uart-opened';
|
||||
export const RenodeQuitted = 'renode-quitted';
|
||||
export const ClearCommand = 'clear-command';
|
||||
|
||||
export interface UartOpenedArgs {
|
||||
port: number;
|
||||
name: string;
|
||||
machineName: string;
|
||||
}
|
||||
export type UartOpenedCallback = (event: UartOpenedArgs) => void;
|
||||
|
||||
export const LedStateChanged = 'led-state-changed';
|
||||
export interface LedStateChangedArgs {
|
||||
machineName: string;
|
||||
name: string;
|
||||
value: boolean;
|
||||
}
|
||||
export type LedStateChangedCallback = (event: LedStateChangedArgs) => void;
|
||||
|
||||
export const ButtonStateChanged = 'button-state-changed';
|
||||
export interface ButtonStateChangedArgs {
|
||||
machineName: string;
|
||||
name: string;
|
||||
value: boolean;
|
||||
}
|
||||
export type ButtonStateChangedCallback = (
|
||||
event: ButtonStateChangedArgs,
|
||||
) => void;
|
||||
637
ws-api/ts/src/index.ts
Normal file
637
ws-api/ts/src/index.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { version, isOutdatedClientVersion } from './version';
|
||||
import { z } from 'zod';
|
||||
import * as s from './schema';
|
||||
import WebSocket from 'isomorphic-ws';
|
||||
import { Buffer } from 'buffer';
|
||||
import { tryConnectWs } from './utils';
|
||||
import {
|
||||
UartOpened,
|
||||
UartOpenedCallback,
|
||||
UartOpenedArgs,
|
||||
RenodeQuitted,
|
||||
ClearCommand,
|
||||
LedStateChanged,
|
||||
LedStateChangedCallback,
|
||||
LedStateChangedArgs,
|
||||
ButtonStateChanged,
|
||||
ButtonStateChangedCallback,
|
||||
ButtonStateChangedArgs,
|
||||
} from './events';
|
||||
import {
|
||||
GetSensorValue,
|
||||
Sensor,
|
||||
SensorType,
|
||||
SensorTypeFromString,
|
||||
SensorValue,
|
||||
} from './sensor';
|
||||
|
||||
export {
|
||||
Sensor,
|
||||
SensorType,
|
||||
SensorTypeFromString,
|
||||
GetSensorValue,
|
||||
SensorValue,
|
||||
EventCallback,
|
||||
EmptyEventCallback,
|
||||
UartOpenedArgs,
|
||||
UartOpenedCallback,
|
||||
};
|
||||
|
||||
class SocketClosedEvent extends Event {
|
||||
constructor() {
|
||||
super('close');
|
||||
}
|
||||
}
|
||||
|
||||
export class RenodeProxySession extends EventTarget {
|
||||
private requestHandlers: RequestHandlers = {};
|
||||
private eventHandlers: EventHandlers = {};
|
||||
private id: number = 1;
|
||||
private defaultTimeout: number = 60000; // in ms
|
||||
private errorCallback: (event: WebSocket.ErrorEvent) => void;
|
||||
|
||||
public static async tryConnect(wsUri: string, workspace: string) {
|
||||
const uri = new URL(`/proxy/${workspace}`, wsUri);
|
||||
const socket = await tryConnectWs(uri.toString());
|
||||
return new RenodeProxySession(socket, wsUri);
|
||||
}
|
||||
|
||||
public static fromWs(ws: WebSocket) {
|
||||
return new RenodeProxySession(ws);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private sessionSocket: WebSocket,
|
||||
private sessionUri?: string,
|
||||
) {
|
||||
super();
|
||||
this.errorCallback = _ =>
|
||||
console.error('RenodeProxySession: WebSocket error');
|
||||
this.sessionSocket.addEventListener('message', ev =>
|
||||
this.onData(ev.data.toString()),
|
||||
);
|
||||
this.sessionSocket.addEventListener('error', ev => this.errorCallback(ev));
|
||||
this.sessionSocket.addEventListener('close', () => this.onClose());
|
||||
}
|
||||
|
||||
public set onError(cb: (event: WebSocket.ErrorEvent) => void) {
|
||||
this.errorCallback = cb;
|
||||
}
|
||||
|
||||
public get sessionBase(): string | undefined {
|
||||
return this.sessionUri;
|
||||
}
|
||||
|
||||
public get socketReady() {
|
||||
const state = this.sessionSocket.readyState ?? WebSocket.CLOSED;
|
||||
return state === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
public async startRenode(cwd?: string, gui: boolean = false): Promise<void> {
|
||||
await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'spawn',
|
||||
payload: {
|
||||
name: 'renode',
|
||||
cwd,
|
||||
gui,
|
||||
},
|
||||
},
|
||||
s.SpawnResponse,
|
||||
10500, // ms
|
||||
);
|
||||
}
|
||||
|
||||
public execMonitor(commands: string[]): Promise<void> {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-monitor',
|
||||
payload: {
|
||||
commands,
|
||||
},
|
||||
},
|
||||
s.ExecMonitorResponse,
|
||||
10000, // ms
|
||||
);
|
||||
}
|
||||
|
||||
public getUarts(machine: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'uarts',
|
||||
args: { machine },
|
||||
},
|
||||
},
|
||||
s.GetUartsResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async getButtons(machine: string) {
|
||||
return await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'buttons',
|
||||
args: { machine },
|
||||
},
|
||||
},
|
||||
s.GetButtonsResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async getLeds(machine: string) {
|
||||
return await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'leds',
|
||||
args: { machine },
|
||||
},
|
||||
},
|
||||
s.GetLedsResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async setButtonValue(
|
||||
machine: string,
|
||||
peripheral: string,
|
||||
value: boolean,
|
||||
): Promise<void> {
|
||||
await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'button-set',
|
||||
args: {
|
||||
machine: machine,
|
||||
peripheral: peripheral,
|
||||
value: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
s.EmptyExecResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public getMachines() {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'machines',
|
||||
},
|
||||
},
|
||||
s.GetMachinesResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async getSensors(
|
||||
machine: string,
|
||||
type?: SensorType,
|
||||
): Promise<Sensor[]> {
|
||||
const sensorType = type === undefined ? {} : { type };
|
||||
const result = await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'sensors',
|
||||
args: { machine, ...sensorType },
|
||||
},
|
||||
},
|
||||
s.GetSensorsResponse,
|
||||
);
|
||||
return result.map(
|
||||
entry =>
|
||||
new Sensor(
|
||||
machine,
|
||||
entry.name,
|
||||
entry.types.map(type => SensorTypeFromString(type)!),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public async getSensorValue(
|
||||
sensor: Sensor,
|
||||
type: SensorType,
|
||||
): Promise<SensorValue> {
|
||||
const result = await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'sensor-get',
|
||||
args: { machine: sensor.machine, peripheral: sensor.name, type },
|
||||
},
|
||||
},
|
||||
s.GetSensorResponse,
|
||||
);
|
||||
return GetSensorValue(type, result);
|
||||
}
|
||||
|
||||
public async setSensorValue(
|
||||
sensor: Sensor,
|
||||
type: SensorType,
|
||||
value: SensorValue,
|
||||
): Promise<void> {
|
||||
await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'exec-renode',
|
||||
payload: {
|
||||
command: 'sensor-set',
|
||||
args: {
|
||||
machine: sensor.machine,
|
||||
peripheral: sensor.name,
|
||||
type,
|
||||
value: value.sample,
|
||||
},
|
||||
},
|
||||
},
|
||||
s.EmptyExecResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async stopRenode(): Promise<void> {
|
||||
await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'kill',
|
||||
payload: {
|
||||
name: 'renode',
|
||||
},
|
||||
},
|
||||
s.KillResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async fetchZipToFs(zipUrl: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/zip',
|
||||
payload: {
|
||||
args: [zipUrl],
|
||||
},
|
||||
},
|
||||
s.FsZipResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async fetchFileToFs(fileUrl: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/fetch',
|
||||
payload: {
|
||||
args: [fileUrl],
|
||||
},
|
||||
},
|
||||
s.FsFetchResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async downloadFile(path: string): Promise<Uint8Array> {
|
||||
const encoded = await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/dwnl',
|
||||
payload: {
|
||||
args: [path],
|
||||
},
|
||||
},
|
||||
s.FsDwnlResponse,
|
||||
);
|
||||
return Buffer.from(encoded, 'base64');
|
||||
}
|
||||
|
||||
public async createDirectory(path: string): Promise<void> {
|
||||
await this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/mkdir',
|
||||
payload: {
|
||||
args: [path],
|
||||
},
|
||||
},
|
||||
s.FsMkdirResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public sendFile(path: string, contents: Uint8Array) {
|
||||
const buf = Buffer.from(contents);
|
||||
const enc = buf.toString('base64');
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/upld',
|
||||
payload: {
|
||||
args: [path],
|
||||
data: enc,
|
||||
},
|
||||
},
|
||||
s.FsUpldResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public async listFiles(path: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/list',
|
||||
payload: {
|
||||
args: [path],
|
||||
},
|
||||
},
|
||||
s.FsListResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public statFile(path: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/stat',
|
||||
payload: {
|
||||
args: [path],
|
||||
},
|
||||
},
|
||||
s.FsStatResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public removeFile(path: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/remove',
|
||||
payload: {
|
||||
args: [path],
|
||||
},
|
||||
},
|
||||
s.FsRemoveResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public moveFile(from: string, to: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/move',
|
||||
payload: { args: [from, to] },
|
||||
},
|
||||
s.FsMoveResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public copyFile(from: string, to: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'fs/copy',
|
||||
payload: { args: [from, to] },
|
||||
},
|
||||
s.FsCopyResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public replaceAnalyzers(path: string) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'tweak/socket',
|
||||
payload: { args: [path] },
|
||||
},
|
||||
s.ReplaceAnalyzersResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public filterEvents(allowedEvents: string[]) {
|
||||
return this.sendSessionRequestTyped(
|
||||
{
|
||||
action: 'filter-events',
|
||||
payload: { args: allowedEvents },
|
||||
},
|
||||
s.FilterEventsResponse,
|
||||
);
|
||||
}
|
||||
|
||||
public registerEventCallback(event: string, callback: EventCallback) {
|
||||
if (!this.eventHandlers[event]) {
|
||||
this.eventHandlers[event] = [];
|
||||
}
|
||||
this.eventHandlers[event].push(callback);
|
||||
}
|
||||
|
||||
public unregisterEventCallback(
|
||||
event: string,
|
||||
callback: EventCallback,
|
||||
): boolean {
|
||||
if (!this.eventHandlers[event]) {
|
||||
return false;
|
||||
}
|
||||
const index = this.eventHandlers[event].indexOf(callback);
|
||||
if (index == -1) {
|
||||
return false;
|
||||
}
|
||||
this.eventHandlers[event].splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
public registerUartOpenedCallback(
|
||||
callback: UartOpenedCallback,
|
||||
): EventCallback {
|
||||
const wrapped = (data: object) => {
|
||||
callback(data as UartOpenedArgs);
|
||||
};
|
||||
this.registerEventCallback(UartOpened, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
public unregisterUartOpenedCallback(callback: EventCallback): boolean {
|
||||
return this.unregisterEventCallback(UartOpened, callback);
|
||||
}
|
||||
|
||||
public registerRenodeQuittedCallback(callback: EmptyEventCallback): void {
|
||||
this.registerEventCallback(RenodeQuitted, callback);
|
||||
}
|
||||
|
||||
public unregisterRenodeQuittedCallback(
|
||||
callback: EmptyEventCallback,
|
||||
): boolean {
|
||||
return this.unregisterEventCallback(RenodeQuitted, callback);
|
||||
}
|
||||
|
||||
public registerClearCommandCallback(callback: EmptyEventCallback): void {
|
||||
this.registerEventCallback(ClearCommand, callback);
|
||||
}
|
||||
|
||||
public unregisterClearCommandCallback(callback: EmptyEventCallback): boolean {
|
||||
return this.unregisterEventCallback(ClearCommand, callback);
|
||||
}
|
||||
|
||||
public registerLedStateChangedCallback(
|
||||
callback: LedStateChangedCallback,
|
||||
): EventCallback {
|
||||
const wrapped = (data: object) => {
|
||||
callback(data as LedStateChangedArgs);
|
||||
};
|
||||
this.registerEventCallback(LedStateChanged, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
public unregisterLedStateChangedCallback(callback: EventCallback): boolean {
|
||||
return this.unregisterEventCallback(LedStateChanged, callback);
|
||||
}
|
||||
|
||||
public registerButtonStateChangedCallback(
|
||||
callback: ButtonStateChangedCallback,
|
||||
): EventCallback {
|
||||
const wrapped = (data: object) => {
|
||||
callback(data as ButtonStateChangedArgs);
|
||||
};
|
||||
this.registerEventCallback(ButtonStateChanged, wrapped);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
public unregisterButtonStateChangedCallback(
|
||||
callback: EventCallback,
|
||||
): boolean {
|
||||
return this.unregisterEventCallback(ButtonStateChanged, callback);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.sessionSocket.close();
|
||||
}
|
||||
|
||||
// *** Event handlers ***
|
||||
|
||||
private onData(data: string) {
|
||||
try {
|
||||
const obj: object = JSON.parse(data);
|
||||
if ('id' in obj && typeof obj.id === 'number') {
|
||||
this.onResponse(obj.id, obj);
|
||||
} else if ('event' in obj && typeof obj.event === 'string') {
|
||||
this.onEvent(obj.event, 'data' in obj ? (obj.data as object) : {});
|
||||
} else {
|
||||
console.error('RenodeProxySession: Received malformed packet', obj);
|
||||
}
|
||||
} catch {
|
||||
console.error('RenodeProxySession: Received malformed data', data);
|
||||
}
|
||||
}
|
||||
|
||||
private onResponse(id: number, data: object) {
|
||||
const handler = this.requestHandlers[id];
|
||||
delete this.requestHandlers[id];
|
||||
if (handler) {
|
||||
handler(data);
|
||||
} else {
|
||||
console.error(
|
||||
'RenodeProxySession: Received response without request',
|
||||
data,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private onEvent(event: string, data: object) {
|
||||
if (!this.eventHandlers[event]) {
|
||||
console.error(
|
||||
`RenodeProxySession: Received event '${event}' with no listeners`,
|
||||
data,
|
||||
);
|
||||
} else {
|
||||
this.eventHandlers[event].forEach(handler => handler(data));
|
||||
}
|
||||
}
|
||||
|
||||
private onClose() {
|
||||
Object.values(this.requestHandlers).forEach(handler =>
|
||||
handler?.(undefined, new Error('WebSocket closed')),
|
||||
);
|
||||
this.requestHandlers = {};
|
||||
this.eventHandlers = {};
|
||||
|
||||
this.dispatchEvent(new SocketClosedEvent());
|
||||
}
|
||||
|
||||
// *** Utilities ***
|
||||
|
||||
private async sendSessionRequestTyped<Res extends s.Response>(
|
||||
req: PartialRequest,
|
||||
resParser: z.ZodType<Res, z.ZodTypeDef, object>,
|
||||
timeout?: number,
|
||||
): Promise<ResData<Res>> {
|
||||
if (!this.socketReady) {
|
||||
throw new Error('Not connected');
|
||||
}
|
||||
|
||||
const res: object = await this.sendInner(
|
||||
req,
|
||||
timeout ?? this.defaultTimeout,
|
||||
);
|
||||
console.debug('Got answer from session', res);
|
||||
|
||||
const resParsed = await resParser.safeParseAsync(res);
|
||||
|
||||
if (!resParsed.success) {
|
||||
throw resParsed.error;
|
||||
}
|
||||
|
||||
const obj = resParsed.data as Res;
|
||||
if (obj.status !== 'success') {
|
||||
throw new Error(obj.error);
|
||||
}
|
||||
|
||||
if (isOutdatedClientVersion(obj.version)) {
|
||||
throw new Error(
|
||||
`Protocol version (${obj.version}) is incompatible with the JS client (${version})`,
|
||||
);
|
||||
}
|
||||
|
||||
return obj.data;
|
||||
}
|
||||
|
||||
private sendInner(req: PartialRequest, timeout: number): Promise<object> {
|
||||
const id = this.id++;
|
||||
const msg = JSON.stringify({
|
||||
...req,
|
||||
version,
|
||||
id,
|
||||
});
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
|
||||
return Promise.race([
|
||||
new Promise<object>((resolve, reject) => {
|
||||
console.debug('Sending message to session', msg);
|
||||
|
||||
if (this.sessionSocket) {
|
||||
this.requestHandlers[id] = (res, err) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res!);
|
||||
}
|
||||
};
|
||||
this.sessionSocket.send(msg);
|
||||
} else {
|
||||
reject(new Error('Not connected'));
|
||||
}
|
||||
}),
|
||||
new Promise<object>(
|
||||
(_resolve, reject) =>
|
||||
(timeoutId = setTimeout(() => {
|
||||
delete this.requestHandlers[id];
|
||||
console.debug('Timeout for id', id);
|
||||
reject(new Error(`Request reached timeout after ${timeout}ms`));
|
||||
}, timeout)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
interface PartialRequest {
|
||||
action: string;
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
// Helper to properly type response payload
|
||||
type ResData<T> = T extends { status: 'success'; data?: infer U } ? U : never;
|
||||
|
||||
type RequestHandlers = { [key: number]: RequestCallback };
|
||||
type RequestCallback = (response: object | undefined, error?: Error) => void;
|
||||
type EventHandlers = { [key: string]: EventCallback[] };
|
||||
type EventCallback = (event: object) => void;
|
||||
type EmptyEventCallback = () => void;
|
||||
10
ws-api/ts/src/peripheral.ts
Normal file
10
ws-api/ts/src/peripheral.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export class Peripheral {
|
||||
public constructor(
|
||||
public readonly machine: string,
|
||||
public readonly name: string,
|
||||
) {}
|
||||
}
|
||||
165
ws-api/ts/src/schema.ts
Normal file
165
ws-api/ts/src/schema.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { z, ZodRawShape } from 'zod';
|
||||
import * as semver from 'semver';
|
||||
|
||||
export const BaseResponse = z.object({
|
||||
status: z.string(),
|
||||
version: z.string().refine(s => semver.valid(s) != null),
|
||||
});
|
||||
export const OkResponse = BaseResponse.extend({
|
||||
id: z.number(),
|
||||
status: z.literal('success'),
|
||||
data: z.any(),
|
||||
});
|
||||
export type OkResponse = z.infer<typeof OkResponse>;
|
||||
export const ErrorResponse = BaseResponse.extend({
|
||||
status: z.literal('failure'),
|
||||
error: z
|
||||
.string()
|
||||
.nullish()
|
||||
.transform(err => err ?? 'Unknown error'),
|
||||
});
|
||||
export type ErrorResponse = z.infer<typeof ErrorResponse>;
|
||||
export const Response = OkResponse.or(ErrorResponse);
|
||||
export type Response = z.infer<typeof Response>;
|
||||
|
||||
function resp<T extends ZodRawShape>(obj: T) {
|
||||
return OkResponse.extend(obj).or(ErrorResponse);
|
||||
}
|
||||
|
||||
export const SpawnResponse = resp({
|
||||
data: z.object({}),
|
||||
});
|
||||
export type SpawnResponse = z.infer<typeof SpawnResponse>;
|
||||
|
||||
export const KillResponse = resp({
|
||||
data: z.object({}),
|
||||
});
|
||||
export type KillResponse = z.infer<typeof KillResponse>;
|
||||
|
||||
export const ExecMonitorResponse = resp({});
|
||||
export type ExecMonitorResponse = z.infer<typeof ExecMonitorResponse>;
|
||||
|
||||
export const GetUartsResponse = resp({
|
||||
data: z.string().array(),
|
||||
});
|
||||
export const GetButtonsResponse = resp({
|
||||
data: z.string().array(),
|
||||
});
|
||||
export const GetLedsResponse = resp({
|
||||
data: z.string().array(),
|
||||
});
|
||||
export type GetUartsResponse = z.infer<typeof GetUartsResponse>;
|
||||
|
||||
export const GetMachinesResponse = resp({
|
||||
data: z.string().array(),
|
||||
});
|
||||
export type GetMachinesResponse = z.infer<typeof GetMachinesResponse>;
|
||||
|
||||
export const GetSensorsResponse = resp({
|
||||
data: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
types: z.string().array(),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
export type GetSensorsResponse = z.infer<typeof GetSensorsResponse>;
|
||||
|
||||
export const GetSensorResponse = resp({
|
||||
data: z.unknown(),
|
||||
});
|
||||
export type GetSensorResponse = z.infer<typeof GetSensorResponse>;
|
||||
|
||||
export const EmptyExecResponse = resp({
|
||||
data: z.literal('ok'),
|
||||
});
|
||||
export type EmptyExecResponse = z.infer<typeof EmptyExecResponse>;
|
||||
|
||||
export const FsListResponse = resp({
|
||||
data: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
isfile: z.boolean(),
|
||||
islink: z.boolean(),
|
||||
})
|
||||
.array(),
|
||||
});
|
||||
export type FsListResponse = z.infer<typeof FsListResponse>;
|
||||
|
||||
export const FsStatResponse = resp({
|
||||
data: z.object({
|
||||
size: z.number(),
|
||||
isfile: z.boolean(),
|
||||
ctime: z.number(),
|
||||
mtime: z.number(),
|
||||
}),
|
||||
});
|
||||
export type FsStatResponse = z.infer<typeof FsStatResponse>;
|
||||
|
||||
export const FsDwnlResponse = resp({
|
||||
data: z.string().base64(),
|
||||
});
|
||||
export type FsDwnlResponse = z.infer<typeof FsDwnlResponse>;
|
||||
|
||||
export const FsUpldResponse = resp({
|
||||
data: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
});
|
||||
export type FsUpldResponse = z.infer<typeof FsUpldResponse>;
|
||||
|
||||
export const FsRemoveResponse = resp({
|
||||
data: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
});
|
||||
export type FsRemoveResponse = z.infer<typeof FsRemoveResponse>;
|
||||
|
||||
export const FsMkdirResponse = resp({
|
||||
data: z.object({}),
|
||||
});
|
||||
export type FsMkdirResponse = z.infer<typeof FsMkdirResponse>;
|
||||
|
||||
export const FsZipResponse = resp({
|
||||
data: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
});
|
||||
export type FsZipResponse = z.infer<typeof FsZipResponse>;
|
||||
|
||||
export const FsFetchResponse = resp({
|
||||
data: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
});
|
||||
export type FsFetchResponse = z.infer<typeof FsFetchResponse>;
|
||||
|
||||
export const FsMoveResponse = resp({
|
||||
data: z.object({
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
}),
|
||||
});
|
||||
export type FsMoveResponse = z.infer<typeof FsMoveResponse>;
|
||||
|
||||
export const FsCopyResponse = resp({
|
||||
data: z.object({
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
}),
|
||||
});
|
||||
export type FsCopyResponse = z.infer<typeof FsCopyResponse>;
|
||||
|
||||
export const ReplaceAnalyzersResponse = resp({
|
||||
data: z.object({}),
|
||||
});
|
||||
export type ReplaceAnalyzersResponse = z.infer<typeof ReplaceAnalyzersResponse>;
|
||||
|
||||
export const FilterEventsResponse = resp({
|
||||
data: z.object({}),
|
||||
});
|
||||
export type FilterEventsResponse = z.infer<typeof FilterEventsResponse>;
|
||||
232
ws-api/ts/src/sensor.ts
Normal file
232
ws-api/ts/src/sensor.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { z } from 'zod';
|
||||
import { Peripheral } from './peripheral';
|
||||
|
||||
export enum SensorType {
|
||||
Temperature = 'temperature',
|
||||
Acceleration = 'acceleration',
|
||||
AngularRate = 'angular-rate',
|
||||
Voltage = 'voltage',
|
||||
ECG = 'ecg',
|
||||
Humidity = 'humidity',
|
||||
Pressure = 'pressure',
|
||||
MagneticFluxDensity = 'magnetic-flux-density',
|
||||
}
|
||||
|
||||
export function SensorTypeFromString(value: string): SensorType | undefined {
|
||||
return (Object.values(SensorType) as string[]).includes(value)
|
||||
? (value as SensorType)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function sensorConstructorForType(
|
||||
type: SensorType,
|
||||
): (sample: unknown, pretty: boolean) => SensorValue {
|
||||
const mapping = {
|
||||
[SensorType.Temperature]: TemperatureValue,
|
||||
[SensorType.Acceleration]: AccelerationValue,
|
||||
[SensorType.AngularRate]: AngularRateValue,
|
||||
[SensorType.Voltage]: VoltageValue,
|
||||
[SensorType.ECG]: ECGValue,
|
||||
[SensorType.Humidity]: HumidityValue,
|
||||
[SensorType.Pressure]: PressureValue,
|
||||
[SensorType.MagneticFluxDensity]: MagneticFluxDensityValue,
|
||||
};
|
||||
|
||||
const ctor = mapping[type];
|
||||
return ctor.fromValueChecked.bind(ctor);
|
||||
}
|
||||
|
||||
export function GetSensorValue(
|
||||
type: SensorType,
|
||||
value: unknown,
|
||||
pretty: boolean = false,
|
||||
): SensorValue {
|
||||
return sensorConstructorForType(type)(value, pretty);
|
||||
}
|
||||
|
||||
export class Sensor extends Peripheral {
|
||||
public constructor(
|
||||
machine: string,
|
||||
name: string,
|
||||
public readonly types: SensorType[],
|
||||
) {
|
||||
super(machine, name);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class SensorValue<SampleType = unknown> {
|
||||
public abstract readonly sample: SampleType;
|
||||
public abstract readonly unit: string;
|
||||
public abstract get value(): SampleType;
|
||||
|
||||
public static fromValueChecked(
|
||||
_sample: unknown,
|
||||
_pretty: boolean,
|
||||
): SensorValue {
|
||||
throw new Error('Cannot construct abstract class SensorValue');
|
||||
}
|
||||
}
|
||||
|
||||
const ScalarSampleValue = z.number();
|
||||
export type ScalarSampleValue = z.infer<typeof ScalarSampleValue>;
|
||||
|
||||
const Vec3SampleValue = z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
z: z.number(),
|
||||
});
|
||||
export type Vec3SampleValue = z.infer<typeof Vec3SampleValue>;
|
||||
|
||||
function vec3Map(
|
||||
v: Vec3SampleValue,
|
||||
fun: (value: number) => number,
|
||||
): Vec3SampleValue {
|
||||
return {
|
||||
x: fun(v.x),
|
||||
y: fun(v.y),
|
||||
z: fun(v.z),
|
||||
};
|
||||
}
|
||||
|
||||
class ScalarSensorValue extends SensorValue<ScalarSampleValue> {
|
||||
protected constructor(
|
||||
readonly sample: ScalarSampleValue,
|
||||
readonly unit: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get value() {
|
||||
return this.sample;
|
||||
}
|
||||
|
||||
public static fromValue(
|
||||
_sample: ScalarSampleValue,
|
||||
_pretty: boolean,
|
||||
): ScalarSensorValue {
|
||||
throw new Error('Cannot construct abstract class ScalarSensorValue');
|
||||
}
|
||||
|
||||
public static fromValueChecked(sample: unknown, pretty: boolean) {
|
||||
const sampleChecked = ScalarSampleValue.parse(sample);
|
||||
return this.fromValue(sampleChecked, pretty);
|
||||
}
|
||||
}
|
||||
|
||||
class Vec3SensorValue extends SensorValue<Vec3SampleValue> {
|
||||
protected constructor(
|
||||
readonly sample: Vec3SampleValue,
|
||||
readonly unit: string,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get value() {
|
||||
return this.sample;
|
||||
}
|
||||
|
||||
public static fromValue(
|
||||
_sample: Vec3SampleValue,
|
||||
_pretty: boolean,
|
||||
): Vec3SensorValue {
|
||||
throw new Error('Cannot construct abstract class ScalarSensorValue');
|
||||
}
|
||||
|
||||
public static fromValueChecked(sample: unknown, pretty: boolean) {
|
||||
const sampleChecked = Vec3SampleValue.parse(sample);
|
||||
return this.fromValue(sampleChecked, pretty);
|
||||
}
|
||||
}
|
||||
|
||||
export class TemperatureValue extends ScalarSensorValue {
|
||||
override get value() {
|
||||
return this.sample / 1e3;
|
||||
}
|
||||
|
||||
public static fromValue(sample: number, pretty: boolean): TemperatureValue {
|
||||
if (pretty) sample = Math.round(sample * 1e3);
|
||||
return new this(sample, '°C');
|
||||
}
|
||||
}
|
||||
|
||||
export class AccelerationValue extends Vec3SensorValue {
|
||||
override get value() {
|
||||
return vec3Map(this.sample, v => v / 1e6);
|
||||
}
|
||||
|
||||
public static fromValue(
|
||||
sample: Vec3SampleValue,
|
||||
pretty: boolean,
|
||||
): AccelerationValue {
|
||||
if (pretty) sample = vec3Map(sample, val => Math.round(val * 1e6));
|
||||
return new this(sample, 'g');
|
||||
}
|
||||
}
|
||||
|
||||
export class AngularRateValue extends Vec3SensorValue {
|
||||
override get value() {
|
||||
return vec3Map(this.sample, v => v / 1e5);
|
||||
}
|
||||
|
||||
public static fromValue(
|
||||
sample: Vec3SampleValue,
|
||||
pretty: boolean,
|
||||
): AccelerationValue {
|
||||
if (pretty) sample = vec3Map(sample, val => Math.round(val * 1e5));
|
||||
return new this(sample, 'rad/s');
|
||||
}
|
||||
}
|
||||
|
||||
export class VoltageValue extends ScalarSensorValue {
|
||||
override get value() {
|
||||
return this.sample / 1e6;
|
||||
}
|
||||
|
||||
public static fromValue(sample: number, pretty: boolean): TemperatureValue {
|
||||
if (pretty) sample = Math.round(sample * 1e6);
|
||||
return new this(sample, 'V');
|
||||
}
|
||||
}
|
||||
|
||||
export class ECGValue extends ScalarSensorValue {
|
||||
public static fromValue(sample: number, pretty: boolean): TemperatureValue {
|
||||
if (pretty) sample = Math.round(sample);
|
||||
return new this(sample, 'nV');
|
||||
}
|
||||
}
|
||||
|
||||
export class HumidityValue extends ScalarSensorValue {
|
||||
override get value() {
|
||||
return this.sample / 1e3;
|
||||
}
|
||||
|
||||
public static fromValue(sample: number, pretty: boolean): TemperatureValue {
|
||||
if (pretty) sample = Math.round(sample * 1e3);
|
||||
return new this(sample, '%RH');
|
||||
}
|
||||
}
|
||||
|
||||
export class PressureValue extends ScalarSensorValue {
|
||||
get value() {
|
||||
return this.sample / 1e3;
|
||||
}
|
||||
|
||||
public static fromValue(sample: number, pretty: boolean): TemperatureValue {
|
||||
if (pretty) sample = Math.round(sample * 1e3);
|
||||
return new this(sample, 'Pa');
|
||||
}
|
||||
}
|
||||
|
||||
export class MagneticFluxDensityValue extends Vec3SensorValue {
|
||||
public static fromValue(
|
||||
sample: Vec3SampleValue,
|
||||
pretty: boolean,
|
||||
): AccelerationValue {
|
||||
if (pretty) sample = vec3Map(sample, val => Math.round(val));
|
||||
return new this(sample, 'nT');
|
||||
}
|
||||
}
|
||||
31
ws-api/ts/src/utils.ts
Normal file
31
ws-api/ts/src/utils.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import WebSocket from 'isomorphic-ws';
|
||||
|
||||
export function tryJsonParse(input: string): object | string {
|
||||
try {
|
||||
return JSON.parse(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
export function tryConnectWs(uri: string): Promise<WebSocket> {
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
const socket = new WebSocket(uri);
|
||||
|
||||
socket.addEventListener('open', () => resolve(socket), { once: true });
|
||||
socket.addEventListener(
|
||||
'error',
|
||||
() => reject(new Error('Error while connecting')),
|
||||
{ once: true },
|
||||
);
|
||||
socket.addEventListener(
|
||||
'close',
|
||||
() => reject(new Error('Could not connect')),
|
||||
{ once: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
16
ws-api/ts/src/version.ts
Normal file
16
ws-api/ts/src/version.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2026 Antmicro <www.antmicro.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import * as semver from 'semver';
|
||||
|
||||
export const version: string = '1.5.0';
|
||||
export const clientVersion: semver.SemVer = semver.parse(version)!;
|
||||
|
||||
export const isOutdatedClientVersion = (val: string): boolean => {
|
||||
const apiVersion = semver.parse(val);
|
||||
return (
|
||||
apiVersion?.major != clientVersion.major ||
|
||||
apiVersion.minor > clientVersion.minor
|
||||
);
|
||||
};
|
||||
15
ws-api/ts/tsconfig.json
Normal file
15
ws-api/ts/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
},
|
||||
"exclude": ["dist/"]
|
||||
}
|
||||
Reference in New Issue
Block a user