diff --git a/README.md b/README.md index 43e65d8..221d9c4 100644 --- a/README.md +++ b/README.md @@ -62,5 +62,17 @@ docker exec -it ggcs-rosbridge /bin/bash -c \ ```bash docker exec -it ggcs-rosbridge /bin/bash -c \ 'source /opt/ros/humble/setup.bash && \ - ros2 topic pub /gps sensor_msgs/msg/NavSatFix "{latitude: 36.000, longitude: 42.000}"' + ros2 topic pub /roll std_msgs/msg/Float64 "{data: 30.0}"' +``` + +```bash +docker exec -it ggcs-rosbridge /bin/bash -c \ + 'source /opt/ros/humble/setup.bash && \ + ros2 topic pub /pitch std_msgs/msg/Float64 "{data: 30.0}"' +``` + +```bash +docker exec -it ggcs-rosbridge /bin/bash -c \ + 'source /opt/ros/humble/setup.bash && \ + ros2 topic pub /gps sensor_msgs/msg/NavSatFix "{latitude: 36.000, longitude: 42.000, altitude: 10.5}"' ``` diff --git a/src/config.ts b/src/config.ts index c580330..f4f2ea1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -25,11 +25,6 @@ const targetSpeedTopic: Topic = { type: "std_msgs/msg/Float64" } -const altitudeTopic: Topic = { - name: "/altitude", - type: "std_msgs/msg/Float64" -}; - const accelTopic: Topic = { name: "/accel", type: "std_msgs/msg/Float64" @@ -65,8 +60,8 @@ const hudWidget: Widget = { }, altitude: { type: "subscriber", - topic: altitudeTopic, - topicField: ".data" + topic: gpsTopic, + topicField: ".altitude" } } }; @@ -178,6 +173,7 @@ export const config: Entity = { setSpeed: { type: "publisher", topic: targetSpeedTopic, + topicField: ".data" }, targetSpeed: { type: "subscriber", diff --git a/src/interfaces.ts b/src/interfaces.ts index c110b42..9ab9fe7 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -41,6 +41,7 @@ export interface ConstantValue { export interface PublisherValue { type: typeof ValueTypes.publisher; topic: Topic; + topicField?: string; } export type Value = SubscriberValue | ServiceValue | ConstantValue | PublisherValue; diff --git a/src/middleware/binders/publishers.ts b/src/middleware/binders/publishers.ts index 9926071..2c77f23 100644 --- a/src/middleware/binders/publishers.ts +++ b/src/middleware/binders/publishers.ts @@ -8,15 +8,32 @@ export type PublisherBinding = { instanceId: string; attrName: string; topic: Topic; + topicField?: string; // optional dot-path like ".data" }; +function setByPath(target: any, dotPath: string, value: any) { + const parts = dotPath.replace(/^\./, "").split(".").filter(Boolean); + let cur = target; + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + const isLast = i === parts.length - 1; + if (isLast) { + cur[p] = value; + } else { + cur[p] = cur[p] ?? {}; + cur = cur[p]; + } + } +} + export function collectPublisherBindings(config: unknown): PublisherBinding[] { return collectBindings( config, (e) => e.type === ValueTypes.publisher && isObject(e.topic), ({ instanceId, attrName, entry }) => { const topic = entry.topic as Topic; - return { instanceId, attrName, topic }; + const topicField = typeof (entry as any).topicField === "string" ? String((entry as any).topicField) : undefined; + return { instanceId, attrName, topic, topicField }; } ); } @@ -27,13 +44,36 @@ export function attachPublisherBindings(bindings?: PublisherBinding[]) { const disposers: Array<() => void> = []; for (const b of list) { - console.log("attachPublisherBindings", b); const pub = sharedTopics.getPublisher({ name: b.topic.name, type: b.topic.type }); - const unregister = publisherBus.register(b.instanceId, b.attrName, (msg: any) => { + + // Expose function: accepts primitive or full message. + const unregister = publisherBus.register(b.instanceId, b.attrName, (input: any) => { try { + let msg: any = input; + if (b.topicField) { + if (input !== null && typeof input === "object") { + // If field missing, set it with the whole input (common case: user passes primitive -> not here) + const parts = b.topicField.replace(/^\./, "").split(".").filter(Boolean); + let cur = msg, missing = false; + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + const isLast = i === parts.length - 1; + if (!(p in cur)) { + missing = true; + if (!isLast) cur[p] = {}; + } + if (isLast) break; + cur = cur[p]; + } + if (missing) setByPath(msg, b.topicField, input); + } else { + // Primitive or non-object: wrap into an object via topicField + msg = {}; + setByPath(msg, b.topicField, input); + } + } pub.publish(msg); - } catch { - } + } catch {} }); disposers.push(() => { diff --git a/src/widgets/SampleWidget.tsx b/src/widgets/SampleWidget.tsx index 0c34e12..e597e37 100644 --- a/src/widgets/SampleWidget.tsx +++ b/src/widgets/SampleWidget.tsx @@ -23,7 +23,9 @@ export function SampleWidget({ speedLabel }: Props) { const publishSpeed = () => { const v = Number(targetSpeed); if (!Number.isFinite(v)) return; - publishSetSpeed({ data: v }); + // Middleware now supports topicField mapping, so send primitive + publishSetSpeed(v); + // publishSetSpeed({ data: v }); // This is also valid }; let _speed: string = "—";