diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 50ee96f..c10f57e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -11,5 +11,9 @@ module.exports = { ], parserOptions: { ecmaVersion: 'latest' + }, + rules: { + 'no-fallthrough': 1, + 'no-inner-declarations': 1 } } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1fedb78..43c71b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,21 +9,20 @@ on: jobs: build: - runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 19.x, 20.x] + node-version: [18.x, 20.x, 22.x, 23.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - cache: 'yarn' - - run: yarn install - - run: yarn lint - - run: yarn build + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' + - run: yarn install + - run: yarn lint + - run: yarn build diff --git a/src/App.vue b/src/App.vue index 7a73fe4..b06f53d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -33,10 +33,10 @@ import { useROSStore } from './stores/ros' import { useMessasgeStore } from './stores/message' import { usePackageStore } from './stores/package' import { useNodesStore } from './stores/nodes' -import { computed, onMounted, ref, watch } from 'vue' +import { onMounted, ref, watch } from 'vue' import PackageLoader from './components/PackageLoader.vue' -import type { Messages, NodeMsg, Packages, SubtreeInfo, TreeMsg } from './types/types' -import { EditorSkin, useEditorStore } from './stores/editor' +import type { MessageTypes, NodeMsg, Packages, SubtreeInfo, TreeMsg } from './types/types' +import { useEditorStore } from './stores/editor' import { useEditNodeStore } from './stores/edit_node' import NodeList from './components/NodeList.vue' import SelectSubtree from './components/SelectSubtree.vue' @@ -80,11 +80,11 @@ function updatePackagesSubscription() { ros_store.packages_sub.subscribe(onNewPackagesMsg) } -function onNewMessagesMsg(msg: Messages) { +function onNewMessagesMsg(msg: MessageTypes) { if (!messages_store.messages_available) { messages_store.areMessagesAvailable(true) } - messages_store.updateAvailableMessages(msg.messages) + messages_store.updateAvailableMessages(msg) } function updateMessagesSubscription() { @@ -107,6 +107,10 @@ function updateSubtreeInfoSubscription() { ros_store.subtree_info_sub.subscribe(onNewSubtreeInfoMsg) } +function updateChannelssubscription() { + ros_store.channels_sub.subscribe(messages_store.updateMessageChannels) +} + function handleNodeSearch(event: Event) { const target = event.target as HTMLInputElement node_search.value = target.value @@ -154,6 +158,7 @@ ros_store.$onAction(({ name, after }) => { updateTreeSubscription() updateSubtreeInfoSubscription() updateMessagesSubscription() + updateChannelssubscription() updatePackagesSubscription() }) }) @@ -163,6 +168,7 @@ onMounted(() => { updateTreeSubscription() updateSubtreeInfoSubscription() updateMessagesSubscription() + updateChannelssubscription() updatePackagesSubscription() }) diff --git a/src/assets/utils.scss b/src/assets/utils.scss index 5570fc7..218ad79 100644 --- a/src/assets/utils.scss +++ b/src/assets/utils.scss @@ -60,3 +60,15 @@ --bs-btn-disabled-border-color: var(--bs-body-color); --bs-gradient: none; } + +.search-results { + padding-left: 5px; + padding-right: 5px; + + max-height: 11em; + overflow-y: auto; +} + +.search-result:hover { + background-color: #007bff; +} diff --git a/src/components/D3BehaviorTreeEditor.vue b/src/components/D3BehaviorTreeEditor.vue index b2c77b3..6414407 100644 --- a/src/components/D3BehaviorTreeEditor.vue +++ b/src/components/D3BehaviorTreeEditor.vue @@ -42,27 +42,19 @@ import type { NodeDataLocation, NodeMsg, ParamData, - PyEnum, - PyLogger, - PyOperand, - PyOperator, - TreeMsg, TrimmedNode, TrimmedNodeData, NodeDataWiring } from '@/types/types' import { Position, IOKind } from '@/types/types' -import { getDefaultValue, prettyprint_type, python_builtin_types, typesCompatible } from '@/utils' -import { faArrowDown19 } from '@fortawesome/free-solid-svg-icons' +import { getDefaultValue, prettyprint_type, serializeNodeOptions, typesCompatible } from '@/utils' import { notify } from '@kyvg/vue3-notification' import * as d3 from 'd3' -import type { ZoomBehavior } from 'd3' import { onMounted, ref, watch, watchEffect } from 'vue' import type { HierarchyNode, HierarchyLink } from 'd3-hierarchy' import { flextree, type FlextreeNode } from 'd3-flextree' import type { MoveNodeRequest, MoveNodeResponse } from '@/types/services/MoveNode' import type { RemoveNodeRequest, RemoveNodeResponse } from '@/types/services/RemoveNode' -import type { ReplaceNodeRequest, ReplaceNodeResponse } from '@/types/services/ReplaceNode' import type { WireNodeDataRequest, WireNodeDataResponse } from '@/types/services/WireNodeData' const editor_store = useEditorStore() @@ -152,34 +144,10 @@ function resetView() { } function buildNodeMessage(node: DocumentedNode): NodeMsg { - function getDefaultValues(paramList: NodeData[], options?: NodeData[] | null) { - options = options || [] - - return paramList.map((x) => { - return { - key: x.key, - value: getDefaultValue(prettyprint_type(x.serialized_value), options) - } - }) - } - const default_options = getDefaultValues(node.options) - const options = default_options.map((x) => { - if (x.value.type === 'unset_optionref') { - const optionref: string = x.value.value as string - const optionTypeName = optionref.substring('Ref to "'.length, optionref.length - 1) - const optionType = default_options.find((x) => { - return x.key === optionTypeName - }) - if (optionType && optionType.value) { - return { - key: x.key, - value: getDefaultValue(optionType.value.value as string) - } - } - } + const options = node.options.map((opt: NodeData) => { return { - key: x.key, - value: x.value + key: opt.key, + value: getDefaultValue(prettyprint_type(opt.serialized_value), node.options) } as ParamData }) @@ -187,32 +155,7 @@ function buildNodeMessage(node: DocumentedNode): NodeMsg { module: node.module, node_class: node.node_class, name: node.name, - options: options.map((x) => { - const option: NodeData = { - key: x.key, - serialized_value: '', - serialized_type: '' - } - if (x.value.type === 'type') { - if (python_builtin_types.indexOf(x.value.value as string) >= 0) { - x.value.value = '__builtin__.' + x.value.value - } - option.serialized_value = JSON.stringify({ - 'py/type': x.value.value - }) - } else if (x.value.type.startsWith('__')) { - const py_value: PyLogger | PyOperator | PyOperand | PyEnum = x.value.value as - | PyLogger - | PyOperator - | PyOperand - | PyEnum - py_value['py/object' as keyof typeof py_value] = x.value.type.substring('__'.length) - option.serialized_value = JSON.stringify(x.value.value) - } else { - option.serialized_value = JSON.stringify(x.value.value) - } - return option - }), + options: serializeNodeOptions(options), child_names: [], inputs: [], outputs: [], diff --git a/src/components/EditableNode.vue b/src/components/EditableNode.vue index 55b8783..32775ac 100644 --- a/src/components/EditableNode.vue +++ b/src/components/EditableNode.vue @@ -28,16 +28,12 @@ * POSSIBILITY OF SUCH DAMAGE. --> diff --git a/src/components/JSONInput.vue b/src/components/JSONInput.vue index f718dd5..c5110a4 100644 --- a/src/components/JSONInput.vue +++ b/src/components/JSONInput.vue @@ -29,23 +29,13 @@ --> diff --git a/src/components/ParamInputs.vue b/src/components/ParamInputs.vue index 16052e7..909be6e 100644 --- a/src/components/ParamInputs.vue +++ b/src/components/ParamInputs.vue @@ -36,6 +36,23 @@ import MathOperandParam from './param_inputs/MathOperandParam.vue' import { computed } from 'vue' import { useEditNodeStore } from '@/stores/edit_node' import { useEditorStore } from '@/stores/editor' +import FilePathParam from './param_inputs/FilePathParam.vue' +import { + FilePath_Name, + MathBinaryOperator_Name, + MathOperandType_Name, + MathUnaryOperandType_Name, + MathUnaryOperator_Name, + OrderedDict_Name, + RosTopicType_Name, + RosTopicName_Name, + RosServiceType_Name, + RosServiceName_Name, + RosActionType_Name, + RosActionName_Name +} from '@/types/python_types' +import RosTypeParam from './param_inputs/RosTypeParam.vue' +import RosNameParam from './param_inputs/RosNameParam.vue' const props = defineProps<{ category: 'options' @@ -104,7 +121,7 @@ const json_attrs = computed(() => { case 'list': break case 'dict': - case 'collections.OrderedDict': + case OrderedDict_Name: break default: break @@ -163,28 +180,73 @@ function onFocus() { /> + + + + + + + + + + +
diff --git a/src/components/SelectedNode.vue b/src/components/SelectedNode.vue index 10f8cd8..bd64687 100644 --- a/src/components/SelectedNode.vue +++ b/src/components/SelectedNode.vue @@ -28,27 +28,14 @@ * POSSIBILITY OF SUCH DAMAGE. --> + + diff --git a/src/components/modals/SelectLocationModal.vue b/src/components/modals/SelectLocationModal.vue index bb07b9e..b5c080f 100644 --- a/src/components/modals/SelectLocationModal.vue +++ b/src/components/modals/SelectLocationModal.vue @@ -44,7 +44,7 @@ const emit = defineEmits<{ }>() // Specify valid file extensions as regex (multiple with | in the capture group) -const file_type_regex: RegExp = new RegExp('\.(yaml)') +const file_type_regex: RegExp = /\.(yaml)$/ const file_filter = ref(file_type_regex) @@ -102,7 +102,7 @@ function setLocation(path: string[], dir: boolean) { diff --git a/src/components/param_inputs/FilePathParam.vue b/src/components/param_inputs/FilePathParam.vue new file mode 100644 index 0000000..a60a74f --- /dev/null +++ b/src/components/param_inputs/FilePathParam.vue @@ -0,0 +1,105 @@ + + + + diff --git a/src/components/param_inputs/MathOperandParam.vue b/src/components/param_inputs/MathOperandParam.vue index cb40879..173c513 100644 --- a/src/components/param_inputs/MathOperandParam.vue +++ b/src/components/param_inputs/MathOperandParam.vue @@ -30,7 +30,8 @@ + + + + \ No newline at end of file diff --git a/src/components/param_inputs/RosTypeParam.vue b/src/components/param_inputs/RosTypeParam.vue new file mode 100644 index 0000000..65096ce --- /dev/null +++ b/src/components/param_inputs/RosTypeParam.vue @@ -0,0 +1,172 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/param_inputs/TypeParam.vue b/src/components/param_inputs/TypeParam.vue index 074dd0f..6db1011 100644 --- a/src/components/param_inputs/TypeParam.vue +++ b/src/components/param_inputs/TypeParam.vue @@ -31,7 +31,7 @@ import { useEditNodeStore } from '@/stores/edit_node' import { useEditorStore } from '@/stores/editor' import { useMessasgeStore } from '@/stores/message' -import type { Message, ParamData } from '@/types/types' +import type { ParamData } from '@/types/types' import { python_builtin_types } from '@/utils' import { computed, ref } from 'vue' @@ -44,12 +44,23 @@ const editor_store = useEditorStore() const edit_node_store = useEditNodeStore() const messages_store = useMessasgeStore() -let messages_results = ref([]) +let messages_results = ref([]) const param = computed(() => edit_node_store.new_node_options.find((x) => x.key === props.data_key) ) +const display_value = computed(() => { + if (param.value === undefined) { + return '' + } + let value = param.value.value.value as string + if (python_builtin_types.includes(value)) { + value = 'builtins.' + value + } + return value +}) + // These track two conditions for displaying the result dropdown. // One is for focusing the input, the other for navigating the result menu let hide_results = ref(true) @@ -65,17 +76,13 @@ function onChange(event: Event) { let new_type_name = target.value || '' new_type_name = new_type_name.replace('__builtin__.', '').replace('builtins.', '') const results = messages_store.messages_fuse.search(new_type_name) - messages_results.value = results.slice(0, 5).map((x) => x.item) + messages_results.value = results.map((x) => x.item) - if (python_builtin_types.indexOf(new_type_name) >= 0) { - edit_node_store.updateParamValue(props.category, props.data_key, '__builtin__.' + new_type_name) - } else { - edit_node_store.updateParamValue(props.category, props.data_key, new_type_name) - } + edit_node_store.updateParamValue(props.category, props.data_key, new_type_name) } -function selectSearchResult(search_result: Message) { - edit_node_store.updateParamValue(props.category, props.data_key, search_result.msg) +function selectSearchResult(search_result: string) { + edit_node_store.updateParamValue(props.category, props.data_key, search_result) releaseDropdown() } @@ -104,7 +111,7 @@ function releaseDropdown() {
- {{ result.msg }} + + {{ result.replace(/\./g, '.\u200B') }}
@@ -143,15 +151,5 @@ function releaseDropdown() { diff --git a/src/stores/edit_node.ts b/src/stores/edit_node.ts index e48a9cf..3c63e60 100644 --- a/src/stores/edit_node.ts +++ b/src/stores/edit_node.ts @@ -33,7 +33,7 @@ import { defineStore } from 'pinia' import { computed, ref } from 'vue' import { useEditorStore } from './editor' import { useNodesStore } from './nodes' -import { getDefaultValue, prettyprint_type } from '@/utils' +import { getDefaultValue, prettyprint_type, serializeNodeOptions } from '@/utils' export enum EditorSelectionSource { NONE = 'none', @@ -198,7 +198,7 @@ export const useEditNodeStore = defineStore('edit_node', () => { const type = prettyprint_type(x.serialized_type) let json_value = JSON.parse(x.serialized_value) if (type === 'type') { - json_value = json_value['py/type'].replace('__builtin__.', '').replace('builtins.', '') + json_value = json_value['py/type'] } return { key: x.key, @@ -312,38 +312,25 @@ export const useEditNodeStore = defineStore('edit_node', () => { return } - const option_ref_keys = reference_node.value.options - .filter((x) => prettyprint_type(x.serialized_value).startsWith('OptionRef(')) - .map((x): [string, string] => [ - x.key, - prettyprint_type(x.serialized_value).substring( - 'OptionRef('.length, - prettyprint_type(x.serialized_value).length - 1 + function findOptionRefs(ref_list: NodeData[]): [string, string][] { + return ref_list.filter((x) => + prettyprint_type(x.serialized_value).startsWith('OptionRef(') ) - ]) - .filter((x) => x[1] === key) - - const input_ref_keys = reference_node.value.inputs - .filter((x) => prettyprint_type(x.serialized_value).startsWith('OptionRef(')) - .map((x): [string, string] => [ - x.key, - prettyprint_type(x.serialized_value).substring( - 'OptionRef('.length, - prettyprint_type(x.serialized_value).length - 1 - ) - ]) - .filter((x) => x[1] === key) - - const output_ref_keys = reference_node.value.outputs - .filter((x) => prettyprint_type(x.serialized_value).startsWith('OptionRef(')) - .map((x): [string, string] => [ - x.key, - prettyprint_type(x.serialized_value).substring( - 'OptionRef('.length, - prettyprint_type(x.serialized_value).length - 1 - ) - ]) - .filter((x) => x[1] === key) + .map((x): [string, string] => [ + x.key, + prettyprint_type(x.serialized_value).substring( + 'OptionRef('.length, + prettyprint_type(x.serialized_value).length - 1 + ) + ]) + .filter((x) => x[1] === key) + } + + const option_ref_keys = findOptionRefs(reference_node.value.options) + + const input_ref_keys = findOptionRefs(reference_node.value.inputs) + + const output_ref_keys = findOptionRefs(reference_node.value.outputs) new_node_options.value = new_node_options.value.map(map_fun) function resolve_refs(refs: [string, string][], current_item: ParamData): ParamData { @@ -358,7 +345,7 @@ export const useEditNodeStore = defineStore('edit_node', () => { // referenced option return { key: current_item.key, - value: getDefaultValue(opt_value.replace('__builtin__.', '').replace('builtins.', '')) + value: getDefaultValue(opt_value) } } } @@ -385,6 +372,21 @@ export const useEditNodeStore = defineStore('edit_node', () => { } } + function buildNodeMsg(): NodeMsg { + return { + module: new_node_module.value, + node_class: new_node_class.value, + name: new_node_name.value, + max_children: 0, + child_names: [], + options: serializeNodeOptions(new_node_options.value), + inputs: [], + outputs: [], + version: '', + state: '' + } + } + return { selected_node, reference_node, @@ -411,6 +413,7 @@ export const useEditNodeStore = defineStore('edit_node', () => { changeCopyMode, changeNodeName, changeNodeClass, - updateParamValue + updateParamValue, + buildNodeMsg } }) diff --git a/src/stores/editor.ts b/src/stores/editor.ts index 89ba0c7..c70d32a 100644 --- a/src/stores/editor.ts +++ b/src/stores/editor.ts @@ -34,11 +34,9 @@ import type { NodeDataWiring, DocumentedNode, TreeMsg, - NodeMsg, TrimmedNode, DataEdgeTerminal } from '@/types/types' -import { useNodesStore } from './nodes' import { notify } from '@kyvg/vue3-notification' export enum EditorSkin { @@ -53,8 +51,6 @@ export type SelectedSubtree = { } export const useEditorStore = defineStore('editor', () => { - const nodes_store = useNodesStore() - const tree = ref(undefined) //const debug_info = ref(undefined) const subtree_states = ref([]) diff --git a/src/stores/message.ts b/src/stores/message.ts index 4094f5e..28e966d 100644 --- a/src/stores/message.ts +++ b/src/stores/message.ts @@ -27,8 +27,8 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -import { MessageType, type Message } from '@/types/types' -import Fuse from 'fuse.js' +import type { Channel, Channels, MessageTypes } from '@/types/types' +import Fuse, { type IFuseOptions } from 'fuse.js' import { defineStore } from 'pinia' import { ref } from 'vue' @@ -40,136 +40,72 @@ export const useMessasgeStore = defineStore('messages', () => { distance: 100, maxPatternLength: 200, minMatchCharLength: 1, - keys: ['msg'], isCaseSensitive: false, ignoreLocation: true, useExtendedSearch: true - } - const messages = ref([]) - const messages_fuse = ref>(new Fuse([], messages_fuse_options)) + } as IFuseOptions + const messages = ref([]) + const messages_fuse = ref>(new Fuse([], messages_fuse_options)) const messages_available = ref(false) // These additional fuses are meant to substitute/replace the above messages_fuse // to allow to search specific kinds of ros types dependent on what is needed. - let ros_fuse_options = structuredClone(messages_fuse_options) - ros_fuse_options.keys = [] - const ros_msg_fuse = ref>(new Fuse([], ros_fuse_options)) - const ros_srv_fuse = ref>(new Fuse([], ros_fuse_options)) - const ros_action_fuse = ref>(new Fuse([], ros_fuse_options)) + const ros_type_fuse_options = structuredClone(messages_fuse_options) + ros_type_fuse_options.keys = [] + const ros_name_fuse_options = structuredClone(messages_fuse_options) + ros_name_fuse_options.keys = ['name', 'type'] + + const ros_topic_type_fuse = ref>(new Fuse([], ros_type_fuse_options)) + const ros_service_type_fuse = ref>(new Fuse([], ros_type_fuse_options)) + const ros_action_type_fuse = ref>(new Fuse([], ros_type_fuse_options)) + + const ros_topic_name_fuse = ref>(new Fuse([], ros_name_fuse_options)) + const ros_service_name_fuse = ref>(new Fuse([], ros_name_fuse_options)) + const ros_action_name_fuse = ref>(new Fuse([], ros_name_fuse_options)) function areMessagesAvailable(available: boolean) { messages_available.value = available } - function mapMessageTypes(message: Message): Message[] { - const message_parts = message.msg.split('/') + function addMessageTypes(message: string): void { + const message_parts = message.split('/') if (message_parts.length !== 3) { - return [] - } - if (message.service) { - const new_msg = message_parts[0] + '.srv.' + message_parts[2] - return [ - { - msg: new_msg, - service: true, - action: false, - type: MessageType.MESSAGE //TODO is this correct? - }, - { - msg: new_msg + '.Request', - service: true, - action: false, - type: MessageType.REQUEST - }, - { - msg: new_msg + '.Response', - service: true, - action: false, - type: MessageType.RESPONSE - } - ] + return } - if (message.action) { - const new_msg = message_parts[0] + '.action.' + message_parts[2] - return [ - { - msg: new_msg, - action: true, - service: false, - type: MessageType.MESSAGE //TODO is this correct? - }, - { - msg: new_msg + '.Goal', - action: true, - service: false, - type: MessageType.GOAL - }, - { - msg: new_msg + '.Result', - action: true, - service: false, - type: MessageType.RESULT - }, - { - msg: new_msg + '.Feedback', - action: true, - service: false, - type: MessageType.FEEDBACK - } - ] - } - return [ - { - msg: message_parts[0] + '.msg.' + message_parts[2], - service: false, - action: false, - type: MessageType.MESSAGE - } - ] + messages.value.push( + message_parts[0] + '.msg.' + message_parts[2] + ) } - // This is a temporary function to avoid code duplication with mapMessageTypes - // it populates the additional ros_fuses, but the parsing of the mapMessageTypes - // output is a bit convoluted and not stable against changes. - //TODO if the big messages_fuse is ever phased out, merge and redo this with - // the parsing in mapMessageTypes - function fillRosFuses() { - ros_msg_fuse.value.setCollection([]) - ros_srv_fuse.value.setCollection([]) - ros_action_fuse.value.setCollection([]) + function updateAvailableMessages(new_messages: MessageTypes) { + messages.value = [] + ros_topic_type_fuse.value.setCollection(new_messages.topics) + ros_service_type_fuse.value.setCollection(new_messages.services) + ros_action_type_fuse.value.setCollection(new_messages.actions) - messages.value.forEach((element) => { - // All service and action compontents (eg .Request .Response) are messages - if (element.msg.split('.').length > 3) { - ros_msg_fuse.value.add(element.msg) - return - } - if (element.service) { - ros_srv_fuse.value.add(element.msg) - return - } - if (element.action) { - ros_action_fuse.value.add(element.msg) - return - } - ros_msg_fuse.value.add(element.msg) - }) - } + new_messages.topics.forEach(addMessageTypes) - function updateAvailableMessages(new_messages: Message[]) { - messages.value = new_messages.flatMap(mapMessageTypes) - fillRosFuses() messages_fuse.value.setCollection(messages.value) } + function updateMessageChannels(new_channels: Channels) { + ros_topic_name_fuse.value.setCollection(new_channels.topics) + ros_service_name_fuse.value.setCollection(new_channels.services) + ros_action_name_fuse.value.setCollection(new_channels.actions) + } + return { messages, messages_fuse, messages_available, - ros_msg_fuse, - ros_srv_fuse, - ros_action_fuse, + ros_topic_type_fuse, + ros_service_type_fuse, + ros_action_type_fuse, + ros_topic_name_fuse, + ros_service_name_fuse, + ros_action_name_fuse, areMessagesAvailable, - updateAvailableMessages + updateAvailableMessages, + updateMessageChannels } }) diff --git a/src/stores/ros.ts b/src/stores/ros.ts index 138965f..014fc69 100644 --- a/src/stores/ros.ts +++ b/src/stores/ros.ts @@ -30,7 +30,7 @@ import { ref, computed } from 'vue' import { defineStore } from 'pinia' import ROSLIB from 'roslib' -import type { Packages, Messages, TreeMsg, SubtreeInfo } from '@/types/types' +import type { Packages, MessageTypes, TreeMsg, SubtreeInfo, Channels } from '@/types/types' import type { ServicesForTypeRequest, ServicesForTypeResponse @@ -186,11 +186,20 @@ export const useROSStore = defineStore( reconnect_on_close: true }) ) - const messages_sub = ref>( + const messages_sub = ref>( new ROSLIB.Topic({ ros: ros.value, - name: namespace.value + 'messages', - messageType: 'ros_bt_py_interfaces/msg/Messages', + name: namespace.value + 'message_types', + messageType: 'ros_bt_py_interfaces/msg/MessageTypes', + latch: true, + reconnect_on_close: true + }) + ) + const channels_sub = ref>( + new ROSLIB.Topic({ + ros: ros.value, + name: namespace.value + 'message_channels', + messageType: 'ros_bt_py_interfaces/msg/MessageChannels', latch: true, reconnect_on_close: true }) @@ -390,8 +399,8 @@ export const useROSStore = defineStore( messages_sub.value.removeAllListeners() messages_sub.value = new ROSLIB.Topic({ ros: ros.value, - name: namespace.value + 'messages', - messageType: 'ros_bt_py_interfaces/msg/Messages', + name: namespace.value + 'message_types', + messageType: 'ros_bt_py_interfaces/msg/MessageTypes', latch: true, reconnect_on_close: true }) @@ -406,6 +415,16 @@ export const useROSStore = defineStore( reconnect_on_close: true }) + channels_sub.value.unsubscribe() + channels_sub.value.removeAllListeners() + channels_sub.value = new ROSLIB.Topic({ + ros: ros.value, + name: namespace.value + 'message_channels', + messageType: 'ros_bt_py_interfaces/msg/MessageChannels', + latch: true, + reconnect_on_close: true + }) + set_publish_subtrees_service.value = new ROSLIB.Service({ ros: ros.value, name: namespace.value + 'debug/set_publish_subtrees', @@ -616,6 +635,7 @@ export const useROSStore = defineStore( subtree_info_sub, packages_sub, messages_sub, + channels_sub, connect, setUrl, changeNamespace, diff --git a/src/types/python_types.ts b/src/types/python_types.ts new file mode 100644 index 0000000..4463eaf --- /dev/null +++ b/src/types/python_types.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2024 FZI Forschungszentrum Informatik + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the {copyright_holder} nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +import type { PyObject, PyReduce } from './types' + +const PyDefaultValues = new Map() + +export const OrderedDict_Name = 'collections.OrderedDict' +PyDefaultValues.set(OrderedDict_Name, { + 'py/reduce': [{ 'py/type': OrderedDict_Name }, { 'py/tuple': [[]] }, null, null, null] +} as PyReduce ) + +export type PyLogger = PyObject & { + logger_level: string +} +export const LoggerLevel_Name = 'ros_bt_py.ros_helpers.LoggerLevel' +PyDefaultValues.set(LoggerLevel_Name, { + 'py/object': LoggerLevel_Name, + logger_level: 'Debug' +} as PyLogger ) + +export type PyOperator = PyObject & { + operator: string +} +export const MathUnaryOperator_Name = 'ros_bt_py.helpers.MathUnaryOperator' +PyDefaultValues.set(MathUnaryOperator_Name, { + 'py/object': MathUnaryOperator_Name, + operator: 'sqrt' +} as PyOperator ) +export const MathBinaryOperator_Name = 'ros_bt_py.helpers.MathBinaryOperator' +PyDefaultValues.set(MathBinaryOperator_Name, { + 'py/object': MathBinaryOperator_Name, + operator: '+' +} as PyOperator ) + +export type PyOperand = PyObject & { + operand_type: string +} +export const MathOperandType_Name = 'ros_bt_py.helpers.MathOperandType' +PyDefaultValues.set(MathOperandType_Name, { + 'py/object': MathOperandType_Name, + operand_type: 'float' +} as PyOperand ) +export const MathUnaryOperandType_Name = 'ros_bt_py.helpers.MathUnaryOperandType' +PyDefaultValues.set(MathUnaryOperandType_Name, { + 'py/object': MathUnaryOperandType_Name, + operand_type: 'float' +} as PyOperand ) + +export type PyEnum = PyObject & { + enum_value: string + field_names: string[] +} +export const EnumValue_Name = 'ros_bt_py.ros_helpers.EnumValue' +PyDefaultValues.set(EnumValue_Name, { + 'py/object': EnumValue_Name, + enum_value: '', + field_names: [] +} as PyEnum ) + +export type PyFilePath = PyObject & { + path: string +} +export const FilePath_Name = 'ros_bt_py.custom_types.FilePath' +PyDefaultValues.set(FilePath_Name, { + 'py/object': FilePath_Name, + path: '' +} as PyFilePath ) + +export type RosType = PyObject & { + type_str: string +} +export const RosTopicType_Name = 'ros_bt_py.custom_types.RosTopicType' +PyDefaultValues.set(RosTopicType_Name, { + 'py/object': RosTopicType_Name, + type_str: '' +} as RosType ) +export const RosServiceType_Name = 'ros_bt_py.custom_types.RosServiceType' +PyDefaultValues.set(RosServiceType_Name, { + 'py/object': RosServiceType_Name, + type_str: '' +} as RosType ) +export const RosActionType_Name = 'ros_bt_py.custom_types.RosActionType' +PyDefaultValues.set(RosActionType_Name, { + 'py/object': RosActionType_Name, + type_str: '' +} as RosType ) + +export type RosName = PyObject & { + name: string +} +export const RosTopicName_Name = 'ros_bt_py.custom_types.RosTopicName' +PyDefaultValues.set(RosTopicName_Name, { + 'py/object': RosTopicName_Name, + name: '' +} as RosName ) +export const RosServiceName_Name = 'ros_bt_py.custom_types.RosServiceName' +PyDefaultValues.set(RosServiceName_Name, { + 'py/object': RosServiceName_Name, + name: '' +} as RosName ) +export const RosActionName_Name = 'ros_bt_py.custom_types.RosActionName' +PyDefaultValues.set(RosActionName_Name, { + 'py/object': RosActionName_Name, + name: '' +} as RosName ) + +export function isPythonTypeWithDefault(type: string) { + return PyDefaultValues.has(type) +} + +export function getPythonTypeDefault(type: string) { + return structuredClone( + PyDefaultValues.get(type) + ) +} diff --git a/src/types/services/GetMessageFields.ts b/src/types/services/GetMessageFields.ts index 6c1d662..0afd8d7 100644 --- a/src/types/services/GetMessageFields.ts +++ b/src/types/services/GetMessageFields.ts @@ -27,13 +27,12 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -import type { MessageType } from '../types' export type GetMessageFieldsRequest = { message_type: string service: boolean action: boolean - type: MessageType + type: number } export type GetMessageFieldsResponse = { diff --git a/src/types/types.ts b/src/types/types.ts index 48ef77f..df6199e 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -86,24 +86,21 @@ export type Packages = { packages: Package[] } -export type Message = { - msg: string - service: boolean - action: boolean - type: MessageType +export type MessageTypes = { + topics: string[] + services: string[] + actions: string[] } -export const enum MessageType { - MESSAGE = 0, - REQUEST = 1, - RESPONSE = 2, - GOAL = 3, - FEEDBACK = 4, - RESULT = 5 +export type Channel = { + name: string + type: string } -export type Messages = { - messages: Message[] +export type Channels = { + topics: Channel[] + services: Channel[] + actions: Channel[] } export type DocumentedNode = NodeMsg & { @@ -136,25 +133,10 @@ export type PackageStructure = { type: FileType } +export type PyObject = { 'py/object': string } + export type PyType = { 'py/type': string } export type PyTuple = { 'py/tuple': never[][] } -export type PyLogger = { - 'py/object': string - logger_level: string -} -export type PyOperator = { - 'py/object': string - operator: string -} -export type PyOperand = { - 'py/object': string - operand_type: string -} -export type PyEnum = { - 'py/object': string - enum_value: string - field_names: string[] -} export type PyReduce = { 'py/reduce': (PyType | PyTuple | null)[] } @@ -165,10 +147,7 @@ export type ValueTypes = | [] | Record | PyReduce - | PyLogger - | PyOperator - | PyOperand - | PyEnum + | PyObject export type ParamType = { type: string diff --git a/src/utils.ts b/src/utils.ts index 2dbec88..2a3e0b9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -27,18 +27,16 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ -import { useMessasgeStore } from './stores/message' -import type { - NodeData, - NodeDataLocation, - TreeMsg, - ValueTypes, - DataEdgeTerminal, - Message +import { getPythonTypeDefault, isPythonTypeWithDefault } from './types/python_types' +import type { + NodeData, + TreeMsg, + DataEdgeTerminal, + ParamData, + PyObject, + ParamType } from './types/types' -import { IOKind, MessageType } from './types/types' - -import * as d3 from 'd3' +import { IOKind } from './types/types' // uuid is used to assign unique IDs to tags so we can use labels properly let idx = 0 @@ -71,13 +69,15 @@ export function typesCompatible(a: DataEdgeTerminal, b: DataEdgeTerminal) { return prettyprint_type(from.type) === prettyprint_type(to.type) } +//TODO This appears to be wrong or outdated. +// How do we want to handle unsupported types? export const python_builtin_types = [ 'int', 'float', - 'long', + //'long', 'str', - 'basestring', - 'unicode', + //'basestring', + //'unicode', 'bool', 'list', 'dict', @@ -125,7 +125,7 @@ export function prettyprint_type(jsonpickled_type: string) { export function getDefaultValue( typeName: string, options: NodeData[] | null = null -): { type: string; value: ValueTypes } { +): ParamType { if (typeName === 'type') { return { type: 'type', @@ -179,9 +179,11 @@ export function getDefaultValue( }) if (optionType) { // This double call is necessary to dereference first the type of the optionref target and then its default value - //TODO Check if this is consistent (are the `options` always populated in the same manner?) return getDefaultValue( - getDefaultValue(prettyprint_type(optionType.serialized_value), options).value as string, + getDefaultValue( + prettyprint_type(optionType.serialized_value), + options + ).value as string, options ) } else { @@ -190,70 +192,14 @@ export function getDefaultValue( value: 'Ref to "' + optionTypeName + '"' } } - } else if (typeName === 'collections.OrderedDict') { - return { - type: 'collections.OrderedDict', - value: { - 'py/reduce': [ - { 'py/type': 'collections.OrderedDict' }, - { 'py/tuple': [[]] }, - null, - null, - null - ] - } - } - } else if (typeName === 'ros_bt_py.ros_helpers.LoggerLevel') { - return { - type: 'ros_bt_py.ros_helpers.LoggerLevel', - value: { - 'py/object': 'ros_bt_py.ros_helpers.LoggerLevel', - logger_level: 'Debug' - } - } - } else if (typeName === 'ros_bt_py.helpers.MathUnaryOperator') { - return { - type: 'ros_bt_py.helpers.MathUnaryOperator', - value: { - 'py/object': 'ros_bt_py.helpers.MathUnaryOperator', - operator: 'sqrt' - } - } - } else if (typeName === 'ros_bt_py.helpers.MathBinaryOperator') { + // This checks all types defined in `python_types` + // which provide default values + } else if (isPythonTypeWithDefault(typeName)) { return { - type: 'ros_bt_py.helpers.MathBinaryOperator', - value: { - 'py/object': 'ros_bt_py.helpers.MathBinaryOperator', - operator: '+' - } - } - } else if (typeName === 'ros_bt_py.helpers.MathOperandType') { - return { - type: 'ros_bt_py.helpers.MathOperandType', - value: { - 'py/object': 'ros_bt_py.helpers.MathOperandType', - operand_type: 'float' - } - } - } else if (typeName === 'ros_bt_py.helpers.MathUnaryOperandType') { - return { - type: 'ros_bt_py.helpers.MathUnaryOperandType', - value: { - 'py/object': 'ros_bt_py.helpers.MathUnaryOperandType', - operand_type: 'float' - } - } - } else if (typeName === 'ros_bt_py.ros_helpers.EnumValue') { - return { - type: 'ros_bt_py.ros_helpers.EnumValue', - value: { - 'py/object': 'ros_bt_py.ros_helpers.EnumValue', - enum_value: '', - field_names: [] - } + type: typeName, + value: getPythonTypeDefault(typeName) || {} } } else { - //TODO should this check for general ros_types? return { type: '__' + typeName, value: {} @@ -261,6 +207,32 @@ export function getDefaultValue( } } +export function serializeNodeOptions(node_options: ParamData[]): NodeData[] { + return node_options.map((x) => { + const option: NodeData = { + key: x.key, + serialized_value: '', + serialized_type: '' // This is left blank intentionally + } + if (x.value.type === 'type') { + if (python_builtin_types.indexOf(x.value.value as string) >= 0) { + x.value.value = 'builtins.' + x.value.value; + } + option.serialized_value = JSON.stringify({ + 'py/type': x.value.value + }) + } else if (x.value.type.startsWith('__')) { + //TODO This should be changed to not generate "bad" defaults + const val = x.value.value as PyObject + val['py/object'] = x.value.type.substring('__'.length) + option.serialized_value = JSON.stringify(x.value.value) + } else { + option.serialized_value = JSON.stringify(x.value.value) + } + return option + }) +} + // Get the distance between two sets of coordinates (expected to be // arrays with 2 elements each) export function getDist(a: number[], b: number[]) { @@ -271,45 +243,6 @@ export function treeIsEditable(tree_msg: TreeMsg) { return tree_msg.state === 'EDITABLE' } -export function getMessageType(str: string): Message { - const message_store = useMessasgeStore() // This doesn't work outside functions in .ts files - const message_parts = str.split('.') - if (message_parts.length < 3) { - console.error('Invalid message passed') - return { msg: '', action: false, service: false, type: MessageType.MESSAGE } - } - - let new_message_parts = message_parts.slice(0, 2) - // Standardize type .../.../_name/Name to .../.../Name - if ( - message_parts.length > 3 && - message_parts[2] === message_parts[3].replace(/[A-Z]/g, (x) => '_' + x.toLowerCase()) - ) { - new_message_parts.push(...message_parts.slice(3)) - } else { - new_message_parts.push(...message_parts.slice(2)) - } - - // Caution, since this is the store member, don't edit it - const msg_ref = message_store.messages.find((item) => item.msg === new_message_parts.join('.')) - - if (msg_ref === undefined) { - console.error('Invalid message passed') - return { - msg: new_message_parts.slice(0, 3).join('/'), - action: false, - service: false, - type: MessageType.MESSAGE - } - } - return { - msg: new_message_parts.slice(0, 3).join('/'), - type: msg_ref.type, - action: msg_ref.action, - service: msg_ref.service - } -} - export function getShortDoc(doc: string) { if (!doc || doc == null || doc.length == 0) { return 'No documentation provided'