takagi 0.1.0 → 1.1.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +70 -7
- data/.yard/templates/default/layout/html/layout.erb +34 -0
- data/AGENTS.md +16 -0
- data/CHANGELOG.md +158 -1
- data/CODE_OF_CONDUCT.md +1 -1
- data/README.md +590 -23
- data/ROADMAP.md +55 -0
- data/Rakefile +4 -4
- data/Steepfile +39 -0
- data/bin/takagi-dev +159 -0
- data/docs/FIRST_PLUGIN_GUIDE.md +224 -0
- data/docs/HOOKS.md +31 -0
- data/examples/client_lifecycle_example.rb +118 -0
- data/examples/cloud_gateway_app.rb +217 -0
- data/examples/nested_api_app.rb +258 -0
- data/examples/simple_device_app.rb +71 -0
- data/examples/takagi.yml +138 -0
- data/lib/takagi/application.rb +256 -0
- data/lib/takagi/base/middleware_management.rb +39 -0
- data/lib/takagi/base/plugin_management.rb +75 -0
- data/lib/takagi/base/reactor_management.rb +104 -0
- data/lib/takagi/base/server_lifecycle.rb +156 -0
- data/lib/takagi/base.rb +103 -11
- data/lib/takagi/branding.rb +88 -0
- data/lib/takagi/cbor/decoder.rb +385 -0
- data/lib/takagi/cbor/encoder.rb +260 -0
- data/lib/takagi/cbor/error.rb +17 -0
- data/lib/takagi/cbor/version.rb +9 -0
- data/lib/takagi/client/response.rb +236 -0
- data/lib/takagi/client.rb +265 -0
- data/lib/takagi/client_base.rb +204 -0
- data/lib/takagi/coap/code_helpers.rb +190 -0
- data/lib/takagi/coap/registries/base.rb +165 -0
- data/lib/takagi/coap/registries/content_format.rb +71 -0
- data/lib/takagi/coap/registries/message_type.rb +69 -0
- data/lib/takagi/coap/registries/method.rb +38 -0
- data/lib/takagi/coap/registries/option.rb +71 -0
- data/lib/takagi/coap/registries/response.rb +93 -0
- data/lib/takagi/coap/registries/signaling.rb +34 -0
- data/lib/takagi/coap/signaling.rb +10 -0
- data/lib/takagi/coap.rb +37 -0
- data/lib/takagi/composite_router.rb +186 -0
- data/lib/takagi/config.rb +337 -0
- data/lib/takagi/controller/resource_allocator.rb +164 -0
- data/lib/takagi/controller/thread_pool.rb +144 -0
- data/lib/takagi/controller.rb +319 -0
- data/lib/takagi/core/attribute_set.rb +128 -0
- data/lib/takagi/discovery/core_link_format.rb +137 -0
- data/lib/takagi/errors.rb +536 -0
- data/lib/takagi/event_bus/address_prefix.rb +142 -0
- data/lib/takagi/event_bus/async_executor.rb +235 -0
- data/lib/takagi/event_bus/coap_bridge.rb +208 -0
- data/lib/takagi/event_bus/future.rb +153 -0
- data/lib/takagi/event_bus/lru_cache.rb +157 -0
- data/lib/takagi/event_bus/message_buffer.rb +237 -0
- data/lib/takagi/event_bus/observer_cleanup.rb +110 -0
- data/lib/takagi/event_bus/scope.rb +74 -0
- data/lib/takagi/event_bus.rb +594 -0
- data/lib/takagi/helpers.rb +88 -0
- data/lib/takagi/hooks.rb +82 -0
- data/lib/takagi/initializer.rb +18 -0
- data/lib/takagi/logger.rb +15 -6
- data/lib/takagi/message/base.rb +155 -0
- data/lib/takagi/message/deduplication_cache.rb +84 -0
- data/lib/takagi/message/inbound.rb +147 -0
- data/lib/takagi/message/outbound.rb +223 -0
- data/lib/takagi/message/request.rb +158 -0
- data/lib/takagi/message/retransmission_manager.rb +193 -0
- data/lib/takagi/middleware/authentication.rb +19 -0
- data/lib/takagi/middleware/caching.rb +23 -0
- data/lib/takagi/middleware/debugging.rb +16 -0
- data/lib/takagi/middleware/logging.rb +14 -0
- data/lib/takagi/middleware/metrics.rb +440 -0
- data/lib/takagi/middleware/rate_limiting.rb +24 -0
- data/lib/takagi/middleware_stack.rb +166 -0
- data/lib/takagi/network/base.rb +76 -0
- data/lib/takagi/network/framing/tcp.rb +222 -0
- data/lib/takagi/network/framing/udp.rb +110 -0
- data/lib/takagi/network/registry.rb +72 -0
- data/lib/takagi/network/tcp.rb +60 -0
- data/lib/takagi/network/tcp_sender.rb +21 -0
- data/lib/takagi/network/udp.rb +61 -0
- data/lib/takagi/network/udp_sender.rb +20 -0
- data/lib/takagi/observable/emitter.rb +62 -0
- data/lib/takagi/observable/reactor.rb +488 -0
- data/lib/takagi/observable/registry.rb +122 -0
- data/lib/takagi/observe_registry.rb +10 -0
- data/lib/takagi/observer/client.rb +68 -0
- data/lib/takagi/observer/registry.rb +137 -0
- data/lib/takagi/observer/sender.rb +39 -0
- data/lib/takagi/observer/watcher.rb +43 -0
- data/lib/takagi/plugin.rb +313 -0
- data/lib/takagi/profiles.rb +176 -0
- data/lib/takagi/reactor.rb +23 -0
- data/lib/takagi/reactor_registry.rb +64 -0
- data/lib/takagi/registry/base.rb +268 -0
- data/lib/takagi/response_builder.rb +141 -0
- data/lib/takagi/router/metadata_extractor.rb +133 -0
- data/lib/takagi/router/route_matcher.rb +83 -0
- data/lib/takagi/router.rb +284 -25
- data/lib/takagi/serialization/base.rb +102 -0
- data/lib/takagi/serialization/cbor_serializer.rb +92 -0
- data/lib/takagi/serialization/json_serializer.rb +96 -0
- data/lib/takagi/serialization/octet_stream_serializer.rb +82 -0
- data/lib/takagi/serialization/registry.rb +187 -0
- data/lib/takagi/serialization/text_serializer.rb +87 -0
- data/lib/takagi/serialization.rb +117 -0
- data/lib/takagi/server/multi.rb +41 -0
- data/lib/takagi/server/registry.rb +71 -0
- data/lib/takagi/server/tcp.rb +249 -0
- data/lib/takagi/server/udp.rb +139 -0
- data/lib/takagi/server/udp_worker.rb +174 -0
- data/lib/takagi/server.rb +1 -31
- data/lib/takagi/server_registry.rb +10 -0
- data/lib/takagi/tcp_client.rb +142 -0
- data/lib/takagi/version.rb +2 -1
- data/lib/takagi.rb +24 -3
- data/sig/takagi/application.rbs +48 -0
- data/sig/takagi/base/middleware_management.rbs +33 -0
- data/sig/takagi/base/reactor_management.rbs +52 -0
- data/sig/takagi/base/server_lifecycle.rbs +54 -0
- data/sig/takagi/base.rbs +48 -0
- data/sig/takagi/cbor/decoder.rbs +171 -0
- data/sig/takagi/cbor/encoder.rbs +146 -0
- data/sig/takagi/cbor/error.rbs +19 -0
- data/sig/takagi/cbor/version.rbs +7 -0
- data/sig/takagi/client/response.rbs +148 -0
- data/sig/takagi/client.rbs +119 -0
- data/sig/takagi/client_base.rbs +135 -0
- data/sig/takagi/coap/code_helpers.rbs +91 -0
- data/sig/takagi/coap/registries/base.rbs +95 -0
- data/sig/takagi/coap/registries/content_format.rbs +47 -0
- data/sig/takagi/coap/registries/message_type.rbs +53 -0
- data/sig/takagi/coap/registries/method.rbs +27 -0
- data/sig/takagi/coap/registries/option.rbs +43 -0
- data/sig/takagi/coap/registries/response.rbs +52 -0
- data/sig/takagi/coap.rbs +24 -0
- data/sig/takagi/composite_router.rbs +46 -0
- data/sig/takagi/config.rbs +134 -0
- data/sig/takagi/controller.rbs +73 -0
- data/sig/takagi/core/attribute_set.rbs +57 -0
- data/sig/takagi/discovery/core_link_format.rbs +50 -0
- data/sig/takagi/event_bus/address_prefix.rbs +78 -0
- data/sig/takagi/event_bus/async_executor.rbs +88 -0
- data/sig/takagi/event_bus/coap_bridge.rbs +93 -0
- data/sig/takagi/event_bus/future.rbs +78 -0
- data/sig/takagi/event_bus/lru_cache.rbs +86 -0
- data/sig/takagi/event_bus/message_buffer.rbs +133 -0
- data/sig/takagi/event_bus/observer_cleanup.rbs +62 -0
- data/sig/takagi/event_bus.rbs +320 -0
- data/sig/takagi/helpers.rbs +34 -0
- data/sig/takagi/initializer.rbs +9 -0
- data/sig/takagi/logger.rbs +17 -0
- data/sig/takagi/message/base.rbs +64 -0
- data/sig/takagi/message/deduplication_cache.rbs +49 -0
- data/sig/takagi/message/inbound.rbs +76 -0
- data/sig/takagi/message/outbound.rbs +48 -0
- data/sig/takagi/message/request.rbs +32 -0
- data/sig/takagi/message/retransmission_manager.rbs +76 -0
- data/sig/takagi/middleware/authentication.rbs +11 -0
- data/sig/takagi/middleware/caching.rbs +13 -0
- data/sig/takagi/middleware/debugging.rbs +9 -0
- data/sig/takagi/middleware/logging.rbs +7 -0
- data/sig/takagi/middleware/metrics.rbs +15 -0
- data/sig/takagi/middleware/rate_limiting.rbs +13 -0
- data/sig/takagi/middleware_stack.rbs +69 -0
- data/sig/takagi/network/tcp_sender.rbs +10 -0
- data/sig/takagi/network/udp_sender.rbs +14 -0
- data/sig/takagi/observe_registry.rbs +36 -0
- data/sig/takagi/observer/client.rbs +36 -0
- data/sig/takagi/observer/sender.rbs +12 -0
- data/sig/takagi/observer/watcher.rbs +18 -0
- data/sig/takagi/profiles.rbs +33 -0
- data/sig/takagi/reactor.rbs +20 -0
- data/sig/takagi/reactor_registry.rbs +14 -0
- data/sig/takagi/response_builder.rbs +12 -0
- data/sig/takagi/router/metadata_extractor.rbs +71 -0
- data/sig/takagi/router/route_matcher.rbs +43 -0
- data/sig/takagi/router.rbs +166 -0
- data/sig/takagi/serialization.rbs +32 -0
- data/sig/takagi/server/multi.rbs +16 -0
- data/sig/takagi/server/tcp.rbs +42 -0
- data/sig/takagi/server/udp.rbs +52 -0
- data/sig/takagi/server/udp_worker.rbs +42 -0
- data/sig/takagi/server.rbs +4 -0
- data/sig/takagi/server_registry.rbs +71 -0
- data/sig/takagi/tcp_client.rbs +23 -0
- data/sig/takagi/version.rbs +5 -0
- data/takagi.gemspec +37 -35
- metadata +204 -31
- data/.idea/.gitignore +0 -8
- data/.idea/misc.xml +0 -4
- data/.idea/modules.xml +0 -8
- data/.idea/takagi.iml +0 -81
- data/.idea/vcs.xml +0 -6
- data/lib/takagi/message.rb +0 -75
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'forwardable'
|
|
5
|
+
require_relative 'client_base'
|
|
6
|
+
require_relative 'message/retransmission_manager'
|
|
7
|
+
|
|
8
|
+
module Takagi
|
|
9
|
+
# Unified Takagi Client for communicating with Takagi servers over CoAP.
|
|
10
|
+
#
|
|
11
|
+
# Supports multiple protocols (UDP, TCP) with automatic protocol detection
|
|
12
|
+
# based on URI scheme or explicit protocol parameter.
|
|
13
|
+
#
|
|
14
|
+
# @example Block-based with protocol auto-detection (recommended)
|
|
15
|
+
# Takagi::Client.new('coap://localhost:5683') do |client|
|
|
16
|
+
# client.get('/temperature')
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# @example Block-based with explicit protocol
|
|
20
|
+
# Takagi::Client.new('localhost:5683', protocol: :tcp) do |client|
|
|
21
|
+
# client.get('/temperature')
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# @example Manual lifecycle management
|
|
25
|
+
# client = Takagi::Client.new('coap://localhost:5683')
|
|
26
|
+
# begin
|
|
27
|
+
# client.get('/temperature')
|
|
28
|
+
# ensure
|
|
29
|
+
# client.close
|
|
30
|
+
# end
|
|
31
|
+
class Client < ClientBase
|
|
32
|
+
extend Forwardable
|
|
33
|
+
|
|
34
|
+
# Delegate public methods to the implementation
|
|
35
|
+
def_delegators :@impl, :server_uri, :timeout, :callbacks, :closed?
|
|
36
|
+
def_delegators :@impl, :get, :post, :put, :delete, :on
|
|
37
|
+
def_delegators :@impl, :get_json, :post_json, :put_json, :close
|
|
38
|
+
|
|
39
|
+
# Creates a new client and optionally yields it to a block.
|
|
40
|
+
#
|
|
41
|
+
# @param server_uri [String] URL of the Takagi server (e.g., 'coap://localhost:5683', 'localhost:5683')
|
|
42
|
+
# @param timeout [Integer] Maximum time to wait for a response
|
|
43
|
+
# @param protocol [Symbol, nil] Protocol to use (:udp, :tcp, or nil for auto-detection from URI)
|
|
44
|
+
# @param use_retransmission [Boolean] Enable RFC 7252 §4.2 compliant retransmission for UDP (default: true)
|
|
45
|
+
# @yield [client] Optionally yields the client to a block and auto-closes afterward
|
|
46
|
+
# @return [Client, Object] Returns the client instance, or the block's return value if a block is given
|
|
47
|
+
#
|
|
48
|
+
# @example Protocol auto-detection from URI
|
|
49
|
+
# client = Takagi::Client.new('coap://localhost:5683') # Uses UDP
|
|
50
|
+
# client = Takagi::Client.new('coap+tcp://localhost:5683') # Uses TCP
|
|
51
|
+
#
|
|
52
|
+
# @example Explicit protocol specification
|
|
53
|
+
# client = Takagi::Client.new('localhost:5683', protocol: :tcp)
|
|
54
|
+
# client = Takagi::Client.new('localhost:5683', protocol: :udp)
|
|
55
|
+
#
|
|
56
|
+
# @example With block (auto-close)
|
|
57
|
+
# Takagi::Client.new('coap://localhost:5683') do |client|
|
|
58
|
+
# client.get('/resource')
|
|
59
|
+
# end
|
|
60
|
+
def initialize(server_uri, timeout: 5, protocol: nil, use_retransmission: true)
|
|
61
|
+
# Detect protocol from URI if not explicitly specified
|
|
62
|
+
@protocol = protocol || detect_protocol(server_uri)
|
|
63
|
+
|
|
64
|
+
# Delegate to the appropriate client implementation
|
|
65
|
+
@impl = create_client_impl(server_uri, timeout, use_retransmission)
|
|
66
|
+
|
|
67
|
+
# If a block is given, yield and auto-close
|
|
68
|
+
return unless block_given?
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
yield(self)
|
|
72
|
+
ensure
|
|
73
|
+
close
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
# Detects the protocol from the URI scheme using transport registry
|
|
80
|
+
# @param uri_string [String] The URI to parse
|
|
81
|
+
# @return [Symbol] :tcp or :udp
|
|
82
|
+
def detect_protocol(uri_string)
|
|
83
|
+
uri = URI(uri_string.start_with?('coap') ? uri_string : "coap://#{uri_string}")
|
|
84
|
+
|
|
85
|
+
# NEW: Use transport registry to find transport for scheme
|
|
86
|
+
transport = Takagi::Network::Registry.for_scheme(uri.scheme)
|
|
87
|
+
|
|
88
|
+
if transport
|
|
89
|
+
# Map transport class to symbol
|
|
90
|
+
case transport.scheme
|
|
91
|
+
when 'coap+tcp', 'coaps+tcp'
|
|
92
|
+
:tcp
|
|
93
|
+
else
|
|
94
|
+
:udp
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
:udp # Default to UDP if transport not found
|
|
98
|
+
end
|
|
99
|
+
rescue URI::InvalidURIError, Takagi::Network::Registry::TransportNotFoundError
|
|
100
|
+
:udp # Default to UDP if URI parsing fails or transport not found
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Creates the appropriate client implementation based on protocol
|
|
104
|
+
# @param server_uri [String] Server URI
|
|
105
|
+
# @param timeout [Integer] Request timeout
|
|
106
|
+
# @param use_retransmission [Boolean] Enable retransmission for UDP
|
|
107
|
+
# @return [UdpClient, TcpClient] The client implementation
|
|
108
|
+
def create_client_impl(server_uri, timeout, use_retransmission)
|
|
109
|
+
# Normalize URI to include scheme if not present
|
|
110
|
+
normalized_uri = normalize_uri(server_uri)
|
|
111
|
+
|
|
112
|
+
case @protocol
|
|
113
|
+
when :tcp
|
|
114
|
+
require_relative 'tcp_client'
|
|
115
|
+
TcpClient.new(normalized_uri, timeout: timeout)
|
|
116
|
+
when :udp
|
|
117
|
+
UdpClient.new(normalized_uri, timeout: timeout, use_retransmission: use_retransmission)
|
|
118
|
+
else
|
|
119
|
+
raise ArgumentError, "Unknown protocol: #{@protocol}. Use :udp or :tcp"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Normalizes URI to include appropriate scheme
|
|
124
|
+
# @param uri_string [String] The URI string
|
|
125
|
+
# @return [String] Normalized URI with scheme
|
|
126
|
+
def normalize_uri(uri_string)
|
|
127
|
+
return uri_string if uri_string.start_with?('coap')
|
|
128
|
+
|
|
129
|
+
scheme = @protocol == :tcp ? 'coap+tcp' : 'coap'
|
|
130
|
+
"#{scheme}://#{uri_string}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# UDP-specific client implementation (internal)
|
|
135
|
+
# Users should use Takagi::Client with protocol: :udp instead
|
|
136
|
+
class UdpClient < ClientBase
|
|
137
|
+
# Initializes the UDP client
|
|
138
|
+
# @param server_uri [String] URL of the Takagi server
|
|
139
|
+
# @param timeout [Integer] Maximum time to wait for a response
|
|
140
|
+
# @param use_retransmission [Boolean] Enable RFC 7252 §4.2 compliant retransmission (default: true)
|
|
141
|
+
def initialize(server_uri, timeout: 5, use_retransmission: true)
|
|
142
|
+
super(server_uri, timeout: timeout)
|
|
143
|
+
@use_retransmission = use_retransmission
|
|
144
|
+
|
|
145
|
+
return unless @use_retransmission
|
|
146
|
+
|
|
147
|
+
@retransmission_manager = Takagi::Message::RetransmissionManager.new
|
|
148
|
+
@retransmission_manager.start
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
protected
|
|
152
|
+
|
|
153
|
+
# Stops the retransmission manager thread
|
|
154
|
+
def cleanup_resources
|
|
155
|
+
@retransmission_manager&.stop
|
|
156
|
+
super
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# Executes a request to the server using Takagi::Message::Request
|
|
162
|
+
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
|
|
163
|
+
# @param path [String] Resource path
|
|
164
|
+
# @param payload [String] (optional) Data for POST/PUT requests
|
|
165
|
+
# @param options [Hash] (optional) CoAP options { option_number => value }
|
|
166
|
+
# @param type [Integer, nil] (optional) CoAP message type
|
|
167
|
+
# @param callback [Proc] (optional) Callback function for processing the response
|
|
168
|
+
def request(method, path, payload = nil, options: {}, type: nil, &callback)
|
|
169
|
+
uri = URI.join(server_uri.to_s, path)
|
|
170
|
+
message = Takagi::Message::Request.new(
|
|
171
|
+
method: method,
|
|
172
|
+
uri: uri,
|
|
173
|
+
payload: payload,
|
|
174
|
+
type: type,
|
|
175
|
+
options: options
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if @use_retransmission
|
|
179
|
+
request_with_retransmission(message, uri, &callback)
|
|
180
|
+
else
|
|
181
|
+
request_simple(message, uri, &callback)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Simple request without retransmission (legacy mode)
|
|
186
|
+
def request_simple(message, uri, &callback)
|
|
187
|
+
socket = UDPSocket.new
|
|
188
|
+
socket.send(message.to_bytes, 0, uri.host, uri.port || 5683)
|
|
189
|
+
|
|
190
|
+
unless socket.wait_readable(@timeout)
|
|
191
|
+
puts 'TakagiClient Error: Request timeout'
|
|
192
|
+
return
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
response, = socket.recvfrom(1024)
|
|
196
|
+
deliver_raw_response(response, &callback)
|
|
197
|
+
rescue StandardError => e
|
|
198
|
+
puts "TakagiClient Error: #{e.message}"
|
|
199
|
+
ensure
|
|
200
|
+
socket&.close unless socket&.closed?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# RFC 7252 §4.2 compliant request with automatic retransmission
|
|
204
|
+
def request_with_retransmission(message, uri, &callback)
|
|
205
|
+
socket = UDPSocket.new
|
|
206
|
+
state = send_with_retransmission(message, socket, uri)
|
|
207
|
+
socket.close
|
|
208
|
+
handle_response_state(state, &callback)
|
|
209
|
+
rescue StandardError => e
|
|
210
|
+
puts "TakagiClient Error: #{e.message}"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def send_with_retransmission(message, socket, uri)
|
|
214
|
+
state = { response_received: false, response_data: nil, error: nil }
|
|
215
|
+
|
|
216
|
+
@retransmission_manager.send_confirmable(
|
|
217
|
+
message.message_id, message.to_bytes, socket, uri.host, uri.port || 5683
|
|
218
|
+
) { |resp, err| update_state(state, resp, err) }
|
|
219
|
+
|
|
220
|
+
wait_for_response(message, socket, state)
|
|
221
|
+
state
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def wait_for_response(message, socket, state)
|
|
225
|
+
start_time = Time.now
|
|
226
|
+
check_socket_for_response(message, socket, state) until state[:response_received] || (Time.now - start_time) > @timeout
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def check_socket_for_response(message, socket, state)
|
|
230
|
+
return unless socket.wait_readable(0.1)
|
|
231
|
+
|
|
232
|
+
state[:response_data], = socket.recvfrom(1024)
|
|
233
|
+
@retransmission_manager.handle_response(message.message_id, state[:response_data])
|
|
234
|
+
state[:response_received] = true
|
|
235
|
+
rescue StandardError => e
|
|
236
|
+
update_state(state, nil, e.message)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def update_state(state, response_data, error)
|
|
240
|
+
state[:response_data] = response_data
|
|
241
|
+
state[:error] = error
|
|
242
|
+
state[:response_received] = true
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def handle_response_state(state, &callback)
|
|
246
|
+
if state[:response_received] && !state[:error]
|
|
247
|
+
deliver_response(state[:response_data], &callback)
|
|
248
|
+
elsif state[:error]
|
|
249
|
+
puts "TakagiClient Error: #{state[:error]}"
|
|
250
|
+
else
|
|
251
|
+
puts 'TakagiClient Error: Request timeout'
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def deliver_raw_response(response, &callback)
|
|
256
|
+
if callback
|
|
257
|
+
callback.call(response)
|
|
258
|
+
elsif @callbacks[:response]
|
|
259
|
+
@callbacks[:response].call(response)
|
|
260
|
+
else
|
|
261
|
+
puts response
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Takagi
|
|
7
|
+
# Base class for Takagi clients, providing common functionality
|
|
8
|
+
# for both UDP (CoAP) and TCP (CoAP over TCP) clients.
|
|
9
|
+
#
|
|
10
|
+
# This class defines the common interface and lifecycle management
|
|
11
|
+
# that all Takagi clients should follow.
|
|
12
|
+
class ClientBase
|
|
13
|
+
attr_reader :server_uri, :timeout, :callbacks
|
|
14
|
+
|
|
15
|
+
# Initializes the base client
|
|
16
|
+
# @param server_uri [String] URL of the Takagi server
|
|
17
|
+
# @param timeout [Integer] Maximum time to wait for a response
|
|
18
|
+
def initialize(server_uri, timeout: 5)
|
|
19
|
+
@server_uri = URI(server_uri)
|
|
20
|
+
@timeout = timeout
|
|
21
|
+
@callbacks = {}
|
|
22
|
+
@closed = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Registers a callback for a given event
|
|
26
|
+
# @param event [Symbol] Event name (e.g., :response)
|
|
27
|
+
# @param callback [Proc] Callback function to handle the event
|
|
28
|
+
def on(event, &callback)
|
|
29
|
+
@callbacks[event] = callback
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Sends a GET request
|
|
33
|
+
# @param path [String] Resource path
|
|
34
|
+
# @param options [Hash] CoAP options (e.g. { 17 => 50 } for Accept=application/json)
|
|
35
|
+
# @param type [Integer, nil] CoAP message type (CON/NON/ACK/RST). Defaults to CON.
|
|
36
|
+
# @param callback [Proc] (optional) Callback function for processing the response
|
|
37
|
+
def get(path, options: {}, type: nil, &block)
|
|
38
|
+
request(:get, path, nil, options: options, type: type, &block)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Sends a POST request
|
|
42
|
+
# @param path [String] Resource path
|
|
43
|
+
# @param payload [String, Hash] Data to send (Hash is JSON-encoded automatically)
|
|
44
|
+
# @param options [Hash] CoAP options (e.g. { 12 => 50 } for Content-Format=application/json)
|
|
45
|
+
# @param type [Integer, nil] CoAP message type
|
|
46
|
+
# @param callback [Proc] (optional) Callback function for processing the response
|
|
47
|
+
def post(path, payload = nil, options: {}, type: nil, &block)
|
|
48
|
+
request(:post, path, payload, options: options, type: type, &block)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sends a PUT request
|
|
52
|
+
# @param path [String] Resource path
|
|
53
|
+
# @param payload [String, Hash] Data to send
|
|
54
|
+
# @param options [Hash] CoAP options
|
|
55
|
+
# @param type [Integer, nil] CoAP message type
|
|
56
|
+
# @param callback [Proc] (optional) Callback function for processing the response
|
|
57
|
+
def put(path, payload = nil, options: {}, type: nil, &block)
|
|
58
|
+
request(:put, path, payload, options: options, type: type, &block)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Sends a DELETE request
|
|
62
|
+
# @param path [String] Resource path
|
|
63
|
+
# @param options [Hash] CoAP options
|
|
64
|
+
# @param type [Integer, nil] CoAP message type
|
|
65
|
+
# @param callback [Proc] (optional) Callback function for processing the response
|
|
66
|
+
def delete(path, options: {}, type: nil, &block)
|
|
67
|
+
request(:delete, path, nil, options: options, type: type, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Sends an OBSERVE request (RFC 7641). The Observe option is added automatically;
|
|
71
|
+
# additional CoAP options can be supplied via `options:`.
|
|
72
|
+
# @param path [String] Resource path
|
|
73
|
+
# @param options [Hash] CoAP options (Observe is injected automatically)
|
|
74
|
+
# @param callback [Proc] (optional) Callback function for processing notifications
|
|
75
|
+
def observe(path, options: {}, &block)
|
|
76
|
+
merged = { Takagi::CoAP::Registries::Option::OBSERVE => 0 }.merge(options)
|
|
77
|
+
request(:get, path, nil, options: merged, &block)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Sends a POST request with JSON payload (convenience method)
|
|
81
|
+
# @param path [String] Resource path
|
|
82
|
+
# @param data [Hash, Array] Data to encode as JSON
|
|
83
|
+
# @param options [Hash] CoAP options
|
|
84
|
+
# @param callback [Proc] (optional) Callback function for processing the response
|
|
85
|
+
def post_json(path, data, options: {}, &block)
|
|
86
|
+
cf = Takagi::CoAP::Registries::ContentFormat::JSON
|
|
87
|
+
merged = { Takagi::CoAP::Registries::Option::CONTENT_FORMAT => cf }.merge(options)
|
|
88
|
+
request(:post, path, JSON.generate(data), options: merged, &block)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Sends a PUT request with JSON payload (convenience method)
|
|
92
|
+
def put_json(path, data, options: {}, &block)
|
|
93
|
+
cf = Takagi::CoAP::Registries::ContentFormat::JSON
|
|
94
|
+
merged = { Takagi::CoAP::Registries::Option::CONTENT_FORMAT => cf }.merge(options)
|
|
95
|
+
request(:put, path, JSON.generate(data), options: merged, &block)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Sends a GET request and automatically parses JSON response (convenience method)
|
|
99
|
+
def get_json(path, options: {}, &block)
|
|
100
|
+
cf = Takagi::CoAP::Registries::ContentFormat::JSON
|
|
101
|
+
merged = { Takagi::CoAP::Registries::Option::ACCEPT => cf }.merge(options)
|
|
102
|
+
if block_given?
|
|
103
|
+
get(path, options: merged) do |response|
|
|
104
|
+
data = response.is_a?(String) ? parse_json_response(response) : response.json
|
|
105
|
+
block.call(data)
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
result = nil
|
|
109
|
+
get(path, options: merged) { |response| result = response.is_a?(String) ? parse_json_response(response) : response.json }
|
|
110
|
+
result
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Closes the client and releases any resources.
|
|
115
|
+
# This should be called when the client is no longer needed to prevent
|
|
116
|
+
# resource leaks in long-running processes.
|
|
117
|
+
#
|
|
118
|
+
# Subclasses should override this method to perform specific cleanup
|
|
119
|
+
# and then call super.
|
|
120
|
+
def close
|
|
121
|
+
return if @closed
|
|
122
|
+
|
|
123
|
+
cleanup_resources
|
|
124
|
+
@closed = true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Check if the client has been closed
|
|
128
|
+
# @return [Boolean] true if the client is closed
|
|
129
|
+
def closed?
|
|
130
|
+
@closed
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Creates a new client and yields it to the block, ensuring it's closed afterward.
|
|
134
|
+
# This is the recommended way to use clients to prevent resource leaks.
|
|
135
|
+
#
|
|
136
|
+
# @param server_uri [String] URL of the Takagi server
|
|
137
|
+
# @param timeout [Integer] Maximum time to wait for a response
|
|
138
|
+
# @param options [Hash] Additional options passed to the subclass constructor
|
|
139
|
+
# @yield [client] Gives the client to the block
|
|
140
|
+
# @return [Object] The return value of the block
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
# Takagi::Client.open('coap://localhost:5683') do |client|
|
|
144
|
+
# client.get('/temperature')
|
|
145
|
+
# end
|
|
146
|
+
def self.open(server_uri, timeout: 5, **options, &block)
|
|
147
|
+
client = new(server_uri, timeout: timeout, **options)
|
|
148
|
+
block.call(client)
|
|
149
|
+
ensure
|
|
150
|
+
client&.close
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
protected
|
|
154
|
+
|
|
155
|
+
# Subclasses must implement this method to perform the actual request
|
|
156
|
+
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
|
|
157
|
+
# @param path [String] Resource path
|
|
158
|
+
# @param payload [String] (optional) Data for POST/PUT requests
|
|
159
|
+
# @param options [Hash] (optional) CoAP options { option_number => value }
|
|
160
|
+
# @param type [Integer, nil] (optional) CoAP message type
|
|
161
|
+
# @param callback [Proc] (optional) Callback function for processing the response
|
|
162
|
+
def request(_method, _path, _payload = nil, options: {}, type: nil, &_callback)
|
|
163
|
+
raise NotImplementedError, "#{self.class} must implement #request"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Subclasses can override this to perform specific cleanup
|
|
167
|
+
# Called by #close before marking the client as closed
|
|
168
|
+
def cleanup_resources
|
|
169
|
+
# Default: no-op
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Delivers a response using the callback or registered callback
|
|
173
|
+
# Wraps raw response data in a Response object for convenience
|
|
174
|
+
# @param response_data [String] The response data to deliver
|
|
175
|
+
# @param callback [Proc] Optional callback for this specific request
|
|
176
|
+
def deliver_response(response_data, &callback)
|
|
177
|
+
# Wrap response in Response object for better DX
|
|
178
|
+
require_relative 'client/response'
|
|
179
|
+
response = Client::Response.new(response_data)
|
|
180
|
+
|
|
181
|
+
return callback.call(response) if callback
|
|
182
|
+
return @callbacks[:response].call(response) if @callbacks[:response]
|
|
183
|
+
|
|
184
|
+
# Default: print response details
|
|
185
|
+
if response.success?
|
|
186
|
+
puts "[#{response.code_name}] #{response.payload}"
|
|
187
|
+
else
|
|
188
|
+
puts "[ERROR #{response.code_name}] #{response.payload}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Helper to parse JSON response from raw data
|
|
193
|
+
# @param response_data [String] Raw response data
|
|
194
|
+
# @return [Hash, Array, nil] Parsed JSON or nil
|
|
195
|
+
def parse_json_response(response_data)
|
|
196
|
+
inbound = Takagi::Message::Inbound.new(response_data)
|
|
197
|
+
return nil unless inbound.payload
|
|
198
|
+
|
|
199
|
+
JSON.parse(inbound.payload)
|
|
200
|
+
rescue JSON::ParserError
|
|
201
|
+
nil
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Takagi
|
|
4
|
+
module CoAP
|
|
5
|
+
# Utility methods for working with CoAP codes.
|
|
6
|
+
#
|
|
7
|
+
# Provides conversion between different code representations and
|
|
8
|
+
# lookups across all registries.
|
|
9
|
+
module CodeHelpers
|
|
10
|
+
# Convert a code to its human-readable string representation
|
|
11
|
+
#
|
|
12
|
+
# @param code [Integer, String, Symbol] Code to convert
|
|
13
|
+
# @return [String] Human-readable code string
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# CodeHelpers.to_string(69) # => "2.05 Content"
|
|
17
|
+
# CodeHelpers.to_string(:get) # => "GET"
|
|
18
|
+
# CodeHelpers.to_string("2.05") # => "2.05 Content"
|
|
19
|
+
def self.to_string(code)
|
|
20
|
+
case code
|
|
21
|
+
when Integer
|
|
22
|
+
# Try method registry first
|
|
23
|
+
if code < 32
|
|
24
|
+
Registries::Method.name_for(code) || numeric_to_string(code)
|
|
25
|
+
# Try signaling registry for 7.xx codes
|
|
26
|
+
elsif code >= 224
|
|
27
|
+
Registries::Signaling.name_for(code) || numeric_to_string(code)
|
|
28
|
+
# Try response registry
|
|
29
|
+
elsif code >= 64
|
|
30
|
+
Registries::Response.name_for(code) || numeric_to_string(code)
|
|
31
|
+
else
|
|
32
|
+
numeric_to_string(code)
|
|
33
|
+
end
|
|
34
|
+
when Symbol
|
|
35
|
+
# Try method registry
|
|
36
|
+
Registries::Method.value_for(code)&.then { |v| Registries::Method.name_for(v) } ||
|
|
37
|
+
# Try response registry
|
|
38
|
+
Registries::Response.value_for(code)&.then { |v| Registries::Response.name_for(v) } ||
|
|
39
|
+
code.to_s
|
|
40
|
+
when String
|
|
41
|
+
# If already in dotted format, try to lookup
|
|
42
|
+
if code =~ /^(\d)\.(\d{2})$/
|
|
43
|
+
val = string_to_numeric(code)
|
|
44
|
+
Registries::Response.name_for(val) || code
|
|
45
|
+
else
|
|
46
|
+
code
|
|
47
|
+
end
|
|
48
|
+
else
|
|
49
|
+
code.to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert a code to its numeric representation
|
|
54
|
+
#
|
|
55
|
+
# @param code [Integer, String, Symbol] Code to convert
|
|
56
|
+
# @return [Integer] Numeric code
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# CodeHelpers.to_numeric(:get) # => 1
|
|
60
|
+
# CodeHelpers.to_numeric("2.05") # => 69
|
|
61
|
+
# CodeHelpers.to_numeric("2.05 Content") # => 69
|
|
62
|
+
# CodeHelpers.to_numeric(:content) # => 69
|
|
63
|
+
def self.to_numeric(code)
|
|
64
|
+
case code
|
|
65
|
+
when Integer
|
|
66
|
+
code
|
|
67
|
+
when Symbol
|
|
68
|
+
Registries::Method.value_for(code) || Registries::Response.value_for(code) || Registries::Signaling.value_for(code) || 0
|
|
69
|
+
when String
|
|
70
|
+
# Handle "2.05" or "2.05 Content" formats
|
|
71
|
+
if code =~ /^(\d)\.(\d{2})/
|
|
72
|
+
string_to_numeric("#{::Regexp.last_match(1)}.#{::Regexp.last_match(2)}")
|
|
73
|
+
else
|
|
74
|
+
Registries::Method.value_for(code.downcase.to_sym) ||
|
|
75
|
+
Registries::Response.value_for(code.downcase.to_sym) ||
|
|
76
|
+
0
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
0
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Convert numeric code to dotted string format (e.g., 69 => "2.05")
|
|
84
|
+
#
|
|
85
|
+
# @param code [Integer] Numeric code
|
|
86
|
+
# @return [String] Dotted format string
|
|
87
|
+
def self.numeric_to_string(code)
|
|
88
|
+
class_num = code / 32
|
|
89
|
+
detail_num = code % 32
|
|
90
|
+
"#{class_num}.#{detail_num.to_s.rjust(2, '0')}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Convert dotted string to numeric code (e.g., "2.05" => 69)
|
|
94
|
+
#
|
|
95
|
+
# @param code_string [String] Dotted format string
|
|
96
|
+
# @return [Integer] Numeric code
|
|
97
|
+
def self.string_to_numeric(code_string)
|
|
98
|
+
return 0 unless code_string =~ /^(\d)\.(\d{2})$/
|
|
99
|
+
|
|
100
|
+
class_num = ::Regexp.last_match(1).to_i
|
|
101
|
+
detail_num = ::Regexp.last_match(2).to_i
|
|
102
|
+
(class_num * 32) + detail_num
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if a code represents success
|
|
106
|
+
#
|
|
107
|
+
# @param code [Integer, String, Symbol] Code to check
|
|
108
|
+
# @return [Boolean] true if success code (2.xx)
|
|
109
|
+
def self.success?(code)
|
|
110
|
+
numeric = to_numeric(code)
|
|
111
|
+
Registries::Response.success?(numeric)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if a code represents an error
|
|
115
|
+
#
|
|
116
|
+
# @param code [Integer, String, Symbol] Code to check
|
|
117
|
+
# @return [Boolean] true if error code (4.xx or 5.xx)
|
|
118
|
+
def self.error?(code)
|
|
119
|
+
numeric = to_numeric(code)
|
|
120
|
+
Registries::Response.error?(numeric)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Check if a code represents a client error
|
|
124
|
+
#
|
|
125
|
+
# @param code [Integer, String, Symbol] Code to check
|
|
126
|
+
# @return [Boolean] true if client error (4.xx)
|
|
127
|
+
def self.client_error?(code)
|
|
128
|
+
numeric = to_numeric(code)
|
|
129
|
+
Registries::Response.client_error?(numeric)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Check if a code represents a server error
|
|
133
|
+
#
|
|
134
|
+
# @param code [Integer, String, Symbol] Code to check
|
|
135
|
+
# @return [Boolean] true if server error (5.xx)
|
|
136
|
+
def self.server_error?(code)
|
|
137
|
+
numeric = to_numeric(code)
|
|
138
|
+
Registries::Response.server_error?(numeric)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Lookup code in all registries
|
|
142
|
+
#
|
|
143
|
+
# @param code [Integer, String, Symbol] Code to lookup
|
|
144
|
+
# @return [Hash, nil] Registry information
|
|
145
|
+
def self.lookup(code)
|
|
146
|
+
numeric = to_numeric(code)
|
|
147
|
+
return nil if numeric.zero?
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
value: numeric,
|
|
151
|
+
string: numeric_to_string(numeric),
|
|
152
|
+
name: to_string(numeric),
|
|
153
|
+
type: code_type(numeric),
|
|
154
|
+
rfc: find_rfc(numeric)
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get the type of code (method, response, or unknown)
|
|
159
|
+
#
|
|
160
|
+
# @param code [Integer] Numeric code
|
|
161
|
+
# @return [Symbol] :method, :response, or :unknown
|
|
162
|
+
def self.code_type(code)
|
|
163
|
+
if code < 32
|
|
164
|
+
:method
|
|
165
|
+
elsif code.between?(64, 191)
|
|
166
|
+
:response
|
|
167
|
+
elsif code.between?(224, 255)
|
|
168
|
+
:signaling
|
|
169
|
+
else
|
|
170
|
+
:unknown
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Find RFC reference for a code
|
|
175
|
+
#
|
|
176
|
+
# @param code [Integer] Numeric code
|
|
177
|
+
# @return [String, nil] RFC reference
|
|
178
|
+
def self.find_rfc(code)
|
|
179
|
+
Registries::Method.rfc_for(code) || Registries::Response.rfc_for(code) || Registries::Signaling.rfc_for(code)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Get all registered codes across all registries
|
|
183
|
+
#
|
|
184
|
+
# @return [Hash] Map of value => name for all codes
|
|
185
|
+
def self.all
|
|
186
|
+
Registries::Method.all.merge(Registries::Response.all)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|