stinger-ipc 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl

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.
Files changed (48) hide show
  1. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/METADATA +3 -3
  2. stinger_ipc-0.0.2.dist-info/RECORD +48 -0
  3. stinger_ipc-0.0.2.dist-info/entry_points.txt +4 -0
  4. stingeripc/args.py +6 -5
  5. stingeripc/asyncapi.py +249 -167
  6. stingeripc/components.py +301 -136
  7. stingeripc/connection.py +2 -1
  8. stingeripc/exceptions.py +1 -2
  9. stingeripc/interface.py +8 -4
  10. stingeripc/lang_symb.py +68 -0
  11. stingeripc/templates/cpp/CMakeLists.txt.jinja2 +26 -0
  12. stingeripc/templates/cpp/examples/client_main.cpp.jinja2 +47 -0
  13. stingeripc/templates/cpp/examples/server_main.cpp.jinja2 +35 -0
  14. stingeripc/templates/cpp/include/broker.hpp.jinja2 +132 -0
  15. stingeripc/templates/cpp/include/client.hpp.jinja2 +53 -0
  16. stingeripc/templates/cpp/include/enums.hpp.jinja2 +17 -0
  17. stingeripc/templates/cpp/include/ibrokerconnection.hpp.jinja2 +42 -0
  18. stingeripc/templates/cpp/include/return_types.hpp.jinja2 +14 -0
  19. stingeripc/templates/cpp/include/server.hpp.jinja2 +44 -0
  20. stingeripc/templates/cpp/include/structs.hpp.jinja2 +13 -0
  21. stingeripc/templates/cpp/src/broker.cpp.jinja2 +243 -0
  22. stingeripc/templates/cpp/src/client.cpp.jinja2 +202 -0
  23. stingeripc/templates/cpp/src/server.cpp.jinja2 +170 -0
  24. stingeripc/templates/markdown/index.md.jinja2 +142 -0
  25. stingeripc/templates/python/__init__.py.jinja2 +1 -0
  26. stingeripc/templates/python/client.py.jinja2 +309 -0
  27. stingeripc/templates/python/connection.py.jinja2 +164 -0
  28. stingeripc/templates/python/interface_types.py.jinja2 +48 -0
  29. stingeripc/templates/python/method_codes.py.jinja2 +30 -0
  30. stingeripc/templates/python/pyproject.toml.jinja2 +9 -0
  31. stingeripc/templates/python/server.py.jinja2 +214 -0
  32. stingeripc/templates/rust/Cargo.toml.jinja2 +4 -0
  33. stingeripc/templates/rust/client/Cargo.toml.jinja2 +25 -0
  34. stingeripc/templates/rust/client/examples/client.rs.jinja2 +53 -0
  35. stingeripc/templates/rust/client/src/lib.rs.jinja2 +247 -0
  36. stingeripc/templates/rust/connection/Cargo.toml.jinja2 +21 -0
  37. stingeripc/templates/rust/connection/examples/pub_and_recv.rs.jinja2 +44 -0
  38. stingeripc/templates/rust/connection/src/handler.rs.jinja2 +0 -0
  39. stingeripc/templates/rust/connection/src/lib.rs.jinja2 +262 -0
  40. stingeripc/templates/rust/connection/src/payloads.rs.jinja2 +131 -0
  41. stingeripc/templates/rust/server/Cargo.toml.jinja2 +19 -0
  42. stingeripc/templates/rust/server/examples/server.rs.jinja2 +83 -0
  43. stingeripc/templates/rust/server/src/lib.rs.jinja2 +272 -0
  44. stingeripc/topic.py +11 -8
  45. stinger_ipc-0.0.1.dist-info/RECORD +0 -13
  46. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/WHEEL +0 -0
  47. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/licenses/LICENSE +0 -0
  48. {stinger_ipc-0.0.1.dist-info → stinger_ipc-0.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,30 @@
1
+
2
+ from typing import Optional
3
+ from enum import IntEnum
4
+
5
+ class MethodResultCode(IntEnum):{%for rc_i, rc_name in stinger.method_return_codes.items()%}
6
+ {{rc_name | CONST_CASE}} = {{rc_i}}
7
+ {%-endfor%}
8
+
9
+ class StingerMethodException(Exception):
10
+
11
+ def __init__(self, result_code: MethodResultCode, message: str):
12
+ super().__init__(message)
13
+ self._result_code = result_code
14
+
15
+ @property
16
+ def result_code(self) -> MethodResultCode:
17
+ return self._result_code
18
+ {%for rc_i, rc_name in stinger.method_return_codes.items()%}
19
+ class {{rc_name | UpperCamelCase}}StingerMethodException(StingerMethodException):
20
+ def __init__(self, message: str):
21
+ super().__init__(MethodResultCode.{{rc_name | CONST_CASE}}, message)
22
+ {%endfor%}
23
+
24
+ def stinger_exception_factory(result_code: int, message: Optional[str]=None):
25
+ exc_classes = { {%for rc_i, rc_name in stinger.method_return_codes.items()%}
26
+ {{rc_i}}: {{rc_name | UpperCamelCase}}StingerMethodException,
27
+ {%endfor%}
28
+ }
29
+ exc_class = exc_classes[result_code]
30
+ return exc_class(message or "")
@@ -0,0 +1,9 @@
1
+ [project]
2
+ name = "{{stinger.name}}"
3
+ version = "{{stinger.version}}"
4
+ description = "{{stinger.title}}"
5
+ requires-python = ">=3.9"
6
+ dependencies = [
7
+ "pydantic>=2.11.7",
8
+ "paho-mqtt>=2.0.1",
9
+ ]
@@ -0,0 +1,214 @@
1
+ """
2
+ DO NOT MODIFY THIS FILE. {# Unless you see this comment, in which case you are modifying the template. #} It is automatically generated and changes will be over-written
3
+ on the next generation.
4
+
5
+ This is the Server for the {{stinger.name}} interface.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+
11
+ logging.basicConfig(level=logging.DEBUG)
12
+
13
+ from typing import Callable, Dict, Any, Optional, List
14
+ from connection import BrokerConnection
15
+ from method_codes import *
16
+ import {{stinger.get_enum_module_name()}} as {{stinger.get_enum_module_alias()}}
17
+ {%macro method_type_annotation(method) %}Callable[[{%if method.arg_list | length > 0%}{%for arg in method.arg_list%}{{arg.python_type}}{%if not loop.last%}, {%endif%}{%endfor%}{%else%}None{%endif%}], {{method.return_value_python_type}}]{%endmacro%}
18
+
19
+ class {{stinger.python.server_class_name}}:
20
+
21
+ def __init__(self, connection: BrokerConnection):
22
+ self._logger = logging.getLogger('{{stinger.python.server_class_name}}')
23
+ self._logger.setLevel(logging.DEBUG)
24
+ self._logger.debug("Initializing {{stinger.python.server_class_name}}")
25
+ self._conn = connection
26
+ self._conn.set_message_callback(self._receive_message)
27
+ self._conn.set_last_will(topic="{{stinger.interface_info.0}}", payload=None, qos=1, retain=True)
28
+ {%for prop_name, prop_spec in stinger.properties.items()-%}
29
+ self._property_{{prop_name}} = None
30
+ self._conn.subscribe("{{prop_spec.update_topic}}")
31
+ self.changed_value_callback_for_{{pprop_name}} = None
32
+ self._publish_interface_info()
33
+ {%endfor-%}
34
+ {%-for method in stinger.methods.values()%}
35
+ self._conn.subscribe("{{method.topic}}")
36
+ {%endfor-%}
37
+ {%for method_name, method in stinger.methods.items()-%}
38
+ self._{{method_name|snake_case}}_method_handler: Optional[{{method_type_annotation(method)}}] = None
39
+ {%endfor%}
40
+
41
+ def _receive_message(self, topic: str, payload: str, properties: Dict[str, Any]):
42
+ """ This is the callback that is called whenever any message is received on a subscribed topic.
43
+ """
44
+ self._logger.debug("Received message to %s", topic)
45
+ {%if stinger.methods | length > 0 -%}
46
+ {%for method_name, method in stinger.methods.items()-%}
47
+ {%if not loop.first%}el{%endif%}if self._conn.is_topic_sub(topic, "{{method.topic}}"):
48
+ try:
49
+ payload_obj = json.loads(payload)
50
+ except json.decoder.JSONDecodeError:
51
+ self._logger.warning("Invalid JSON payload received at topic '%s'", topic)
52
+ else:
53
+ self._process_{{method_name | snake_case}}_call(topic, payload_obj, properties)
54
+ {%endfor%}
55
+ {%-else%}pass{%-endif%}
56
+
57
+ def _publish_interface_info(self):
58
+ self._conn.publish("{{stinger.interface_info.0}}", '''{{stinger.interface_info.1 | tojson}}''', qos=1, retain=True)
59
+
60
+ {%for sig_name, sig in stinger.signals.items()-%}
61
+ def emit_{{sig_name}}(self, {%for arg in sig.arg_list%}{{arg.name}}: {{arg.python_type}}{%if not loop.last%}, {%endif%}{%endfor%}):
62
+ """ Server application code should call this method to emit the '{{sig_name}}' signal.
63
+ """
64
+ {%-for arg in sig.arg_list%}
65
+ if not isinstance({{arg.name}}, {{arg.python_class}}){%if arg.optional%} and {{arg.name}} is not None{%endif%}:
66
+ raise ValueError(f"The '{{arg.name}}' value must be {{arg.python_type}}.")
67
+ {%-endfor%}
68
+
69
+ payload = { {%-for arg in sig.arg_list%}
70
+ {%if arg.arg_type.name.lower() == 'primitive'%}"{{arg.name}}": {{arg.python_type}}({{arg.name}}){%if arg.optional%} if {{arg.name}} is not None else None{%endif%},{%endif-%}
71
+ {%if arg.arg_type.name.lower() == 'enum'%}"{{arg.name}}": {{arg.python_class}}({{arg.name}}).value{%if arg.optional%} if {{arg.name}} is not None else None{%endif%},{%endif%}{%endfor%}
72
+ }
73
+ self._conn.publish("{{sig.topic}}", json.dumps(payload), qos=1, retain=False)
74
+
75
+ {%endfor%}
76
+
77
+ {%for method_name, method in stinger.methods.items()%}
78
+ def handle_{{method_name | snake_case}}(self, handler: {{method_type_annotation(method)}}):
79
+ """ This is a decorator to decorate a method that will handle the '{{method_name}}' method calls.
80
+ """
81
+ if self._{{method_name|snake_case}}_method_handler is None and handler is not None:
82
+ self._{{method_name|snake_case}}_method_handler = handler
83
+ else:
84
+ raise Exception("Method handler already set")
85
+
86
+ def _process_{{method_name | snake_case}}_call(self, topic: str, payload: Dict[str, Any], properties: Dict[str, Any]):
87
+ """ This processes a call to the '{{method_name}}' method. It deserializes the payload to find the method arguments,
88
+ then calls the method handler with those arguments. It then builds and serializes a response and publishes it to the response topic.
89
+ """
90
+ correlation_id = properties.get('CorrelationData') # type: Optional[bytes]
91
+ response_topic = properties.get('ResponseTopic') # type: Optional[str]
92
+ self._logger.info("Correlation Data %s", correlation_id)
93
+ if self._{{method_name|snake_case}}_method_handler is not None:
94
+ method_args = [] # type: List[Any]
95
+ {%for arg in method.arg_list -%}
96
+ if "{{arg.name}}" in payload:
97
+ if not isinstance(payload["{{arg.name}}"], {{arg.python_class}}){%if arg.optional%} or {{arg.name}} is None{%endif%}:
98
+ self._logger.warning("The '{{arg.name}}' property in the payload to '%s' wasn't the correct type. It should have been {{arg.python_type}}.", topic)
99
+ # TODO: return an error via MQTT
100
+ return
101
+ else:
102
+ method_args.append(payload["{{arg.name}}"])
103
+ else:
104
+ {%if arg.optional%}
105
+ method_args.append(None)
106
+ {%else%}
107
+ self._logger.warning("The '{{arg.name}}' property in the payload to '%s' wasn't present", topic)
108
+ # TODO: return an error via MQTT
109
+ return
110
+ {%endif%}
111
+ {%endfor%}
112
+
113
+ if response_topic is not None:
114
+ return_json = ""
115
+ debug_msg = None # type: Optional[str]
116
+ try:
117
+ return_struct = self._{{method_name|snake_case}}_method_handler(*method_args)
118
+ self._logger.debug("Return value is %s", return_struct)
119
+ {%if method.return_value is false%}
120
+ return_json = "{}"
121
+ {%else%}
122
+ if return_struct is not None:
123
+ {%-if method.return_value_type == "primitive" %}
124
+ return_json = json.dumps({
125
+ "{{method.return_value_property_name}}": return_struct
126
+ })
127
+ {%-elif method.return_value_type == "enum" %}
128
+ return_json = json.dumps({
129
+ "{{method.return_value_property_name}}": return_struct.value
130
+ })
131
+ {%-elif method.return_value_type == "struct" %}
132
+ return_json = json.dumps({
133
+ "{{method.return_value_property_name}}": return_struct.model_dump_json()
134
+ })
135
+ {%else%}
136
+ return_json = return_struct.model_dump_json()
137
+ {%endif%}
138
+ {%-endif%}
139
+ except Exception as e:
140
+ self._logger.exception("Exception while handling {{method_name}}", exc_info=e)
141
+ return_code = MethodResultCode.SERVER_ERROR
142
+ debug_msg = str(e)
143
+ else:
144
+ return_code = MethodResultCode.SUCCESS
145
+ debug_msg = None
146
+
147
+ self._conn.publish(response_topic, return_json, qos=1, retain=False,
148
+ correlation_id=correlation_id, return_value=return_code, debug_info=debug_msg)
149
+ {%endfor%}
150
+
151
+
152
+ class {{stinger.python.server_class_name}}Builder:
153
+ """
154
+ This is a builder for the {{stinger.python.server_class_name}}. It is used to create a server with the desired parameters.
155
+ """
156
+
157
+ def __init__(self, connection: BrokerConnection):
158
+ self._conn = connection
159
+ {%for method_name, method in stinger.methods.items()%}
160
+ self._{{method_name|snake_case}}_method_handler: Optional[{{method_type_annotation(method)}}] = None
161
+ {%-endfor%}
162
+ {%for method_name, method in stinger.methods.items()%}
163
+ def handle_{{method_name | snake_case}}(self, handler: {{method_type_annotation(method)}}):
164
+ if self._{{method_name|snake_case}}_method_handler is None and handler is not None:
165
+ self._{{method_name|snake_case}}_method_handler = handler
166
+ else:
167
+ raise Exception("Method handler already set")
168
+ {%endfor%}
169
+ def build(self) -> {{stinger.python.server_class_name}}:
170
+ new_server = {{stinger.python.server_class_name}}(self._conn)
171
+ {%for method_name, method in stinger.methods.items()%}
172
+ if self._{{method_name|snake_case}}_method_handler is not None:
173
+ new_server.handle_{{method_name|snake_case}}(self._{{method_name|snake_case}}_method_handler)
174
+ {%-endfor%}
175
+ return new_server
176
+
177
+ if __name__ == '__main__':
178
+ """
179
+ This shows an example on how to run the code. Ideally, your app should do something similar, but use the methods in
180
+ a more meaningful way.
181
+ """
182
+ from time import sleep
183
+ import signal
184
+ {%set broker = stinger.get_example_broker()%}
185
+ from connection import {{broker.class_name}}
186
+
187
+ conn = {{broker.class_name}}({%if broker.hostname is none%}'localhost', 1883{%endif%})
188
+ server = {{stinger.python.server_class_name}}(conn)
189
+
190
+ {%for method_name, method in stinger.methods.items()%}
191
+ @server.handle_{{method_name | snake_case}}
192
+ def {{method_name | snake_case}}({%for arg in method.arg_list%}{{arg.name}}: {{arg.python_type}}{%if not loop.last%}, {%endif%}{%endfor%}) -> {{method.return_value_python_type}}:
193
+ """ This is an example handler for the '{{method_name}}' method. """
194
+ print(f"Running {{method_name | snake_case}}'({%for arg in method.arg_list %}{ {{-arg.name-}} }{%if not loop.last%}, {%endif%}{%endfor%})'")
195
+ return {{method.get_return_value_random_example_value('python')}}
196
+ {%endfor%}
197
+
198
+ print("Ctrl-C will stop the program.")
199
+
200
+ while True:
201
+ try:
202
+ {%for sig_name, sig in stinger.signals.items()-%}
203
+ server.emit_{{sig_name}}({%for arg in sig.arg_list%}{{arg.get_random_example_value()}}{%if not loop.last%}, {%endif%}{%endfor%})
204
+ {%endfor%}
205
+ sleep(4)
206
+ {%for sig_name, sig in stinger.signals.items()-%}
207
+ server.emit_{{sig_name}}({%for arg in sig.arg_list%}{{arg.name}}={{arg.get_random_example_value()}}{%if not loop.last%}, {%endif%}{%endfor%})
208
+ {%endfor%}
209
+ sleep(6)
210
+ except KeyboardInterrupt:
211
+ break
212
+
213
+
214
+ signal.pause()
@@ -0,0 +1,4 @@
1
+ [workspace]
2
+ members = ["server", "client", "connection"]
3
+
4
+ resolver = "2"
@@ -0,0 +1,25 @@
1
+ [package]
2
+ name = "{{stinger.name | snake_case}}_client"
3
+ version = "{{stinger.version}}"
4
+ edition = "2024"
5
+
6
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7
+
8
+ [dependencies]
9
+ connection = { path = "../connection" }
10
+ futures = "0.3"
11
+ json = "0.12.4"
12
+ paho-mqtt = "0.13.3"
13
+ num-derive = "0.4.2"
14
+ num-traits = "0.2.19"
15
+ tokio = { version = "1", features = ["full"] }
16
+ serde = { version = "1.0.219", features = ["derive"] }
17
+ serde_json = "1.0.142"
18
+
19
+ [dependencies.uuid]
20
+ version = "1.2.1"
21
+ features = [
22
+ "v4", # Lets you generate random UUIDs
23
+ "fast-rng", # Use a faster (but still sufficiently random) RNG
24
+ "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
25
+ ]
@@ -0,0 +1,53 @@
1
+ use futures::{executor::block_on};
2
+ use {{stinger.name | snake_case}}_client::{{stinger.rust.client_struct_name}};
3
+ use connection::Connection;
4
+ use tokio::time::{sleep, Duration};
5
+ use tokio::join;
6
+
7
+ #[tokio::main]
8
+ async fn main() {
9
+ block_on(async {
10
+ {%set broker = stinger.get_example_broker()%}
11
+ let mut connection = Connection::new_{{broker.class_name|snake_case}}().await.expect("Failed to create connection");
12
+ let mut client = {{stinger.rust.client_struct_name}}::new(&mut connection).await;
13
+
14
+ tokio::spawn(async move {
15
+ println!("Making call to start connection loop");
16
+ let _conn_loop = connection.start_loop().await;
17
+ });
18
+
19
+ {%for sig_name, sig in stinger.signals.items()%}
20
+ let mut sig_rx = client.get_{{sig_name|snake_case}}_receiver();
21
+ println!("Got signal receiver for {{sig_name}}");
22
+
23
+ sleep(Duration::from_secs(5)).await;
24
+
25
+ let sig_rx_task = tokio::spawn(async move {
26
+ println!("Looping for signals");
27
+ loop {
28
+ match sig_rx.recv().await {
29
+ Ok(payload) => {
30
+ println!("Received {{sig_name}} signal with payload: {:?}", payload);
31
+ },
32
+ Err(e) => {
33
+ eprintln!("Error receiving {{sig_name}} signal: {:?}", e);
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ });
39
+ {%endfor%}
40
+
41
+ println!("Starting client receive loop");
42
+ let _client_loop = client.receive_loop().await;
43
+
44
+ {%for method_name, method in stinger.methods.items()%}
45
+ println!("Calling {{method_name}} with example values...");
46
+ let result = client.{{method_name | snake_case}}({%for arg in method.arg_list%}{{arg.get_random_example_value(lang='rust')}}{%if not loop.last%}, {%endif%}{%endfor%}).await.expect("Failed to call {{method_name}}");
47
+ println!("{{method_name}} response: {:?}", result);
48
+ {%endfor%}
49
+
50
+ join!(sig_rx_task);
51
+ });
52
+ // Ctrl-C to stop
53
+ }
@@ -0,0 +1,247 @@
1
+ /*
2
+ DO NOT MODIFY THIS FILE{# <-- Ignore this because you are editing the template file. #}. It is automatically generated and changes will be over-written
3
+ on the next generation.
4
+
5
+ This is the Client for the {{stinger.name}} interface.
6
+ */
7
+
8
+ extern crate paho_mqtt as mqtt;
9
+ use connection::{MessagePublisher, Connection, ReceivedMessage};
10
+
11
+ use json::{JsonValue};
12
+ use std::collections::HashMap;
13
+ use uuid::Uuid;
14
+ use serde_json;
15
+
16
+ #[allow(unused_imports)]
17
+ use connection::payloads::{*, MethodResultCode};
18
+
19
+ use std::sync::{Arc, Mutex};
20
+ use tokio::sync::{mpsc, broadcast, oneshot};
21
+ use tokio::join;
22
+ use tokio::task::JoinError;
23
+
24
+ /// This struct is used to store all the MQTTv5 subscription ids
25
+ /// for the subscriptions the client will make.
26
+ #[derive(Clone, Debug)]
27
+ struct {{stinger.name | UpperCamelCase }}SubscriptionIds {
28
+ {%for method_name, method in stinger.methods.items()-%}
29
+ {{method_name | snake_case}}_method_resp: i32,
30
+ {%-endfor%}
31
+ {%for sig_name, sig in stinger.signals.items()-%}
32
+ {{sig_name | snake_case}}_signal: Option<i32>,
33
+ {%-endfor%}
34
+ }
35
+
36
+ /// This struct holds the tx side of a broadcast channels used when receiving signals.
37
+ /// The rx side of the broadcast channels can be created from the tx side later.
38
+ /// When {{stinger.name | UpperCamelCase }}Client gets a message and determines that it
39
+ /// is a signal, it will send the signal payload via the tx channel that is in this struct.
40
+ #[derive(Clone)]
41
+ struct {{stinger.name | UpperCamelCase }}SignalChannels {
42
+ {%for sig_name, sig in stinger.signals.items()-%}
43
+ {{sig_name|snake_case}}_sender: broadcast::Sender<{%if sig.arg_list | length == 0 %}{{sig.arg_list[0]}}{% else %}{{sig_name | UpperCamelCase}}SignalPayload{% endif %}>,
44
+ {%endfor%}
45
+ }
46
+
47
+ /// This is the struct for our API client.
48
+ pub struct {{stinger.name | UpperCamelCase }}Client {
49
+ {%if stinger.methods | length > 0 %}/// Temporarily holds oneshot channels for responses to method calls.
50
+ pending_responses: Arc<Mutex<HashMap::<Uuid, oneshot::Sender::<JsonValue>>>>,
51
+ {%endif%}
52
+
53
+ /// Temporarily holds the receiver for the MPSC channel. The Receiver will be moved
54
+ /// to a process loop when it is needed. MQTT messages will be received with this.
55
+ msg_streamer_rx: Option<mpsc::Receiver<ReceivedMessage>>,
56
+
57
+ /// The Sender side of MQTT messages that are received from the broker. This tx
58
+ /// side is cloned for each subscription made.
59
+ msg_streamer_tx: mpsc::Sender<ReceivedMessage>,
60
+
61
+ {%if stinger.methods | length > 0 %}/// Through this MessagePublisher object, we can publish messages to MQTT.
62
+ msg_publisher: MessagePublisher,
63
+ {%endif%}
64
+ /// Contains all the MQTTv5 subscription ids.
65
+ subscription_ids: {{stinger.name | UpperCamelCase }}SubscriptionIds,
66
+
67
+ {%if stinger.signals | length > 0 %}/// Holds the channels used for sending signals to the application.
68
+ signal_channels: {{stinger.name | UpperCamelCase }}SignalChannels,
69
+ {%endif%}
70
+ /// Copy of MQTT Client ID
71
+ client_id: String,
72
+ }
73
+
74
+ impl {{stinger.name | UpperCamelCase }}Client {
75
+
76
+ /// Creates a new {{stinger.name | UpperCamelCase }}Client that uses elements from the provided Connection object.
77
+ pub async fn new(connection: &mut Connection) -> Self {
78
+ let _ = connection.connect().await.expect("Could not connect to MQTT broker");
79
+
80
+ // Create a channel for messages to get from the Connection object to this {{stinger.name | UpperCamelCase }}Client object.
81
+ // The Connection object uses a clone of the tx side of the channel.
82
+ let (message_received_tx, message_received_rx) = mpsc::channel(64);
83
+
84
+ {%if stinger.methods | length > 0 %}// Create the publisher object.
85
+ let publisher = connection.get_publisher();
86
+
87
+ // Subscribe to all the topics needed for method responses.
88
+ {%endif-%}
89
+ {%for method_name, method in stinger.methods.items()-%}
90
+ let topic_{{method_name | snake_case}}_method_resp = format!("{{method.response_topic('{}')}}", connection.client_id);
91
+ let subscription_id_{{method_name | snake_case}}_method_resp = connection.subscribe(&topic_{{method_name | snake_case}}_method_resp, message_received_tx.clone()).await;
92
+ let subscription_id_{{method_name | snake_case}}_method_resp = subscription_id_{{method_name | snake_case}}_method_resp.unwrap_or_else(|_| -1);
93
+ {%endfor%}
94
+
95
+ // Subscribe to all the topics needed for signals.
96
+ {%for signal_name, signal in stinger.signals.items()-%}
97
+ let topic_{{signal_name | snake_case}}_signal = "{{signal.topic}}";
98
+ let subscription_id_{{signal_name | snake_case}}_signal = connection.subscribe(&topic_{{signal_name | snake_case}}_signal, message_received_tx.clone()).await;
99
+ let subscription_id_{{signal_name | snake_case}}_signal = subscription_id_{{signal_name | snake_case}}_signal.unwrap_or_else(|_| -1);
100
+ {%endfor%}
101
+
102
+ // Create structure for subscription ids.
103
+ let sub_ids = {{stinger.name | UpperCamelCase }}SubscriptionIds {
104
+ {%for method_name, method in stinger.methods.items()-%}
105
+ {{method_name | snake_case}}_method_resp: subscription_id_{{method_name | snake_case}}_method_resp,
106
+ {%endfor%}
107
+ {%-for sig_name, sig in stinger.signals.items()-%}
108
+ {{sig_name | snake_case}}_signal: Some(subscription_id_{{sig_name | snake_case}}_signal),
109
+ {%endfor%}
110
+ };
111
+
112
+ {%if stinger.signals | length > 0 %}// Create structure for the tx side of broadcast channels for signals.
113
+ let signal_channels = {{stinger.name | UpperCamelCase }}SignalChannels {
114
+ {%for sig_name, sig in stinger.signals.items()-%}
115
+ {{sig_name|snake_case}}_sender: broadcast::channel(64).0,
116
+ {%endfor%}
117
+ };{%endif%}
118
+
119
+ // Create {{stinger.name | UpperCamelCase }}Client structure.
120
+ let inst = {{stinger.name | UpperCamelCase }}Client {
121
+ {%if stinger.methods | length > 0 %}pending_responses: Arc::new(Mutex::new(HashMap::new())),{%endif%}
122
+ msg_streamer_rx: Some(message_received_rx),
123
+ msg_streamer_tx: message_received_tx,
124
+ {%if stinger.methods | length > 0 %}msg_publisher: publisher,{%endif%}
125
+ subscription_ids: sub_ids,
126
+ {%if stinger.signals | length > 0 %}signal_channels: signal_channels,{%endif%}
127
+ client_id: connection.client_id.to_string(),
128
+ };
129
+ inst
130
+ }
131
+
132
+ {%for sig_name, sig in stinger.signals.items()-%}
133
+ /// Get the RX receiver side of the broadcast channel for the {{sig_name}} signal.
134
+ /// The signal payload, `{{sig_name | UpperCamelCase}}SignalPayload`, will be put onto the channel whenever it is received.
135
+ pub fn get_{{sig_name|snake_case}}_receiver(&self) -> broadcast::Receiver<{{sig_name | UpperCamelCase}}SignalPayload> {
136
+ self.signal_channels.{{sig_name | snake_case}}_sender.subscribe()
137
+ }
138
+ {%endfor%}
139
+
140
+ {%for method_name, method in stinger.methods.items()-%}
141
+ /// The `{{method_name}}` method.
142
+ /// Method arguments are packed into a {{method_name | UpperCamelCase}}RequestObject structure
143
+ /// and published to the `{{method.topic}}` MQTT topic.
144
+ ///
145
+ /// This method awaits on the response to the call before returning.
146
+ pub async fn {{method_name|snake_case}}(&mut self, {%for arg in method.arg_list%}{{arg.name|snake_case}}: {{arg.rust_type}}{%if not loop.last%}, {%endif%}{%endfor%})->Result<{{method.return_value_rust_type}}, MethodResultCode> {
147
+ let correlation_id = Uuid::new_v4();
148
+ let (sender, receiver) = oneshot::channel();
149
+ {
150
+ let mut hashmap = self.pending_responses.lock().expect("Mutex was poisoned");
151
+ hashmap.insert(correlation_id.clone(), sender);
152
+ }
153
+
154
+ let data = connection::payloads::{{method_name | UpperCamelCase}}RequestObject {
155
+ {%-for arg in method.arg_list%}
156
+ {{arg.name}}: {{arg.name|snake_case}},
157
+ {%-endfor%}
158
+ };
159
+
160
+ let response_topic = format!("{{method.response_topic('{}')}}", self.client_id);
161
+ let _ = self.msg_publisher.publish_request_structure("{{method.topic}}".to_string(), &data, response_topic.as_str(), correlation_id).await;
162
+ let resp_obj = receiver.await.unwrap();
163
+
164
+ {%-if method.return_value_type == 'primitive' %}
165
+ Ok(resp_obj["{{method.return_value_property_name}}"].as_{{method.return_value_rust_type}}().unwrap())
166
+ {%-elif method.return_value_type == 'enum'%}
167
+ Ok({{arg.rust_type}}::from_u32(payload_object["{{arg.name}}"].as_u32().unwrap())
168
+ {%-elif method.return_value_type == 'struct'%}
169
+ Ok({{method.return_value_rust_type}} {
170
+ {%-for member in method.return_value -%}
171
+ {%if member.arg_type.name.lower() == 'primitive'%}
172
+ {%if member.rust_type == 'String'%}
173
+ {{member.name|snake_case}}: resp_obj["{{member.name}}"].as_str().unwrap().to_string(),
174
+ {%-else%}
175
+ {{member.name|snake_case}}: resp_obj["{{member.name}}"].as_{{member.rust_type}}().unwrap(),
176
+ {%-endif-%}
177
+ {%elif member.arg_type.name.lower() == 'enum'%}
178
+ {{member.name|snake_case}}: {{member.rust_type}}::from_u32(resp_obj["{{member.name}}"].as_u32().unwrap()).unwrap(),
179
+ {%endif%}
180
+ {%endfor-%} })
181
+ {%elif method.return_value_type is false%}
182
+ Ok(())
183
+ {%else%}
184
+ TEMPLATE ERROR CASE NOT HANDLED
185
+ {%-endif %}
186
+ }
187
+
188
+ /// Handler for responses to `{{method_name}}` method calls.
189
+ /// It finds oneshot channel created for the method call, and sends the response to that channel.
190
+ fn handle_{{method_name|snake_case}}_response(pending_responses: Arc<Mutex<HashMap::<Uuid, oneshot::Sender::<JsonValue>>>>, payload: String, opt_correlation_id: Option<Uuid>) {
191
+
192
+ let payload_object = json::parse(&payload).unwrap();
193
+ if opt_correlation_id.is_some() {
194
+ let sender_opt = opt_correlation_id
195
+ .and_then(|uuid| {
196
+ let mut hashmap = pending_responses.lock().expect("Mutex was poisoned");
197
+ hashmap.remove(&uuid)
198
+ });
199
+ match sender_opt {
200
+ Some(sender) => {
201
+ let oss: oneshot::Sender<JsonValue> = sender;
202
+ match oss.send(payload_object) {
203
+ Ok(_) => (),
204
+ Err(_) => ()
205
+ }
206
+ },
207
+ None => ()
208
+ }
209
+ }
210
+ }
211
+ {%endfor%}
212
+
213
+ /// Starts the tasks that process messages received.
214
+ pub async fn receive_loop(&mut self) -> Result<(), JoinError> {
215
+ {%if stinger.methods | length > 0%}// Clone the Arc pointer to the map. This will be moved into the loop_task.
216
+ let resp_map: Arc<Mutex<HashMap::<Uuid, oneshot::Sender::<JsonValue>>>> = self.pending_responses.clone();
217
+ {%endif%}
218
+ // Take ownership of the RX channel that receives MQTT messages. This will be moved into the loop_task.
219
+ let mut message_receiver = self.msg_streamer_rx.take().expect("msg_streamer_rx should be Some");
220
+
221
+ let sig_chans = self.signal_channels.clone();
222
+ let sub_ids = self.subscription_ids.clone();
223
+
224
+ let loop_task = tokio::spawn(async move {
225
+ while let Some(msg) = message_receiver.recv().await {
226
+ let msg_props = msg.message.properties();
227
+ let opt_corr_id_bin: Option<Vec<u8>> = msg_props.get_binary(mqtt::PropertyCode::CorrelationData);
228
+ let corr_id: Option<Uuid> = opt_corr_id_bin.and_then(|b| Uuid::from_slice(&b).ok());
229
+ {%-for method_name, method in stinger.methods.items()%}
230
+ {%if not loop.first%}else {%endif%}if msg.subscription_id == sub_ids.{{method_name | snake_case}}_method_resp {
231
+ {{stinger.name | UpperCamelCase }}Client::handle_{{method_name|snake_case}}_response(resp_map.clone(), msg.message.payload_str().to_string(), corr_id);
232
+ }
233
+ {%-endfor%}
234
+ {%-for signal_name, signal in stinger.signals.items()%}
235
+ {%if not loop.first%}else {%endif%}if msg.subscription_id == sub_ids.{{signal_name | snake_case}}_signal.unwrap_or_default() {
236
+ let chan = sig_chans.{{signal_name | snake_case}}_sender.clone();
237
+ let pl: connection::payloads::{{signal_name | UpperCamelCase}}SignalPayload = serde_json::from_str(&msg.message.payload_str()).expect("Failed to deserialize");
238
+ let _send_result = chan.send(pl);
239
+ }
240
+ {%endfor%}
241
+ }
242
+ });
243
+
244
+ println!("Started client receive task");
245
+ Ok(())
246
+ }
247
+ }
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "connection"
3
+ version = "0.1.0"
4
+ edition = "2024"
5
+
6
+ [dependencies]
7
+ futures = "0.3.31"
8
+ paho-mqtt = "0.13.3"
9
+ serde = { version = "1.0.219", features = ["derive"] }
10
+ serde_json = "1.0.142"
11
+ tokio = { version = "1", features = ["full"] }
12
+ num-derive = "0.4.2"
13
+ num-traits = "0.2.19"
14
+
15
+ [dependencies.uuid]
16
+ version = "1.17.0"
17
+ features = [
18
+ "v4", # Lets you generate random UUIDs
19
+ "fast-rng", # Use a faster (but still sufficiently random) RNG
20
+ "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
21
+ ]
@@ -0,0 +1,44 @@
1
+
2
+ use connection::Connection;
3
+ use tokio::time::{sleep, Duration};
4
+ use tokio::join;
5
+ use tokio::sync::mpsc;
6
+
7
+ #[tokio::main]
8
+ async fn main() {
9
+ {%set broker = stinger.get_example_broker()%}
10
+ let mut connection = Connection::new_{{broker.class_name|snake_case}}().await.expect("Failed to create connection");
11
+ let (recv_chan_tx, mut recv_chan_rx) = mpsc::channel(32);
12
+ connection.connect().await.expect("Failed to connect to broker");
13
+ connection.subscribe("example/recv_topic", recv_chan_tx.clone()).await.expect("Failed to subscribe to topic");
14
+ let mut publisher = connection.get_publisher();
15
+
16
+ let loop_task = tokio::spawn(async move {
17
+ connection.start_loop().await;
18
+ });
19
+
20
+ let _pub_task = tokio::spawn(async move {
21
+ let mut i = 0;
22
+ loop {
23
+ i = i + 1;
24
+ sleep(Duration::from_secs(1)).await; // Simulate periodic publishing
25
+ publisher.publish_simple("example/pub_topic".to_string(), format!("Periodic message {}",i).to_string()).await.expect("Failed to publish periodic message");
26
+ }
27
+ });
28
+
29
+ let _receive_task = tokio::spawn(async move {
30
+ loop {
31
+ match recv_chan_rx.recv().await {
32
+ Some(message) => {
33
+ println!("Received message: {:?}", message);
34
+ },
35
+ None => {
36
+ eprintln!("Receiver channel closed");
37
+ break;
38
+ }
39
+ }
40
+ }
41
+ });
42
+
43
+ let _result = join!(loop_task);
44
+ }