Simple Service

// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtRos2.StdSrvs

Window {
    id: root
    width: 480
    height: 500
    visible: true
    title: `Service ${lampService.topic}`

    property int cycles: 0
    readonly property bool burnedOut: cycles >= 10
    property string lastRequest: qsTr("No requests received yet.")

    Node {
        id: rosNode
        nodeName: "simple_service_node"

        SetBoolServiceServer {
            id: lampService
            topic: topicField.text

            // Effects and bookkeeping are imperative, in the signal handler...
            onRequestReceived: request => {
                ++root.cycles
                root.lastRequest = `Request #${root.cycles}: data=${request}`
            }

            // ...while the response is declarative: each request is answered
            // with the current value of this binding. It re-evaluates after
            // request and onRequestReceived update, so root.cycles is already
            // current — the request that burns out the bulb gets the failure.
            response: root.burnedOut
                ? ({ success: false, message: "the bulb is burned out" })
                : ({ success: true, message: `lamp is now ${lampService.request ? "on" : "off"}` })

            // Deferred style, selected by the switch: a callable handler takes
            // precedence over the declarative response. This one parks the
            // reply and answers from the timer below when the "work" is done.
            handler: delaySwitch.checked
                ? (request, reply) => {
                      replyTimer.pendingReplies.push(reply)
                      replyTimer.restart()
                  }
                : undefined
        }
    }

    Timer {
        id: replyTimer
        interval: 1000
        property var pendingReplies: []
        onTriggered: {
            for (const reply of pendingReplies)
                reply.send(lampService.response)   // same declarative value, later
            pendingReplies = []
        }
    }

    GridLayout {
        columns: 2
        columnSpacing: 10
        anchors.fill: parent
        anchors.margins: 12

        Label {
            text: rosNode.initialized
                  ? (lampService.active ? qsTr("Service ready") : qsTr("Service inactive"))
                  : qsTr("Initializing ROS 2 Node...")
            font.bold: true
            Layout.columnSpan: 2
        }

        Label {
            text: qsTr("Topic")
            font.bold: true
        }
        TextField {
            id: topicField
            Layout.fillWidth: true
            text: "/simple_service_lamp"
        }

        Label {
            text: qsTr("Lamp")
            font.bold: true
        }
        Rectangle {
            width: 48
            height: 48
            radius: 24
            color: root.burnedOut ? "black"
                 : lampService.request ? "gold" : "dimgray"
            border.color: "black"
            Behavior on color { ColorAnimation { duration: 150 } }
        }

        Label {
            text: qsTr("Bulb wear")
        }
        RowLayout {
            Label {
                text: root.burnedOut ? qsTr("%1 cycles — burned out!").arg(root.cycles)
                                     : qsTr("%1 of 10 cycles").arg(root.cycles)
            }
            Button {
                text: qsTr("Replace bulb")
                visible: root.burnedOut
                onClicked: root.cycles = 0
            }
        }

        Switch {
            id: delaySwitch
            text: qsTr("Delay the response by 1 s (deferred reply)")
            Layout.columnSpan: 2
        }

        Label {
            text: qsTr("Call with")
            font.bold: true
        }
        TextField {
            Layout.fillWidth: true
            readOnly: true
            text: `ros2 service call ${lampService.topic} std_srvs/srv/SetBool "{data: true}"`
            onTextChanged: cursorPosition = 0
        }

        TextArea {
            Layout.columnSpan: 2
            Layout.fillWidth: true
            Layout.fillHeight: true
            readOnly: true
            wrapMode: TextEdit.Wrap
            text: root.lastRequest
        }
    }
}