@viamrobotics/test-widgets 0.1.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin.js +3 -1
- package/dist/client-map.d.ts +5 -2
- package/dist/client-map.js +7 -1
- package/dist/components/error.svelte +40 -9
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/queries.svelte +8 -2
- package/dist/components/refetch-interval-store.svelte.d.ts +1 -1
- package/dist/components/section-group.svelte +1 -1
- package/dist/components/widgets/audio-input/audio-input.svelte +153 -0
- package/dist/components/widgets/audio-input/audio-input.svelte.d.ts +7 -0
- package/dist/components/widgets/audio-input/create-audio-capturer.svelte.d.ts +13 -0
- package/dist/components/widgets/audio-input/create-audio-capturer.svelte.js +85 -0
- package/dist/components/widgets/audio-input/properties.svelte +26 -0
- package/dist/components/widgets/audio-input/properties.svelte.d.ts +8 -0
- package/dist/components/widgets/audio-output/audio-output.svelte +200 -0
- package/dist/components/widgets/audio-output/audio-output.svelte.d.ts +7 -0
- package/dist/components/widgets/audio-output/codec.d.ts +2 -0
- package/dist/components/widgets/audio-output/codec.js +16 -0
- package/dist/components/widgets/audio-output/create-audio-player.svelte.d.ts +10 -0
- package/dist/components/widgets/audio-output/create-audio-player.svelte.js +28 -0
- package/dist/components/widgets/audio-output/properties.svelte +26 -0
- package/dist/components/widgets/audio-output/properties.svelte.d.ts +8 -0
- package/dist/components/widgets/do-command/create-do-command-client.svelte.d.ts +7 -0
- package/dist/components/widgets/do-command/create-do-command-client.svelte.js +26 -0
- package/dist/components/widgets/do-command/do-command.svelte +21 -22
- package/dist/components/widgets/do-command/do-command.svelte.d.ts +1 -1
- package/package.json +5 -7
package/dist/builtin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArmWidget, BaseWidget, BoardWidget, ButtonWidget, CameraWidget, DiscoveryWidget, EncoderWidget, GantryWidget, GripperWidget, InputControllerWidget, MLModelServiceWidget, MotorWidget, MovementSensorWidget, NavigationServiceWidget, PowerSensorWidget, SensorWidget, ServoWidget, SlamWidget, SwitchWidget, VisionServiceWidget, } from './';
|
|
1
|
+
import { ArmWidget, AudioInputWidget, AudioOutputWidget, BaseWidget, BoardWidget, ButtonWidget, CameraWidget, DiscoveryWidget, EncoderWidget, GantryWidget, GripperWidget, InputControllerWidget, MLModelServiceWidget, MotorWidget, MovementSensorWidget, NavigationServiceWidget, PowerSensorWidget, SensorWidget, ServoWidget, SlamWidget, SwitchWidget, VisionServiceWidget, } from './';
|
|
2
2
|
import { clientMap } from "./client-map.js";
|
|
3
3
|
import { getResourceAPI } from "./resource.js";
|
|
4
4
|
export { clientForBuiltinResource } from "./client-map.js";
|
|
@@ -9,6 +9,8 @@ const resourceMap =
|
|
|
9
9
|
// list created via `cat rdkbuiltins/viam-server-stable.json | rg "api" | sort | uniq`
|
|
10
10
|
{
|
|
11
11
|
'rdk:component:arm': [clientMap['rdk:component:arm'], ArmWidget, true],
|
|
12
|
+
'rdk:component:audio_in': [clientMap['rdk:component:audio_in'], AudioInputWidget, true],
|
|
13
|
+
'rdk:component:audio_out': [clientMap['rdk:component:audio_out'], AudioOutputWidget, true],
|
|
12
14
|
'rdk:component:base': [clientMap['rdk:component:base'], BaseWidget, true],
|
|
13
15
|
'rdk:component:board': [clientMap['rdk:component:board'], BoardWidget, true],
|
|
14
16
|
'rdk:component:button': [clientMap['rdk:component:button'], ButtonWidget, true],
|
package/dist/client-map.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ArmClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, type ResourceName, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient } from '@viamrobotics/sdk';
|
|
1
|
+
import { ArmClient, AudioInClient, AudioOutClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, type ResourceName, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient } from '@viamrobotics/sdk';
|
|
2
2
|
export declare const clientMap: {
|
|
3
3
|
readonly 'rdk:component:arm': typeof ArmClient;
|
|
4
|
+
readonly 'rdk:component:audio_in': typeof AudioInClient;
|
|
5
|
+
readonly 'rdk:component:audio_out': typeof AudioOutClient;
|
|
4
6
|
readonly 'rdk:component:base': typeof BaseClient;
|
|
5
7
|
readonly 'rdk:component:board': typeof BoardClient;
|
|
6
8
|
readonly 'rdk:component:button': typeof ButtonClient;
|
|
@@ -28,4 +30,5 @@ export declare const clientMap: {
|
|
|
28
30
|
readonly 'rdk:service:world_state_store': typeof WorldStateStoreClient;
|
|
29
31
|
readonly 'rdk:service:video': typeof VideoClient;
|
|
30
32
|
};
|
|
31
|
-
export declare const clientForBuiltinResource: (resource: ResourceName) => typeof ArmClient | typeof BaseClient | typeof GantryClient | typeof GripperClient | typeof MotorClient | typeof ServoClient | typeof BoardClient | typeof ButtonClient | typeof CameraClient | typeof DiscoveryClient | typeof EncoderClient | typeof GenericComponentClient | typeof InputControllerClient | typeof MovementSensorClient | typeof PoseTrackerClient | typeof PowerSensorClient | typeof SensorClient | typeof SwitchClient | typeof DataManagerClient | typeof GenericServiceClient | typeof MLModelClient | typeof MotionClient | typeof NavigationClient | typeof SlamClient | typeof VisionClient | typeof WorldStateStoreClient | typeof VideoClient | undefined;
|
|
33
|
+
export declare const clientForBuiltinResource: (resource: ResourceName) => typeof ArmClient | typeof BaseClient | typeof GantryClient | typeof GripperClient | typeof MotorClient | typeof ServoClient | typeof AudioInClient | typeof AudioOutClient | typeof BoardClient | typeof ButtonClient | typeof CameraClient | typeof DiscoveryClient | typeof EncoderClient | typeof GenericComponentClient | typeof InputControllerClient | typeof MovementSensorClient | typeof PoseTrackerClient | typeof PowerSensorClient | typeof SensorClient | typeof SwitchClient | typeof DataManagerClient | typeof GenericServiceClient | typeof MLModelClient | typeof MotionClient | typeof NavigationClient | typeof SlamClient | typeof VisionClient | typeof WorldStateStoreClient | typeof VideoClient | undefined;
|
|
34
|
+
export declare const supportsDoCommand: (resource: ResourceName) => boolean;
|
package/dist/client-map.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { ArmClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient, } from '@viamrobotics/sdk';
|
|
1
|
+
import { ArmClient, AudioInClient, AudioOutClient, BaseClient, BoardClient, ButtonClient, CameraClient, DataManagerClient, DiscoveryClient, EncoderClient, GantryClient, GenericComponentClient, GenericServiceClient, GripperClient, InputControllerClient, MLModelClient, MotionClient, MotorClient, MovementSensorClient, NavigationClient, PoseTrackerClient, PowerSensorClient, SensorClient, ServoClient, SlamClient, SwitchClient, VideoClient, VisionClient, WorldStateStoreClient, } from '@viamrobotics/sdk';
|
|
2
2
|
import { getResourceAPI } from "./resource.js";
|
|
3
3
|
export const clientMap = {
|
|
4
4
|
'rdk:component:arm': ArmClient,
|
|
5
|
+
'rdk:component:audio_in': AudioInClient,
|
|
6
|
+
'rdk:component:audio_out': AudioOutClient,
|
|
5
7
|
'rdk:component:base': BaseClient,
|
|
6
8
|
'rdk:component:board': BoardClient,
|
|
7
9
|
'rdk:component:button': ButtonClient,
|
|
@@ -33,3 +35,7 @@ export const clientForBuiltinResource = (resource) => {
|
|
|
33
35
|
const resAPI = getResourceAPI(resource);
|
|
34
36
|
return resAPI in clientMap ? clientMap[resAPI] : undefined;
|
|
35
37
|
};
|
|
38
|
+
export const supportsDoCommand = (resource) => {
|
|
39
|
+
const client = clientForBuiltinResource(resource);
|
|
40
|
+
return client !== undefined && client !== MLModelClient;
|
|
41
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { IconButton, Tooltip } from '@viamrobotics/prime-core'
|
|
2
3
|
import { twMerge } from 'tailwind-merge'
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
@@ -11,15 +12,45 @@
|
|
|
11
12
|
|
|
12
13
|
const { lastError, id = `error_${defaultId}`, class: className = '' }: Props = $props()
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
-
|
|
15
|
+
const errorText = $derived(
|
|
16
|
+
lastError?.name && lastError.message ? `${lastError.name}: ${lastError.message}` : ''
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
let showCopySuccess = $state(false)
|
|
20
|
+
let copySuccessTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
21
|
+
|
|
22
|
+
const copyErrorToClipboard = async () => {
|
|
23
|
+
try {
|
|
24
|
+
await globalThis.navigator.clipboard.writeText(errorText)
|
|
25
|
+
showCopySuccess = true
|
|
26
|
+
clearTimeout(copySuccessTimeoutId)
|
|
27
|
+
copySuccessTimeoutId = setTimeout(() => {
|
|
28
|
+
showCopySuccess = false
|
|
29
|
+
}, 750)
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Failed to copy error to clipboard', error)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
$effect(() => () => clearTimeout(copySuccessTimeoutId))
|
|
16
36
|
</script>
|
|
17
37
|
|
|
18
|
-
{#if
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
{#if errorText}
|
|
39
|
+
<div class="flex items-center justify-between gap-1">
|
|
40
|
+
<p
|
|
41
|
+
{id}
|
|
42
|
+
class={twMerge('font-roboto-mono text-danger-dark text-xs', className)}
|
|
43
|
+
>
|
|
44
|
+
{errorText}
|
|
45
|
+
</p>
|
|
46
|
+
<Tooltip let:tooltipID>
|
|
47
|
+
<IconButton
|
|
48
|
+
aria-describedby={tooltipID}
|
|
49
|
+
icon={showCopySuccess ? 'check' : 'content-copy'}
|
|
50
|
+
label="Copy error"
|
|
51
|
+
on:click={copyErrorToClipboard}
|
|
52
|
+
/>
|
|
53
|
+
<div slot="description">Copy error</div>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
</div>
|
|
25
56
|
{/if}
|
|
@@ -3,6 +3,8 @@ export { default as ArmWidget } from './widgets/arm/arm.svelte';
|
|
|
3
3
|
export { default as ArmMoveToJointPositionsWidget } from './widgets/arm/move-to-joint-positions-widget.svelte';
|
|
4
4
|
export { default as ArmMoveToPositionWidget } from './widgets/arm/move-to-position-widget.svelte';
|
|
5
5
|
export { default as ArmQuickMoveWidget } from './widgets/arm/quick-move-widget.svelte';
|
|
6
|
+
export { default as AudioInputWidget } from './widgets/audio-input/audio-input.svelte';
|
|
7
|
+
export { default as AudioOutputWidget } from './widgets/audio-output/audio-output.svelte';
|
|
6
8
|
export { default as BaseWidget } from './widgets/base/base.svelte';
|
|
7
9
|
export { default as BaseMoveStraightWidget } from './widgets/base/move-straight-widget.svelte';
|
|
8
10
|
export { default as BaseQuickMoveWidget } from './widgets/base/quick-move-widget.svelte';
|
package/dist/components/index.js
CHANGED
|
@@ -3,6 +3,8 @@ export { default as ArmWidget } from './widgets/arm/arm.svelte';
|
|
|
3
3
|
export { default as ArmMoveToJointPositionsWidget } from './widgets/arm/move-to-joint-positions-widget.svelte';
|
|
4
4
|
export { default as ArmMoveToPositionWidget } from './widgets/arm/move-to-position-widget.svelte';
|
|
5
5
|
export { default as ArmQuickMoveWidget } from './widgets/arm/quick-move-widget.svelte';
|
|
6
|
+
export { default as AudioInputWidget } from './widgets/audio-input/audio-input.svelte';
|
|
7
|
+
export { default as AudioOutputWidget } from './widgets/audio-output/audio-output.svelte';
|
|
6
8
|
export { default as BaseWidget } from './widgets/base/base.svelte';
|
|
7
9
|
export { default as BaseMoveStraightWidget } from './widgets/base/move-straight-widget.svelte';
|
|
8
10
|
export { default as BaseQuickMoveWidget } from './widgets/base/quick-move-widget.svelte';
|
|
@@ -29,11 +29,17 @@
|
|
|
29
29
|
let errors = $state.raw<Error[]>([])
|
|
30
30
|
let data = $state.raw<unknown[]>([])
|
|
31
31
|
|
|
32
|
+
const serializeErrors = (errs: Error[]) =>
|
|
33
|
+
errs.map((error) => `${error.name}\0${error.message}`).join('\0\0')
|
|
34
|
+
|
|
32
35
|
// Errors are null during loading, so keep the latest errors during polling.
|
|
36
|
+
// Only update errors if the content has changed to avoid re-rendering identical errors.
|
|
33
37
|
$effect.pre(() => {
|
|
34
38
|
const nextErrors = queries.map((query) => query.error).filter((error) => error !== null)
|
|
35
39
|
if (nextErrors.length > 0) {
|
|
36
|
-
errors
|
|
40
|
+
if (serializeErrors(nextErrors) !== serializeErrors(errors)) {
|
|
41
|
+
errors = nextErrors
|
|
42
|
+
}
|
|
37
43
|
} else if (queries.every((query) => query.isSuccess)) {
|
|
38
44
|
errors = []
|
|
39
45
|
}
|
|
@@ -55,7 +61,7 @@
|
|
|
55
61
|
{contentRect}
|
|
56
62
|
cx={contentCx}
|
|
57
63
|
>
|
|
58
|
-
{#each errors as error (error)}
|
|
64
|
+
{#each errors as error (`${error.name}\0${error.message}`)}
|
|
59
65
|
<ErrorDisplay lastError={error} />
|
|
60
66
|
{/each}
|
|
61
67
|
</ContentRect>
|
|
@@ -14,5 +14,5 @@ export declare const RefetchIntervals: {
|
|
|
14
14
|
};
|
|
15
15
|
export type RefetchRate = ValueOf<typeof RefetchRates>;
|
|
16
16
|
export type RefetchInterval = ValueOf<typeof RefetchIntervals>;
|
|
17
|
-
export declare const RefetchRateToInterval: ["Live" | "Refresh every second" | "Refresh every 5 seconds" | "Manual refresh", false |
|
|
17
|
+
export declare const RefetchRateToInterval: ["Live" | "Refresh every second" | "Refresh every 5 seconds" | "Manual refresh", false | 33.3333 | 1000 | 5000][];
|
|
18
18
|
export declare const createRefetchIntervalStore: (partID: () => string, resourceName: () => string, scope: string, defaultInterval?: RefetchInterval) => PersistedState<RefetchInterval>;
|
|
@@ -32,7 +32,7 @@ A group of sections in a card. For example, "test" or "do command"
|
|
|
32
32
|
>
|
|
33
33
|
<header class={['border-gray-3 bg-light hover:bg-medium h-7', !isCollapsed && 'border-b']}>
|
|
34
34
|
<button
|
|
35
|
-
class="group flex h-full w-full flex-row items-center gap-2 px-
|
|
35
|
+
class="group flex h-full w-full flex-row items-center gap-2 px-2.75"
|
|
36
36
|
aria-controls={collapseID}
|
|
37
37
|
aria-expanded={!isCollapsed}
|
|
38
38
|
aria-label={`${isCollapsed ? 'expand' : 'collapse'} ${title}`}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Label, NumericInput, Select } from '@viamrobotics/prime-core'
|
|
3
|
+
import { AudioInClient } from '@viamrobotics/sdk'
|
|
4
|
+
import { createResourceClient, createResourceQuery } from '@viamrobotics/svelte-sdk'
|
|
5
|
+
|
|
6
|
+
import ApiSection from '../../api-section.svelte'
|
|
7
|
+
import ConnectionStatus from '../../connection-status.svelte'
|
|
8
|
+
import MutationSection from '../../mutation-section.svelte'
|
|
9
|
+
import Query from '../../query.svelte'
|
|
10
|
+
import RefetchController from '../../refetch-controller.svelte'
|
|
11
|
+
import { createRefetchIntervalStore } from '../../refetch-interval-store.svelte'
|
|
12
|
+
import { numberValueFromEvent } from '../../../event-handlers'
|
|
13
|
+
|
|
14
|
+
import { createAudioCapturer } from './create-audio-capturer.svelte.js'
|
|
15
|
+
import Properties from './properties.svelte'
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
partID: string
|
|
19
|
+
resourceName: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { partID, resourceName }: Props = $props()
|
|
23
|
+
|
|
24
|
+
const refetchInterval = createRefetchIntervalStore(
|
|
25
|
+
() => partID,
|
|
26
|
+
() => resourceName,
|
|
27
|
+
'audio-input'
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const client = createResourceClient(
|
|
31
|
+
AudioInClient,
|
|
32
|
+
() => partID,
|
|
33
|
+
() => resourceName
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const propertiesQuery = createResourceQuery(client, 'getProperties', () => ({
|
|
37
|
+
refetchInterval: refetchInterval.current,
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
let captureCodec = $state('')
|
|
41
|
+
let captureDuration = $state(3)
|
|
42
|
+
|
|
43
|
+
const capture = createAudioCapturer(client)
|
|
44
|
+
|
|
45
|
+
const availableCodecs = $derived(
|
|
46
|
+
propertiesQuery.data?.supportedCodecs.length ? propertiesQuery.data.supportedCodecs : ['pcm16']
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const selectedCodec = $derived(captureCodec || availableCodecs[0]!)
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<ConnectionStatus {partID}>
|
|
53
|
+
{#snippet connected()}
|
|
54
|
+
<div class="p-4 pb-3">
|
|
55
|
+
<RefetchController
|
|
56
|
+
{refetchInterval}
|
|
57
|
+
queries={[propertiesQuery]}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="flex flex-row divide-x">
|
|
62
|
+
<div class="flex w-full flex-col divide-y">
|
|
63
|
+
<MutationSection
|
|
64
|
+
title="GetAudio"
|
|
65
|
+
description="Capture audio from the device"
|
|
66
|
+
lastError={capture.error}
|
|
67
|
+
>
|
|
68
|
+
<div class="flex flex-col gap-2">
|
|
69
|
+
<Label cx="gap-1 text-xs">
|
|
70
|
+
Codec
|
|
71
|
+
<Select
|
|
72
|
+
slot="input"
|
|
73
|
+
value={selectedCodec}
|
|
74
|
+
on:change={(e) => {
|
|
75
|
+
captureCodec = (e.target as HTMLSelectElement).value
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{#each availableCodecs as codec (codec)}
|
|
79
|
+
<option value={codec}>{codec}</option>
|
|
80
|
+
{/each}
|
|
81
|
+
</Select>
|
|
82
|
+
</Label>
|
|
83
|
+
<Label cx="gap-1 text-xs">
|
|
84
|
+
Duration (seconds, 0 = stream until stopped)
|
|
85
|
+
<NumericInput
|
|
86
|
+
slot="input"
|
|
87
|
+
value={captureDuration}
|
|
88
|
+
on:change={(e) => {
|
|
89
|
+
captureDuration = numberValueFromEvent(e) ?? 3
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</Label>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div class="mt-auto flex flex-col items-start gap-2">
|
|
96
|
+
{#if capture.status === 'recording'}
|
|
97
|
+
<p class="font-roboto-mono text-subtle-1 text-xs">
|
|
98
|
+
{(capture.totalBytes / 1024).toFixed(1)} kB captured
|
|
99
|
+
</p>
|
|
100
|
+
<Button
|
|
101
|
+
icon="stop-circle-outline"
|
|
102
|
+
onclick={capture.stop}
|
|
103
|
+
>
|
|
104
|
+
Stop
|
|
105
|
+
</Button>
|
|
106
|
+
{:else}
|
|
107
|
+
{#if capture.downloadUrl}
|
|
108
|
+
<p class="font-roboto-mono text-subtle-1 text-xs">
|
|
109
|
+
{(capture.totalBytes / 1024).toFixed(1)} kB captured
|
|
110
|
+
</p>
|
|
111
|
+
<a
|
|
112
|
+
href={capture.downloadUrl}
|
|
113
|
+
download="audio-capture.{selectedCodec}"
|
|
114
|
+
rel="external"
|
|
115
|
+
>
|
|
116
|
+
<Button icon="download">Download</Button>
|
|
117
|
+
</a>
|
|
118
|
+
{/if}
|
|
119
|
+
<Button
|
|
120
|
+
icon="play-circle-outline"
|
|
121
|
+
onclick={() => capture.start(selectedCodec, captureDuration)}
|
|
122
|
+
disabled={!client.current}
|
|
123
|
+
>
|
|
124
|
+
{capture.status === 'done' ? 'Capture Again' : 'Start Capture'}
|
|
125
|
+
</Button>
|
|
126
|
+
{/if}
|
|
127
|
+
</div>
|
|
128
|
+
</MutationSection>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div class="ml-auto flex w-full max-w-1/2 flex-col divide-y sm:max-w-1/3">
|
|
132
|
+
<ApiSection
|
|
133
|
+
title="GetProperties"
|
|
134
|
+
description="Audio input properties"
|
|
135
|
+
class="relative"
|
|
136
|
+
>
|
|
137
|
+
<Query
|
|
138
|
+
query={propertiesQuery}
|
|
139
|
+
contentCx="h-6"
|
|
140
|
+
>
|
|
141
|
+
{#if propertiesQuery.data !== undefined}
|
|
142
|
+
<Properties
|
|
143
|
+
supportedCodecs={propertiesQuery.data.supportedCodecs}
|
|
144
|
+
sampleRateHz={propertiesQuery.data.sampleRateHz}
|
|
145
|
+
numChannels={propertiesQuery.data.numChannels}
|
|
146
|
+
/>
|
|
147
|
+
{/if}
|
|
148
|
+
</Query>
|
|
149
|
+
</ApiSection>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
{/snippet}
|
|
153
|
+
</ConnectionStatus>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AudioInClient } from '@viamrobotics/sdk';
|
|
2
|
+
type CaptureStatus = 'idle' | 'recording' | 'done' | 'error';
|
|
3
|
+
export declare const createAudioCapturer: (client: {
|
|
4
|
+
current: AudioInClient | undefined;
|
|
5
|
+
}) => {
|
|
6
|
+
readonly status: CaptureStatus;
|
|
7
|
+
readonly error: Error | null;
|
|
8
|
+
readonly totalBytes: number;
|
|
9
|
+
readonly downloadUrl: string | undefined;
|
|
10
|
+
start: (codec: string, duration: number) => Promise<void>;
|
|
11
|
+
stop: () => void;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AudioInClient } from '@viamrobotics/sdk';
|
|
2
|
+
export const createAudioCapturer = (client) => {
|
|
3
|
+
let captureStatus = $state('idle');
|
|
4
|
+
let captureError = $state(null);
|
|
5
|
+
let captureTotalBytes = $state(0);
|
|
6
|
+
let captureDownloadUrl = $state();
|
|
7
|
+
let captureAbortController = $state.raw();
|
|
8
|
+
$effect(() => {
|
|
9
|
+
return () => {
|
|
10
|
+
if (captureDownloadUrl)
|
|
11
|
+
URL.revokeObjectURL(captureDownloadUrl);
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
const start = async (codec, duration) => {
|
|
15
|
+
if (!client.current)
|
|
16
|
+
return;
|
|
17
|
+
captureStatus = 'recording';
|
|
18
|
+
captureError = null;
|
|
19
|
+
captureTotalBytes = 0;
|
|
20
|
+
if (captureDownloadUrl) {
|
|
21
|
+
URL.revokeObjectURL(captureDownloadUrl);
|
|
22
|
+
captureDownloadUrl = undefined;
|
|
23
|
+
}
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
captureAbortController = controller;
|
|
26
|
+
const chunks = [];
|
|
27
|
+
try {
|
|
28
|
+
const stream = client.current.getAudio(codec, duration, 0n, {}, { ...client.current.callOptions, signal: controller.signal });
|
|
29
|
+
for await (const chunk of stream) {
|
|
30
|
+
chunks.push(chunk.audioData);
|
|
31
|
+
captureTotalBytes += chunk.audioData.byteLength;
|
|
32
|
+
}
|
|
33
|
+
captureStatus = 'done';
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (controller.signal.aborted) {
|
|
37
|
+
captureStatus = 'done';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
captureStatus = 'error';
|
|
41
|
+
captureError = error instanceof Error ? error : new Error(String(error));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
captureAbortController = undefined;
|
|
46
|
+
}
|
|
47
|
+
if (chunks.length > 0) {
|
|
48
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
|
49
|
+
const merged = new Uint8Array(totalLength);
|
|
50
|
+
let offset = 0;
|
|
51
|
+
for (const chunk of chunks) {
|
|
52
|
+
merged.set(chunk, offset);
|
|
53
|
+
offset += chunk.byteLength;
|
|
54
|
+
}
|
|
55
|
+
const mimeTypes = {
|
|
56
|
+
mp3: 'audio/mpeg',
|
|
57
|
+
wav: 'audio/wav',
|
|
58
|
+
aac: 'audio/aac',
|
|
59
|
+
opus: 'audio/ogg',
|
|
60
|
+
flac: 'audio/flac',
|
|
61
|
+
};
|
|
62
|
+
const mimeType = mimeTypes[codec] ?? 'audio/octet-stream';
|
|
63
|
+
captureDownloadUrl = URL.createObjectURL(new Blob([merged], { type: mimeType }));
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const stop = () => {
|
|
67
|
+
captureAbortController?.abort();
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
get status() {
|
|
71
|
+
return captureStatus;
|
|
72
|
+
},
|
|
73
|
+
get error() {
|
|
74
|
+
return captureError;
|
|
75
|
+
},
|
|
76
|
+
get totalBytes() {
|
|
77
|
+
return captureTotalBytes;
|
|
78
|
+
},
|
|
79
|
+
get downloadUrl() {
|
|
80
|
+
return captureDownloadUrl;
|
|
81
|
+
},
|
|
82
|
+
start,
|
|
83
|
+
stop,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
supportedCodecs: string[]
|
|
4
|
+
sampleRateHz: number
|
|
5
|
+
numChannels: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { supportedCodecs, sampleRateHz, numChannels }: Props = $props()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<dl class="font-roboto-mono flex flex-col gap-y-2 text-sm">
|
|
12
|
+
<div class="flex flex-col gap-0.5">
|
|
13
|
+
<dt class="text-default w-50 min-w-50 pr-2 font-medium">Supported Codecs</dt>
|
|
14
|
+
<dd class="text-subtle-1">
|
|
15
|
+
{supportedCodecs.length > 0 ? supportedCodecs.join(', ') : 'None'}
|
|
16
|
+
</dd>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="flex flex-col gap-0.5">
|
|
19
|
+
<dt class="text-default w-50 min-w-50 pr-2 font-medium">Sample Rate</dt>
|
|
20
|
+
<dd class="text-subtle-1">{sampleRateHz} Hz</dd>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="flex flex-col gap-0.5">
|
|
23
|
+
<dt class="text-default w-50 min-w-50 pr-2 font-medium">Channels</dt>
|
|
24
|
+
<dd class="text-subtle-1">{numChannels}</dd>
|
|
25
|
+
</div>
|
|
26
|
+
</dl>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, Label, NumericInput, Select } from '@viamrobotics/prime-core'
|
|
3
|
+
import { AudioOutClient } from '@viamrobotics/sdk'
|
|
4
|
+
import { createResourceClient, createResourceQuery } from '@viamrobotics/svelte-sdk'
|
|
5
|
+
|
|
6
|
+
import ApiSection from '../../api-section.svelte'
|
|
7
|
+
import ConnectionStatus from '../../connection-status.svelte'
|
|
8
|
+
import MutationSection from '../../mutation-section.svelte'
|
|
9
|
+
import Query from '../../query.svelte'
|
|
10
|
+
import RefetchController from '../../refetch-controller.svelte'
|
|
11
|
+
import { createRefetchIntervalStore } from '../../refetch-interval-store.svelte'
|
|
12
|
+
import { numberValueFromEvent } from '../../../event-handlers'
|
|
13
|
+
|
|
14
|
+
import { ExtToCodec, MimeToCodec } from './codec.js'
|
|
15
|
+
import { createAudioPlayer } from './create-audio-player.svelte.js'
|
|
16
|
+
import Properties from './properties.svelte'
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
partID: string
|
|
20
|
+
resourceName: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { partID, resourceName }: Props = $props()
|
|
24
|
+
|
|
25
|
+
const refetchInterval = createRefetchIntervalStore(
|
|
26
|
+
() => partID,
|
|
27
|
+
() => resourceName,
|
|
28
|
+
'audio-output'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const client = createResourceClient(
|
|
32
|
+
AudioOutClient,
|
|
33
|
+
() => partID,
|
|
34
|
+
() => resourceName
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const propertiesQuery = createResourceQuery(client, 'getProperties', () => ({
|
|
38
|
+
refetchInterval: refetchInterval.current,
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
const availableCodecs = $derived(
|
|
42
|
+
propertiesQuery.data?.supportedCodecs.length ? propertiesQuery.data.supportedCodecs : ['pcm16']
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const playContext = createAudioPlayer(client)
|
|
46
|
+
|
|
47
|
+
let selectedFile = $state.raw<File | null>(null)
|
|
48
|
+
let fileInputError = $state<string | null>(null)
|
|
49
|
+
let playCodec = $state('')
|
|
50
|
+
let playSampleRateHz = $state<number | null>(null)
|
|
51
|
+
let playNumChannels = $state<number | null>(null)
|
|
52
|
+
|
|
53
|
+
const selectedCodec = $derived(playCodec || availableCodecs[0]!)
|
|
54
|
+
const selectedSampleRateHz = $derived(
|
|
55
|
+
playSampleRateHz ?? propertiesQuery.data?.sampleRateHz ?? 48000
|
|
56
|
+
)
|
|
57
|
+
const selectedNumChannels = $derived(playNumChannels ?? propertiesQuery.data?.numChannels ?? 1)
|
|
58
|
+
|
|
59
|
+
const handleFileChange = (event: Event) => {
|
|
60
|
+
const input = event.target as HTMLInputElement
|
|
61
|
+
const file = input.files?.[0] ?? null
|
|
62
|
+
selectedFile = file
|
|
63
|
+
fileInputError = null
|
|
64
|
+
if (file) {
|
|
65
|
+
const ext = file.name.split('.').pop()?.toLowerCase() ?? ''
|
|
66
|
+
if (MimeToCodec[file.type]) {
|
|
67
|
+
playCodec = MimeToCodec[file.type]
|
|
68
|
+
} else if (MimeToCodec[`audio/${ext}`]) {
|
|
69
|
+
playCodec = MimeToCodec[`audio/${ext}`]
|
|
70
|
+
} else if (ExtToCodec[ext]) {
|
|
71
|
+
playCodec = ExtToCodec[ext]
|
|
72
|
+
} else {
|
|
73
|
+
fileInputError = 'Unsupported audio format'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const play = async () => {
|
|
79
|
+
if (!selectedFile) {
|
|
80
|
+
fileInputError = 'Please select an audio file'
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (!client.current) return
|
|
84
|
+
|
|
85
|
+
const buffer = await selectedFile.arrayBuffer()
|
|
86
|
+
const audioData = new Uint8Array(buffer)
|
|
87
|
+
await playContext.play(audioData, selectedCodec, selectedSampleRateHz, selectedNumChannels)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const playButtonLabel = $derived.by(() => {
|
|
91
|
+
if (playContext.status === 'playing') return 'Playing...'
|
|
92
|
+
if (playContext.status === 'done') return 'Play Again'
|
|
93
|
+
return 'Play'
|
|
94
|
+
})
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<ConnectionStatus {partID}>
|
|
98
|
+
{#snippet connected()}
|
|
99
|
+
<div class="p-4 pb-3">
|
|
100
|
+
<RefetchController
|
|
101
|
+
{refetchInterval}
|
|
102
|
+
queries={[propertiesQuery]}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="flex flex-row divide-x">
|
|
107
|
+
<div class="flex w-full flex-col divide-y">
|
|
108
|
+
<MutationSection
|
|
109
|
+
title="Play"
|
|
110
|
+
description="Send audio data to the device"
|
|
111
|
+
lastError={playContext.error}
|
|
112
|
+
>
|
|
113
|
+
<div class="flex flex-col gap-2">
|
|
114
|
+
<Label cx="gap-1 text-xs">
|
|
115
|
+
Audio file
|
|
116
|
+
<input
|
|
117
|
+
slot="input"
|
|
118
|
+
type="file"
|
|
119
|
+
accept={availableCodecs.map((c) => `.${c}`).join(',')}
|
|
120
|
+
class="text-xs"
|
|
121
|
+
onchange={handleFileChange}
|
|
122
|
+
/>
|
|
123
|
+
</Label>
|
|
124
|
+
{#if fileInputError}
|
|
125
|
+
<p class="text-xs text-red-500">{fileInputError}</p>
|
|
126
|
+
{/if}
|
|
127
|
+
{#if selectedFile}
|
|
128
|
+
<p class="font-roboto-mono text-subtle-1 text-xs">{selectedFile.name}</p>
|
|
129
|
+
{/if}
|
|
130
|
+
<Label cx="gap-1 text-xs">
|
|
131
|
+
Codec
|
|
132
|
+
<Select
|
|
133
|
+
slot="input"
|
|
134
|
+
value={selectedCodec}
|
|
135
|
+
on:change={(e) => {
|
|
136
|
+
playCodec = (e.target as HTMLSelectElement).value
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
{#each availableCodecs as codec (codec)}
|
|
140
|
+
<option value={codec}>{codec}</option>
|
|
141
|
+
{/each}
|
|
142
|
+
</Select>
|
|
143
|
+
</Label>
|
|
144
|
+
<Label cx="gap-1 text-xs">
|
|
145
|
+
Sample rate (Hz)
|
|
146
|
+
<NumericInput
|
|
147
|
+
slot="input"
|
|
148
|
+
value={selectedSampleRateHz}
|
|
149
|
+
on:change={(e) => {
|
|
150
|
+
playSampleRateHz = numberValueFromEvent(e) ?? null
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</Label>
|
|
154
|
+
<Label cx="gap-1 text-xs">
|
|
155
|
+
Channels
|
|
156
|
+
<NumericInput
|
|
157
|
+
slot="input"
|
|
158
|
+
value={selectedNumChannels}
|
|
159
|
+
on:change={(e) => {
|
|
160
|
+
playNumChannels = numberValueFromEvent(e) ?? null
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
</Label>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="mt-auto">
|
|
167
|
+
<Button
|
|
168
|
+
icon="play-circle-outline"
|
|
169
|
+
onclick={play}
|
|
170
|
+
disabled={!client.current || playContext.status === 'playing'}
|
|
171
|
+
>
|
|
172
|
+
{playButtonLabel}
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</MutationSection>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class="ml-auto flex w-full max-w-1/2 flex-col divide-y sm:max-w-1/3">
|
|
179
|
+
<ApiSection
|
|
180
|
+
title="GetProperties"
|
|
181
|
+
description="Audio output properties"
|
|
182
|
+
class="relative"
|
|
183
|
+
>
|
|
184
|
+
<Query
|
|
185
|
+
query={propertiesQuery}
|
|
186
|
+
contentCx="h-6"
|
|
187
|
+
>
|
|
188
|
+
{#if propertiesQuery.data !== undefined}
|
|
189
|
+
<Properties
|
|
190
|
+
supportedCodecs={propertiesQuery.data.supportedCodecs}
|
|
191
|
+
sampleRateHz={propertiesQuery.data.sampleRateHz}
|
|
192
|
+
numChannels={propertiesQuery.data.numChannels}
|
|
193
|
+
/>
|
|
194
|
+
{/if}
|
|
195
|
+
</Query>
|
|
196
|
+
</ApiSection>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
{/snippet}
|
|
200
|
+
</ConnectionStatus>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const MimeToCodec = {
|
|
2
|
+
'audio/mpeg': 'mp3',
|
|
3
|
+
'audio/mp3': 'mp3',
|
|
4
|
+
'audio/wav': 'wav',
|
|
5
|
+
'audio/x-wav': 'wav',
|
|
6
|
+
'audio/aac': 'aac',
|
|
7
|
+
'audio/ogg': 'opus',
|
|
8
|
+
'audio/flac': 'flac',
|
|
9
|
+
'audio/x-flac': 'flac',
|
|
10
|
+
};
|
|
11
|
+
// Raw PCM formats have no standard MIME type; map by file extension directly.
|
|
12
|
+
export const ExtToCodec = {
|
|
13
|
+
pcm16: 'pcm16',
|
|
14
|
+
pcm32: 'pcm32',
|
|
15
|
+
pcm32_float: 'pcm32_float',
|
|
16
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AudioOutClient } from '@viamrobotics/sdk';
|
|
2
|
+
type PlayStatus = 'idle' | 'playing' | 'done' | 'error';
|
|
3
|
+
export declare const createAudioPlayer: (client: {
|
|
4
|
+
current: AudioOutClient | undefined;
|
|
5
|
+
}) => {
|
|
6
|
+
readonly status: PlayStatus;
|
|
7
|
+
readonly error: Error | null;
|
|
8
|
+
play: (audioData: Uint8Array, codec: string, sampleRateHz: number, numChannels: number) => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AudioOutClient, commonApi } from '@viamrobotics/sdk';
|
|
2
|
+
export const createAudioPlayer = (client) => {
|
|
3
|
+
let playStatus = $state('idle');
|
|
4
|
+
let playError = $state(null);
|
|
5
|
+
const play = async (audioData, codec, sampleRateHz, numChannels) => {
|
|
6
|
+
if (!client.current)
|
|
7
|
+
return;
|
|
8
|
+
playStatus = 'playing';
|
|
9
|
+
playError = null;
|
|
10
|
+
try {
|
|
11
|
+
await client.current.play(audioData, commonApi.AudioInfo.fromJson({ codec, sampleRateHz, numChannels }), {});
|
|
12
|
+
playStatus = 'done';
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
playStatus = 'error';
|
|
16
|
+
playError = error instanceof Error ? error : new Error(String(error));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
get status() {
|
|
21
|
+
return playStatus;
|
|
22
|
+
},
|
|
23
|
+
get error() {
|
|
24
|
+
return playError;
|
|
25
|
+
},
|
|
26
|
+
play,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
supportedCodecs: string[]
|
|
4
|
+
sampleRateHz: number
|
|
5
|
+
numChannels: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { supportedCodecs, sampleRateHz, numChannels }: Props = $props()
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<dl class="font-roboto-mono flex flex-col gap-y-2 text-sm">
|
|
12
|
+
<div class="flex flex-col gap-0.5">
|
|
13
|
+
<dt class="text-default w-50 min-w-50 pr-2 font-medium">Supported Codecs</dt>
|
|
14
|
+
<dd class="text-subtle-1">
|
|
15
|
+
{supportedCodecs.length > 0 ? supportedCodecs.join(', ') : 'None'}
|
|
16
|
+
</dd>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="flex flex-col gap-0.5">
|
|
19
|
+
<dt class="text-default w-50 min-w-50 pr-2 font-medium">Sample Rate</dt>
|
|
20
|
+
<dd class="text-subtle-1">{sampleRateHz} Hz</dd>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="flex flex-col gap-0.5">
|
|
23
|
+
<dt class="text-default w-50 min-w-50 pr-2 font-medium">Channels</dt>
|
|
24
|
+
<dd class="text-subtle-1">{numChannels}</dd>
|
|
25
|
+
</div>
|
|
26
|
+
</dl>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { MLModelClient, ResourceName } from '@viamrobotics/sdk';
|
|
2
|
+
import { clientMap } from '../../../client-map';
|
|
3
|
+
type DoCommandable = InstanceType<Exclude<(typeof clientMap)[keyof typeof clientMap], typeof MLModelClient>>;
|
|
4
|
+
export declare const createDoCommandClient: (resource: () => ResourceName, partID: () => string, resourceName: () => string) => {
|
|
5
|
+
current: DoCommandable | undefined;
|
|
6
|
+
};
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MachineConnectionEvent, MLModelClient, ResourceName } from '@viamrobotics/sdk';
|
|
2
|
+
import { useConnectionStatus, useRobotClient } from '@viamrobotics/svelte-sdk';
|
|
3
|
+
import { clientForBuiltinResource, clientMap } from '../../../client-map';
|
|
4
|
+
export const createDoCommandClient = (resource, partID, resourceName) => {
|
|
5
|
+
const robotClient = useRobotClient(partID);
|
|
6
|
+
const connectionStatus = useConnectionStatus(partID);
|
|
7
|
+
const resourceClient = $derived.by(() => {
|
|
8
|
+
if (!robotClient.current)
|
|
9
|
+
return;
|
|
10
|
+
if (connectionStatus.current !== MachineConnectionEvent.CONNECTED)
|
|
11
|
+
return;
|
|
12
|
+
const constructor = clientForBuiltinResource(resource());
|
|
13
|
+
if (!constructor)
|
|
14
|
+
return;
|
|
15
|
+
if (constructor === MLModelClient)
|
|
16
|
+
return;
|
|
17
|
+
const nextClient = new constructor(robotClient.current, resourceName());
|
|
18
|
+
nextClient.partID = partID();
|
|
19
|
+
return nextClient;
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
get current() {
|
|
23
|
+
return resourceClient;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Button, Progress } from '@viamrobotics/prime-core'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { CodeEditor } from '@viamrobotics/prime-core/code-editor'
|
|
4
|
+
import { type ResourceName, Struct } from '@viamrobotics/sdk'
|
|
5
|
+
import { createResourceMutation } from '@viamrobotics/svelte-sdk'
|
|
6
6
|
import { PersistedState } from 'runed'
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { supportsDoCommand } from '../../../client-map'
|
|
9
9
|
import { getResourceAPI, getResourceKey } from '../../../resource'
|
|
10
10
|
|
|
11
11
|
import ErrorDisplay from '../../error.svelte'
|
|
12
|
+
import { createDoCommandClient } from './create-do-command-client.svelte'
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
partID: string
|
|
@@ -17,19 +18,15 @@
|
|
|
17
18
|
|
|
18
19
|
const { partID, resource }: Props = $props()
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const client = $derived(
|
|
25
|
-
createResourceClient(
|
|
26
|
-
resourceClientType,
|
|
27
|
-
() => partID,
|
|
28
|
-
() => resource.name
|
|
29
|
-
)
|
|
21
|
+
const client = createDoCommandClient(
|
|
22
|
+
() => resource,
|
|
23
|
+
() => partID,
|
|
24
|
+
() => resource.name
|
|
30
25
|
)
|
|
31
26
|
|
|
32
|
-
const doCommandMutation =
|
|
27
|
+
const doCommandMutation = createResourceMutation(client, 'doCommand')
|
|
28
|
+
|
|
29
|
+
const isSupported = $derived(supportsDoCommand(resource))
|
|
33
30
|
|
|
34
31
|
let lastErr = $state<Error | null>()
|
|
35
32
|
|
|
@@ -53,17 +50,18 @@
|
|
|
53
50
|
}
|
|
54
51
|
</script>
|
|
55
52
|
|
|
56
|
-
{#if
|
|
53
|
+
{#if isSupported}
|
|
57
54
|
<div class="flex flex-row items-center justify-between">
|
|
58
55
|
<div class="flex w-[45%] flex-col gap-2 border-r py-2">
|
|
59
56
|
<span class="text-gray-9 px-4 text-sm font-medium">Input</span>
|
|
60
|
-
<
|
|
57
|
+
<CodeEditor
|
|
61
58
|
label="input"
|
|
62
|
-
|
|
59
|
+
language="json"
|
|
60
|
+
value={input.current ?? '{}'}
|
|
63
61
|
onChange={(nextInput: string) => {
|
|
64
62
|
input.current = nextInput
|
|
65
63
|
}}
|
|
66
|
-
|
|
64
|
+
class="h-56 overflow-y-auto"
|
|
67
65
|
errorMessageID={lastErr ? uid : undefined}
|
|
68
66
|
/>
|
|
69
67
|
</div>
|
|
@@ -84,11 +82,12 @@
|
|
|
84
82
|
{/if}
|
|
85
83
|
</div>
|
|
86
84
|
{#if !lastErr}
|
|
87
|
-
<
|
|
85
|
+
<CodeEditor
|
|
88
86
|
label="output"
|
|
89
|
-
|
|
87
|
+
language="json"
|
|
88
|
+
value={output}
|
|
90
89
|
readonly
|
|
91
|
-
|
|
90
|
+
class="h-56 overflow-y-auto"
|
|
92
91
|
/>
|
|
93
92
|
{:else}
|
|
94
93
|
<ErrorDisplay
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viamrobotics/test-widgets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"@threlte/core": ">=8",
|
|
32
32
|
"@threlte/extras": ">=9",
|
|
33
33
|
"@viamrobotics/prime-core": ">=0.1",
|
|
34
|
-
"@viamrobotics/prime-editor": ">=0.0.2",
|
|
35
34
|
"@viamrobotics/sdk": ">=0.68",
|
|
36
35
|
"@viamrobotics/svelte-sdk": ">=1.1",
|
|
37
36
|
"lodash-es": ">=4",
|
|
@@ -55,8 +54,8 @@
|
|
|
55
54
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
56
55
|
"@tailwindcss/postcss": "^4.2.4",
|
|
57
56
|
"@tailwindcss/vite": "^4.2.4",
|
|
58
|
-
"@tanstack/svelte-query": "^6.1.
|
|
59
|
-
"@tanstack/svelte-query-devtools": "^6.1.
|
|
57
|
+
"@tanstack/svelte-query": "^6.1.25",
|
|
58
|
+
"@tanstack/svelte-query-devtools": "^6.1.25",
|
|
60
59
|
"@testing-library/dom": "^10.4.1",
|
|
61
60
|
"@testing-library/jest-dom": "^6.9.1",
|
|
62
61
|
"@testing-library/svelte": "^5.3.1",
|
|
@@ -68,9 +67,8 @@
|
|
|
68
67
|
"@types/node": "^25.6.0",
|
|
69
68
|
"@types/three": "^0.183.1",
|
|
70
69
|
"@viamrobotics/motion-tools": "^1.19.1",
|
|
71
|
-
"@viamrobotics/prime-core": "^0.1.
|
|
72
|
-
"@viamrobotics/
|
|
73
|
-
"@viamrobotics/sdk": "^0.68.2",
|
|
70
|
+
"@viamrobotics/prime-core": "^0.1.21",
|
|
71
|
+
"@viamrobotics/sdk": "^0.69.0",
|
|
74
72
|
"@viamrobotics/svelte-sdk": "^1.2.1",
|
|
75
73
|
"@viamrobotics/tailwind-config": "^1.0.0",
|
|
76
74
|
"@viamrobotics/three": "^0.0.9",
|