mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: convert serial console into the serial plugin
This commit is contained in:
1 parent
708eedb861
commit
791d8656a1
121 files changed
+2216
-1425
No files matched your search
@@ -585,27 +585,6 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
}
|
||||
|
||||
location ^~ /serial/websocket/ {
|
||||
proxy_pass http://127.0.0.1:30011/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
proxy_connect_timeout 10s;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /app/html;
|
||||
|
||||
@@ -563,27 +563,6 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
}
|
||||
|
||||
location ^~ /serial/websocket/ {
|
||||
proxy_pass http://127.0.0.1:30011/;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
|
||||
proxy_read_timeout 86400s;
|
||||
proxy_send_timeout 86400s;
|
||||
proxy_connect_timeout 10s;
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /app/html;
|
||||
|
||||
Generated
+14
-1
@@ -46,7 +46,6 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"redis": "^6.2.1",
|
||||
"semver": "^7.8.5",
|
||||
"serialport": "^13.0.0",
|
||||
"sharp": "^0.35.4",
|
||||
"socks": "^2.8.10",
|
||||
"speakeasy": "^2.0.0",
|
||||
@@ -7013,6 +7012,10 @@
|
||||
"resolved": "plugins/remote-desktop",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@termix-plugin/serial": {
|
||||
"resolved": "plugins/serial",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@termix-plugin/snippets": {
|
||||
"resolved": "plugins/snippets",
|
||||
"link": true
|
||||
@@ -18596,6 +18599,16 @@
|
||||
"@termix/plugin-sdk": "*"
|
||||
}
|
||||
},
|
||||
"plugins/serial": {
|
||||
"name": "@termix-plugin/serial",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"serialport": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@termix/plugin-sdk": "*"
|
||||
}
|
||||
},
|
||||
"plugins/snippets": {
|
||||
"name": "@termix-plugin/snippets",
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -88,7 +88,6 @@
|
||||
"qrcode": "^1.5.4",
|
||||
"redis": "^6.2.1",
|
||||
"semver": "^7.8.5",
|
||||
"serialport": "^13.0.0",
|
||||
"sharp": "^0.35.4",
|
||||
"socks": "^2.8.10",
|
||||
"speakeasy": "^2.0.0",
|
||||
|
||||
@@ -1031,6 +1031,7 @@ not do.
|
||||
"frontend": "dist/frontend.js", // default
|
||||
"locales": "locales", // default
|
||||
"platforms": ["linux", "win32", "darwin"], // optional
|
||||
"nativeDependencies": ["serialport"], // optional, see "Native dependencies"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1104,6 +1105,7 @@ deciding, not the mechanism.
|
||||
| `users:write` | high | Create and change user accounts |
|
||||
| `auth:provide` | high | Add a login method, second factor or SSH auth type |
|
||||
| `system:tls` | high | Request and replace the server certificate |
|
||||
| `device:serial` | high | Open a physical serial or USB device on the Termix server |
|
||||
| `hosts:write` | medium | Create and change hosts |
|
||||
| `credentials:use` | medium | Connect using a host's stored credentials, without seeing them |
|
||||
| `network:outbound` | medium | Make outbound requests |
|
||||
@@ -1156,6 +1158,7 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`.
|
||||
| `ctx.sync.registerEntity` | none | **A3** |
|
||||
| `ctx.http.router` / `ctx.ws.route` / `.upgrade` | `network:serve` | **A4** |
|
||||
| `ctx.rbac.has` / `.hasFor` / `.require` | own permissions only | **A5** |
|
||||
| `ctx.capabilities.has` / `.require` | the capability itself | **B11** |
|
||||
| `ctx.hosts.*` | `hosts:read` / `hosts:write` | **B4**, extended **B5** |
|
||||
| `ctx.ssh.*` | `ssh:connect`, `credentials:use` | **A8** |
|
||||
| `ctx.settings.*` | `settings:read-core` (readCore only) | **A6** |
|
||||
@@ -1165,6 +1168,20 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`.
|
||||
| `ctx.audit.record` | none, the actor is the runtime's | **B9** |
|
||||
| `ctx.fetch` | `network:outbound` | B |
|
||||
|
||||
**B11** added `ctx.capabilities.has(capability)` / `.require(capability)`, a
|
||||
generic check for a capability no other ctx member wraps. Unlike every other
|
||||
row above, the capability it checks is not fixed at the call site: the
|
||||
plugin names it. That is right for exactly one kind of caller - privileged
|
||||
code core cannot mediate through its own primitives, because it is not core's
|
||||
to mediate. The serial plugin bundling `serialport` and opening an OS device
|
||||
file is the first: there is no `ctx.serial.open()` for core to gate, because
|
||||
the whole point is that the plugin talks to the hardware directly. `has`
|
||||
answers without an audit line, for deciding whether to offer something;
|
||||
`require` throws `PluginCapabilityError` and audits like a guarded member. A
|
||||
plugin reaching for this first checks whether its need is actually one of the
|
||||
rows above - `device:serial` is not a general "trust me" capability, it is
|
||||
specifically for code a dedicated ctx member cannot cover.
|
||||
|
||||
`ctx.ssh` was pulled forward from B so plugin transports go through core's
|
||||
connect pipeline instead of importing ssh2 helpers from core:
|
||||
|
||||
@@ -1429,6 +1446,70 @@ it.
|
||||
|
||||
The lists live in `packages/plugin-sdk/cli/lib/externals.mjs`.
|
||||
|
||||
### Native dependencies
|
||||
|
||||
A package with a compiled `.node` binding (`serialport`, `better-sqlite3`,
|
||||
`node-pty`) cannot be bundled the way an ordinary npm dependency is. esbuild
|
||||
would inline its JS, but the binding is loaded through a path computed
|
||||
relative to the original package's own directory, which breaks once that JS
|
||||
moves into `dist/backend.js` somewhere else entirely. It also cannot go on
|
||||
the host-provided list above: that list is for packages every plugin shares
|
||||
one instance of, and a serial port binding is one plugin's business, not
|
||||
core's.
|
||||
|
||||
**B11** (the serial plugin, the first bundled plugin with a native
|
||||
dependency) resolved this by declaring the package in the manifest's
|
||||
`nativeDependencies` array (`["serialport"]`) as well as a real dependency in
|
||||
the plugin's own `package.json`. `termix-plugin build` adds every entry to
|
||||
esbuild's `external` list for that plugin alone, so the compiled bundle keeps
|
||||
a plain `import { SerialPort } from "serialport"` instead of inlining it, and
|
||||
`termix-plugin validate` checks the name is actually declared in the
|
||||
plugin's own `dependencies`.
|
||||
|
||||
At runtime that import is a bare specifier Node resolves by walking up from
|
||||
wherever `dist/backend.js` is loaded, through `dist/plugins/serial/`,
|
||||
`dist/plugins/`, `dist/`, to the repo (or image) root. **The three
|
||||
deployments this repo builds all end up with the package there because of
|
||||
how npm workspaces already install it**, not because of anything specific to
|
||||
native code:
|
||||
|
||||
- **Dev and CI.** `npm install` at the repo root installs every workspace's
|
||||
own dependencies, including a plugin's, and hoists them into the root
|
||||
`node_modules` unless a version conflict forces one to stay nested. Nothing
|
||||
about `serialport` is special here; it lands exactly where any of this
|
||||
plugin's other dependencies would.
|
||||
- **Docker.** The image's `npm ci` runs once at the repo root over the whole
|
||||
workspace and produces one `node_modules`, which is copied into the final
|
||||
image wholesale (`docker/Dockerfile`'s `production-deps` stage). Its native
|
||||
binding compiles against the image's own `node:26-slim` toolchain
|
||||
(`python3 make g++`, already installed for `better-sqlite3`), once per
|
||||
target architecture under `docker buildx`, so linux x64 and arm64 each get
|
||||
a binary actually built for that machine rather than a cross-compiled one.
|
||||
- **Electron.** `electron-builder.json`'s `asarUnpack` already unpacks all of
|
||||
`node_modules` (not a curated subset), and `npm run electron:rebuild` runs
|
||||
`electron-rebuild` over the same hoisted `node_modules`, rebuilding
|
||||
`@serialport/bindings-cpp` against Electron's ABI by name, exactly as it
|
||||
already did for `better-sqlite3` and `node-pty`. Nothing in that script
|
||||
changed: a package's native binding gets rebuilt because the script names
|
||||
it, not because of which `package.json` declared the package as a
|
||||
dependency.
|
||||
|
||||
None of this required a new packaging mechanism, which is the honest reason
|
||||
it was chosen over shipping the plugin its own `node_modules`: this repo's
|
||||
build already treats the whole workspace as one `node_modules`, so a
|
||||
native dependency declared in a plugin's `package.json` rides along for
|
||||
free. A **community plugin installed from a tarball has none of this**: there
|
||||
is no repo-root `npm install` to hoist its dependency into, no shared
|
||||
`node_modules` its bundle can walk up to. Shipping a native dependency in a
|
||||
plugin nobody's build system already vendors is an open problem this step
|
||||
does not solve - see `packages/plugin-sdk/FINISH-LIST.md`. A community plugin
|
||||
author who hits this today has three honest options, worst to best: ask the
|
||||
person installing it to `npm install` the native package into the server's
|
||||
own `node_modules` by hand (fragile, easy to get wrong on upgrade); avoid a
|
||||
native dependency and reach for a WASM build of the same functionality if one
|
||||
exists; or wait for prebuild-per-platform packaging support, which is not
|
||||
built yet.
|
||||
|
||||
---
|
||||
|
||||
## Converting a feature into a plugin
|
||||
@@ -1797,11 +1878,12 @@ Known specifics:
|
||||
an `optionalDependency` (added in A2; it previously declared nothing, and
|
||||
the loader had no reason to order the two).
|
||||
- Core feature servers that are not plugins yet still own ports: dashboard
|
||||
30006, tmux 30010, serial 30011 and homepage 30012. Each keeps its nginx
|
||||
block until its own Phase B step. No plugin owns a port any more (A4).
|
||||
**B6** moved file-manager off port 30004 onto `/plugin-api/file-manager/`,
|
||||
and **B7** moved tunnels off port 30003 onto `/plugin-api/tunnels/` and
|
||||
`/plugin-ws/tunnels/c2s/stream`.
|
||||
30006 and homepage 30012. Each keeps its nginx block until its own Phase B
|
||||
step. No plugin owns a port any more (A4). **B6** moved file-manager off
|
||||
port 30004 onto `/plugin-api/file-manager/`, **B7** moved tunnels off port
|
||||
30003 onto `/plugin-api/tunnels/` and `/plugin-ws/tunnels/c2s/stream`,
|
||||
**B10** moved tmux monitoring off port 30010, and **B11** moved serial off
|
||||
port 30011 onto `/plugin-ws/serial/console`.
|
||||
- **B9** moved the terminal, its session manager, the local terminal and the
|
||||
command history panel into ssh-terminal, which imports nothing from core.
|
||||
The file manager's terminal window renders the `terminal.view` component
|
||||
|
||||
@@ -247,6 +247,28 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
`app.registerAction` opening the singleton tab) but is a shared gap across
|
||||
four plugins, not specific to this step. Owner: D1, or whoever revisits the
|
||||
toolbar's quick-link row.
|
||||
- **B11 (serial):** `plugins/serial/src/frontend/Serial.tsx` uses a small
|
||||
hardcoded light/dark xterm palette and a generic monospace font stack
|
||||
instead of the app's real terminal theme system
|
||||
(`resolveTermixThemeColors`, `DEFAULT_TERMINAL_CONFIG`, `TERMINAL_FONTS`,
|
||||
font loading), which is still core-only (`src/ui/features/terminal/`,
|
||||
`src/ui/lib/terminal-themes.ts`) and not yet exposed through the SDK. The
|
||||
serial console therefore does not match a user's chosen terminal theme or
|
||||
font. Promoting that system into `@termix/plugin-sdk/ui` is really
|
||||
ssh-terminal's own Phase B step's call to make (it is the plugin that
|
||||
currently owns the terminal component itself, per the "Known specifics"
|
||||
note above), so this line just points at the gap rather than building a
|
||||
premature shared surface for one caller. Owner: ssh-terminal's own Phase B
|
||||
step, or D1.
|
||||
- **B11 (serial):** shipping a plugin's own native dependency (`serialport`)
|
||||
works for this repo's three bundled deployments only because they all
|
||||
install the whole npm workspace at once and copy/rebuild one shared
|
||||
`node_modules` - see "Native dependencies" in ARCHITECTURE.md for the full
|
||||
reasoning. A community plugin installed later from a tarball has no such
|
||||
shared `node_modules` to land in, so it cannot ship a native dependency the
|
||||
same way. No fix attempted here; the honest workarounds are listed in that
|
||||
section. Owner: whoever designs the 3.0.0 install flow (prebuild-per-platform
|
||||
packaging, or a documented "avoid native code" rule for community plugins).
|
||||
|
||||
## Manual checks after 2.9.0
|
||||
|
||||
@@ -254,3 +276,5 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
||||
prompt), TOTP keyboard-interactive, a jump host, Warpgate, split view,
|
||||
snippets sent to a terminal, reconnect after a network drop and after a
|
||||
page reload (session reattach), and the local terminal in the desktop app.
|
||||
- Serial: the Electron backend path against a real device path/COM port, and
|
||||
the Web Serial browser picker in Chrome or Edge.
|
||||
@@ -52,7 +52,12 @@ export async function build({ cwd }) {
|
||||
target: "node22",
|
||||
sourcemap: true,
|
||||
logLevel: "warning",
|
||||
external: BACKEND_EXTERNALS,
|
||||
// A native dependency's compiled .node binary is resolved by the
|
||||
// package's own relative paths, which break once esbuild inlines its JS
|
||||
// elsewhere. Declaring it in nativeDependencies keeps it a real
|
||||
// node_modules import instead, resolved at runtime like a host-provided
|
||||
// package.
|
||||
external: [...BACKEND_EXTERNALS, ...(manifest.nativeDependencies ?? [])],
|
||||
plugins: [legacyCoreImports({ pluginId, platform: "node" })],
|
||||
});
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export async function validate({ cwd }) {
|
||||
}
|
||||
|
||||
problems.push(...validateMigrations(cwd, raw.id ?? path.basename(cwd)));
|
||||
problems.push(...validateNativeDependencies(cwd, raw));
|
||||
|
||||
if (problems.length > 0) {
|
||||
for (const problem of problems) console.error(` ${problem}`);
|
||||
@@ -46,6 +47,33 @@ export async function validate({ cwd }) {
|
||||
console.log(`ok ${raw.id ?? path.basename(cwd)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* A native dependency has to actually be a real dependency of the plugin, or
|
||||
* the build's external declaration points at nothing node_modules can
|
||||
* resolve at runtime.
|
||||
*/
|
||||
function validateNativeDependencies(cwd, raw) {
|
||||
const problems = [];
|
||||
const nativeDependencies = raw.nativeDependencies ?? [];
|
||||
if (nativeDependencies.length === 0) return problems;
|
||||
|
||||
const pkgPath = path.join(cwd, "package.json");
|
||||
const pkg = fs.existsSync(pkgPath)
|
||||
? JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
||||
: {};
|
||||
const deps = { ...pkg.dependencies };
|
||||
|
||||
for (const name of nativeDependencies) {
|
||||
if (!(name in deps)) {
|
||||
problems.push(
|
||||
`nativeDependencies names "${name}", which is not in this plugin's own package.json dependencies`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return problems;
|
||||
}
|
||||
|
||||
const DIALECTS = ["sqlite", "postgres", "mysql"];
|
||||
|
||||
const CREATE_OR_ALTER =
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"users:write",
|
||||
"auth:provide",
|
||||
"system:tls",
|
||||
"device:serial",
|
||||
"hosts:write",
|
||||
"credentials:use",
|
||||
"network:outbound",
|
||||
@@ -742,6 +743,15 @@
|
||||
"type": "string",
|
||||
"enum": ["linux", "win32", "darwin"]
|
||||
}
|
||||
},
|
||||
"nativeDependencies": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "Bare npm package names this plugin depends on that carry a native (.node) binding. The build never bundles them: they stay a real dependency in the plugin's own package.json and are resolved from node_modules at runtime, the same as a host-provided package, so the compiled native binary is never copied into the JS bundle.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
|
||||
@@ -1111,6 +1111,21 @@ export interface PluginAudit {
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic capability check, for a privileged action that has no dedicated
|
||||
* ctx method to wrap it: a plugin bundling its own native dependency to touch
|
||||
* hardware (a serial port, a USB device) is the first caller. Core cannot
|
||||
* mediate that access the way it mediates ctx.ssh or ctx.db, so the
|
||||
* capability here is a declared, reviewable, audited statement of intent
|
||||
* rather than a technical gate, matching credentials:read and process:spawn.
|
||||
*/
|
||||
export interface PluginCapabilities {
|
||||
/** Whether this plugin currently holds `capability`. Not audited: for deciding whether to offer something, not for gating an action. */
|
||||
has: (capability: string) => Promise<boolean>;
|
||||
/** Throws PluginCapabilityError if this plugin does not hold `capability`. Audited like every guarded ctx method. */
|
||||
require: (capability: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginContext {
|
||||
readonly pluginId: string;
|
||||
readonly manifest: PluginManifest;
|
||||
@@ -1127,6 +1142,8 @@ export interface PluginContext {
|
||||
readonly rbac: PluginRbac;
|
||||
readonly settings: PluginSettings;
|
||||
readonly disposables: PluginDisposables;
|
||||
/** A generic capability check, for privileged code no ctx method wraps. */
|
||||
readonly capabilities: PluginCapabilities;
|
||||
/** Hosts the actor can see, and host-sharing operations. Needs hosts:read / hosts:write. */
|
||||
readonly hosts: PluginHosts;
|
||||
/** SSH through core's connect pipeline. Needs ssh:connect and credentials:use. */
|
||||
|
||||
@@ -45,6 +45,7 @@ export const CAPABILITY_CATALOG: readonly CapabilityInfo[] = [
|
||||
entry("users:write", "high"),
|
||||
entry("auth:provide", "high"),
|
||||
entry("system:tls", "high"),
|
||||
entry("device:serial", "high"),
|
||||
|
||||
entry("hosts:write", "medium"),
|
||||
entry("credentials:use", "medium"),
|
||||
|
||||
@@ -232,6 +232,12 @@ export interface TabOptions {
|
||||
singleton?: boolean;
|
||||
/** A live session: asks before closing and can be refreshed. */
|
||||
session?: boolean;
|
||||
/**
|
||||
* Whether a session tab offers "Share session". Default true. A plugin
|
||||
* whose ref does not implement session sharing (a local device, not a
|
||||
* remote connection another user could join) sets this false.
|
||||
*/
|
||||
shareable?: boolean;
|
||||
/** Restorable without a host. */
|
||||
hostless?: boolean;
|
||||
/** Decides whether a saved tab for this host may be restored. */
|
||||
|
||||
@@ -298,6 +298,15 @@ export interface PluginManifest {
|
||||
/** Locales directory, relative to the plugin root. */
|
||||
locales?: string;
|
||||
platforms?: string[];
|
||||
/**
|
||||
* Bare npm package names carrying a native (.node) binding. The CLI build
|
||||
* never bundles them: they stay a real dependency in the plugin's own
|
||||
* package.json and are resolved from node_modules at runtime instead,
|
||||
* because esbuild bundling a native addon breaks the relative path it uses
|
||||
* to locate its compiled binary. See ARCHITECTURE.md under "native
|
||||
* dependencies".
|
||||
*/
|
||||
nativeDependencies?: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_BACKEND_ENTRY = "dist/backend.js";
|
||||
@@ -331,6 +340,7 @@ const ALLOWED_TOP_LEVEL = new Set([
|
||||
"frontend",
|
||||
"locales",
|
||||
"platforms",
|
||||
"nativeDependencies",
|
||||
]);
|
||||
|
||||
const ALLOWED_CONTRIBUTES = new Set([
|
||||
@@ -441,6 +451,7 @@ export function validateManifest(manifest: unknown): string[] {
|
||||
validateCategory(m.category, errors);
|
||||
validateCapabilities(m.capabilities, errors);
|
||||
validatePlatforms(m.platforms, errors);
|
||||
validateNativeDependencies(m.nativeDependencies, errors);
|
||||
validateDependencyMap(m.dependencies, "dependencies", errors);
|
||||
validateDependencyMap(m.optionalDependencies, "optionalDependencies", errors);
|
||||
validateProvides(m.provides, errors);
|
||||
@@ -535,6 +546,21 @@ function validatePlatforms(platforms: unknown, errors: string[]): void {
|
||||
});
|
||||
}
|
||||
|
||||
function validateNativeDependencies(value: unknown, errors: string[]): void {
|
||||
if (value === undefined) return;
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
errors.push(
|
||||
'Field "nativeDependencies" must be a non-empty array when present',
|
||||
);
|
||||
return;
|
||||
}
|
||||
value.forEach((entry, index) => {
|
||||
if (typeof entry !== "string" || entry.length === 0) {
|
||||
errors.push(`nativeDependencies[${index}] must be a non-empty string`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validateDependencyMap(
|
||||
value: unknown,
|
||||
field: string,
|
||||
|
||||
@@ -406,6 +406,13 @@ export function createFakeContext(
|
||||
}) as PluginMiddleware,
|
||||
},
|
||||
|
||||
// Nothing here is capability-checked, per the module doc; createMockCtx
|
||||
// gates this against options.capabilities.
|
||||
capabilities: {
|
||||
has: async () => true,
|
||||
require: async () => {},
|
||||
},
|
||||
|
||||
settings: {
|
||||
get: async (key) =>
|
||||
settings.get(settingsKey("admin", undefined, key)) as never,
|
||||
@@ -936,6 +943,13 @@ export function createMockCtx(
|
||||
return ctx.desktop.openIsolatedWindow(request);
|
||||
},
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
has: async (capability) => granted.has(capability),
|
||||
require: async (capability) => {
|
||||
require(capability);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { ...base, ctx: gatedCtx, checked };
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Converted from core's `src/backend/hosts/serial.ts` and
|
||||
`src/ui/features/serial/` in Termix 2.9.0. Its own port (30011) and nginx
|
||||
blocks are gone; it now serves `/plugin-ws/serial/console` like every other
|
||||
plugin.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Serial
|
||||
|
||||
Serial console access over a device attached to the machine running Termix:
|
||||
the desktop app talks to it through the backend's `serialport` binding, and a
|
||||
browser without the desktop app falls back to the Web Serial API.
|
||||
|
||||
Bundled with Termix and enabled by default.
|
||||
|
||||
## Layout
|
||||
|
||||
manifest.json id, capabilities and what this plugin contributes
|
||||
src/backend/ activate(ctx) / deactivate(), the /console WS route
|
||||
src/frontend/ the rail panel, the tab and their shared transport
|
||||
locales/en.json its strings (English only; the rest are translated)
|
||||
tests/backend/ vitest, node
|
||||
tests/frontend/ vitest, jsdom
|
||||
|
||||
## Native dependency
|
||||
|
||||
`serialport` (and its native `@serialport/bindings-cpp` binding) is this
|
||||
plugin's own npm dependency rather than a host-provided one, but the build
|
||||
never bundles it: see "native dependencies" in
|
||||
packages/plugin-sdk/ARCHITECTURE.md for why, and how Docker, Electron and dev
|
||||
each end up with a working binary anyway.
|
||||
|
||||
## Commands
|
||||
|
||||
npm run build bundle into dist/
|
||||
npm run test run this plugin's tests
|
||||
npm run typecheck
|
||||
|
||||
See packages/plugin-sdk/ARCHITECTURE.md for the contract this follows.
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"nav": {
|
||||
"serial": "Serial"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baud Rate",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parity",
|
||||
"parityNone": "None",
|
||||
"parityEven": "Even",
|
||||
"parityOdd": "Odd",
|
||||
"connect": "Connect to Serial",
|
||||
"disconnect": "Disconnect",
|
||||
"refreshPorts": "Refresh ports",
|
||||
"connected": "Connected to {{path}} at {{baud}} baud",
|
||||
"disconnected": "Serial port disconnected",
|
||||
"connectionError": "Failed to open serial port",
|
||||
"wsError": "WebSocket error",
|
||||
"errorNoServerUrl": "No server URL configured",
|
||||
"notSupportedTitle": "Serial Not Supported",
|
||||
"notSupported": "Serial connections require a browser with Web Serial API support (Chrome, Edge, or Firefox 151+), or the Termix desktop app.",
|
||||
"hideHint": "You can hide the Serial tab under User Profile > Appearance > Sidebar > Navigation.",
|
||||
"browserPickerHint": "Click Connect and your browser will open a port picker to choose the device."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Reeks",
|
||||
"portLabel": "Hawe",
|
||||
"portPlaceholder": "/dev/ttyUSB0 of COM3",
|
||||
"baudRateLabel": "Baudtempo",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteit",
|
||||
"parityNone": "Geen",
|
||||
"parityEven": "Selfs",
|
||||
"parityOdd": "Vreemd",
|
||||
"connect": "Koppel aan serieel",
|
||||
"disconnect": "Ontkoppel",
|
||||
"refreshPorts": "Verfris poorte",
|
||||
"connected": "Verbind aan {{path}} teen {{baud}} baud",
|
||||
"disconnected": "Seriepoort ontkoppel",
|
||||
"connectionError": "Kon nie die seriepoort oopmaak nie",
|
||||
"wsError": "WebSocket-fout",
|
||||
"errorNoServerUrl": "Geen bediener-URL gekonfigureer nie",
|
||||
"notSupportedTitle": "Serienommer word nie ondersteun nie",
|
||||
"notSupported": "Seriële verbindings vereis 'n blaaier met Web Serial API-ondersteuning (Chrome, Edge of Firefox 151+), of die Termix-lessenaarprogram.",
|
||||
"hideHint": "Jy kan die Seriële oortjie versteek onder Gebruikersprofiel > Voorkoms > Sybalk > Navigasie.",
|
||||
"browserPickerHint": "Klik op Verbind en jou blaaier sal 'n poortkieser oopmaak om die toestel te kies."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Reeks"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "منفذ تسلسلي",
|
||||
"portLabel": "المنفذ",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "معدل الباود",
|
||||
"dataBitsLabel": "البيانات",
|
||||
"stopBitsLabel": "التوقف",
|
||||
"parityLabel": "التكافؤ",
|
||||
"parityNone": "بلا",
|
||||
"parityEven": "زوجي",
|
||||
"parityOdd": "فردي",
|
||||
"connect": "اتصال",
|
||||
"disconnect": "قطع الاتصال",
|
||||
"refreshPorts": "تحديث المنافذ",
|
||||
"connected": "متصل بـ {{path}} بسرعة {{baud}} باود",
|
||||
"disconnected": "تم قطع الاتصال بالمنفذ التسلسلي",
|
||||
"connectionError": "فشل في فتح المنفذ التسلسلي",
|
||||
"wsError": "خطأ في WebSocket",
|
||||
"errorNoServerUrl": "لم يتم تكوين عنوان URL للخادم",
|
||||
"notSupportedTitle": "المنفذ التسلسلي غير مدعوم",
|
||||
"notSupported": "تتطلب الاتصالات التسلسلية متصفحًا يدعم Web Serial API (Chrome، أو Edge، أو Firefox 151+)، أو تطبيق Termix لسطح المكتب.",
|
||||
"hideHint": "يمكنك إخفاء تبويب المنفذ التسلسلي من الملف الشخصي > المظهر > الشريط الجانبي > التنقل.",
|
||||
"browserPickerHint": "انقر على Connect وسيفتح متصفحك نافذة اختيار المنفذ لتحديد الجهاز."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "تسلسلي"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Сериен",
|
||||
"portLabel": "Порт",
|
||||
"portPlaceholder": "/dev/ttyUSB0 или COM3",
|
||||
"baudRateLabel": "Скорост на бодове",
|
||||
"dataBitsLabel": "Данни",
|
||||
"stopBitsLabel": "Спри",
|
||||
"parityLabel": "Паритет",
|
||||
"parityNone": "Няма",
|
||||
"parityEven": "Дори",
|
||||
"parityOdd": "Странно",
|
||||
"connect": "Свързване към сериен",
|
||||
"disconnect": "Изключване",
|
||||
"refreshPorts": "Обновяване на портове",
|
||||
"connected": "Свързан към {{path}} при {{baud}} бод",
|
||||
"disconnected": "Серийният порт е прекъснат",
|
||||
"connectionError": "Неуспешно отваряне на серийния порт",
|
||||
"wsError": "Грешка в WebSocket",
|
||||
"errorNoServerUrl": "Няма конфигуриран URL адрес на сървъра",
|
||||
"notSupportedTitle": "Серийният номер не се поддържа",
|
||||
"notSupported": "Серийните връзки изискват браузър с поддръжка на Web Serial API (Chrome, Edge или Firefox 151+) или настолното приложение Termix.",
|
||||
"hideHint": "Можете да скриете раздела „Сериен номер“ в Потребителски профил > Външен вид > Странична лента > Навигация.",
|
||||
"browserPickerHint": "Щракнете върху „Свързване“ и браузърът ви ще отвори инструмент за избор на порт, за да изберете устройството."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Сериен"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "সিরিয়াল",
|
||||
"portLabel": "বন্দর",
|
||||
"portPlaceholder": "/dev/ttyUSB0 অথবা COM3",
|
||||
"baudRateLabel": "বড রেট",
|
||||
"dataBitsLabel": "ডেটা",
|
||||
"stopBitsLabel": "থামুন",
|
||||
"parityLabel": "সমতা",
|
||||
"parityNone": "কোনোটিই না",
|
||||
"parityEven": "এমনকি",
|
||||
"parityOdd": "অদ্ভুত",
|
||||
"connect": "সিরিয়ালের সাথে সংযোগ করুন",
|
||||
"disconnect": "সংযোগ বিচ্ছিন্ন করুন",
|
||||
"refreshPorts": "পোর্টগুলি রিফ্রেশ করুন",
|
||||
"connected": "{{path}} এর সাথে {{baud}} বাউডে সংযুক্ত।",
|
||||
"disconnected": "সিরিয়াল পোর্ট সংযোগ বিচ্ছিন্ন",
|
||||
"connectionError": "সিরিয়াল পোর্ট খুলতে ব্যর্থ হয়েছে",
|
||||
"wsError": "ওয়েব সকেট ত্রুটি",
|
||||
"errorNoServerUrl": "কোন সার্ভার URL কনফিগার করা নেই",
|
||||
"notSupportedTitle": "সিরিয়াল সমর্থিত নয়",
|
||||
"notSupported": "সিরিয়াল সংযোগের জন্য ওয়েব সিরিয়াল এপিআই সমর্থনসহ একটি ব্রাউজার (ক্রোম, এজ, বা ফায়ারফক্স ১৫১+), অথবা টারমিক্স ডেস্কটপ অ্যাপ প্রয়োজন।",
|
||||
"hideHint": "আপনি ইউজার প্রোফাইল > অ্যাপিয়ারেন্স > সাইডবার > নেভিগেশন-এর অধীনে সিরিয়াল ট্যাবটি লুকাতে পারেন।",
|
||||
"browserPickerHint": "কানেক্ট-এ ক্লিক করলে আপনার ব্রাউজারে ডিভাইসটি বেছে নেওয়ার জন্য একটি পোর্ট পিকার খুলবে।"
|
||||
},
|
||||
"nav": {
|
||||
"serial": "সিরিয়াল"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Sèrie",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 o COM3",
|
||||
"baudRateLabel": "velocitat de bauds",
|
||||
"dataBitsLabel": "Dades",
|
||||
"stopBitsLabel": "Atura",
|
||||
"parityLabel": "Paritat",
|
||||
"parityNone": "Cap",
|
||||
"parityEven": "Fins i tot",
|
||||
"parityOdd": "Estrany",
|
||||
"connect": "Connecta't al serial",
|
||||
"disconnect": "Desconnecta",
|
||||
"refreshPorts": "Actualitza els ports",
|
||||
"connected": "Connectat a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Port sèrie desconnectat",
|
||||
"connectionError": "No s'ha pogut obrir el port sèrie",
|
||||
"wsError": "Error de WebSocket",
|
||||
"errorNoServerUrl": "No s'ha configurat cap URL del servidor",
|
||||
"notSupportedTitle": "Número de sèrie no compatible",
|
||||
"notSupported": "Les connexions en sèrie requereixen un navegador compatible amb l'API Web Serial (Chrome, Edge o Firefox 151+) o l'aplicació d'escriptori Termix.",
|
||||
"hideHint": "Podeu amagar la pestanya Sèrie a Perfil d'usuari > Aparença > Barra lateral > Navegació.",
|
||||
"browserPickerHint": "Feu clic a Connecta i el navegador obrirà un selector de ports per triar el dispositiu."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Sèrie"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Seriál",
|
||||
"portLabel": "Přístav",
|
||||
"portPlaceholder": "/dev/ttyUSB0 nebo COM3",
|
||||
"baudRateLabel": "Přenosová rychlost",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Zastávka",
|
||||
"parityLabel": "Parita",
|
||||
"parityNone": "Žádný",
|
||||
"parityEven": "Dokonce",
|
||||
"parityOdd": "Zvláštní",
|
||||
"connect": "Připojení k sériovému",
|
||||
"disconnect": "Odpojit",
|
||||
"refreshPorts": "Obnovit porty",
|
||||
"connected": "Připojeno k {{path}} rychlostí {{baud}} baudu",
|
||||
"disconnected": "Sériový port odpojen",
|
||||
"connectionError": "Nepodařilo se otevřít sériový port",
|
||||
"wsError": "Chyba WebSocketu",
|
||||
"errorNoServerUrl": "Není nakonfigurována žádná adresa URL serveru",
|
||||
"notSupportedTitle": "Sériové číslo není podporováno",
|
||||
"notSupported": "Sériová připojení vyžadují prohlížeč s podporou webového sériového API (Chrome, Edge nebo Firefox 151+) nebo desktopovou aplikaci Termix.",
|
||||
"hideHint": "Kartu Sériové číslo můžete skrýt v části Profil uživatele > Vzhled > Postranní panel > Navigace.",
|
||||
"browserPickerHint": "Klikněte na tlačítko Připojit a prohlížeč otevře výběr portu pro výběr zařízení."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Seriál"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Seriel",
|
||||
"portLabel": "Havn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighed",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Endog",
|
||||
"parityOdd": "Ulige",
|
||||
"connect": "Tilslut til seriel",
|
||||
"disconnect": "Afbryde",
|
||||
"refreshPorts": "Opdater porte",
|
||||
"connected": "Forbundet til {{path}} ved {{baud}} baud",
|
||||
"disconnected": "Seriel port afbrudt",
|
||||
"connectionError": "Kunne ikke åbne seriel port",
|
||||
"wsError": "WebSocket-fejl",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigureret",
|
||||
"notSupportedTitle": "Seriel understøttes ikke",
|
||||
"notSupported": "Serielle forbindelser kræver en browser med Web Serial API-understøttelse (Chrome, Edge eller Firefox 151+) eller Termix-desktopappen.",
|
||||
"hideHint": "Du kan skjule fanen Seriel under Brugerprofil > Udseende > Sidebjælke > Navigation.",
|
||||
"browserPickerHint": "Klik på Opret forbindelse, og din browser åbner en portvælger, hvor du kan vælge enheden."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Seriel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 oder COM3",
|
||||
"baudRateLabel": "Baudrate",
|
||||
"dataBitsLabel": "Daten",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parität",
|
||||
"parityNone": "Keine",
|
||||
"parityEven": "Gerade",
|
||||
"parityOdd": "Ungerade",
|
||||
"connect": "Mit Seriell verbinden",
|
||||
"disconnect": "Trennen",
|
||||
"refreshPorts": "Ports aktualisieren",
|
||||
"connected": "Verbunden mit {{path}} bei {{baud}} Baud",
|
||||
"disconnected": "Serielle Schnittstelle getrennt",
|
||||
"connectionError": "Fehler beim Öffnen der seriellen Schnittstelle",
|
||||
"wsError": "WebSocket-Fehler",
|
||||
"errorNoServerUrl": "Keine Server-URL konfiguriert",
|
||||
"notSupportedTitle": "Seriell nicht unterstützt",
|
||||
"notSupported": "Serielle Verbindungen erfordern einen Browser mit Web Serial API-Unterstützung (Chrome, Edge oder Firefox 151+) oder die Termix-Desktop-App.",
|
||||
"hideHint": "Sie können den Seriell-Tab unter Benutzerprofil > Erscheinungsbild > Seitenleiste > Navigation ausblenden.",
|
||||
"browserPickerHint": "Klicken Sie auf Verbinden, und Ihr Browser öffnet eine Port-Auswahl, um das Gerät auszuwählen."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Seriell"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Σειράς",
|
||||
"portLabel": "Λιμάνι",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ή COM3",
|
||||
"baudRateLabel": "Ρυθμός Baud",
|
||||
"dataBitsLabel": "Δεδομένα",
|
||||
"stopBitsLabel": "Στάση",
|
||||
"parityLabel": "Ισοτιμία",
|
||||
"parityNone": "Κανένας",
|
||||
"parityEven": "Ακόμη και",
|
||||
"parityOdd": "Περιττός",
|
||||
"connect": "Σύνδεση σε σειριακή σύνδεση",
|
||||
"disconnect": "Αποσυνδέω",
|
||||
"refreshPorts": "Ανανέωση θυρών",
|
||||
"connected": "Συνδεδεμένο με {{path}} σε {{baud}} baud",
|
||||
"disconnected": "Η σειριακή θύρα αποσυνδέθηκε",
|
||||
"connectionError": "Αποτυχία ανοίγματος σειριακής θύρας",
|
||||
"wsError": "Σφάλμα WebSocket",
|
||||
"errorNoServerUrl": "Δεν έχει ρυθμιστεί διεύθυνση URL διακομιστή",
|
||||
"notSupportedTitle": "Σειριακή Δεν Υποστηρίζεται",
|
||||
"notSupported": "Οι σειριακές συνδέσεις απαιτούν ένα πρόγραμμα περιήγησης με υποστήριξη Web Serial API (Chrome, Edge ή Firefox 151+) ή την εφαρμογή Termix για υπολογιστές.",
|
||||
"hideHint": "Μπορείτε να αποκρύψετε την καρτέλα Σειριακός αριθμός στην περιοχή Προφίλ χρήστη > Εμφάνιση > Πλαϊνή μπάρα > Πλοήγηση.",
|
||||
"browserPickerHint": "Κάντε κλικ στην επιλογή Σύνδεση και το πρόγραμμα περιήγησής σας θα ανοίξει ένα πρόγραμμα επιλογής θυρών για να επιλέξετε τη συσκευή."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Σειράς"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Puerto",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baudios",
|
||||
"dataBitsLabel": "Datos",
|
||||
"stopBitsLabel": "Parada",
|
||||
"parityLabel": "Paridad",
|
||||
"parityNone": "Ninguna",
|
||||
"parityEven": "Par",
|
||||
"parityOdd": "Impar",
|
||||
"connect": "Conectar a Serial",
|
||||
"disconnect": "Desconectar",
|
||||
"refreshPorts": "Actualizar puertos",
|
||||
"connected": "Conectado a {{path}} a {{baud}} baudios",
|
||||
"disconnected": "Puerto serie desconectado",
|
||||
"connectionError": "Error al abrir el puerto serie",
|
||||
"wsError": "Error WebSocket",
|
||||
"errorNoServerUrl": "No se ha configurado URL de servidor",
|
||||
"notSupportedTitle": "Serial no compatible",
|
||||
"notSupported": "Las conexiones serie requieren un navegador compatible con Web Serial API (Chrome, Edge o Firefox 151+), o la aplicación de escritorio Termix.",
|
||||
"hideHint": "Puedes ocultar la pestaña Serial en Perfil de usuario > Apariencia > Barra lateral > Navegación.",
|
||||
"browserPickerHint": "Haz clic en Conectar y el navegador abrirá un selector de puertos para elegir el dispositivo."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Serial"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Sarjanumero",
|
||||
"portLabel": "Portti",
|
||||
"portPlaceholder": "/dev/ttyUSB0 tai COM3",
|
||||
"baudRateLabel": "Baud-nopeus",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteetti",
|
||||
"parityNone": "Ei mitään",
|
||||
"parityEven": "Jopa",
|
||||
"parityOdd": "Pariton",
|
||||
"connect": "Yhdistä sarjaporttiin",
|
||||
"disconnect": "Katkaise yhteys",
|
||||
"refreshPorts": "Päivitä portit",
|
||||
"connected": "Yhdistetty {{path}} -laitteeseen {{baud}} baudilla",
|
||||
"disconnected": "Sarjaportti irrotettu",
|
||||
"connectionError": "Sarjaportin avaaminen epäonnistui",
|
||||
"wsError": "WebSocket-virhe",
|
||||
"errorNoServerUrl": "Palvelimen URL-osoitetta ei ole määritetty",
|
||||
"notSupportedTitle": "Sarjanumeroa ei tueta",
|
||||
"notSupported": "Sarjaliikenne vaatii selaimen, jossa on Web Serial API -tuki (Chrome, Edge tai Firefox 151+), tai Termix-työpöytäsovelluksen.",
|
||||
"hideHint": "Voit piilottaa Sarja-välilehden kohdassa Käyttäjäprofiili > Ulkoasu > Sivupalkki > Navigointi.",
|
||||
"browserPickerHint": "Napsauta Yhdistä, niin selaimesi avaa porttivalitsimen laitteen valitsemiseksi."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Sarjanumero"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Série",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Débit en bauds",
|
||||
"dataBitsLabel": "Données",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parité",
|
||||
"parityNone": "Aucune",
|
||||
"parityEven": "Paire",
|
||||
"parityOdd": "Impaire",
|
||||
"connect": "Connecter",
|
||||
"disconnect": "Déconnecter",
|
||||
"refreshPorts": "Actualiser les ports",
|
||||
"connected": "Connecté à {{path}} à {{baud}} bauds",
|
||||
"disconnected": "Port série déconnecté",
|
||||
"connectionError": "Échec de l'ouverture du port série",
|
||||
"wsError": "Erreur WebSocket",
|
||||
"errorNoServerUrl": "Aucune URL de serveur configurée",
|
||||
"notSupportedTitle": "Port série non pris en charge",
|
||||
"notSupported": "Les connexions série nécessitent un navigateur prenant en charge l'API Web Serial (Chrome, Edge, ou Firefox 151+), ou l'application de bureau Termix.",
|
||||
"hideHint": "Vous pouvez masquer l'onglet Série sous Profil utilisateur > Apparence > Barre latérale > Navigation.",
|
||||
"browserPickerHint": "Cliquez sur Connexion et votre navigateur ouvrira un sélecteur de port pour choisir le périphérique."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Série"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "סִדרָתִי",
|
||||
"portLabel": "נָמָל",
|
||||
"portPlaceholder": "/dev/ttyUSB0 או COM3",
|
||||
"baudRateLabel": "קצב באוד",
|
||||
"dataBitsLabel": "נְתוּנִים",
|
||||
"stopBitsLabel": "לְהַפְסִיק",
|
||||
"parityLabel": "שִׁוּוּי",
|
||||
"parityNone": "אַף לֹא אֶחָד",
|
||||
"parityEven": "אֲפִילוּ",
|
||||
"parityOdd": "מְשׁוּנֶה",
|
||||
"connect": "חיבור סריאלי",
|
||||
"disconnect": "לְנַתֵק",
|
||||
"refreshPorts": "רענון פורטים",
|
||||
"connected": "מחובר ל- {{path}} ב- {{baud}} באוד",
|
||||
"disconnected": "יציאה טורית מנותקת",
|
||||
"connectionError": "נכשלה פתיחת הפורט הטורי",
|
||||
"wsError": "שגיאת WebSocket",
|
||||
"errorNoServerUrl": "לא הוגדרה כתובת URL של שרת",
|
||||
"notSupportedTitle": "סריאלי לא נתמך",
|
||||
"notSupported": "חיבורים טוריים דורשים דפדפן עם תמיכה ב-Web Serial API (Chrome, Edge או Firefox 151+), או את אפליקציית שולחן העבודה Termix.",
|
||||
"hideHint": "ניתן להסתיר את הכרטיסייה 'סדרתי' תחת פרופיל משתמש > מראה > סרגל צד > ניווט.",
|
||||
"browserPickerHint": "לחץ על התחבר והדפדפן שלך יפתח בורר פורטים לבחירת המכשיר."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "סִדרָתִי"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "सीरियल",
|
||||
"portLabel": "पोर्ट",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "बॉड दर",
|
||||
"dataBitsLabel": "डेटा",
|
||||
"stopBitsLabel": "स्टॉप",
|
||||
"parityLabel": "पैरिटी",
|
||||
"parityNone": "कोई नहीं",
|
||||
"parityEven": "सम",
|
||||
"parityOdd": "विषम",
|
||||
"connect": "सीरियल कनेक्ट करें",
|
||||
"disconnect": "डिस्कनेक्ट करें",
|
||||
"refreshPorts": "पोर्ट रीफ़्रेश करें",
|
||||
"connected": "{{path}} से {{baud}} बॉड पर कनेक्टेड",
|
||||
"disconnected": "सीरियल पोर्ट डिस्कनेक्ट हो गया",
|
||||
"connectionError": "सीरियल पोर्ट खोलने में विफल",
|
||||
"wsError": "WebSocket त्रुटि",
|
||||
"errorNoServerUrl": "कोई सर्वर URL कॉन्फ़िगर नहीं है",
|
||||
"notSupportedTitle": "सीरियल समर्थित नहीं",
|
||||
"notSupported": "सीरियल कनेक्शन के लिए Web Serial API समर्थन वाले ब्राउज़र (Chrome, Edge, या Firefox 151+) या Termix डेस्कटॉप ऐप की आवश्यकता है।",
|
||||
"hideHint": "आप सीरियल टैब को User Profile > Appearance > Sidebar > Navigation में छिपा सकते हैं।",
|
||||
"browserPickerHint": "Connect पर क्लिक करें और आपका ब्राउज़र डिवाइस चुनने के लिए एक पोर्ट पिकर खोलेगा।"
|
||||
},
|
||||
"nav": {
|
||||
"serial": "सीरियल"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Sorozatszám",
|
||||
"portLabel": "Kikötő",
|
||||
"portPlaceholder": "/dev/ttyUSB0 vagy COM3",
|
||||
"baudRateLabel": "Baud sebesség",
|
||||
"dataBitsLabel": "Adat",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritás",
|
||||
"parityNone": "Egyik sem",
|
||||
"parityEven": "Még",
|
||||
"parityOdd": "Páratlan",
|
||||
"connect": "Csatlakozás soros porthoz",
|
||||
"disconnect": "Leválasztás",
|
||||
"refreshPorts": "Frissítse a portokat",
|
||||
"connected": "{{path}} -hoz csatlakoztatva {{baud}} baud sebességgel",
|
||||
"disconnected": "Soros port leválasztva",
|
||||
"connectionError": "Nem sikerült megnyitni a soros portot",
|
||||
"wsError": "WebSocket hiba",
|
||||
"errorNoServerUrl": "Nincs konfigurálva szerver URL",
|
||||
"notSupportedTitle": "Sorozatszám Nem támogatott",
|
||||
"notSupported": "A soros kapcsolatokhoz Web Serial API-t támogató böngésző (Chrome, Edge vagy Firefox 151+), vagy a Termix asztali alkalmazás szükséges.",
|
||||
"hideHint": "A Sorozat fület elrejtheted a Felhasználói profil > Megjelenés > Oldalsáv > Navigáció alatt.",
|
||||
"browserPickerHint": "Kattintson a Csatlakozás gombra, és a böngészője megnyit egy portválasztót az eszköz kiválasztásához."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Sorozatszám"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Pelabuhan",
|
||||
"portPlaceholder": "/dev/ttyUSB0 atau COM3",
|
||||
"baudRateLabel": "Kecepatan Baud",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Berhenti",
|
||||
"parityLabel": "Keseimbangan",
|
||||
"parityNone": "Tidak ada",
|
||||
"parityEven": "Bahkan",
|
||||
"parityOdd": "Aneh",
|
||||
"connect": "Hubungkan ke Serial",
|
||||
"disconnect": "Memutuskan",
|
||||
"refreshPorts": "Penyegaran port",
|
||||
"connected": "Terhubung ke {{path}} pada kecepatan {{baud}} baud",
|
||||
"disconnected": "Port serial terputus",
|
||||
"connectionError": "Gagal membuka port serial",
|
||||
"wsError": "Kesalahan WebSocket",
|
||||
"errorNoServerUrl": "Tidak ada URL server yang dikonfigurasi.",
|
||||
"notSupportedTitle": "Nomor Seri Tidak Didukung",
|
||||
"notSupported": "Koneksi serial memerlukan browser dengan dukungan Web Serial API (Chrome, Edge, atau Firefox 151+), atau aplikasi desktop Termix.",
|
||||
"hideHint": "Anda dapat menyembunyikan tab Serial di bawah Profil Pengguna > Tampilan > Bilah Samping > Navigasi.",
|
||||
"browserPickerHint": "Klik Sambungkan dan browser Anda akan membuka pemilih port untuk memilih perangkat."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Serial"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Seriale",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baud rate",
|
||||
"dataBitsLabel": "Dati",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parità",
|
||||
"parityNone": "Nessuna",
|
||||
"parityEven": "Pari",
|
||||
"parityOdd": "Dispari",
|
||||
"connect": "Connetti alla Seriale",
|
||||
"disconnect": "Disconnetti",
|
||||
"refreshPorts": "Aggiorna porte",
|
||||
"connected": "Connesso a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Porta seriale disconnessa",
|
||||
"connectionError": "Impossibile aprire la porta seriale",
|
||||
"wsError": "Errore WebSocket",
|
||||
"errorNoServerUrl": "Nessun URL del server configurato",
|
||||
"notSupportedTitle": "Seriale non supportato",
|
||||
"notSupported": "Le connessioni seriali richiedono un browser con supporto Web Serial API (Chrome, Edge o Firefox 151+), oppure l'app desktop Termix.",
|
||||
"hideHint": "Puoi nascondere la scheda Seriale in Profilo Utente > Aspetto > Barra laterale > Navigazione.",
|
||||
"browserPickerHint": "Fai clic su Connetti e il browser aprirà un selettore di porte per scegliere il dispositivo."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Seriale"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "シリアル",
|
||||
"portLabel": "ポート",
|
||||
"portPlaceholder": "/dev/ttyUSB0 または COM3",
|
||||
"baudRateLabel": "ボーレート",
|
||||
"dataBitsLabel": "データ",
|
||||
"stopBitsLabel": "ストップ",
|
||||
"parityLabel": "パリティ",
|
||||
"parityNone": "なし",
|
||||
"parityEven": "偶数",
|
||||
"parityOdd": "奇数",
|
||||
"connect": "シリアルに接続",
|
||||
"disconnect": "切断",
|
||||
"refreshPorts": "ポートを更新",
|
||||
"connected": "{{path}} に {{baud}} ボーで接続しました",
|
||||
"disconnected": "シリアルポートが切断されました",
|
||||
"connectionError": "シリアルポートを開けませんでした",
|
||||
"wsError": "WebSocket エラー",
|
||||
"errorNoServerUrl": "サーバー URL が設定されていません",
|
||||
"notSupportedTitle": "シリアル非対応",
|
||||
"notSupported": "シリアル接続には Web Serial API をサポートするブラウザ (Chrome, Edge, Firefox 151+) または Termix デスクトップアプリが必要です。",
|
||||
"hideHint": "シリアルタブは「ユーザープロファイル > 外観 > サイドバー > ナビゲーション」で非表示にできます。",
|
||||
"browserPickerHint": "接続をクリックすると、ブラウザがポートピッカーを開き、デバイスを選択できます。"
|
||||
},
|
||||
"nav": {
|
||||
"serial": "シリアル"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "시리얼",
|
||||
"portLabel": "포트",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "보레이트",
|
||||
"dataBitsLabel": "데이터",
|
||||
"stopBitsLabel": "스톱",
|
||||
"parityLabel": "패리티",
|
||||
"parityNone": "없음",
|
||||
"parityEven": "짝수",
|
||||
"parityOdd": "홀수",
|
||||
"connect": "시리얼 연결",
|
||||
"disconnect": "연결 해제",
|
||||
"refreshPorts": "포트 새로 고침",
|
||||
"connected": "{{path}}에 {{baud}} 보로 연결됨",
|
||||
"disconnected": "시리얼 포트 연결 해제됨",
|
||||
"connectionError": "시리얼 포트를 열지 못했습니다.",
|
||||
"wsError": "WebSocket 오류",
|
||||
"errorNoServerUrl": "서버 URL이 구성되지 않았습니다.",
|
||||
"notSupportedTitle": "시리얼 미지원",
|
||||
"notSupported": "시리얼 연결에는 Web Serial API를 지원하는 브라우저(Chrome, Edge, Firefox 151+) 또는 Termix 데스크톱 앱이 필요합니다.",
|
||||
"hideHint": "Serial 탭은 사용자 프로필 > 모양 > 사이드바 > 탐색에서 숨길 수 있습니다.",
|
||||
"browserPickerHint": "연결 버튼을 클릭하면 브라우저에서 장치 선택을 위한 포트 선택기가 열립니다."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "시리얼"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Serieel",
|
||||
"portLabel": "Haven",
|
||||
"portPlaceholder": "/dev/ttyUSB0 of COM3",
|
||||
"baudRateLabel": "Baudrate",
|
||||
"dataBitsLabel": "Gegevens",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteit",
|
||||
"parityNone": "Geen",
|
||||
"parityEven": "Zelfs",
|
||||
"parityOdd": "Vreemd",
|
||||
"connect": "Verbinden via serieel",
|
||||
"disconnect": "Verbinding verbreken",
|
||||
"refreshPorts": "Vernieuw poorten",
|
||||
"connected": "Verbonden met {{path}} met {{baud}} baud",
|
||||
"disconnected": "Seriële poort losgekoppeld",
|
||||
"connectionError": "Het openen van de seriële poort is mislukt.",
|
||||
"wsError": "WebSocket-fout",
|
||||
"errorNoServerUrl": "Geen server-URL geconfigureerd",
|
||||
"notSupportedTitle": "Serienummer niet ondersteund",
|
||||
"notSupported": "Voor seriële verbindingen is een browser met Web Serial API-ondersteuning vereist (Chrome, Edge of Firefox 151+), of de Termix-desktopapp.",
|
||||
"hideHint": "Je kunt het tabblad 'Serienummer' verbergen via Gebruikersprofiel > Uiterlijk > Zijbalk > Navigatie.",
|
||||
"browserPickerHint": "Klik op Verbinden en uw browser opent een poortkiezer waarmee u het apparaat kunt selecteren."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Serieel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Havn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighet",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stoppe",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Til og med",
|
||||
"parityOdd": "Merkelig",
|
||||
"connect": "Koble til seriell",
|
||||
"disconnect": "Frakople",
|
||||
"refreshPorts": "Oppdater porter",
|
||||
"connected": "Koblet til {{path}} ved {{baud}} baud",
|
||||
"disconnected": "Seriell port frakoblet",
|
||||
"connectionError": "Klarte ikke å åpne serieporten",
|
||||
"wsError": "WebSocket-feil",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigurert",
|
||||
"notSupportedTitle": "Serienummer støttes ikke",
|
||||
"notSupported": "Serielle tilkoblinger krever en nettleser med støtte for Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivebordsappen.",
|
||||
"hideHint": "Du kan skjule Seriell-fanen under Brukerprofil > Utseende > Sidefelt > Navigasjon.",
|
||||
"browserPickerHint": "Klikk på Koble til, så åpner nettleseren din en portvelger der du kan velge enheten."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Seriell"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Port szeregowy",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 lub COM3",
|
||||
"baudRateLabel": "Szybkość transmisji",
|
||||
"dataBitsLabel": "Dane",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parzystość",
|
||||
"parityNone": "Brak",
|
||||
"parityEven": "Parzysta",
|
||||
"parityOdd": "Nieparzysta",
|
||||
"connect": "Połącz z portem szeregowym",
|
||||
"disconnect": "Odłącz",
|
||||
"refreshPorts": "Odśwież porty",
|
||||
"connected": "Połączono z {{path}} z szybkością {{baud}} bodów",
|
||||
"disconnected": "Port szeregowy rozłączony",
|
||||
"connectionError": "Nie udało się otworzyć portu szeregowego",
|
||||
"wsError": "Błąd WebSocket",
|
||||
"errorNoServerUrl": "Brak skonfigurowanego adresu URL serwera",
|
||||
"notSupportedTitle": "Port szeregowy nieobsługiwany",
|
||||
"notSupported": "Połączenia szeregowe wymagają przeglądarki obsługującej Web Serial API (Chrome, Edge lub Firefox 151+) albo aplikacji desktopowej Termix.",
|
||||
"hideHint": "Kartę Port szeregowy można ukryć w: Profil użytkownika > Wygląd > Pasek boczny > Nawigacja.",
|
||||
"browserPickerHint": "Kliknij Połącz, a przeglądarka otworzy okno wyboru portu, w którym wskażesz urządzenie."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Port szeregowy"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ou COM3",
|
||||
"baudRateLabel": "Taxa de transmissão (Baud rate)",
|
||||
"dataBitsLabel": "Dados",
|
||||
"stopBitsLabel": "Parar",
|
||||
"parityLabel": "Paridade",
|
||||
"parityNone": "Nenhum",
|
||||
"parityEven": "Até",
|
||||
"parityOdd": "Chance",
|
||||
"connect": "Conectar à porta serial",
|
||||
"disconnect": "Desconectar",
|
||||
"refreshPorts": "Atualizar portas",
|
||||
"connected": "Conectado a {{path}} na taxa de transmissão de {{baud}}",
|
||||
"disconnected": "Porta serial desconectada",
|
||||
"connectionError": "Falha ao abrir a porta serial",
|
||||
"wsError": "Erro no WebSocket",
|
||||
"errorNoServerUrl": "Nenhuma URL de servidor configurada",
|
||||
"notSupportedTitle": "Serial não suportado",
|
||||
"notSupported": "As conexões seriais exigem um navegador com suporte à API Web Serial (Chrome, Edge ou Firefox 151+) ou o aplicativo Termix para desktop.",
|
||||
"hideHint": "Você pode ocultar a guia Serial em Perfil do Usuário > Aparência > Barra lateral > Navegação.",
|
||||
"browserPickerHint": "Clique em Conectar e seu navegador abrirá um seletor de portas para que você escolha o dispositivo."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Serial"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ou COM3",
|
||||
"baudRateLabel": "Taxa Baud",
|
||||
"dataBitsLabel": "Dados",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paridade",
|
||||
"parityNone": "Nenhuma",
|
||||
"parityEven": "Par",
|
||||
"parityOdd": "Ímpar",
|
||||
"connect": "Ligar à Serial",
|
||||
"disconnect": "Desligar",
|
||||
"refreshPorts": "Atualizar portas",
|
||||
"connected": "Ligado a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Porta série desligada",
|
||||
"connectionError": "Falha ao abrir a porta série",
|
||||
"wsError": "Erro de WebSocket",
|
||||
"errorNoServerUrl": "Nenhum URL de servidor configurado",
|
||||
"notSupportedTitle": "Série não suportada",
|
||||
"notSupported": "As ligações série requerem um navegador com suporte à Web Serial API (Chrome, Edge ou Firefox 151+) ou a aplicação de ambiente de trabalho Termix.",
|
||||
"hideHint": "Pode ocultar o separador Série em Perfil do Utilizador > Aparência > Barra Lateral > Navegação.",
|
||||
"browserPickerHint": "Clique em Ligar e o seu navegador abrirá um seletor de porta para escolher o dispositivo."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Serial"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 sau COM3",
|
||||
"baudRateLabel": "Rată de baud",
|
||||
"dataBitsLabel": "Date",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritate",
|
||||
"parityNone": "Nici unul",
|
||||
"parityEven": "Chiar",
|
||||
"parityOdd": "Ciudat",
|
||||
"connect": "Conectare la serial",
|
||||
"disconnect": "Deconecta",
|
||||
"refreshPorts": "Reîmprospătați porturile",
|
||||
"connected": "Conectat la {{path}} la {{baud}} baud",
|
||||
"disconnected": "Portul serial deconectat",
|
||||
"connectionError": "Nu s-a putut deschide portul serial",
|
||||
"wsError": "Eroare WebSocket",
|
||||
"errorNoServerUrl": "Nu este configurată nicio adresă URL a serverului",
|
||||
"notSupportedTitle": "Număr de serie neacceptat",
|
||||
"notSupported": "Conexiunile seriale necesită un browser cu suport Web Serial API (Chrome, Edge sau Firefox 151+) sau aplicația desktop Termix.",
|
||||
"hideHint": "Puteți ascunde fila Serial din Profil utilizator > Aspect > Bara laterală > Navigare.",
|
||||
"browserPickerHint": "Faceți clic pe Conectare și browserul dvs. va deschide un selector de porturi pentru a alege dispozitivul."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Serial"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Последовательный порт",
|
||||
"portLabel": "Порт",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Скорость передачи",
|
||||
"dataBitsLabel": "Биты данных",
|
||||
"stopBitsLabel": "Стоп-биты",
|
||||
"parityLabel": "Чётность",
|
||||
"parityNone": "Нет",
|
||||
"parityEven": "Чётный",
|
||||
"parityOdd": "Нечётный",
|
||||
"connect": "Подключиться",
|
||||
"disconnect": "Отключиться",
|
||||
"refreshPorts": "Обновить порты",
|
||||
"connected": "Подключено к {{path}} на скорости {{baud}} бод",
|
||||
"disconnected": "Последовательный порт отключен",
|
||||
"connectionError": "Ошибка открытия последовательного порта",
|
||||
"wsError": "Ошибка WebSocket",
|
||||
"errorNoServerUrl": "Не настроен URL сервера",
|
||||
"notSupportedTitle": "Последовательный порт не поддерживается",
|
||||
"notSupported": "Последовательные подключения требуют браузер с поддержкой Web Serial API (Chrome, Edge или Firefox 151+) или десктопное приложение Termix.",
|
||||
"hideHint": "Вы можете скрыть вкладку «Serial» в разделе «Профиль пользователя» > «Оформление» > «Боковая панель» > «Навигация».",
|
||||
"browserPickerHint": "Нажмите «Подключиться», и браузер откроет окно выбора порта для выбора устройства."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Последовательный порт"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Серијски",
|
||||
"portLabel": "Лука",
|
||||
"portPlaceholder": "/dev/ttyUSB0 или COM3",
|
||||
"baudRateLabel": "Брзина преноса података",
|
||||
"dataBitsLabel": "Подаци",
|
||||
"stopBitsLabel": "Заустави",
|
||||
"parityLabel": "Паритет",
|
||||
"parityNone": "Ниједан",
|
||||
"parityEven": "Чак",
|
||||
"parityOdd": "Чудно",
|
||||
"connect": "Повежи се са серијским",
|
||||
"disconnect": "Прекини везу",
|
||||
"refreshPorts": "Освежи портове",
|
||||
"connected": "Повезано са {{path}} брзином {{baud}} бауда",
|
||||
"disconnected": "Серијски порт је искључен",
|
||||
"connectionError": "Отварање серијског порта није успело",
|
||||
"wsError": "Грешка WebSocket-а",
|
||||
"errorNoServerUrl": "Није конфигурисан URL сервера",
|
||||
"notSupportedTitle": "Серијски број није подржан",
|
||||
"notSupported": "Серијске везе захтевају прегледач са подршком за Web Serial API (Chrome, Edge или Firefox 151+) или Termix десктоп апликацију.",
|
||||
"hideHint": "Картицу Серијски број можете сакрити под Кориснички профил > Изглед > Бочна трака > Навигација.",
|
||||
"browserPickerHint": "Кликните на Повежи се и ваш прегледач ће отворити бирач портова за избор уређаја."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Серијски"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Serie",
|
||||
"portLabel": "Hamn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighet",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stopp",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Även",
|
||||
"parityOdd": "Udda",
|
||||
"connect": "Anslut till seriellt",
|
||||
"disconnect": "Koppla från",
|
||||
"refreshPorts": "Uppdatera portar",
|
||||
"connected": "Ansluten till {{path}} vid {{baud}} baud",
|
||||
"disconnected": "Seriell port frånkopplad",
|
||||
"connectionError": "Misslyckades med att öppna serieporten",
|
||||
"wsError": "WebSocket-fel",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigurerad",
|
||||
"notSupportedTitle": "Seriellt stöds inte",
|
||||
"notSupported": "Seriella anslutningar kräver en webbläsare med stöd för Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivbordsappen.",
|
||||
"hideHint": "Du kan dölja fliken Seriell under Användarprofil > Utseende > Sidofält > Navigering.",
|
||||
"browserPickerHint": "Klicka på Anslut så öppnar din webbläsare en portväljare där du kan välja enheten."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Serie"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "ซีเรียล",
|
||||
"portLabel": "ท่าเรือ",
|
||||
"portPlaceholder": "/dev/ttyUSB0 หรือ COM3",
|
||||
"baudRateLabel": "อัตราบิต",
|
||||
"dataBitsLabel": "ข้อมูล",
|
||||
"stopBitsLabel": "หยุด",
|
||||
"parityLabel": "ความเท่าเทียมกัน",
|
||||
"parityNone": "ไม่มี",
|
||||
"parityEven": "สม่ำเสมอ",
|
||||
"parityOdd": "แปลก",
|
||||
"connect": "เชื่อมต่อกับพอร์ตอนุกรม",
|
||||
"disconnect": "ตัดการเชื่อมต่อ",
|
||||
"refreshPorts": "พอร์ตรีเฟรช",
|
||||
"connected": "เชื่อมต่อกับ {{path}} ที่ความเร็ว {{baud}} บอด",
|
||||
"disconnected": "พอร์ตอนุกรมถูกตัดการเชื่อมต่อ",
|
||||
"connectionError": "ไม่สามารถเปิดพอร์ตอนุกรมได้",
|
||||
"wsError": "ข้อผิดพลาด WebSocket",
|
||||
"errorNoServerUrl": "ไม่ได้กำหนดค่า URL เซิร์ฟเวอร์",
|
||||
"notSupportedTitle": "ไม่รองรับหมายเลขซีเรียล",
|
||||
"notSupported": "การเชื่อมต่อแบบอนุกรมจำเป็นต้องใช้เบราว์เซอร์ที่รองรับ Web Serial API (Chrome, Edge หรือ Firefox เวอร์ชัน 151 ขึ้นไป) หรือแอปพลิเคชัน Termix บนเดสก์ท็อป",
|
||||
"hideHint": "คุณสามารถซ่อนแท็บ Serial ได้ที่ โปรไฟล์ผู้ใช้ > ลักษณะที่ปรากฏ > แถบด้านข้าง > การนำทาง",
|
||||
"browserPickerHint": "คลิก \"เชื่อมต่อ\" แล้วเบราว์เซอร์ของคุณจะเปิดตัวเลือกพอร์ตเพื่อเลือกอุปกรณ์"
|
||||
},
|
||||
"nav": {
|
||||
"serial": "ซีเรียล"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Seri",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 veya COM3",
|
||||
"baudRateLabel": "Baud Hızı",
|
||||
"dataBitsLabel": "Veri",
|
||||
"stopBitsLabel": "Durdurma",
|
||||
"parityLabel": "Eşlik",
|
||||
"parityNone": "Yok",
|
||||
"parityEven": "Çift",
|
||||
"parityOdd": "Tek",
|
||||
"connect": "Seri Bağlantıya Bağlan",
|
||||
"disconnect": "Bağlantıyı Kes",
|
||||
"refreshPorts": "Portları yenile",
|
||||
"connected": "{{path}} ile {{baud}} baud hızında bağlandı.",
|
||||
"disconnected": "Seri port bağlantısı kesildi.",
|
||||
"connectionError": "Seri bağlantı noktası açılamadı",
|
||||
"wsError": "WebSocket hatası",
|
||||
"errorNoServerUrl": "Sunucu URL'si yapılandırılmamış",
|
||||
"notSupportedTitle": "Seri Bağlantı Desteklenmiyor",
|
||||
"notSupported": "Seri bağlantılar, Web Serial API desteği olan bir tarayıcı (Chrome, Edge veya Firefox 151+) ya da Termix masaüstü uygulaması gerektirir.",
|
||||
"hideHint": "Seri sekmesini Kullanıcı Profili > Görünüm > Kenar Çubuğu > Gezinme altından gizleyebilirsiniz.",
|
||||
"browserPickerHint": "Bağlan'a tıklayın, tarayıcınız cihazı seçmek için bir bağlantı noktası seçici açacaktır."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Seri"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Серійний",
|
||||
"portLabel": "Порт",
|
||||
"portPlaceholder": "/dev/ttyUSB0 або COM3",
|
||||
"baudRateLabel": "Швидкість передачі даних",
|
||||
"dataBitsLabel": "Дані",
|
||||
"stopBitsLabel": "СТІЙ",
|
||||
"parityLabel": "Паритет",
|
||||
"parityNone": "Жоден",
|
||||
"parityEven": "Навіть",
|
||||
"parityOdd": "Дивний",
|
||||
"connect": "Підключення до послідовного порту",
|
||||
"disconnect": "Відключитися",
|
||||
"refreshPorts": "Оновити порти",
|
||||
"connected": "Підключено до {{path}} зі швидкістю {{baud}} бод",
|
||||
"disconnected": "Послідовний порт відключено",
|
||||
"connectionError": "Не вдалося відкрити послідовний порт",
|
||||
"wsError": "Помилка WebSocket",
|
||||
"errorNoServerUrl": "URL-адреса сервера не налаштована",
|
||||
"notSupportedTitle": "Серійний номер не підтримується",
|
||||
"notSupported": "Для послідовних з’єднань потрібен браузер із підтримкою Web Serial API (Chrome, Edge або Firefox 151+) або настільний додаток Termix.",
|
||||
"hideHint": "Ви можете приховати вкладку «Серійний номер» у розділі «Профіль користувача» > «Зовнішній вигляд» > «Бічна панель» > «Навігація».",
|
||||
"browserPickerHint": "Натисніть «Підключитися», і ваш браузер відкриє вибір порту для вибору пристрою."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Серійний"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "Cổng nối tiếp",
|
||||
"portLabel": "Cổng",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Tốc độ Baud",
|
||||
"dataBitsLabel": "Dữ liệu",
|
||||
"stopBitsLabel": "Bit dừng",
|
||||
"parityLabel": "Kiểm tra chẵn lẻ",
|
||||
"parityNone": "Không",
|
||||
"parityEven": "Chẵn",
|
||||
"parityOdd": "Lẻ",
|
||||
"connect": "Kết nối cổng nối tiếp",
|
||||
"disconnect": "Ngắt kết nối",
|
||||
"refreshPorts": "Làm mới cổng",
|
||||
"connected": "Đã kết nối tới {{path}} với tốc độ {{baud}} baud",
|
||||
"disconnected": "Đã ngắt kết nối cổng nối tiếp",
|
||||
"connectionError": "Không thể mở cổng nối tiếp",
|
||||
"wsError": "Lỗi WebSocket",
|
||||
"errorNoServerUrl": "Chưa cấu hình URL máy chủ",
|
||||
"notSupportedTitle": "Không hỗ trợ Serial",
|
||||
"notSupported": "Kết nối nối tiếp yêu cầu trình duyệt hỗ trợ Web Serial API (Chrome, Edge, hoặc Firefox 151+), hoặc ứng dụng Termix desktop.",
|
||||
"hideHint": "Bạn có thể ẩn tab Serial trong Hồ sơ người dùng > Giao diện > Thanh bên > Điều hướng.",
|
||||
"browserPickerHint": "Nhấn Kết nối và trình duyệt sẽ mở bộ chọn cổng để chọn thiết bị."
|
||||
},
|
||||
"nav": {
|
||||
"serial": "Nối tiếp"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "串口",
|
||||
"portLabel": "端口",
|
||||
"portPlaceholder": "/dev/ttyUSB0 或 COM3",
|
||||
"baudRateLabel": "波特率",
|
||||
"dataBitsLabel": "数据",
|
||||
"stopBitsLabel": "停止位",
|
||||
"parityLabel": "奇偶校验",
|
||||
"parityNone": "无",
|
||||
"parityEven": "偶校验",
|
||||
"parityOdd": "奇校验",
|
||||
"connect": "连接串口",
|
||||
"disconnect": "断开",
|
||||
"refreshPorts": "刷新端口",
|
||||
"connected": "已连接到 {{path}},波特率 {{baud}}",
|
||||
"disconnected": "串口已断开",
|
||||
"connectionError": "打开串口失败",
|
||||
"wsError": "WebSocket 错误",
|
||||
"errorNoServerUrl": "未配置服务器 URL",
|
||||
"notSupportedTitle": "不支持串口",
|
||||
"notSupported": "串口连接需要支持 Web Serial API 的浏览器(Chrome、Edge 或 Firefox 151+),或者 Termix 桌面应用。",
|
||||
"hideHint": "您可以在用户资料 > 外观 > 侧边栏 > 导航中隐藏串口标签页。",
|
||||
"browserPickerHint": "点击连接,您的浏览器将打开端口选择器以选择设备。"
|
||||
},
|
||||
"nav": {
|
||||
"serial": "串口"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"serial": {
|
||||
"title": "序列",
|
||||
"portLabel": "連接埠",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "鮑率",
|
||||
"dataBitsLabel": "資料位元",
|
||||
"stopBitsLabel": "停止位元",
|
||||
"parityLabel": "同位檢查",
|
||||
"parityNone": "無",
|
||||
"parityEven": "偶",
|
||||
"parityOdd": "奇",
|
||||
"connect": "連線至序列埠",
|
||||
"disconnect": "中斷連線",
|
||||
"refreshPorts": "重新整理連接埠",
|
||||
"connected": "已連線至 {{path}},鮑率 {{baud}}",
|
||||
"disconnected": "序列埠已中斷連線",
|
||||
"connectionError": "開啟序列埠失敗",
|
||||
"wsError": "WebSocket 錯誤",
|
||||
"errorNoServerUrl": "未設定伺服器網址",
|
||||
"notSupportedTitle": "不支援序列埠",
|
||||
"notSupported": "序列連線需要支援 Web Serial API 的瀏覽器 (Chrome、Edge 或 Firefox 151+) 或 Termix 桌面應用程式。",
|
||||
"hideHint": "您可以在「使用者設定檔 > 外觀 > 側邊欄 > 導覽」中隱藏序列索引標籤。",
|
||||
"browserPickerHint": "按一下連線,瀏覽器會開啟連接埠選擇器讓您選取裝置。"
|
||||
},
|
||||
"nav": {
|
||||
"serial": "序列"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "serial",
|
||||
"name": "Serial",
|
||||
"version": "1.0.0",
|
||||
"description": "Serial console access over a locally attached device.",
|
||||
"author": {
|
||||
"name": "Termix"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Termix-SSH/Termix",
|
||||
"category": "Terminal",
|
||||
"icon": "Usb",
|
||||
"engine": {
|
||||
"termix": ">=2.9.0",
|
||||
"api": "1"
|
||||
},
|
||||
"capabilities": ["network:serve", "device:serial", "ui:surface"],
|
||||
"nativeDependencies": ["serialport"],
|
||||
"locales": "locales",
|
||||
"platforms": ["linux", "win32", "darwin"],
|
||||
"contributes": {
|
||||
"tabs": [
|
||||
{
|
||||
"id": "serial",
|
||||
"titleKey": "nav.serial",
|
||||
"icon": "Usb",
|
||||
"openFrom": ["rail"]
|
||||
}
|
||||
],
|
||||
"panels": [
|
||||
{
|
||||
"id": "serial",
|
||||
"titleKey": "nav.serial",
|
||||
"icon": "Usb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@termix-plugin/serial",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "termix-plugin build",
|
||||
"test": "termix-plugin test",
|
||||
"typecheck": "tsc -p tsconfig.json",
|
||||
"validate": "termix-plugin validate"
|
||||
},
|
||||
"dependencies": {
|
||||
"serialport": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@termix/plugin-sdk": "*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||
import { createSerialSession } from "./session.js";
|
||||
|
||||
export async function activate(ctx: PluginContext): Promise<void> {
|
||||
ctx.ws.route("/console", createSerialSession(ctx));
|
||||
ctx.log.info("Serial console mounted at /plugin-ws/serial/console");
|
||||
}
|
||||
|
||||
export async function deactivate(): Promise<void> {
|
||||
// The WS route and every open socket are torn down by core: see
|
||||
// src/backend/plugins/ws.ts. Nothing here outlives activate().
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import type { WebSocket } from "ws";
|
||||
import { SerialPort } from "serialport";
|
||||
import type {
|
||||
PluginContext,
|
||||
PluginWebSocketConnection,
|
||||
} from "@termix/plugin-sdk/backend";
|
||||
import { parseWsMessage } from "./ws-message.js";
|
||||
|
||||
interface SerialConnectData {
|
||||
path: string;
|
||||
baudRate: number;
|
||||
dataBits?: 5 | 6 | 7 | 8;
|
||||
stopBits?: 1 | 2;
|
||||
parity?: "none" | "even" | "odd";
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles one /plugin-ws/serial/console connection. Auth already ran (core's
|
||||
* ctx.ws.route authenticates before this is called), so every message here
|
||||
* only has to decide whether to touch the actual hardware.
|
||||
*/
|
||||
export function createSerialSession(ctx: PluginContext) {
|
||||
return async (connection: PluginWebSocketConnection): Promise<void> => {
|
||||
const ws = connection.socket as WebSocket;
|
||||
let port: SerialPort | null = null;
|
||||
|
||||
const send = (msg: object) => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (port?.isOpen) {
|
||||
port.close();
|
||||
}
|
||||
port = null;
|
||||
};
|
||||
|
||||
if (!connection.isDataUnlocked()) {
|
||||
send({ type: "error", data: "Data locked" });
|
||||
ws.close(1008, "Data access required");
|
||||
return;
|
||||
}
|
||||
|
||||
ws.on("message", async (raw) => {
|
||||
let type: string;
|
||||
let data: unknown;
|
||||
try {
|
||||
({ type, data } = parseWsMessage(raw));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connection.isDataUnlocked()) {
|
||||
send({ type: "error", data: "Data access expired" });
|
||||
ws.close(1008, "Data access expired");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case "list_ports": {
|
||||
await ctx.capabilities.require("device:serial");
|
||||
try {
|
||||
const ports = await SerialPort.list();
|
||||
send({ type: "ports_list", data: ports });
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
data: errorMessage(err, "Failed to list ports"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "connect": {
|
||||
await ctx.capabilities.require("device:serial");
|
||||
|
||||
if (port?.isOpen) {
|
||||
port.close();
|
||||
port = null;
|
||||
}
|
||||
|
||||
const cfg = data as SerialConnectData;
|
||||
if (!cfg?.path || !cfg?.baudRate) {
|
||||
send({ type: "error", data: "Missing port path or baud rate" });
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
const opened = new SerialPort({
|
||||
path: cfg.path,
|
||||
baudRate: cfg.baudRate,
|
||||
dataBits: cfg.dataBits ?? 8,
|
||||
stopBits: cfg.stopBits ?? 1,
|
||||
parity: cfg.parity ?? "none",
|
||||
autoOpen: false,
|
||||
});
|
||||
port = opened;
|
||||
|
||||
opened.open((err) => {
|
||||
if (err) {
|
||||
ctx.log.error(`Serial port open failed for ${cfg.path}`, err);
|
||||
send({ type: "error", data: err.message });
|
||||
port = null;
|
||||
return;
|
||||
}
|
||||
ctx.log.info(
|
||||
`Serial port opened: ${cfg.path} at ${cfg.baudRate} baud`,
|
||||
);
|
||||
send({ type: "connected" });
|
||||
});
|
||||
|
||||
opened.on("data", (chunk: Buffer) => {
|
||||
send({ type: "data", data: chunk.toString("binary") });
|
||||
});
|
||||
|
||||
opened.on("error", (err) => {
|
||||
send({ type: "error", data: err.message });
|
||||
});
|
||||
|
||||
opened.on("close", () => {
|
||||
send({ type: "disconnected" });
|
||||
port = null;
|
||||
});
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
data: errorMessage(err, "Failed to open serial port"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "input": {
|
||||
if (!port?.isOpen) break;
|
||||
const input = typeof data === "string" ? data : "";
|
||||
if (!input) break;
|
||||
port.write(Buffer.from(input, "binary"), (err) => {
|
||||
if (err) send({ type: "error", data: err.message });
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "disconnect": {
|
||||
cleanup();
|
||||
send({ type: "disconnected" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
ctx.log.error("Error handling serial WebSocket message", err);
|
||||
send({
|
||||
type: "error",
|
||||
data:
|
||||
err instanceof Error ? err.message : "Failed to process message",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", cleanup);
|
||||
ws.on("error", cleanup);
|
||||
};
|
||||
}
|
||||
|
||||
function errorMessage(err: unknown, fallback: string): string {
|
||||
return err instanceof Error ? err.message : fallback;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { RawData } from "ws";
|
||||
|
||||
// Control messages here are tiny (a path, a baud rate) or raw terminal input,
|
||||
// bounded by what a user can type or paste, so anything larger is abuse.
|
||||
const MAX_MESSAGE_BYTES = 1024 * 1024;
|
||||
|
||||
function rawByteLength(raw: RawData): number {
|
||||
if (Buffer.isBuffer(raw)) return raw.length;
|
||||
if (Array.isArray(raw))
|
||||
return raw.reduce((sum, part) => sum + part.length, 0);
|
||||
if (raw instanceof ArrayBuffer) return raw.byteLength;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Parses a WebSocket frame into `{ type, data }`, throwing on anything malformed. */
|
||||
export function parseWsMessage(raw: RawData): { type: string; data: unknown } {
|
||||
if (rawByteLength(raw) > MAX_MESSAGE_BYTES) {
|
||||
throw new Error("Message too large");
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw.toString());
|
||||
} catch {
|
||||
throw new Error("Invalid JSON");
|
||||
}
|
||||
|
||||
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error("Message must be a JSON object");
|
||||
}
|
||||
|
||||
const { type, data } = parsed as { type?: unknown; data?: unknown };
|
||||
if (typeof type !== "string") {
|
||||
throw new Error("Message type must be a string");
|
||||
}
|
||||
|
||||
return { type, data };
|
||||
}
|
||||
@@ -7,16 +7,24 @@ import {
|
||||
} from "react";
|
||||
import { useXTerm } from "react-xtermjs";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { websocketAuthProtocols } from "@/lib/ws-auth";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme";
|
||||
import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes";
|
||||
import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles";
|
||||
import type { SerialConfig } from "@/types/ui-types";
|
||||
import type { SerialHandle } from "./serial-types";
|
||||
import { useTranslation, useTheme } from "@termix/plugin-sdk/frontend";
|
||||
import type { SerialConfig, SerialHandle } from "./types.js";
|
||||
import { isElectron } from "./electron.js";
|
||||
import { resolveSerialWsUrl } from "./transport.js";
|
||||
|
||||
const XTERM_THEME: Record<"light" | "dark", Record<string, string>> = {
|
||||
dark: {
|
||||
background: "#0b0e14",
|
||||
foreground: "#d4d7de",
|
||||
cursor: "#d4d7de",
|
||||
},
|
||||
light: {
|
||||
background: "#ffffff",
|
||||
foreground: "#1a1a1a",
|
||||
cursor: "#1a1a1a",
|
||||
},
|
||||
};
|
||||
|
||||
type WebSerialPort = {
|
||||
open(options: {
|
||||
@@ -60,52 +68,16 @@ export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
|
||||
[terminal],
|
||||
);
|
||||
|
||||
// ── Theme sync ─────────────────────────────────────────────────────────
|
||||
|
||||
useEffect(() => {
|
||||
if (!terminal) return;
|
||||
const themeColors = resolveTermixThemeColors("termix", appTheme);
|
||||
const fontConfig = TERMINAL_FONTS.find(
|
||||
(f) => f.value === DEFAULT_TERMINAL_CONFIG.fontFamily,
|
||||
);
|
||||
ensureTerminalFontsLoaded(fontConfig?.value ?? TERMINAL_FONTS[0].value);
|
||||
terminal.options.theme = {
|
||||
background: themeColors.background,
|
||||
foreground: themeColors.foreground,
|
||||
cursor: themeColors.cursor,
|
||||
cursorAccent: themeColors.cursorAccent,
|
||||
selectionBackground: themeColors.selectionBackground,
|
||||
selectionForeground: themeColors.selectionForeground,
|
||||
black: themeColors.black,
|
||||
red: themeColors.red,
|
||||
green: themeColors.green,
|
||||
yellow: themeColors.yellow,
|
||||
blue: themeColors.blue,
|
||||
magenta: themeColors.magenta,
|
||||
cyan: themeColors.cyan,
|
||||
white: themeColors.white,
|
||||
brightBlack: themeColors.brightBlack,
|
||||
brightRed: themeColors.brightRed,
|
||||
brightGreen: themeColors.brightGreen,
|
||||
brightYellow: themeColors.brightYellow,
|
||||
brightBlue: themeColors.brightBlue,
|
||||
brightMagenta: themeColors.brightMagenta,
|
||||
brightCyan: themeColors.brightCyan,
|
||||
brightWhite: themeColors.brightWhite,
|
||||
};
|
||||
terminal.options.theme = XTERM_THEME[appTheme];
|
||||
terminal.options.fontFamily =
|
||||
fontConfig?.fallback ?? TERMINAL_FONTS[0].fallback;
|
||||
terminal.options.fontSize = DEFAULT_TERMINAL_CONFIG.fontSize;
|
||||
'"SF Mono", Consolas, "Liberation Mono", monospace';
|
||||
terminal.options.fontSize = 14;
|
||||
}, [terminal, appTheme]);
|
||||
|
||||
// ── WebSocket (Electron) path ──────────────────────────────────────────
|
||||
|
||||
const buildWsUrl = useCallback(() => {
|
||||
// Serial is always local -- the device is physically attached to this
|
||||
// desktop machine, so it never routes through a remote server.
|
||||
return "ws://127.0.0.1:30011";
|
||||
}, []);
|
||||
|
||||
const disconnectWs = useCallback(() => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.onclose = null;
|
||||
@@ -115,18 +87,15 @@ export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
|
||||
connectedRef.current = false;
|
||||
}, []);
|
||||
|
||||
const connectWs = useCallback(() => {
|
||||
const connectWs = useCallback(async () => {
|
||||
disconnectWs();
|
||||
const url = buildWsUrl();
|
||||
if (!url) {
|
||||
const target = await resolveSerialWsUrl();
|
||||
if (!target) {
|
||||
write(`\r\n\x1b[31m${t("serial.errorNoServerUrl")}\x1b[0m\r\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(
|
||||
url,
|
||||
websocketAuthProtocols(localStorage.getItem("jwt")),
|
||||
);
|
||||
const ws = new WebSocket(target.url, target.protocols);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
@@ -171,7 +140,7 @@ export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
|
||||
ws.onerror = () => {
|
||||
write(`\r\n\x1b[31m${t("serial.wsError")}\x1b[0m\r\n`);
|
||||
};
|
||||
}, [buildWsUrl, config, disconnectWs, t, write]);
|
||||
}, [config, disconnectWs, t, write]);
|
||||
|
||||
// ── Web Serial API path ────────────────────────────────────────────────
|
||||
|
||||
@@ -252,7 +221,7 @@ export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
|
||||
if (useWebSerial) {
|
||||
connectWebSerial();
|
||||
} else {
|
||||
connectWs();
|
||||
void connectWs();
|
||||
}
|
||||
}, [useWebSerial, connectWebSerial, connectWs]);
|
||||
|
||||
@@ -267,9 +236,15 @@ export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
|
||||
}
|
||||
}, [useWebSerial, disconnectWebSerial, disconnectWs]);
|
||||
|
||||
const reconnect = useCallback(() => {
|
||||
disconnect();
|
||||
connect();
|
||||
}, [disconnect, connect]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
connect,
|
||||
disconnect,
|
||||
reconnect,
|
||||
isConnected: () => connectedRef.current,
|
||||
sendInput: (data: string) => {
|
||||
if (useWebSerial) {
|
||||
@@ -289,8 +264,8 @@ export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
|
||||
const fitAddon = new FitAddon();
|
||||
fitAddonRef.current = fitAddon;
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.options.cursorBlink = DEFAULT_TERMINAL_CONFIG.cursorBlink;
|
||||
terminal.options.scrollback = DEFAULT_TERMINAL_CONFIG.scrollback;
|
||||
terminal.options.cursorBlink = true;
|
||||
terminal.options.scrollback = 10000;
|
||||
|
||||
terminal.onData((data) => {
|
||||
if (!connectedRef.current) return;
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RefreshCw, Usb, TriangleAlert } from "lucide-react";
|
||||
import { Input } from "@/components/input";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { websocketAuthProtocols } from "@/lib/ws-auth";
|
||||
import type { SerialConfig } from "@/types/ui-types";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import { Input, Select2 } from "@termix/plugin-sdk/ui";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { SerialConfig } from "./types.js";
|
||||
import { isElectron } from "./electron.js";
|
||||
import { resolveSerialWsUrl } from "./transport.js";
|
||||
|
||||
const BAUD_RATES = [
|
||||
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
|
||||
@@ -33,25 +32,16 @@ export function SerialPanel({ onConnect }: SerialPanelProps) {
|
||||
const [availablePorts, setAvailablePorts] = useState<string[]>([]);
|
||||
const [loadingPorts, setLoadingPorts] = useState(false);
|
||||
|
||||
const buildWsUrl = () => {
|
||||
// Serial is always local -- the device is physically attached to this
|
||||
// desktop machine, so it never routes through a remote server.
|
||||
return "ws://127.0.0.1:30011";
|
||||
};
|
||||
|
||||
const refreshPorts = useCallback(() => {
|
||||
const refreshPorts = useCallback(async () => {
|
||||
if (!isElectron()) return;
|
||||
setLoadingPorts(true);
|
||||
const url = buildWsUrl();
|
||||
if (!url) {
|
||||
const target = await resolveSerialWsUrl();
|
||||
if (!target) {
|
||||
setLoadingPorts(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = new WebSocket(
|
||||
url,
|
||||
websocketAuthProtocols(localStorage.getItem("jwt")),
|
||||
);
|
||||
const ws = new WebSocket(target.url, target.protocols);
|
||||
ws.onopen = () => ws.send(JSON.stringify({ type: "list_ports" }));
|
||||
ws.onmessage = (ev) => {
|
||||
try {
|
||||
@@ -62,7 +52,7 @@ export function SerialPanel({ onConnect }: SerialPanelProps) {
|
||||
if (msg.type === "ports_list" && Array.isArray(msg.data)) {
|
||||
const paths = msg.data.map((p) => p.path);
|
||||
setAvailablePorts(paths);
|
||||
if (!path && paths.length > 0) setPath(paths[0]);
|
||||
setPath((current) => current || (paths[0] ?? current));
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -72,10 +62,10 @@ export function SerialPanel({ onConnect }: SerialPanelProps) {
|
||||
}
|
||||
};
|
||||
ws.onerror = () => setLoadingPorts(false);
|
||||
}, [path]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) refreshPorts();
|
||||
if (isElectron()) void refreshPorts();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
@@ -218,7 +208,7 @@ export function SerialPanel({ onConnect }: SerialPanelProps) {
|
||||
{t("serial.portLabel")}
|
||||
</label>
|
||||
<button
|
||||
onClick={refreshPorts}
|
||||
onClick={() => void refreshPorts()}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t("serial.refreshPorts")}
|
||||
>
|
||||
@@ -0,0 +1,16 @@
|
||||
type ElectronWindow = Window &
|
||||
typeof globalThis & {
|
||||
IS_ELECTRON?: boolean;
|
||||
electronAPI?: { isElectron?: boolean };
|
||||
};
|
||||
|
||||
/** Serial is always local: the device is physically attached to this desktop machine. */
|
||||
export function isElectron(): boolean {
|
||||
if (typeof window === "undefined") return false;
|
||||
const win = window as ElectronWindow;
|
||||
return (
|
||||
win.IS_ELECTRON === true ||
|
||||
!!win.electronAPI ||
|
||||
win.electronAPI?.isElectron === true
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { Ref } from "react";
|
||||
import { Usb } from "lucide-react";
|
||||
import type {
|
||||
PanelProps,
|
||||
PluginHostRecord,
|
||||
TabProps,
|
||||
TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { Serial } from "./Serial.js";
|
||||
import { SerialPanel } from "./SerialPanel.js";
|
||||
import { setSerialWsUrl } from "./transport.js";
|
||||
import type { SerialConfig, SerialHandle } from "./types.js";
|
||||
|
||||
/**
|
||||
* A per-connection synthetic host, never a real DB host. The shell only
|
||||
* opens a genuinely hostless (host: null) tab as a singleton, and a serial
|
||||
* console is not one: a user can have several open at once, each with its
|
||||
* own device and settings. A distinct id per connection is what keeps them
|
||||
* separate tabs.
|
||||
*/
|
||||
function buildPseudoHost(config: SerialConfig): PluginHostRecord {
|
||||
return {
|
||||
id: `serial-${Date.now()}`,
|
||||
name: config.path
|
||||
? `${config.path} (${config.baudRate})`
|
||||
: `Serial (${config.baudRate})`,
|
||||
ip: "",
|
||||
port: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function SerialPanelView({ shell }: PanelProps) {
|
||||
return (
|
||||
<SerialPanel
|
||||
onConnect={(config) => {
|
||||
shell.openTab(buildPseudoHost(config), "serial", {
|
||||
forceNewTab: true,
|
||||
data: { serialConfig: config },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SerialTabView({ tab, isVisible, handleRef }: TabProps) {
|
||||
const config = tab.data?.serialConfig as SerialConfig | undefined;
|
||||
if (!config) return null;
|
||||
return (
|
||||
<Serial
|
||||
ref={handleRef as Ref<SerialHandle>}
|
||||
config={config}
|
||||
isVisible={isVisible}
|
||||
instanceId={tab.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
setSerialWsUrl(app.wsUrl);
|
||||
app.onDispose(() => setSerialWsUrl(null));
|
||||
|
||||
app.registerRailItem({
|
||||
id: "serial",
|
||||
icon: Usb,
|
||||
titleKey: "nav.serial",
|
||||
after: "quick-connect",
|
||||
separatorAfter: true,
|
||||
});
|
||||
|
||||
app.registerPanel("serial", SerialPanelView);
|
||||
|
||||
app.registerTab("serial", SerialTabView, {
|
||||
icon: Usb,
|
||||
titleKey: "nav.serial",
|
||||
session: true,
|
||||
// A physical device attached to this machine, not a remote connection
|
||||
// another user could join.
|
||||
shareable: false,
|
||||
// Reconnecting to a COM port automatically after a restart is not a
|
||||
// workflow anyone asked for, and there's no host to restore against
|
||||
// anyway, so a serial tab is a local, ephemeral session like the
|
||||
// desktop app's local terminal rather than something workspaces saves.
|
||||
inLayouts: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
|
||||
/**
|
||||
* The live app.wsUrl, for components that are defined at module scope rather
|
||||
* than inside activate(). Set on activate, cleared on dispose, the same
|
||||
* pattern the tunnels plugin uses for app.api.
|
||||
*/
|
||||
let wsUrl: TermixApp["wsUrl"] | null = null;
|
||||
|
||||
export function setSerialWsUrl(next: TermixApp["wsUrl"] | null): void {
|
||||
wsUrl = next;
|
||||
}
|
||||
|
||||
/**
|
||||
* The console's WebSocket URL and auth subprotocols. No origin option: a
|
||||
* serial device is physically attached to this desktop machine, so it
|
||||
* always dials the embedded local backend rather than a connected remote
|
||||
* sync server (app.wsUrl's default when no origin is given).
|
||||
*/
|
||||
export async function resolveSerialWsUrl() {
|
||||
if (!wsUrl) return null;
|
||||
return wsUrl("/console");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export type SerialConfig = {
|
||||
path: string;
|
||||
baudRate: number;
|
||||
dataBits: 5 | 6 | 7 | 8;
|
||||
stopBits: 1 | 2;
|
||||
parity: "none" | "even" | "odd";
|
||||
};
|
||||
|
||||
export interface SerialHandle {
|
||||
connect: () => void;
|
||||
disconnect: () => void;
|
||||
reconnect: () => void;
|
||||
isConnected: () => boolean;
|
||||
sendInput: (data: string) => void;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createMockCtx,
|
||||
type MockPluginContext,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import { PluginCapabilityError } from "@termix/plugin-sdk/backend";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import manifestJson from "../../manifest.json";
|
||||
import { activate } from "../../src/backend/index.js";
|
||||
|
||||
const manifest = manifestJson as unknown as PluginManifest;
|
||||
|
||||
describe("serial activate", () => {
|
||||
it("mounts the console WS route at /console with every declared capability", async () => {
|
||||
const mock: MockPluginContext = createMockCtx({
|
||||
pluginId: manifest.id,
|
||||
manifest,
|
||||
capabilities: manifest.capabilities,
|
||||
});
|
||||
|
||||
await activate(mock.ctx);
|
||||
|
||||
expect(mock.wsRoutes).toEqual([{ path: "/console", raw: false }]);
|
||||
});
|
||||
|
||||
it("refuses to mount without network:serve", async () => {
|
||||
const mock: MockPluginContext = createMockCtx({
|
||||
pluginId: manifest.id,
|
||||
manifest,
|
||||
capabilities: manifest.capabilities.filter((c) => c !== "network:serve"),
|
||||
});
|
||||
|
||||
await expect(activate(mock.ctx)).rejects.toBeInstanceOf(
|
||||
PluginCapabilityError,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import {
|
||||
createMockCtx,
|
||||
type MockPluginContext,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import type { PluginWebSocketConnection } from "@termix/plugin-sdk/backend";
|
||||
import { createSerialSession } from "../../src/backend/session.js";
|
||||
|
||||
// vi.mock's factory is hoisted above every import, including node:events, so
|
||||
// FakeSerialPort gets its own minimal emitter rather than extending it.
|
||||
const { listMock, openMock, FakeSerialPort } = vi.hoisted(() => {
|
||||
const listMock = vi.fn(async () => [{ path: "/dev/ttyUSB0" }]);
|
||||
const openMock = vi.fn();
|
||||
|
||||
class FakeSerialPort {
|
||||
isOpen = false;
|
||||
private listeners = new Map<string, Set<(...args: unknown[]) => void>>();
|
||||
constructor(public options: Record<string, unknown>) {}
|
||||
on(event: string, listener: (...args: unknown[]) => void) {
|
||||
const set = this.listeners.get(event) ?? new Set();
|
||||
set.add(listener);
|
||||
this.listeners.set(event, set);
|
||||
return this;
|
||||
}
|
||||
private emit(event: string, ...args: unknown[]) {
|
||||
for (const listener of this.listeners.get(event) ?? []) listener(...args);
|
||||
}
|
||||
open(cb: (err?: Error) => void) {
|
||||
openMock(this.options);
|
||||
this.isOpen = true;
|
||||
cb();
|
||||
}
|
||||
write(_data: Buffer, cb: (err?: Error) => void) {
|
||||
cb();
|
||||
}
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.emit("close");
|
||||
}
|
||||
static list = listMock;
|
||||
}
|
||||
|
||||
return { listMock, openMock, FakeSerialPort };
|
||||
});
|
||||
|
||||
vi.mock("serialport", () => ({ SerialPort: FakeSerialPort }));
|
||||
|
||||
class FakeSocket extends EventEmitter {
|
||||
readyState = 1;
|
||||
OPEN = 1;
|
||||
sent: unknown[] = [];
|
||||
send(msg: string) {
|
||||
this.sent.push(JSON.parse(msg));
|
||||
}
|
||||
close(_code?: number, _reason?: string) {
|
||||
this.readyState = 3;
|
||||
}
|
||||
}
|
||||
|
||||
function fakeConnection(
|
||||
socket: FakeSocket,
|
||||
isDataUnlocked: () => boolean = () => true,
|
||||
): PluginWebSocketConnection {
|
||||
return {
|
||||
userId: "user-1",
|
||||
request: {},
|
||||
socket,
|
||||
isDataUnlocked,
|
||||
} as unknown as PluginWebSocketConnection;
|
||||
}
|
||||
|
||||
async function deliver(socket: FakeSocket, message: object) {
|
||||
socket.emit("message", Buffer.from(JSON.stringify(message)));
|
||||
// Let the async message handler's microtasks settle.
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
describe("serial session", () => {
|
||||
let mock: MockPluginContext;
|
||||
|
||||
beforeEach(() => {
|
||||
listMock.mockClear();
|
||||
openMock.mockClear();
|
||||
});
|
||||
|
||||
it("refuses list_ports and connect without device:serial", async () => {
|
||||
mock = createMockCtx({ pluginId: "serial", capabilities: [] });
|
||||
const socket = new FakeSocket();
|
||||
await createSerialSession(mock.ctx)(fakeConnection(socket));
|
||||
|
||||
await deliver(socket, { type: "list_ports" });
|
||||
expect(listMock).not.toHaveBeenCalled();
|
||||
expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]);
|
||||
|
||||
socket.sent.length = 0;
|
||||
await deliver(socket, {
|
||||
type: "connect",
|
||||
data: { path: "/dev/ttyUSB0", baudRate: 9600 },
|
||||
});
|
||||
expect(openMock).not.toHaveBeenCalled();
|
||||
expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]);
|
||||
});
|
||||
|
||||
it("lists ports once granted device:serial", async () => {
|
||||
mock = createMockCtx({
|
||||
pluginId: "serial",
|
||||
capabilities: ["network:serve", "device:serial"],
|
||||
});
|
||||
const socket = new FakeSocket();
|
||||
await createSerialSession(mock.ctx)(fakeConnection(socket));
|
||||
|
||||
await deliver(socket, { type: "list_ports" });
|
||||
|
||||
expect(listMock).toHaveBeenCalledTimes(1);
|
||||
expect(socket.sent).toEqual([
|
||||
{ type: "ports_list", data: [{ path: "/dev/ttyUSB0" }] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("opens, writes to and closes a port through connect/input/disconnect", async () => {
|
||||
mock = createMockCtx({
|
||||
pluginId: "serial",
|
||||
capabilities: ["network:serve", "device:serial"],
|
||||
});
|
||||
const socket = new FakeSocket();
|
||||
await createSerialSession(mock.ctx)(fakeConnection(socket));
|
||||
|
||||
await deliver(socket, {
|
||||
type: "connect",
|
||||
data: { path: "/dev/ttyUSB0", baudRate: 115200 },
|
||||
});
|
||||
expect(openMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ path: "/dev/ttyUSB0", baudRate: 115200 }),
|
||||
);
|
||||
expect(socket.sent).toContainEqual({ type: "connected" });
|
||||
|
||||
socket.sent.length = 0;
|
||||
await deliver(socket, { type: "input", data: "AT\r\n" });
|
||||
// write() succeeds silently; nothing is echoed back until the port emits data.
|
||||
|
||||
socket.sent.length = 0;
|
||||
await deliver(socket, { type: "disconnect" });
|
||||
expect(socket.sent).toContainEqual({ type: "disconnected" });
|
||||
});
|
||||
|
||||
it("rejects a connect with no path or baud rate without touching the port", async () => {
|
||||
mock = createMockCtx({
|
||||
pluginId: "serial",
|
||||
capabilities: ["network:serve", "device:serial"],
|
||||
});
|
||||
const socket = new FakeSocket();
|
||||
await createSerialSession(mock.ctx)(fakeConnection(socket));
|
||||
|
||||
await deliver(socket, { type: "connect", data: { path: "" } });
|
||||
|
||||
expect(openMock).not.toHaveBeenCalled();
|
||||
expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]);
|
||||
});
|
||||
|
||||
it("closes the socket at connect time when data is locked", async () => {
|
||||
mock = createMockCtx({
|
||||
pluginId: "serial",
|
||||
capabilities: ["network:serve", "device:serial"],
|
||||
});
|
||||
const socket = new FakeSocket();
|
||||
await createSerialSession(mock.ctx)(fakeConnection(socket, () => false));
|
||||
|
||||
expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]);
|
||||
});
|
||||
|
||||
it("refuses a message once data locks mid-session", async () => {
|
||||
mock = createMockCtx({
|
||||
pluginId: "serial",
|
||||
capabilities: ["network:serve", "device:serial"],
|
||||
});
|
||||
const socket = new FakeSocket();
|
||||
let unlocked = true;
|
||||
await createSerialSession(mock.ctx)(fakeConnection(socket, () => unlocked));
|
||||
|
||||
unlocked = false;
|
||||
await deliver(socket, { type: "list_ports" });
|
||||
|
||||
expect(listMock).not.toHaveBeenCalled();
|
||||
expect(socket.sent).toEqual([expect.objectContaining({ type: "error" })]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import {
|
||||
renderWithApp,
|
||||
type RenderedPluginApp,
|
||||
} from "@termix/plugin-sdk/testing";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
import * as plugin from "../../src/frontend/index";
|
||||
import manifestJson from "../../manifest.json";
|
||||
import locales from "../../locales/en.json";
|
||||
|
||||
const manifest = manifestJson as unknown as PluginManifest;
|
||||
|
||||
const declared = (views?: Array<{ id: string }>) =>
|
||||
(views ?? []).map((view) => view.id).sort();
|
||||
|
||||
let rendered: RenderedPluginApp | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
await rendered?.deactivate();
|
||||
rendered = null;
|
||||
});
|
||||
|
||||
describe(`${manifest.id} activate`, () => {
|
||||
it("activates and registers only views its manifest declares", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
const contributes = manifest.contributes as unknown as Record<
|
||||
string,
|
||||
Array<{ id: string }> | undefined
|
||||
>;
|
||||
const tabs = declared(contributes.tabs);
|
||||
const panels = [...declared(contributes.panels), ...tabs];
|
||||
|
||||
expect(rendered.registered.tabs()).toEqual(["serial"]);
|
||||
for (const id of rendered.registered.tabs()) expect(tabs).toContain(id);
|
||||
for (const id of rendered.registered.panels()) expect(panels).toContain(id);
|
||||
expect(rendered.registered.railItems().map((item) => item.id)).toContain(
|
||||
"serial",
|
||||
);
|
||||
});
|
||||
|
||||
it("removes everything it registered on deactivate", async () => {
|
||||
const app = await renderWithApp(plugin, { manifest, locales });
|
||||
await app.deactivate();
|
||||
expect(app.registered.tabs()).toEqual([]);
|
||||
expect(app.registered.panels()).toEqual([]);
|
||||
expect(app.registered.railItems()).toEqual([]);
|
||||
});
|
||||
|
||||
it("renders the connect form in the rail panel", async () => {
|
||||
rendered = await renderWithApp(plugin, { manifest, locales });
|
||||
rendered.renderPanel("serial", { active: true });
|
||||
// jsdom has no Web Serial API and isn't Electron, so the panel falls
|
||||
// back to its "not supported" message rather than a live form.
|
||||
expect(
|
||||
await screen.findByText(locales.serial.notSupportedTitle),
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset";
|
||||
|
||||
export default pluginVitestConfig(import.meta.url);
|
||||
@@ -1,195 +0,0 @@
|
||||
import { getErrorMessage } from "../utils/error-message.js";
|
||||
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
||||
import { SerialPort } from "serialport";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { DataCrypto } from "../utils/data-crypto.js";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
import { parseWsMessage } from "../utils/ws-message.js";
|
||||
import { extractWebSocketToken } from "../utils/ws-auth.js";
|
||||
|
||||
interface SerialConnectData {
|
||||
path: string;
|
||||
baudRate: number;
|
||||
dataBits?: 5 | 6 | 7 | 8;
|
||||
stopBits?: 1 | 2;
|
||||
parity?: "none" | "even" | "odd";
|
||||
}
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
|
||||
const wss = new WebSocketServer({ host: "127.0.0.1", port: 30011 });
|
||||
|
||||
wss.on("error", (error) => {
|
||||
sshLogger.error("Serial WebSocket server error", error, {
|
||||
operation: "wss_error",
|
||||
});
|
||||
});
|
||||
|
||||
wss.on("connection", async (ws: WebSocket, req) => {
|
||||
let userId: string | undefined;
|
||||
|
||||
try {
|
||||
const token = extractWebSocketToken(req);
|
||||
|
||||
if (!token) {
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await authManager.verifyJWTToken(token);
|
||||
if (!payload?.userId || payload.pendingTOTP) {
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
userId = payload.userId;
|
||||
} catch {
|
||||
ws.close(1008, "Authentication required");
|
||||
return;
|
||||
}
|
||||
|
||||
const dataKey = DataCrypto.getUserDataKey(userId);
|
||||
if (!dataKey) {
|
||||
ws.send(JSON.stringify({ type: "error", data: "Data locked" }));
|
||||
ws.close(1008, "Data access required");
|
||||
return;
|
||||
}
|
||||
|
||||
let port: SerialPort | null = null;
|
||||
|
||||
const send = (msg: object) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (port?.isOpen) {
|
||||
port.close();
|
||||
}
|
||||
port = null;
|
||||
};
|
||||
|
||||
ws.on("message", async (raw: RawData) => {
|
||||
let type: string;
|
||||
let data: unknown;
|
||||
try {
|
||||
({ type, data } = parseWsMessage(raw));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case "list_ports": {
|
||||
try {
|
||||
const ports = await SerialPort.list();
|
||||
send({ type: "ports_list", data: ports });
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
data: getErrorMessage(err, "Failed to list ports"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "connect": {
|
||||
if (port?.isOpen) {
|
||||
port.close();
|
||||
port = null;
|
||||
}
|
||||
|
||||
const cfg = data as SerialConnectData;
|
||||
if (!cfg?.path || !cfg?.baudRate) {
|
||||
send({ type: "error", data: "Missing port path or baud rate" });
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
port = new SerialPort({
|
||||
path: cfg.path,
|
||||
baudRate: cfg.baudRate,
|
||||
dataBits: cfg.dataBits ?? 8,
|
||||
stopBits: cfg.stopBits ?? 1,
|
||||
parity: cfg.parity ?? "none",
|
||||
autoOpen: false,
|
||||
});
|
||||
|
||||
port.open((err) => {
|
||||
if (err) {
|
||||
sshLogger.error("Serial port open failed", err, {
|
||||
operation: "serial_open",
|
||||
path: cfg.path,
|
||||
userId,
|
||||
});
|
||||
send({ type: "error", data: err.message });
|
||||
port = null;
|
||||
return;
|
||||
}
|
||||
sshLogger.info("Serial port opened", {
|
||||
operation: "serial_open",
|
||||
path: cfg.path,
|
||||
baudRate: cfg.baudRate,
|
||||
userId,
|
||||
});
|
||||
send({ type: "connected" });
|
||||
});
|
||||
|
||||
port.on("data", (chunk: Buffer) => {
|
||||
send({ type: "data", data: chunk.toString("binary") });
|
||||
});
|
||||
|
||||
port.on("error", (err) => {
|
||||
send({ type: "error", data: err.message });
|
||||
});
|
||||
|
||||
port.on("close", () => {
|
||||
send({ type: "disconnected" });
|
||||
port = null;
|
||||
});
|
||||
} catch (err) {
|
||||
send({
|
||||
type: "error",
|
||||
data: getErrorMessage(err, "Failed to open serial port"),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "input": {
|
||||
if (!port?.isOpen) break;
|
||||
const input = typeof data === "string" ? data : "";
|
||||
if (!input) break;
|
||||
port.write(Buffer.from(input, "binary"), (err) => {
|
||||
if (err) {
|
||||
send({ type: "error", data: err.message });
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "disconnect": {
|
||||
cleanup();
|
||||
send({ type: "disconnected" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
sshLogger.error("Error handling serial WebSocket message", err, {
|
||||
operation: "serial_message_handler_error",
|
||||
userId,
|
||||
messageType: type,
|
||||
});
|
||||
send({ type: "error", data: "Failed to process message" });
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
ws.on("error", () => {
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
@@ -26,7 +26,7 @@ import * as serviceRegistry from "./service-registry.js";
|
||||
import type { ServiceRegistration } from "./service-registry.js";
|
||||
import * as secretRegistry from "./secret-registry.js";
|
||||
import type { SecretRegistration } from "./secret-registry.js";
|
||||
import { assertCapability } from "./permissions.js";
|
||||
import { assertCapability, hasCapability } from "./permissions.js";
|
||||
import { getActor, runAsActor } from "./actor.js";
|
||||
import { DisposableBag } from "./disposables.js";
|
||||
import {
|
||||
@@ -322,6 +322,27 @@ export function createPluginContext(
|
||||
},
|
||||
);
|
||||
|
||||
const capabilitiesRequire = async (capability: string) => {
|
||||
try {
|
||||
await assertCapability(pluginId, capability, declared);
|
||||
} catch (error) {
|
||||
await writeAudit(
|
||||
manifest,
|
||||
{ action: "capability_require", details: () => capability },
|
||||
{
|
||||
success: false,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
await writeAudit(
|
||||
manifest,
|
||||
{ action: "capability_require", details: () => capability },
|
||||
{ success: true },
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
manifest,
|
||||
@@ -626,6 +647,11 @@ export function createPluginContext(
|
||||
require: (permission) => createRbacMiddleware(manifest, permission),
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
has: (capability) => hasCapability(pluginId, capability, declared),
|
||||
require: capabilitiesRequire,
|
||||
},
|
||||
|
||||
disposables: {
|
||||
add: (dispose) => {
|
||||
handle.bag.add(dispose, "plugin resource");
|
||||
|
||||
@@ -309,12 +309,12 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
|
||||
// plugins/ssh-terminal, plugins/docker, plugins/host-metrics,
|
||||
// plugins/file-manager and plugins/tmux-monitor.
|
||||
// Every other plugin (AI, Proxmox, Remote Desktop, Fleets, Automations,
|
||||
// Network Topology, Workspaces, Web Endpoint, Tunnels) is absent for the
|
||||
// same reason: each one serves its routes under /plugin-api/<id>/ through
|
||||
// ctx.http on activate, so disabling it answers 503 instead of leaving a
|
||||
// dead import here. Automations' scheduler and tunnel autostart also
|
||||
// start from their own activate() rather than here.
|
||||
await import("./hosts/serial.js");
|
||||
// Network Topology, Workspaces, Web Endpoint, Tunnels, Serial) is absent
|
||||
// for the same reason: each one serves its routes under /plugin-api/<id>/
|
||||
// (or a WS route under /plugin-ws/<id>/) through ctx on activate, so
|
||||
// disabling it answers 503 instead of leaving a dead import here.
|
||||
// Automations' scheduler and tunnel autostart also start from their own
|
||||
// activate() rather than here.
|
||||
await import("./services/dashboard.js");
|
||||
await import("./services/homepage.js");
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* ctx.capabilities: a generic check for privileged code no ctx method wraps,
|
||||
* used first by the serial plugin to gate opening a physical device. Every
|
||||
* `require` call is audited, allowed or refused, the same as a guarded ctx
|
||||
* method.
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const grants = new Map<string, string[]>();
|
||||
const auditEntries: Array<Record<string, unknown>> = [];
|
||||
|
||||
vi.mock("../../database/repositories/factory.js", () => ({
|
||||
createCurrentPluginPermissionGrantRepository: () => ({
|
||||
listByPlugin: async (pluginId: string) =>
|
||||
(grants.get(pluginId) ?? []).map((capability) => ({
|
||||
pluginId,
|
||||
capability,
|
||||
})),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../../utils/audit-logger.js", () => ({
|
||||
logAudit: async (entry: Record<string, unknown>) => {
|
||||
auditEntries.push(entry);
|
||||
},
|
||||
}));
|
||||
|
||||
import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js";
|
||||
import { invalidatePluginPermissionCache } from "../../plugins/permissions.js";
|
||||
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||
|
||||
function manifestFor(pluginId: string, capabilities: string[]): PluginManifest {
|
||||
return {
|
||||
id: pluginId,
|
||||
name: pluginId,
|
||||
version: "1.0.0",
|
||||
description: "",
|
||||
author: { name: "test" },
|
||||
license: "MIT",
|
||||
category: "Productivity",
|
||||
engine: { termix: ">=2.9.0", api: "1" },
|
||||
capabilities,
|
||||
} as PluginManifest;
|
||||
}
|
||||
|
||||
function contextFor(pluginId: string, capabilities: string[]) {
|
||||
const manifest = manifestFor(pluginId, capabilities);
|
||||
const handle = createPluginHandle(pluginId, { activate: () => {} });
|
||||
return { ctx: createPluginContext(manifest, handle), handle };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
grants.clear();
|
||||
auditEntries.length = 0;
|
||||
invalidatePluginPermissionCache();
|
||||
});
|
||||
|
||||
describe("ctx.capabilities", () => {
|
||||
it("has() is false when the manifest never declared the capability", async () => {
|
||||
grants.set("demo", ["device:serial"]);
|
||||
const { ctx } = contextFor("demo", []);
|
||||
|
||||
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("has() is false when declared but not granted", async () => {
|
||||
grants.set("demo", []);
|
||||
const { ctx } = contextFor("demo", ["device:serial"]);
|
||||
|
||||
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("has() is true when declared and granted", async () => {
|
||||
grants.set("demo", ["device:serial"]);
|
||||
const { ctx } = contextFor("demo", ["device:serial"]);
|
||||
|
||||
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("require() throws and audits a refusal without the capability", async () => {
|
||||
grants.set("demo", []);
|
||||
const { ctx } = contextFor("demo", []);
|
||||
|
||||
await expect(ctx.capabilities.require("device:serial")).rejects.toThrow(
|
||||
/device:serial/,
|
||||
);
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_capability_require",
|
||||
success: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("require() resolves and audits success when granted", async () => {
|
||||
grants.set("demo", ["device:serial"]);
|
||||
const { ctx } = contextFor("demo", ["device:serial"]);
|
||||
|
||||
await expect(
|
||||
ctx.capabilities.require("device:serial"),
|
||||
).resolves.toBeUndefined();
|
||||
expect(auditEntries.at(-1)).toMatchObject({
|
||||
action: "plugin_capability_require",
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,10 +30,6 @@ const swaggerOptions: SwaggerJSDocOptions = {
|
||||
url: "http://localhost:30006",
|
||||
description: "Dashboard server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30011",
|
||||
description: "Serial connection server",
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
|
||||
+1
-12
@@ -292,14 +292,6 @@ export type KnownTabType =
|
||||
*/
|
||||
export type TabType = KnownTabType | (string & {});
|
||||
|
||||
export type SerialConfig = {
|
||||
path: string;
|
||||
baudRate: number;
|
||||
dataBits: 5 | 6 | 7 | 8;
|
||||
stopBits: 1 | 2;
|
||||
parity: "none" | "even" | "odd";
|
||||
};
|
||||
|
||||
export type Tab = {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
@@ -319,7 +311,6 @@ export type Tab = {
|
||||
data?: Record<string, unknown>;
|
||||
/** Which collab room a collab tab is showing. */
|
||||
collabRoomId?: string;
|
||||
serialConfig?: SerialConfig;
|
||||
/** Present only on a split-screen container tab. Pane ids reference live child tabs. */
|
||||
splitConfig?: SplitTabConfig;
|
||||
/** Hides this session from the top-level tab bar while it belongs to a split tab. */
|
||||
@@ -423,7 +414,7 @@ export type WorkspaceTabSnapshot = {
|
||||
/** Stable key within the saved tab list, not the live Tab.id (which is regenerated on every open). */
|
||||
slotId: string;
|
||||
type: TabType;
|
||||
/** Set for host-bound tab types, resolved by Host.syncId on apply. Never set for "serial". */
|
||||
/** Set for host-bound tab types, resolved by Host.syncId on apply. */
|
||||
hostSyncId?: string | null;
|
||||
/** Denormalized snapshot for display and graceful-skip messaging if the host is later deleted. */
|
||||
hostNameSnapshot?: string | null;
|
||||
@@ -435,8 +426,6 @@ export type WorkspaceTabSnapshot = {
|
||||
fleetId?: number;
|
||||
/** The tab's plugin payload. */
|
||||
data?: Record<string, unknown>;
|
||||
/** Only present when type === "serial". Fully self-contained, no host resolution needed. */
|
||||
serialConfig?: SerialConfig;
|
||||
};
|
||||
|
||||
/** One dock's arrangement. `view` is a RailView, or null when the dock is closed. */
|
||||
|
||||
@@ -66,9 +66,6 @@ const QuickConnectPanel = lazy(() =>
|
||||
default: m.QuickConnectPanel,
|
||||
})),
|
||||
);
|
||||
const SerialPanel = lazy(() =>
|
||||
import("@/sidebar/SerialPanel").then((m) => ({ default: m.SerialPanel })),
|
||||
);
|
||||
const SplitScreenPanel = lazy(() =>
|
||||
import("@/sidebar/SplitScreenPanel").then((m) => ({
|
||||
default: m.SplitScreenPanel,
|
||||
@@ -146,7 +143,6 @@ import type {
|
||||
HostFolder,
|
||||
ThemeId,
|
||||
FontSizeId,
|
||||
SerialConfig,
|
||||
WorkspacePayload,
|
||||
} from "@/types/ui-types";
|
||||
import { applyAccentColor, applyFontSize, PANE_COUNTS } from "@/lib/theme";
|
||||
@@ -1263,12 +1259,6 @@ export function AppShell({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.kind === "serial" && snapshot.serialConfig) {
|
||||
const newTabId = openSerialTab(snapshot.serialConfig);
|
||||
slotIdToNewTabId.set(snapshot.slotId, newTabId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.kind === "singleton") {
|
||||
openSingletonTab(
|
||||
snapshot.type,
|
||||
@@ -1562,7 +1552,6 @@ export function AppShell({
|
||||
savedLabel?: string;
|
||||
initialFilePath?: string;
|
||||
initialPath?: string;
|
||||
serialConfig?: SerialConfig;
|
||||
joinSharedSessionId?: string | null;
|
||||
joinShareId?: string | null;
|
||||
collabRoomId?: string;
|
||||
@@ -1600,7 +1589,6 @@ export function AppShell({
|
||||
const savedLabel = restore?.savedLabel;
|
||||
const initialFilePath = restore?.initialFilePath;
|
||||
const initialPath = restore?.initialPath;
|
||||
const serialConfig = restore?.serialConfig;
|
||||
const joinSharedSessionId = restore?.joinSharedSessionId ?? null;
|
||||
const joinShareId = restore?.joinShareId ?? null;
|
||||
// A saved label that doesn't match the bare host name or the auto-numbered pattern is a custom label
|
||||
@@ -1628,7 +1616,6 @@ export function AppShell({
|
||||
joinShareId,
|
||||
initialFilePath,
|
||||
initialPath,
|
||||
serialConfig,
|
||||
collabRoomId: restore?.collabRoomId,
|
||||
},
|
||||
];
|
||||
@@ -1667,7 +1654,6 @@ export function AppShell({
|
||||
joinShareId,
|
||||
initialFilePath,
|
||||
initialPath,
|
||||
serialConfig,
|
||||
collabRoomId: restore?.collabRoomId,
|
||||
data: options?.data,
|
||||
},
|
||||
@@ -1717,53 +1703,6 @@ export function AppShell({
|
||||
[loadHosts, t],
|
||||
);
|
||||
|
||||
function openSerialTab(config: SerialConfig): string {
|
||||
const pseudoHost: Host = {
|
||||
id: `serial-${Date.now()}`,
|
||||
name: config.path
|
||||
? `${config.path} (${config.baudRate})`
|
||||
: `Serial (${config.baudRate})`,
|
||||
username: "",
|
||||
ip: "",
|
||||
port: 0,
|
||||
folder: "",
|
||||
online: false,
|
||||
cpu: null,
|
||||
ram: null,
|
||||
lastAccess: new Date().toISOString(),
|
||||
authType: "none",
|
||||
enableTerminal: false,
|
||||
enableCommandHistory: false,
|
||||
enableTunnel: false,
|
||||
enableFileManager: false,
|
||||
enableDocker: false,
|
||||
enableProxmox: false,
|
||||
enableProxmoxStats: false,
|
||||
enableTmuxMonitor: false,
|
||||
enableTerminalToolbar: false,
|
||||
enableAiAssistant: false,
|
||||
enableSsh: false,
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
sshPort: 22,
|
||||
rdpPort: 3389,
|
||||
vncPort: 5900,
|
||||
telnetPort: 23,
|
||||
serverTunnels: [],
|
||||
quickActions: [],
|
||||
};
|
||||
const instanceId =
|
||||
typeof crypto.randomUUID === "function"
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
||||
return openTab(pseudoHost, "serial", {
|
||||
instanceId,
|
||||
restoredSessionId: null,
|
||||
serialConfig: config,
|
||||
});
|
||||
}
|
||||
|
||||
/** A tab type that opens a fresh tab every time (a local shell). */
|
||||
function openMultiInstanceTab(type: TabType): string {
|
||||
const instanceId =
|
||||
@@ -2500,15 +2439,6 @@ export function AppShell({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "serial" && (
|
||||
<SerialPanel
|
||||
onConnect={(config) => {
|
||||
openSerialTab(config);
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{railView === "quick-connect" && (
|
||||
<QuickConnectPanel
|
||||
onConnect={(host, type) => {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface SerialHandle {
|
||||
connect: () => void;
|
||||
disconnect: () => void;
|
||||
isConnected: () => boolean;
|
||||
sendInput: (data: string) => void;
|
||||
}
|
||||
@@ -11,13 +11,15 @@ interface OriginResolvableHost {
|
||||
const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
|
||||
|
||||
/**
|
||||
* Resolves which backend a given host's interactive connection (SSH,
|
||||
* Docker console, Serial, RDP/VNC/Telnet) should dial: the desktop app's
|
||||
* embedded local backend, or a connected remote sync server.
|
||||
* Resolves which backend a given host's interactive connection (SSH, Docker
|
||||
* console, RDP/VNC/Telnet) should dial: the desktop app's embedded local
|
||||
* backend, or a connected remote sync server. A plugin whose connection is
|
||||
* always local regardless of this setting (serial: the hardware is
|
||||
* physically attached to this desktop machine) skips this helper entirely
|
||||
* and calls app.wsUrl() without an origin option, which defaults to "local".
|
||||
*
|
||||
* Serial always resolves to "local" -- the hardware is physically attached
|
||||
* to this desktop machine. Everything else follows the host's own override
|
||||
* if set, falling back to the desktop-wide default.
|
||||
* Everything else follows the host's own override if set, falling back to
|
||||
* the desktop-wide default.
|
||||
*
|
||||
* RDP/VNC/Telnet are the exception to that fallback: left on Default they
|
||||
* resolve to "remote" rather than following the desktop-wide setting. They
|
||||
@@ -30,9 +32,6 @@ const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
|
||||
export async function resolveConnectionOrigin(
|
||||
host: OriginResolvableHost,
|
||||
): Promise<ConnectionOrigin> {
|
||||
if (host.connectionType === "serial") {
|
||||
return "local";
|
||||
}
|
||||
if (!isElectron()) {
|
||||
return "local";
|
||||
}
|
||||
|
||||
+4
-25
@@ -709,7 +709,6 @@
|
||||
"hostManager": "Host Manager",
|
||||
"credentials": "Credentials",
|
||||
"connections": "Connections",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Keep Menu Open",
|
||||
"collapseSideMenu": "Collapse Menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3092,30 +3091,6 @@
|
||||
"openProxmoxStats": "Open Proxmox Stats",
|
||||
"openTmuxMonitor": "Open Tmux Monitor"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baud Rate",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parity",
|
||||
"parityNone": "None",
|
||||
"parityEven": "Even",
|
||||
"parityOdd": "Odd",
|
||||
"connect": "Connect to Serial",
|
||||
"disconnect": "Disconnect",
|
||||
"refreshPorts": "Refresh ports",
|
||||
"connected": "Connected to {{path}} at {{baud}} baud",
|
||||
"disconnected": "Serial port disconnected",
|
||||
"connectionError": "Failed to open serial port",
|
||||
"wsError": "WebSocket error",
|
||||
"errorNoServerUrl": "No server URL configured",
|
||||
"notSupportedTitle": "Serial Not Supported",
|
||||
"notSupported": "Serial connections require a browser with Web Serial API support (Chrome, Edge, or Firefox 151+), or the Termix desktop app.",
|
||||
"hideHint": "You can hide the Serial tab under User Profile > Appearance > Sidebar > Navigation.",
|
||||
"browserPickerHint": "Click Connect and your browser will open a port picker to choose the device."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Add Channel",
|
||||
"editChannel": "Edit Channel",
|
||||
@@ -3256,6 +3231,10 @@
|
||||
"title": "Manage certificates",
|
||||
"consequence": "It can request and replace the server certificate."
|
||||
},
|
||||
"device:serial": {
|
||||
"title": "Access serial and USB devices",
|
||||
"consequence": "It can open a physical serial port on the machine running Termix."
|
||||
},
|
||||
"hosts:write": {
|
||||
"title": "Create and change hosts",
|
||||
"consequence": "It can add hosts and edit the ones you already have."
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gasheerbestuurder",
|
||||
"credentials": "Geloofsbriewe",
|
||||
"connections": "Verbindings",
|
||||
"serial": "Reeks",
|
||||
"keepSideMenuOpen": "Hou Kieslys Oop",
|
||||
"collapseSideMenu": "Vou Kieslys in",
|
||||
"roleAdministrator": "Administrateur",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Maak Proxmox-statistieke oop",
|
||||
"openTmuxMonitor": "Maak Tmux-monitor oop"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Reeks",
|
||||
"portLabel": "Hawe",
|
||||
"portPlaceholder": "/dev/ttyUSB0 of COM3",
|
||||
"baudRateLabel": "Baudtempo",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteit",
|
||||
"parityNone": "Geen",
|
||||
"parityEven": "Selfs",
|
||||
"parityOdd": "Vreemd",
|
||||
"connect": "Koppel aan serieel",
|
||||
"disconnect": "Ontkoppel",
|
||||
"refreshPorts": "Verfris poorte",
|
||||
"connected": "Verbind aan {{path}} teen {{baud}} baud",
|
||||
"disconnected": "Seriepoort ontkoppel",
|
||||
"connectionError": "Kon nie die seriepoort oopmaak nie",
|
||||
"wsError": "WebSocket-fout",
|
||||
"errorNoServerUrl": "Geen bediener-URL gekonfigureer nie",
|
||||
"notSupportedTitle": "Serienommer word nie ondersteun nie",
|
||||
"notSupported": "Seriële verbindings vereis 'n blaaier met Web Serial API-ondersteuning (Chrome, Edge of Firefox 151+), of die Termix-lessenaarprogram.",
|
||||
"hideHint": "Jy kan die Seriële oortjie versteek onder Gebruikersprofiel > Voorkoms > Sybalk > Navigasie.",
|
||||
"browserPickerHint": "Klik op Verbind en jou blaaier sal 'n poortkieser oopmaak om die toestel te kies."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Voeg kanaal by",
|
||||
"editChannel": "Wysig kanaal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "مدير المضيفين",
|
||||
"credentials": "بيانات الاعتماد",
|
||||
"connections": "الاتصالات",
|
||||
"serial": "تسلسلي",
|
||||
"keepSideMenuOpen": "أبقِ قائمة الطعام مفتوحة",
|
||||
"collapseSideMenu": "قائمة قابلة للطي",
|
||||
"roleAdministrator": "مسؤول",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "افتح إحصائيات Proxmox",
|
||||
"openTmuxMonitor": "افتح شاشة Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "منفذ تسلسلي",
|
||||
"portLabel": "المنفذ",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "معدل الباود",
|
||||
"dataBitsLabel": "البيانات",
|
||||
"stopBitsLabel": "التوقف",
|
||||
"parityLabel": "التكافؤ",
|
||||
"parityNone": "بلا",
|
||||
"parityEven": "زوجي",
|
||||
"parityOdd": "فردي",
|
||||
"connect": "اتصال",
|
||||
"disconnect": "قطع الاتصال",
|
||||
"refreshPorts": "تحديث المنافذ",
|
||||
"connected": "متصل بـ {{path}} بسرعة {{baud}} باود",
|
||||
"disconnected": "تم قطع الاتصال بالمنفذ التسلسلي",
|
||||
"connectionError": "فشل في فتح المنفذ التسلسلي",
|
||||
"wsError": "خطأ في WebSocket",
|
||||
"errorNoServerUrl": "لم يتم تكوين عنوان URL للخادم",
|
||||
"notSupportedTitle": "المنفذ التسلسلي غير مدعوم",
|
||||
"notSupported": "تتطلب الاتصالات التسلسلية متصفحًا يدعم Web Serial API (Chrome، أو Edge، أو Firefox 151+)، أو تطبيق Termix لسطح المكتب.",
|
||||
"hideHint": "يمكنك إخفاء تبويب المنفذ التسلسلي من الملف الشخصي > المظهر > الشريط الجانبي > التنقل.",
|
||||
"browserPickerHint": "انقر على Connect وسيفتح متصفحك نافذة اختيار المنفذ لتحديد الجهاز."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "إضافة قناة",
|
||||
"editChannel": "تعديل القناة",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Мениджър на домакини",
|
||||
"credentials": "Пълномощия",
|
||||
"connections": "Връзки",
|
||||
"serial": "Сериен",
|
||||
"keepSideMenuOpen": "Запази менюто отворено",
|
||||
"collapseSideMenu": "Свиване на менюто",
|
||||
"roleAdministrator": "Администратор",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Отворете статистиката на Proxmox",
|
||||
"openTmuxMonitor": "Отворете Tmux монитора"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Сериен",
|
||||
"portLabel": "Порт",
|
||||
"portPlaceholder": "/dev/ttyUSB0 или COM3",
|
||||
"baudRateLabel": "Скорост на бодове",
|
||||
"dataBitsLabel": "Данни",
|
||||
"stopBitsLabel": "Спри",
|
||||
"parityLabel": "Паритет",
|
||||
"parityNone": "Няма",
|
||||
"parityEven": "Дори",
|
||||
"parityOdd": "Странно",
|
||||
"connect": "Свързване към сериен",
|
||||
"disconnect": "Изключване",
|
||||
"refreshPorts": "Обновяване на портове",
|
||||
"connected": "Свързан към {{path}} при {{baud}} бод",
|
||||
"disconnected": "Серийният порт е прекъснат",
|
||||
"connectionError": "Неуспешно отваряне на серийния порт",
|
||||
"wsError": "Грешка в WebSocket",
|
||||
"errorNoServerUrl": "Няма конфигуриран URL адрес на сървъра",
|
||||
"notSupportedTitle": "Серийният номер не се поддържа",
|
||||
"notSupported": "Серийните връзки изискват браузър с поддръжка на Web Serial API (Chrome, Edge или Firefox 151+) или настолното приложение Termix.",
|
||||
"hideHint": "Можете да скриете раздела „Сериен номер“ в Потребителски профил > Външен вид > Странична лента > Навигация.",
|
||||
"browserPickerHint": "Щракнете върху „Свързване“ и браузърът ви ще отвори инструмент за избор на порт, за да изберете устройството."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Добавяне на канал",
|
||||
"editChannel": "Редактиране на канал",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "হোস্ট ম্যানেজার",
|
||||
"credentials": "যোগ্যতা",
|
||||
"connections": "সংযোগ",
|
||||
"serial": "সিরিয়াল",
|
||||
"keepSideMenuOpen": "মেনু খোলা রাখুন",
|
||||
"collapseSideMenu": "মেনু সংকুচিত করুন",
|
||||
"roleAdministrator": "প্রশাসক",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "ওপেন প্রক্সমক্স স্ট্যাটস",
|
||||
"openTmuxMonitor": "ওপেন Tmux মনিটর"
|
||||
},
|
||||
"serial": {
|
||||
"title": "সিরিয়াল",
|
||||
"portLabel": "বন্দর",
|
||||
"portPlaceholder": "/dev/ttyUSB0 অথবা COM3",
|
||||
"baudRateLabel": "বড রেট",
|
||||
"dataBitsLabel": "ডেটা",
|
||||
"stopBitsLabel": "থামুন",
|
||||
"parityLabel": "সমতা",
|
||||
"parityNone": "কোনোটিই না",
|
||||
"parityEven": "এমনকি",
|
||||
"parityOdd": "অদ্ভুত",
|
||||
"connect": "সিরিয়ালের সাথে সংযোগ করুন",
|
||||
"disconnect": "সংযোগ বিচ্ছিন্ন করুন",
|
||||
"refreshPorts": "পোর্টগুলি রিফ্রেশ করুন",
|
||||
"connected": "{{path}} এর সাথে {{baud}} বাউডে সংযুক্ত।",
|
||||
"disconnected": "সিরিয়াল পোর্ট সংযোগ বিচ্ছিন্ন",
|
||||
"connectionError": "সিরিয়াল পোর্ট খুলতে ব্যর্থ হয়েছে",
|
||||
"wsError": "ওয়েব সকেট ত্রুটি",
|
||||
"errorNoServerUrl": "কোন সার্ভার URL কনফিগার করা নেই",
|
||||
"notSupportedTitle": "সিরিয়াল সমর্থিত নয়",
|
||||
"notSupported": "সিরিয়াল সংযোগের জন্য ওয়েব সিরিয়াল এপিআই সমর্থনসহ একটি ব্রাউজার (ক্রোম, এজ, বা ফায়ারফক্স ১৫১+), অথবা টারমিক্স ডেস্কটপ অ্যাপ প্রয়োজন।",
|
||||
"hideHint": "আপনি ইউজার প্রোফাইল > অ্যাপিয়ারেন্স > সাইডবার > নেভিগেশন-এর অধীনে সিরিয়াল ট্যাবটি লুকাতে পারেন।",
|
||||
"browserPickerHint": "কানেক্ট-এ ক্লিক করলে আপনার ব্রাউজারে ডিভাইসটি বেছে নেওয়ার জন্য একটি পোর্ট পিকার খুলবে।"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "চ্যানেল যোগ করুন",
|
||||
"editChannel": "চ্যানেল সম্পাদনা করুন",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestor d'amfitrions",
|
||||
"credentials": "Credencials",
|
||||
"connections": "Connexions",
|
||||
"serial": "Sèrie",
|
||||
"keepSideMenuOpen": "Mantenir el menú obert",
|
||||
"collapseSideMenu": "Menú replegable",
|
||||
"roleAdministrator": "Administrador/a",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Obre les estadístiques de Proxmox",
|
||||
"openTmuxMonitor": "Obre el monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sèrie",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 o COM3",
|
||||
"baudRateLabel": "velocitat de bauds",
|
||||
"dataBitsLabel": "Dades",
|
||||
"stopBitsLabel": "Atura",
|
||||
"parityLabel": "Paritat",
|
||||
"parityNone": "Cap",
|
||||
"parityEven": "Fins i tot",
|
||||
"parityOdd": "Estrany",
|
||||
"connect": "Connecta't al serial",
|
||||
"disconnect": "Desconnecta",
|
||||
"refreshPorts": "Actualitza els ports",
|
||||
"connected": "Connectat a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Port sèrie desconnectat",
|
||||
"connectionError": "No s'ha pogut obrir el port sèrie",
|
||||
"wsError": "Error de WebSocket",
|
||||
"errorNoServerUrl": "No s'ha configurat cap URL del servidor",
|
||||
"notSupportedTitle": "Número de sèrie no compatible",
|
||||
"notSupported": "Les connexions en sèrie requereixen un navegador compatible amb l'API Web Serial (Chrome, Edge o Firefox 151+) o l'aplicació d'escriptori Termix.",
|
||||
"hideHint": "Podeu amagar la pestanya Sèrie a Perfil d'usuari > Aparença > Barra lateral > Navegació.",
|
||||
"browserPickerHint": "Feu clic a Connecta i el navegador obrirà un selector de ports per triar el dispositiu."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Afegeix un canal",
|
||||
"editChannel": "Edita el canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Hostitelský manažer",
|
||||
"credentials": "Pověření",
|
||||
"connections": "Připojení",
|
||||
"serial": "Seriál",
|
||||
"keepSideMenuOpen": "Ponechat nabídku otevřenou",
|
||||
"collapseSideMenu": "Sbalit nabídku",
|
||||
"roleAdministrator": "Správce",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Otevřít statistiky Proxmoxu",
|
||||
"openTmuxMonitor": "Otevřít monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriál",
|
||||
"portLabel": "Přístav",
|
||||
"portPlaceholder": "/dev/ttyUSB0 nebo COM3",
|
||||
"baudRateLabel": "Přenosová rychlost",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Zastávka",
|
||||
"parityLabel": "Parita",
|
||||
"parityNone": "Žádný",
|
||||
"parityEven": "Dokonce",
|
||||
"parityOdd": "Zvláštní",
|
||||
"connect": "Připojení k sériovému",
|
||||
"disconnect": "Odpojit",
|
||||
"refreshPorts": "Obnovit porty",
|
||||
"connected": "Připojeno k {{path}} rychlostí {{baud}} baudu",
|
||||
"disconnected": "Sériový port odpojen",
|
||||
"connectionError": "Nepodařilo se otevřít sériový port",
|
||||
"wsError": "Chyba WebSocketu",
|
||||
"errorNoServerUrl": "Není nakonfigurována žádná adresa URL serveru",
|
||||
"notSupportedTitle": "Sériové číslo není podporováno",
|
||||
"notSupported": "Sériová připojení vyžadují prohlížeč s podporou webového sériového API (Chrome, Edge nebo Firefox 151+) nebo desktopovou aplikaci Termix.",
|
||||
"hideHint": "Kartu Sériové číslo můžete skrýt v části Profil uživatele > Vzhled > Postranní panel > Navigace.",
|
||||
"browserPickerHint": "Klikněte na tlačítko Připojit a prohlížeč otevře výběr portu pro výběr zařízení."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Přidat kanál",
|
||||
"editChannel": "Upravit kanál",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Værtsadministrator",
|
||||
"credentials": "Akkreditiver",
|
||||
"connections": "Forbindelser",
|
||||
"serial": "Seriel",
|
||||
"keepSideMenuOpen": "Hold menuen åben",
|
||||
"collapseSideMenu": "Skjul menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Åbn Proxmox-statistik",
|
||||
"openTmuxMonitor": "Åbn Tmux-skærmen"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriel",
|
||||
"portLabel": "Havn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighed",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Endog",
|
||||
"parityOdd": "Ulige",
|
||||
"connect": "Tilslut til seriel",
|
||||
"disconnect": "Afbryde",
|
||||
"refreshPorts": "Opdater porte",
|
||||
"connected": "Forbundet til {{path}} ved {{baud}} baud",
|
||||
"disconnected": "Seriel port afbrudt",
|
||||
"connectionError": "Kunne ikke åbne seriel port",
|
||||
"wsError": "WebSocket-fejl",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigureret",
|
||||
"notSupportedTitle": "Seriel understøttes ikke",
|
||||
"notSupported": "Serielle forbindelser kræver en browser med Web Serial API-understøttelse (Chrome, Edge eller Firefox 151+) eller Termix-desktopappen.",
|
||||
"hideHint": "Du kan skjule fanen Seriel under Brugerprofil > Udseende > Sidebjælke > Navigation.",
|
||||
"browserPickerHint": "Klik på Opret forbindelse, og din browser åbner en portvælger, hvor du kan vælge enheden."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Tilføj kanal",
|
||||
"editChannel": "Rediger kanal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Host-Manager",
|
||||
"credentials": "Zugangsdaten",
|
||||
"connections": "Verbindungen",
|
||||
"serial": "Seriell",
|
||||
"keepSideMenuOpen": "Menü geöffnet lassen",
|
||||
"collapseSideMenu": "Menü ausklappen",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox-Statistiken öffnen",
|
||||
"openTmuxMonitor": "Tmux-Monitor öffnen"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 oder COM3",
|
||||
"baudRateLabel": "Baudrate",
|
||||
"dataBitsLabel": "Daten",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parität",
|
||||
"parityNone": "Keine",
|
||||
"parityEven": "Gerade",
|
||||
"parityOdd": "Ungerade",
|
||||
"connect": "Mit Seriell verbinden",
|
||||
"disconnect": "Trennen",
|
||||
"refreshPorts": "Ports aktualisieren",
|
||||
"connected": "Verbunden mit {{path}} bei {{baud}} Baud",
|
||||
"disconnected": "Serielle Schnittstelle getrennt",
|
||||
"connectionError": "Fehler beim Öffnen der seriellen Schnittstelle",
|
||||
"wsError": "WebSocket-Fehler",
|
||||
"errorNoServerUrl": "Keine Server-URL konfiguriert",
|
||||
"notSupportedTitle": "Seriell nicht unterstützt",
|
||||
"notSupported": "Serielle Verbindungen erfordern einen Browser mit Web Serial API-Unterstützung (Chrome, Edge oder Firefox 151+) oder die Termix-Desktop-App.",
|
||||
"hideHint": "Sie können den Seriell-Tab unter Benutzerprofil > Erscheinungsbild > Seitenleiste > Navigation ausblenden.",
|
||||
"browserPickerHint": "Klicken Sie auf Verbinden, und Ihr Browser öffnet eine Port-Auswahl, um das Gerät auszuwählen."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Kanal hinzufügen",
|
||||
"editChannel": "Kanal bearbeiten",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Διευθυντής Υποδοχής",
|
||||
"credentials": "Διαπιστευτήρια",
|
||||
"connections": "Συνδέσεις",
|
||||
"serial": "Σειράς",
|
||||
"keepSideMenuOpen": "Διατήρηση μενού ανοιχτού",
|
||||
"collapseSideMenu": "Σύμπτυξη μενού",
|
||||
"roleAdministrator": "Διαχειριστής",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Άνοιγμα στατιστικών Proxmox",
|
||||
"openTmuxMonitor": "Άνοιγμα παρακολούθησης Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Σειράς",
|
||||
"portLabel": "Λιμάνι",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ή COM3",
|
||||
"baudRateLabel": "Ρυθμός Baud",
|
||||
"dataBitsLabel": "Δεδομένα",
|
||||
"stopBitsLabel": "Στάση",
|
||||
"parityLabel": "Ισοτιμία",
|
||||
"parityNone": "Κανένας",
|
||||
"parityEven": "Ακόμη και",
|
||||
"parityOdd": "Περιττός",
|
||||
"connect": "Σύνδεση σε σειριακή σύνδεση",
|
||||
"disconnect": "Αποσυνδέω",
|
||||
"refreshPorts": "Ανανέωση θυρών",
|
||||
"connected": "Συνδεδεμένο με {{path}} σε {{baud}} baud",
|
||||
"disconnected": "Η σειριακή θύρα αποσυνδέθηκε",
|
||||
"connectionError": "Αποτυχία ανοίγματος σειριακής θύρας",
|
||||
"wsError": "Σφάλμα WebSocket",
|
||||
"errorNoServerUrl": "Δεν έχει ρυθμιστεί διεύθυνση URL διακομιστή",
|
||||
"notSupportedTitle": "Σειριακή Δεν Υποστηρίζεται",
|
||||
"notSupported": "Οι σειριακές συνδέσεις απαιτούν ένα πρόγραμμα περιήγησης με υποστήριξη Web Serial API (Chrome, Edge ή Firefox 151+) ή την εφαρμογή Termix για υπολογιστές.",
|
||||
"hideHint": "Μπορείτε να αποκρύψετε την καρτέλα Σειριακός αριθμός στην περιοχή Προφίλ χρήστη > Εμφάνιση > Πλαϊνή μπάρα > Πλοήγηση.",
|
||||
"browserPickerHint": "Κάντε κλικ στην επιλογή Σύνδεση και το πρόγραμμα περιήγησής σας θα ανοίξει ένα πρόγραμμα επιλογής θυρών για να επιλέξετε τη συσκευή."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Προσθήκη καναλιού",
|
||||
"editChannel": "Επεξεργασία καναλιού",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestor de hosts",
|
||||
"credentials": "Credenciales",
|
||||
"connections": "Conexiones",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Mantener el menú abierto",
|
||||
"collapseSideMenu": "Contraer menú",
|
||||
"roleAdministrator": "Administrador",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Abrir estadísticas de Proxmox",
|
||||
"openTmuxMonitor": "Abrir el monitor de Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Puerto",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baudios",
|
||||
"dataBitsLabel": "Datos",
|
||||
"stopBitsLabel": "Parada",
|
||||
"parityLabel": "Paridad",
|
||||
"parityNone": "Ninguna",
|
||||
"parityEven": "Par",
|
||||
"parityOdd": "Impar",
|
||||
"connect": "Conectar a Serial",
|
||||
"disconnect": "Desconectar",
|
||||
"refreshPorts": "Actualizar puertos",
|
||||
"connected": "Conectado a {{path}} a {{baud}} baudios",
|
||||
"disconnected": "Puerto serie desconectado",
|
||||
"connectionError": "Error al abrir el puerto serie",
|
||||
"wsError": "Error WebSocket",
|
||||
"errorNoServerUrl": "No se ha configurado URL de servidor",
|
||||
"notSupportedTitle": "Serial no compatible",
|
||||
"notSupported": "Las conexiones serie requieren un navegador compatible con Web Serial API (Chrome, Edge o Firefox 151+), o la aplicación de escritorio Termix.",
|
||||
"hideHint": "Puedes ocultar la pestaña Serial en Perfil de usuario > Apariencia > Barra lateral > Navegación.",
|
||||
"browserPickerHint": "Haz clic en Conectar y el navegador abrirá un selector de puertos para elegir el dispositivo."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Añadir canal",
|
||||
"editChannel": "Editar canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Isäntäpäällikkö",
|
||||
"credentials": "Valtakirjat",
|
||||
"connections": "Yhteydet",
|
||||
"serial": "Sarjanumero",
|
||||
"keepSideMenuOpen": "Pidä valikko auki",
|
||||
"collapseSideMenu": "Pienennä valikko",
|
||||
"roleAdministrator": "Ylläpitäjä",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Avaa Proxmox-tilastot",
|
||||
"openTmuxMonitor": "Avaa Tmux-näyttö"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sarjanumero",
|
||||
"portLabel": "Portti",
|
||||
"portPlaceholder": "/dev/ttyUSB0 tai COM3",
|
||||
"baudRateLabel": "Baud-nopeus",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteetti",
|
||||
"parityNone": "Ei mitään",
|
||||
"parityEven": "Jopa",
|
||||
"parityOdd": "Pariton",
|
||||
"connect": "Yhdistä sarjaporttiin",
|
||||
"disconnect": "Katkaise yhteys",
|
||||
"refreshPorts": "Päivitä portit",
|
||||
"connected": "Yhdistetty {{path}} -laitteeseen {{baud}} baudilla",
|
||||
"disconnected": "Sarjaportti irrotettu",
|
||||
"connectionError": "Sarjaportin avaaminen epäonnistui",
|
||||
"wsError": "WebSocket-virhe",
|
||||
"errorNoServerUrl": "Palvelimen URL-osoitetta ei ole määritetty",
|
||||
"notSupportedTitle": "Sarjanumeroa ei tueta",
|
||||
"notSupported": "Sarjaliikenne vaatii selaimen, jossa on Web Serial API -tuki (Chrome, Edge tai Firefox 151+), tai Termix-työpöytäsovelluksen.",
|
||||
"hideHint": "Voit piilottaa Sarja-välilehden kohdassa Käyttäjäprofiili > Ulkoasu > Sivupalkki > Navigointi.",
|
||||
"browserPickerHint": "Napsauta Yhdistä, niin selaimesi avaa porttivalitsimen laitteen valitsemiseksi."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Lisää kanava",
|
||||
"editChannel": "Muokkaa kanavaa",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestionnaire d'hôtes",
|
||||
"credentials": "Identifiants",
|
||||
"connections": "Connexions",
|
||||
"serial": "Série",
|
||||
"keepSideMenuOpen": "Garder le menu ouvert",
|
||||
"collapseSideMenu": "Réduire le menu",
|
||||
"roleAdministrator": "Administrateur",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Ouvrir les statistiques de Proxmox",
|
||||
"openTmuxMonitor": "Moniteur Open Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Série",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Débit en bauds",
|
||||
"dataBitsLabel": "Données",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parité",
|
||||
"parityNone": "Aucune",
|
||||
"parityEven": "Paire",
|
||||
"parityOdd": "Impaire",
|
||||
"connect": "Connecter",
|
||||
"disconnect": "Déconnecter",
|
||||
"refreshPorts": "Actualiser les ports",
|
||||
"connected": "Connecté à {{path}} à {{baud}} bauds",
|
||||
"disconnected": "Port série déconnecté",
|
||||
"connectionError": "Échec de l'ouverture du port série",
|
||||
"wsError": "Erreur WebSocket",
|
||||
"errorNoServerUrl": "Aucune URL de serveur configurée",
|
||||
"notSupportedTitle": "Port série non pris en charge",
|
||||
"notSupported": "Les connexions série nécessitent un navigateur prenant en charge l'API Web Serial (Chrome, Edge, ou Firefox 151+), ou l'application de bureau Termix.",
|
||||
"hideHint": "Vous pouvez masquer l'onglet Série sous Profil utilisateur > Apparence > Barre latérale > Navigation.",
|
||||
"browserPickerHint": "Cliquez sur Connexion et votre navigateur ouvrira un sélecteur de port pour choisir le périphérique."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Ajouter un canal",
|
||||
"editChannel": "Modifier le canal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "מנהל מארח",
|
||||
"credentials": "אישורים",
|
||||
"connections": "חיבורים",
|
||||
"serial": "סִדרָתִי",
|
||||
"keepSideMenuOpen": "שמור את התפריט פתוח",
|
||||
"collapseSideMenu": "כווץ תפריט",
|
||||
"roleAdministrator": "מְנַהֵל",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "פתח את סטטיסטיקות Proxmox",
|
||||
"openTmuxMonitor": "פתח את צג Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "סִדרָתִי",
|
||||
"portLabel": "נָמָל",
|
||||
"portPlaceholder": "/dev/ttyUSB0 או COM3",
|
||||
"baudRateLabel": "קצב באוד",
|
||||
"dataBitsLabel": "נְתוּנִים",
|
||||
"stopBitsLabel": "לְהַפְסִיק",
|
||||
"parityLabel": "שִׁוּוּי",
|
||||
"parityNone": "אַף לֹא אֶחָד",
|
||||
"parityEven": "אֲפִילוּ",
|
||||
"parityOdd": "מְשׁוּנֶה",
|
||||
"connect": "חיבור סריאלי",
|
||||
"disconnect": "לְנַתֵק",
|
||||
"refreshPorts": "רענון פורטים",
|
||||
"connected": "מחובר ל- {{path}} ב- {{baud}} באוד",
|
||||
"disconnected": "יציאה טורית מנותקת",
|
||||
"connectionError": "נכשלה פתיחת הפורט הטורי",
|
||||
"wsError": "שגיאת WebSocket",
|
||||
"errorNoServerUrl": "לא הוגדרה כתובת URL של שרת",
|
||||
"notSupportedTitle": "סריאלי לא נתמך",
|
||||
"notSupported": "חיבורים טוריים דורשים דפדפן עם תמיכה ב-Web Serial API (Chrome, Edge או Firefox 151+), או את אפליקציית שולחן העבודה Termix.",
|
||||
"hideHint": "ניתן להסתיר את הכרטיסייה 'סדרתי' תחת פרופיל משתמש > מראה > סרגל צד > ניווט.",
|
||||
"browserPickerHint": "לחץ על התחבר והדפדפן שלך יפתח בורר פורטים לבחירת המכשיר."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "הוסף ערוץ",
|
||||
"editChannel": "עריכת ערוץ",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "होस्ट प्रबंधक",
|
||||
"credentials": "क्रेडेंशियल्स",
|
||||
"connections": "कनेक्शन्स",
|
||||
"serial": "सीरियल",
|
||||
"keepSideMenuOpen": "मेनू खुला रखें",
|
||||
"collapseSideMenu": "मेनू को छोटा करें",
|
||||
"roleAdministrator": "प्रशासक",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "प्रोक्समॉक्स के आँकड़े खोलें",
|
||||
"openTmuxMonitor": "ओपन टीएमएक्स मॉनिटर"
|
||||
},
|
||||
"serial": {
|
||||
"title": "सीरियल",
|
||||
"portLabel": "पोर्ट",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "बॉड दर",
|
||||
"dataBitsLabel": "डेटा",
|
||||
"stopBitsLabel": "स्टॉप",
|
||||
"parityLabel": "पैरिटी",
|
||||
"parityNone": "कोई नहीं",
|
||||
"parityEven": "सम",
|
||||
"parityOdd": "विषम",
|
||||
"connect": "सीरियल कनेक्ट करें",
|
||||
"disconnect": "डिस्कनेक्ट करें",
|
||||
"refreshPorts": "पोर्ट रीफ़्रेश करें",
|
||||
"connected": "{{path}} से {{baud}} बॉड पर कनेक्टेड",
|
||||
"disconnected": "सीरियल पोर्ट डिस्कनेक्ट हो गया",
|
||||
"connectionError": "सीरियल पोर्ट खोलने में विफल",
|
||||
"wsError": "WebSocket त्रुटि",
|
||||
"errorNoServerUrl": "कोई सर्वर URL कॉन्फ़िगर नहीं है",
|
||||
"notSupportedTitle": "सीरियल समर्थित नहीं",
|
||||
"notSupported": "सीरियल कनेक्शन के लिए Web Serial API समर्थन वाले ब्राउज़र (Chrome, Edge, या Firefox 151+) या Termix डेस्कटॉप ऐप की आवश्यकता है।",
|
||||
"hideHint": "आप सीरियल टैब को User Profile > Appearance > Sidebar > Navigation में छिपा सकते हैं।",
|
||||
"browserPickerHint": "Connect पर क्लिक करें और आपका ब्राउज़र डिवाइस चुनने के लिए एक पोर्ट पिकर खोलेगा।"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "चैनल जोड़ें",
|
||||
"editChannel": "चैनल संपादित करें",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Házigazda-kezelő",
|
||||
"credentials": "Hitelesítő adatok",
|
||||
"connections": "Kapcsolatok",
|
||||
"serial": "Sorozatszám",
|
||||
"keepSideMenuOpen": "Menü nyitva tartása",
|
||||
"collapseSideMenu": "Menü összecsukása",
|
||||
"roleAdministrator": "Adminisztrátor",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox statisztikák megnyitása",
|
||||
"openTmuxMonitor": "Tmux monitor megnyitása"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Sorozatszám",
|
||||
"portLabel": "Kikötő",
|
||||
"portPlaceholder": "/dev/ttyUSB0 vagy COM3",
|
||||
"baudRateLabel": "Baud sebesség",
|
||||
"dataBitsLabel": "Adat",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Paritás",
|
||||
"parityNone": "Egyik sem",
|
||||
"parityEven": "Még",
|
||||
"parityOdd": "Páratlan",
|
||||
"connect": "Csatlakozás soros porthoz",
|
||||
"disconnect": "Leválasztás",
|
||||
"refreshPorts": "Frissítse a portokat",
|
||||
"connected": "{{path}} -hoz csatlakoztatva {{baud}} baud sebességgel",
|
||||
"disconnected": "Soros port leválasztva",
|
||||
"connectionError": "Nem sikerült megnyitni a soros portot",
|
||||
"wsError": "WebSocket hiba",
|
||||
"errorNoServerUrl": "Nincs konfigurálva szerver URL",
|
||||
"notSupportedTitle": "Sorozatszám Nem támogatott",
|
||||
"notSupported": "A soros kapcsolatokhoz Web Serial API-t támogató böngésző (Chrome, Edge vagy Firefox 151+), vagy a Termix asztali alkalmazás szükséges.",
|
||||
"hideHint": "A Sorozat fület elrejtheted a Felhasználói profil > Megjelenés > Oldalsáv > Navigáció alatt.",
|
||||
"browserPickerHint": "Kattintson a Csatlakozás gombra, és a böngészője megnyit egy portválasztót az eszköz kiválasztásához."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Csatorna hozzáadása",
|
||||
"editChannel": "Csatorna szerkesztése",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Manajer Host",
|
||||
"credentials": "Kredensial",
|
||||
"connections": "Koneksi",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Biarkan Menu Tetap Terbuka",
|
||||
"collapseSideMenu": "Ciutkan Menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Buka Statistik Proxmox",
|
||||
"openTmuxMonitor": "Buka Monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Pelabuhan",
|
||||
"portPlaceholder": "/dev/ttyUSB0 atau COM3",
|
||||
"baudRateLabel": "Kecepatan Baud",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Berhenti",
|
||||
"parityLabel": "Keseimbangan",
|
||||
"parityNone": "Tidak ada",
|
||||
"parityEven": "Bahkan",
|
||||
"parityOdd": "Aneh",
|
||||
"connect": "Hubungkan ke Serial",
|
||||
"disconnect": "Memutuskan",
|
||||
"refreshPorts": "Penyegaran port",
|
||||
"connected": "Terhubung ke {{path}} pada kecepatan {{baud}} baud",
|
||||
"disconnected": "Port serial terputus",
|
||||
"connectionError": "Gagal membuka port serial",
|
||||
"wsError": "Kesalahan WebSocket",
|
||||
"errorNoServerUrl": "Tidak ada URL server yang dikonfigurasi.",
|
||||
"notSupportedTitle": "Nomor Seri Tidak Didukung",
|
||||
"notSupported": "Koneksi serial memerlukan browser dengan dukungan Web Serial API (Chrome, Edge, atau Firefox 151+), atau aplikasi desktop Termix.",
|
||||
"hideHint": "Anda dapat menyembunyikan tab Serial di bawah Profil Pengguna > Tampilan > Bilah Samping > Navigasi.",
|
||||
"browserPickerHint": "Klik Sambungkan dan browser Anda akan membuka pemilih port untuk memilih perangkat."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Tambahkan Saluran",
|
||||
"editChannel": "Edit Saluran",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gestione host",
|
||||
"credentials": "Credenziali",
|
||||
"connections": "Connessioni",
|
||||
"serial": "Seriale",
|
||||
"keepSideMenuOpen": "Mantieni il menu aperto",
|
||||
"collapseSideMenu": "Comprimi menu",
|
||||
"roleAdministrator": "Amministratore",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Statistiche di Proxmox aperte",
|
||||
"openTmuxMonitor": "Monitor Tmux aperto"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriale",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "Baud rate",
|
||||
"dataBitsLabel": "Dati",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parità",
|
||||
"parityNone": "Nessuna",
|
||||
"parityEven": "Pari",
|
||||
"parityOdd": "Dispari",
|
||||
"connect": "Connetti alla Seriale",
|
||||
"disconnect": "Disconnetti",
|
||||
"refreshPorts": "Aggiorna porte",
|
||||
"connected": "Connesso a {{path}} a {{baud}} baud",
|
||||
"disconnected": "Porta seriale disconnessa",
|
||||
"connectionError": "Impossibile aprire la porta seriale",
|
||||
"wsError": "Errore WebSocket",
|
||||
"errorNoServerUrl": "Nessun URL del server configurato",
|
||||
"notSupportedTitle": "Seriale non supportato",
|
||||
"notSupported": "Le connessioni seriali richiedono un browser con supporto Web Serial API (Chrome, Edge o Firefox 151+), oppure l'app desktop Termix.",
|
||||
"hideHint": "Puoi nascondere la scheda Seriale in Profilo Utente > Aspetto > Barra laterale > Navigazione.",
|
||||
"browserPickerHint": "Fai clic su Connetti e il browser aprirà un selettore di porte per scegliere il dispositivo."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Aggiungi canale",
|
||||
"editChannel": "Modifica canale",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "ホストマネージャー",
|
||||
"credentials": "認証情報",
|
||||
"connections": "接続",
|
||||
"serial": "シリアル",
|
||||
"keepSideMenuOpen": "メニューを開いたままにする",
|
||||
"collapseSideMenu": "メニューを折りたたむ",
|
||||
"roleAdministrator": "管理者",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox統計を開く",
|
||||
"openTmuxMonitor": "Tmux Monitorを開く"
|
||||
},
|
||||
"serial": {
|
||||
"title": "シリアル",
|
||||
"portLabel": "ポート",
|
||||
"portPlaceholder": "/dev/ttyUSB0 または COM3",
|
||||
"baudRateLabel": "ボーレート",
|
||||
"dataBitsLabel": "データ",
|
||||
"stopBitsLabel": "ストップ",
|
||||
"parityLabel": "パリティ",
|
||||
"parityNone": "なし",
|
||||
"parityEven": "偶数",
|
||||
"parityOdd": "奇数",
|
||||
"connect": "シリアルに接続",
|
||||
"disconnect": "切断",
|
||||
"refreshPorts": "ポートを更新",
|
||||
"connected": "{{path}} に {{baud}} ボーで接続しました",
|
||||
"disconnected": "シリアルポートが切断されました",
|
||||
"connectionError": "シリアルポートを開けませんでした",
|
||||
"wsError": "WebSocket エラー",
|
||||
"errorNoServerUrl": "サーバー URL が設定されていません",
|
||||
"notSupportedTitle": "シリアル非対応",
|
||||
"notSupported": "シリアル接続には Web Serial API をサポートするブラウザ (Chrome, Edge, Firefox 151+) または Termix デスクトップアプリが必要です。",
|
||||
"hideHint": "シリアルタブは「ユーザープロファイル > 外観 > サイドバー > ナビゲーション」で非表示にできます。",
|
||||
"browserPickerHint": "接続をクリックすると、ブラウザがポートピッカーを開き、デバイスを選択できます。"
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "チャンネルを追加",
|
||||
"editChannel": "チャンネルを編集",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "호스트 관리자",
|
||||
"credentials": "자격 증명",
|
||||
"connections": "연결",
|
||||
"serial": "시리얼",
|
||||
"keepSideMenuOpen": "메뉴를 열어 두세요",
|
||||
"collapseSideMenu": "메뉴 접기",
|
||||
"roleAdministrator": "관리자",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Proxmox 통계 열기",
|
||||
"openTmuxMonitor": "Tmux 모니터 열기"
|
||||
},
|
||||
"serial": {
|
||||
"title": "시리얼",
|
||||
"portLabel": "포트",
|
||||
"portPlaceholder": "/dev/ttyUSB0 or COM3",
|
||||
"baudRateLabel": "보레이트",
|
||||
"dataBitsLabel": "데이터",
|
||||
"stopBitsLabel": "스톱",
|
||||
"parityLabel": "패리티",
|
||||
"parityNone": "없음",
|
||||
"parityEven": "짝수",
|
||||
"parityOdd": "홀수",
|
||||
"connect": "시리얼 연결",
|
||||
"disconnect": "연결 해제",
|
||||
"refreshPorts": "포트 새로 고침",
|
||||
"connected": "{{path}}에 {{baud}} 보로 연결됨",
|
||||
"disconnected": "시리얼 포트 연결 해제됨",
|
||||
"connectionError": "시리얼 포트를 열지 못했습니다.",
|
||||
"wsError": "WebSocket 오류",
|
||||
"errorNoServerUrl": "서버 URL이 구성되지 않았습니다.",
|
||||
"notSupportedTitle": "시리얼 미지원",
|
||||
"notSupported": "시리얼 연결에는 Web Serial API를 지원하는 브라우저(Chrome, Edge, Firefox 151+) 또는 Termix 데스크톱 앱이 필요합니다.",
|
||||
"hideHint": "Serial 탭은 사용자 프로필 > 모양 > 사이드바 > 탐색에서 숨길 수 있습니다.",
|
||||
"browserPickerHint": "연결 버튼을 클릭하면 브라우저에서 장치 선택을 위한 포트 선택기가 열립니다."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "채널 추가",
|
||||
"editChannel": "채널 편집",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Hostmanager",
|
||||
"credentials": "Referenties",
|
||||
"connections": "Verbindingen",
|
||||
"serial": "Serieel",
|
||||
"keepSideMenuOpen": "Menu open houden",
|
||||
"collapseSideMenu": "Menu inklappen",
|
||||
"roleAdministrator": "Beheerder",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Open Proxmox-statistieken",
|
||||
"openTmuxMonitor": "Open Tmux Monitor"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serieel",
|
||||
"portLabel": "Haven",
|
||||
"portPlaceholder": "/dev/ttyUSB0 of COM3",
|
||||
"baudRateLabel": "Baudrate",
|
||||
"dataBitsLabel": "Gegevens",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Pariteit",
|
||||
"parityNone": "Geen",
|
||||
"parityEven": "Zelfs",
|
||||
"parityOdd": "Vreemd",
|
||||
"connect": "Verbinden via serieel",
|
||||
"disconnect": "Verbinding verbreken",
|
||||
"refreshPorts": "Vernieuw poorten",
|
||||
"connected": "Verbonden met {{path}} met {{baud}} baud",
|
||||
"disconnected": "Seriële poort losgekoppeld",
|
||||
"connectionError": "Het openen van de seriële poort is mislukt.",
|
||||
"wsError": "WebSocket-fout",
|
||||
"errorNoServerUrl": "Geen server-URL geconfigureerd",
|
||||
"notSupportedTitle": "Serienummer niet ondersteund",
|
||||
"notSupported": "Voor seriële verbindingen is een browser met Web Serial API-ondersteuning vereist (Chrome, Edge of Firefox 151+), of de Termix-desktopapp.",
|
||||
"hideHint": "Je kunt het tabblad 'Serienummer' verbergen via Gebruikersprofiel > Uiterlijk > Zijbalk > Navigatie.",
|
||||
"browserPickerHint": "Klik op Verbinden en uw browser opent een poortkiezer waarmee u het apparaat kunt selecteren."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Kanaal toevoegen",
|
||||
"editChannel": "Bewerk kanaal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Vertsadministrator",
|
||||
"credentials": "Legitimasjon",
|
||||
"connections": "Tilkoblinger",
|
||||
"serial": "Seriell",
|
||||
"keepSideMenuOpen": "Hold menyen åpen",
|
||||
"collapseSideMenu": "Skjul menyen",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Åpne Proxmox-statistikk",
|
||||
"openTmuxMonitor": "Åpne Tmux-skjermen"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Seriell",
|
||||
"portLabel": "Havn",
|
||||
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
|
||||
"baudRateLabel": "Baudhastighet",
|
||||
"dataBitsLabel": "Data",
|
||||
"stopBitsLabel": "Stoppe",
|
||||
"parityLabel": "Paritet",
|
||||
"parityNone": "Ingen",
|
||||
"parityEven": "Til og med",
|
||||
"parityOdd": "Merkelig",
|
||||
"connect": "Koble til seriell",
|
||||
"disconnect": "Frakople",
|
||||
"refreshPorts": "Oppdater porter",
|
||||
"connected": "Koblet til {{path}} ved {{baud}} baud",
|
||||
"disconnected": "Seriell port frakoblet",
|
||||
"connectionError": "Klarte ikke å åpne serieporten",
|
||||
"wsError": "WebSocket-feil",
|
||||
"errorNoServerUrl": "Ingen server-URL konfigurert",
|
||||
"notSupportedTitle": "Serienummer støttes ikke",
|
||||
"notSupported": "Serielle tilkoblinger krever en nettleser med støtte for Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivebordsappen.",
|
||||
"hideHint": "Du kan skjule Seriell-fanen under Brukerprofil > Utseende > Sidefelt > Navigasjon.",
|
||||
"browserPickerHint": "Klikk på Koble til, så åpner nettleseren din en portvelger der du kan velge enheten."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Legg til kanal",
|
||||
"editChannel": "Rediger kanal",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Menedżer hostów",
|
||||
"credentials": "Poświadczenia",
|
||||
"connections": "Połączenia",
|
||||
"serial": "Port szeregowy",
|
||||
"keepSideMenuOpen": "Zachowaj otwarte menu",
|
||||
"collapseSideMenu": "Zwiń menu",
|
||||
"roleAdministrator": "Administrator",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Otwórz statystyki Proxmox",
|
||||
"openTmuxMonitor": "Otwórz monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Port szeregowy",
|
||||
"portLabel": "Port",
|
||||
"portPlaceholder": "/dev/ttyUSB0 lub COM3",
|
||||
"baudRateLabel": "Szybkość transmisji",
|
||||
"dataBitsLabel": "Dane",
|
||||
"stopBitsLabel": "Stop",
|
||||
"parityLabel": "Parzystość",
|
||||
"parityNone": "Brak",
|
||||
"parityEven": "Parzysta",
|
||||
"parityOdd": "Nieparzysta",
|
||||
"connect": "Połącz z portem szeregowym",
|
||||
"disconnect": "Odłącz",
|
||||
"refreshPorts": "Odśwież porty",
|
||||
"connected": "Połączono z {{path}} z szybkością {{baud}} bodów",
|
||||
"disconnected": "Port szeregowy rozłączony",
|
||||
"connectionError": "Nie udało się otworzyć portu szeregowego",
|
||||
"wsError": "Błąd WebSocket",
|
||||
"errorNoServerUrl": "Brak skonfigurowanego adresu URL serwera",
|
||||
"notSupportedTitle": "Port szeregowy nieobsługiwany",
|
||||
"notSupported": "Połączenia szeregowe wymagają przeglądarki obsługującej Web Serial API (Chrome, Edge lub Firefox 151+) albo aplikacji desktopowej Termix.",
|
||||
"hideHint": "Kartę Port szeregowy można ukryć w: Profil użytkownika > Wygląd > Pasek boczny > Nawigacja.",
|
||||
"browserPickerHint": "Kliknij Połącz, a przeglądarka otworzy okno wyboru portu, w którym wskażesz urządzenie."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Dodaj kanał",
|
||||
"editChannel": "Edytuj kanał",
|
||||
|
||||
@@ -713,7 +713,6 @@
|
||||
"hostManager": "Gerente de Hospedagem",
|
||||
"credentials": "Credenciais",
|
||||
"connections": "Conexões",
|
||||
"serial": "Serial",
|
||||
"keepSideMenuOpen": "Mantenha o menu aberto",
|
||||
"collapseSideMenu": "Recolher menu",
|
||||
"roleAdministrator": "Administrador",
|
||||
@@ -3055,30 +3054,6 @@
|
||||
"openProxmoxStats": "Estatísticas do Open Proxmox",
|
||||
"openTmuxMonitor": "Abra o monitor Tmux"
|
||||
},
|
||||
"serial": {
|
||||
"title": "Serial",
|
||||
"portLabel": "Porta",
|
||||
"portPlaceholder": "/dev/ttyUSB0 ou COM3",
|
||||
"baudRateLabel": "Taxa de transmissão (Baud rate)",
|
||||
"dataBitsLabel": "Dados",
|
||||
"stopBitsLabel": "Parar",
|
||||
"parityLabel": "Paridade",
|
||||
"parityNone": "Nenhum",
|
||||
"parityEven": "Até",
|
||||
"parityOdd": "Chance",
|
||||
"connect": "Conectar à porta serial",
|
||||
"disconnect": "Desconectar",
|
||||
"refreshPorts": "Atualizar portas",
|
||||
"connected": "Conectado a {{path}} na taxa de transmissão de {{baud}}",
|
||||
"disconnected": "Porta serial desconectada",
|
||||
"connectionError": "Falha ao abrir a porta serial",
|
||||
"wsError": "Erro no WebSocket",
|
||||
"errorNoServerUrl": "Nenhuma URL de servidor configurada",
|
||||
"notSupportedTitle": "Serial não suportado",
|
||||
"notSupported": "As conexões seriais exigem um navegador com suporte à API Web Serial (Chrome, Edge ou Firefox 151+) ou o aplicativo Termix para desktop.",
|
||||
"hideHint": "Você pode ocultar a guia Serial em Perfil do Usuário > Aparência > Barra lateral > Navegação.",
|
||||
"browserPickerHint": "Clique em Conectar e seu navegador abrirá um seletor de portas para que você escolha o dispositivo."
|
||||
},
|
||||
"alerts": {
|
||||
"addChannel": "Adicionar canal",
|
||||
"editChannel": "Editar canal",
|
||||
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user