asterism-zenoh 0.3.0 → 0.4.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.
@@ -0,0 +1,221 @@
1
+ # The part of Asterism::Zenoh written in Ruby that is the same on every
2
+ # backend: this file is byte for byte the same as mrblib/common.rb of the
3
+ # mruby / PicoRuby binding (picoruby-asterism-zenoh). It runs on CRuby,
4
+ # mruby and PicoRuby, so it keeps to what all three have (no Regexp, no
5
+ # Data, while loops where a block would be called from C).
6
+ #
7
+ # What it adds on top of the C binding:
8
+ # - the deprecation helper (Asterism.deprecated, Asterism.deprecations=),
9
+ # shared by every Asterism gem, and Asterism.warn_once (lost replies);
10
+ # - the time limit in seconds (timeout:) next to milliseconds (timeout_ms:)
11
+ # on get and liveliness_get, and keywords for the positional optionals;
12
+ # - connection_count's old name peers, and the one-argument Query#reply
13
+ # that will change in 1.0, both with a deprecation warning;
14
+ # - Error#code and the readable constant names.
15
+ module Asterism
16
+ # The root of every Asterism error (the binding's C code defines it first).
17
+ class Error < StandardError; end
18
+
19
+ # Raised instead of a warning when Asterism.deprecations is :raise.
20
+ class DeprecationError < Error; end
21
+
22
+ # How a deprecated call is reported: :warn (once per name, the default),
23
+ # :raise (DeprecationError; for CI) or :silent. The environment variable
24
+ # ASTERISM_DEPRECATIONS (warn / raise / silent) sets the default.
25
+ def self.deprecations
26
+ if @deprecations.nil?
27
+ mode = nil
28
+ begin
29
+ mode = ENV["ASTERISM_DEPRECATIONS"] if Object.const_defined?(:ENV)
30
+ rescue StandardError
31
+ mode = nil
32
+ end
33
+ @deprecations = (mode == "raise" || mode == "silent") ? mode.to_sym : :warn
34
+ end
35
+ @deprecations
36
+ end
37
+
38
+ def self.deprecations=(mode)
39
+ m = mode.to_s.to_sym
40
+ raise ArgumentError, "deprecations must be :warn, :raise or :silent" unless m == :warn || m == :raise || m == :silent
41
+ @deprecations = m
42
+ end
43
+
44
+ # Reports that `old` is deprecated in favour of `instead`: warns once per
45
+ # `old` per process (per VM on the boards), raises DeprecationError, or
46
+ # stays silent (Asterism.deprecations). `why` is an optional sentence.
47
+ def self.deprecated(old, instead, why = nil)
48
+ mode = deprecations
49
+ return nil if mode == :silent
50
+ msg = "asterism: #{old} is deprecated and goes away in 1.0; use #{instead}"
51
+ msg = "#{msg} (#{why})" if why
52
+ raise DeprecationError, msg if mode == :raise
53
+ @deprecated_seen ||= {}
54
+ return nil if @deprecated_seen[old]
55
+ @deprecated_seen[old] = true
56
+ if respond_to?(:warn, true)
57
+ warn(msg)
58
+ else
59
+ puts(msg)
60
+ end
61
+ nil
62
+ end
63
+
64
+ # Warns msg once for obj (a Get, a watch, a wrapper): later calls for
65
+ # the same object say nothing. For losses the application did not ask
66
+ # for, such as replies dropped from a full queue. Returns true when it
67
+ # warned. Ruby's warn stays silent with $VERBOSE = nil.
68
+ def self.warn_once(obj, msg)
69
+ # By object_id: not every mruby build has instance_variable_get. The
70
+ # objects warned about are few; the table is emptied past 256.
71
+ @warned_once ||= {}
72
+ id = obj.object_id
73
+ return false if @warned_once[id]
74
+ @warned_once = {} if @warned_once.size >= 256
75
+ @warned_once[id] = true
76
+ if respond_to?(:warn, true)
77
+ warn(msg)
78
+ else
79
+ puts(msg)
80
+ end
81
+ true
82
+ end
83
+
84
+ # The names warned about so far (for tests). @api private
85
+ def self.deprecated_names
86
+ (@deprecated_seen || {}).keys
87
+ end
88
+
89
+ # Forget the names warned about (for tests). @api private
90
+ def self.reset_deprecations
91
+ @deprecated_seen = {}
92
+ nil
93
+ end
94
+
95
+ # A time limit in milliseconds from the ways a call may take it: timeout:
96
+ # (seconds), timeout_ms: or the old positional milliseconds (deprecated).
97
+ # Giving more than one raises ArgumentError. @api private
98
+ def self.time_ms(where, timeout, timeout_ms, positional, default_ms)
99
+ given = 0
100
+ given += 1 unless timeout.nil?
101
+ given += 1 unless timeout_ms.nil?
102
+ given += 1 unless positional.nil?
103
+ raise ArgumentError, "#{where}: give the time limit once (timeout: in seconds, or timeout_ms:)" if given > 1
104
+ unless positional.nil?
105
+ if positional.is_a?(Float)
106
+ deprecated("#{where} with a Float as the positional time limit", "timeout: (seconds)",
107
+ "the positional time limit is milliseconds, so #{positional} waits #{positional.to_i} ms; " \
108
+ "a Float there is usually meant as seconds")
109
+ else
110
+ deprecated("#{where} with the time limit as a positional argument", "timeout: (seconds) or timeout_ms:")
111
+ end
112
+ return positional.to_i
113
+ end
114
+ unless timeout_ms.nil?
115
+ raise TypeError, "#{where}: timeout_ms: must be a number" unless timeout_ms.is_a?(Numeric)
116
+ return timeout_ms.round
117
+ end
118
+ return default_ms if timeout.nil?
119
+ raise TypeError, "#{where}: timeout: must be a number of seconds" unless timeout.is_a?(Numeric)
120
+ ms = (timeout * 1000).round
121
+ raise ArgumentError, "#{where}: timeout: #{timeout} s is less than 1 ms" if ms < 1
122
+ ms
123
+ end
124
+
125
+ # A queue depth given positionally (kept) or as depth: (not both).
126
+ # @api private
127
+ def self.depth_of(where, args, depth, default)
128
+ raise ArgumentError, "#{where}: too many arguments" if args.size > 1
129
+ raise ArgumentError, "#{where}: give the depth once (positional or depth:)" if args.size == 1 && !depth.nil?
130
+ return args[0] if args.size == 1
131
+ depth.nil? ? default : depth
132
+ end
133
+
134
+ module Zenoh
135
+ # The readable name of PEER: whether this build has peer mode.
136
+ PEER_SUPPORTED = PEER
137
+ # The default time limit of get and liveliness_get, in seconds.
138
+ DEFAULT_TIMEOUT = 2.0
139
+
140
+ class Error
141
+ # The result code of zenoh-c / zenoh-pico (a negative Integer) when the
142
+ # failure came with one, else nil.
143
+ attr_reader :code
144
+ end
145
+
146
+ class Session
147
+ alias_method :__asterism_get, :get
148
+ alias_method :__asterism_liveliness_get, :liveliness_get
149
+ alias_method :__asterism_subscribe, :subscribe
150
+ alias_method :__asterism_queryable, :queryable
151
+ alias_method :__asterism_liveliness_watch, :liveliness_watch
152
+
153
+ # get(key, timeout: 2.0, params: nil, payload: nil, attachment: nil,
154
+ # target: :all, consolidation: :none, depth: DEFAULT_GET_DEPTH,
155
+ # ...) -> Get.
156
+ # timeout: seconds; or timeout_ms:. depth: the replies kept until
157
+ # taken (past it the oldest go; Get#dropped counts them). The old
158
+ # positional form get(key, timeout_ms, params, payload) still works
159
+ # (deprecated).
160
+ def get(key, *args, timeout: nil, timeout_ms: nil, params: nil, payload: nil, **opts)
161
+ raise ArgumentError, "get: wrong number of arguments (given #{args.size + 1}, expected 1..4)" if args.size > 3
162
+ raise ArgumentError, "get: params given twice" if args.size > 1 && !params.nil?
163
+ raise ArgumentError, "get: payload given twice" if args.size > 2 && !payload.nil?
164
+ ms = ::Asterism.time_ms("Session#get", timeout, timeout_ms, args[0], 2000)
165
+ params = args[1] if args.size > 1
166
+ payload = args[2] if args.size > 2
167
+ __asterism_get(key, ms, params, payload, **opts)
168
+ end
169
+
170
+ # liveliness_get(key, timeout: 2.0, depth: DEFAULT_GET_DEPTH) -> Get
171
+ # (or timeout_ms:).
172
+ def liveliness_get(key, *args, timeout: nil, timeout_ms: nil, depth: nil)
173
+ raise ArgumentError, "liveliness_get: wrong number of arguments (given #{args.size + 1}, expected 1..2)" if args.size > 1
174
+ ms = ::Asterism.time_ms("Session#liveliness_get", timeout, timeout_ms, args[0], 2000)
175
+ __asterism_liveliness_get(key, ms, depth: depth.nil? ? DEFAULT_GET_DEPTH : depth)
176
+ end
177
+
178
+ # subscribe(key, depth = 16) or subscribe(key, depth: 16).
179
+ def subscribe(key, *args, depth: nil)
180
+ __asterism_subscribe(key, ::Asterism.depth_of("subscribe", args, depth, DEFAULT_DEPTH))
181
+ end
182
+
183
+ # queryable(key, depth = 16, complete: false) or with depth:.
184
+ def queryable(key, *args, depth: nil, **opts)
185
+ __asterism_queryable(key, ::Asterism.depth_of("queryable", args, depth, DEFAULT_DEPTH), **opts)
186
+ end
187
+
188
+ # liveliness_watch(key, depth = DEFAULT_WATCH_DEPTH) or with depth:.
189
+ # The tokens alive when it is declared come in one burst.
190
+ def liveliness_watch(key, *args, depth: nil)
191
+ __asterism_liveliness_watch(key, ::Asterism.depth_of("liveliness_watch", args, depth, DEFAULT_WATCH_DEPTH))
192
+ end
193
+
194
+ # Deprecated: the number of connections is connection_count.
195
+ def peers
196
+ ::Asterism.deprecated("Session#peers", "Session#connection_count",
197
+ "for a client session it counts routers, not peers")
198
+ connection_count
199
+ end
200
+ end
201
+
202
+ class Query
203
+ alias_method :__asterism_reply, :reply
204
+
205
+ # reply(payload) answers on the query's key today. From 1.0 it answers
206
+ # on the queryable's own key when that key has no wildcard (as the
207
+ # CRuby block API does); a reply that would change warns now.
208
+ def reply(*args, **kw)
209
+ if args.size == 1
210
+ own = @asterism_queryable_key
211
+ if own && !own.include?("*") && !own.include?("$") && own != key
212
+ ::Asterism.deprecated("Query#reply(payload) for a query whose key differs from the queryable's",
213
+ "q.reply(key, payload)",
214
+ "from 1.0 a reply with only a payload answers on the queryable's own key")
215
+ end
216
+ end
217
+ __asterism_reply(*args, **kw)
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,128 @@
1
+ # What only the CRuby binding (zenoh-c) adds in Ruby: the backend
2
+ # constants, Session.open with a block and connect_timeout:, seconds and
3
+ # keywords on the CRuby-only calls (querier, advanced subscriber,
4
+ # listeners), the long reply names, and scout's argument check.
5
+ module Asterism
6
+ module Zenoh
7
+ # Which Zenoh implementation is underneath (:zenoh_pico on the boards).
8
+ BACKEND = :zenoh_c
9
+ # Its version (C_VERSION here, PICO_VERSION on the boards).
10
+ BACKEND_VERSION = C_VERSION
11
+
12
+ class Session
13
+ class << self
14
+ alias_method :__asterism_open, :open
15
+
16
+ # Session.open(locator = nil, mode:, listen:, scouting:,
17
+ # timestamping:, config:, config_file:, connect_timeout:) -> Session.
18
+ # connect_timeout: seconds to wait for a router or peer
19
+ # (CONNECT_TIMEOUT_MS by default; zenoh's connect/timeout_ms). With a
20
+ # block: yields the session, closes it when the block ends (also on
21
+ # an exception) and returns the block's value.
22
+ def open(locator = nil, connect_timeout: nil, **opts)
23
+ unless connect_timeout.nil?
24
+ ms = ::Asterism.time_ms("Session.open connect_timeout:", connect_timeout, nil, nil, CONNECT_TIMEOUT_MS)
25
+ cfg = opts[:config]
26
+ if cfg.nil?
27
+ opts[:config] = { "connect/timeout_ms" => ms }
28
+ elsif cfg.is_a?(Hash)
29
+ if cfg.key?("connect/timeout_ms") || cfg.key?(:"connect/timeout_ms")
30
+ raise ArgumentError, "connect_timeout: and config: {\"connect/timeout_ms\" => ...} given together"
31
+ end
32
+ opts[:config] = cfg.merge("connect/timeout_ms" => ms)
33
+ else
34
+ raise ArgumentError, "connect_timeout: cannot be added to a config String; set connect/timeout_ms in it"
35
+ end
36
+ end
37
+ s = __asterism_open(locator, **opts)
38
+ return s unless block_given?
39
+ begin
40
+ yield s
41
+ ensure
42
+ s.close
43
+ end
44
+ end
45
+ end
46
+
47
+ alias_method :__asterism_querier, :querier
48
+ alias_method :__asterism_advanced_subscriber, :advanced_subscriber
49
+ alias_method :__asterism_transport_events, :transport_events
50
+ alias_method :__asterism_link_events, :link_events
51
+
52
+ # querier(key, timeout: 2.0, ...) -> Querier (or timeout_ms:).
53
+ def querier(key, timeout: nil, timeout_ms: nil, **opts)
54
+ ms = ::Asterism.time_ms("Session#querier", timeout, timeout_ms, nil, nil)
55
+ opts[:timeout_ms] = ms unless ms.nil?
56
+ __asterism_querier(key, **opts)
57
+ end
58
+
59
+ # advanced_subscriber(key, depth = 16, query_timeout: seconds, ...)
60
+ # (or depth:, query_timeout_ms:).
61
+ def advanced_subscriber(key, *args, depth: nil, query_timeout: nil, query_timeout_ms: nil, **opts)
62
+ d = ::Asterism.depth_of("advanced_subscriber", args, depth, 16)
63
+ ms = ::Asterism.time_ms("Session#advanced_subscriber query_timeout", query_timeout, query_timeout_ms, nil, nil)
64
+ opts[:query_timeout_ms] = ms unless ms.nil?
65
+ __asterism_advanced_subscriber(key, d, **opts)
66
+ end
67
+
68
+ # transport_events(depth = 16, history: false) or with depth:.
69
+ def transport_events(*args, depth: nil, **opts)
70
+ __asterism_transport_events(::Asterism.depth_of("transport_events", args, depth, 16), **opts)
71
+ end
72
+
73
+ # link_events(depth = 16, history: false) or with depth:.
74
+ def link_events(*args, depth: nil, **opts)
75
+ __asterism_link_events(::Asterism.depth_of("link_events", args, depth, 16), **opts)
76
+ end
77
+ end
78
+
79
+ # matching_listener(depth = 16) or matching_listener(depth: 16).
80
+ module MatchingDepth
81
+ def matching_listener(*args, depth: nil)
82
+ super(::Asterism.depth_of("matching_listener", args, depth, 16))
83
+ end
84
+ end
85
+ Publisher.prepend(MatchingDepth)
86
+ AdvancedPublisher.prepend(MatchingDepth)
87
+
88
+ class Querier
89
+ prepend MatchingDepth
90
+ alias_method :__asterism_get, :get
91
+
92
+ # get(params: nil, payload: nil, attachment:, encoding:, depth:) -> Get; the
93
+ # positional get(params, payload) still works.
94
+ def get(*args, params: nil, payload: nil, **opts)
95
+ raise ArgumentError, "get: wrong number of arguments (given #{args.size}, expected 0..2)" if args.size > 2
96
+ raise ArgumentError, "get: params given twice" if args.size > 0 && !params.nil?
97
+ raise ArgumentError, "get: payload given twice" if args.size > 1 && !payload.nil?
98
+ params = args[0] if args.size > 0
99
+ payload = args[1] if args.size > 1
100
+ __asterism_get(params, payload, **opts)
101
+ end
102
+ end
103
+
104
+ class AdvancedSubscriber
105
+ alias_method :__asterism_miss_listener, :miss_listener
106
+ alias_method :__asterism_detect_publishers, :detect_publishers
107
+
108
+ def miss_listener(*args, depth: nil)
109
+ __asterism_miss_listener(::Asterism.depth_of("miss_listener", args, depth, 16))
110
+ end
111
+
112
+ def detect_publishers(*args, depth: nil, **opts)
113
+ __asterism_detect_publishers(::Asterism.depth_of("detect_publishers", args, depth, 16), **opts)
114
+ end
115
+ end
116
+
117
+ class Query
118
+ # The spelled-out names of reply_err and reply_del (those stay).
119
+ def reply_error(payload, **opts)
120
+ reply_err(payload, **opts)
121
+ end
122
+
123
+ def reply_delete(key = nil, **opts)
124
+ reply_del(key, **opts)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -26,6 +26,7 @@ module Asterism
26
26
 
27
27
  def put? = kind == :put
28
28
  def delete? = kind == :delete
29
+ def express? = express
29
30
 
30
31
  def to_s
31
32
  "#{key}: #{payload}"
@@ -59,22 +60,28 @@ module Asterism
59
60
  Hello = Data.define(:zid, :whatami, :locators)
60
61
 
61
62
  # A connection to another peer or router (Session#transports).
62
- Transport = Data.define(:zid, :whatami, :qos, :multicast)
63
+ Transport = Data.define(:zid, :whatami, :qos, :multicast) do
64
+ def multicast? = multicast
65
+ end
63
66
 
64
67
  # A transport that appeared (kind :added) or went (:removed), from
65
68
  # Session#transport_events.
66
69
  TransportEvent = Data.define(:kind, :zid, :whatami, :qos, :multicast) do
70
+ def multicast? = multicast
67
71
  def added? = kind == :added
68
72
  def removed? = kind == :removed
69
73
  end
70
74
 
71
75
  # A link (one connection of a transport; Session#links).
72
- Link = Data.define(:zid, :src, :dst, :mtu, :streamed, :reliability, :interfaces, :group, :auth_identifier)
76
+ Link = Data.define(:zid, :src, :dst, :mtu, :streamed, :reliability, :interfaces, :group, :auth_identifier) do
77
+ def streamed? = streamed
78
+ end
73
79
 
74
80
  # A link that opened (kind :added) or closed (:removed), from
75
81
  # Session#link_events.
76
82
  LinkEvent = Data.define(:kind, :zid, :src, :dst, :mtu, :streamed, :reliability, :interfaces, :group,
77
83
  :auth_identifier) do
84
+ def streamed? = streamed
78
85
  def added? = kind == :added
79
86
  def removed? = kind == :removed
80
87
  end
@@ -87,18 +94,18 @@ module Asterism
87
94
  SCOUT_WHAT = { router: 1, peer: 2, client: 4 }.freeze
88
95
 
89
96
  # Looks for routers and / or peers by multicast scouting for timeout
90
- # seconds (or timeout_ms milliseconds) and returns what answered, as an
91
- # Array of Hello. what: :router, :peer, :client, an Array of them, or
97
+ # seconds (or timeout_ms milliseconds; not both) and returns what
98
+ # answered, as an Array of Hello. what: :router, :peer, :client, an Array of them, or
92
99
  # :all. config: a Hash of zenoh configuration keys, as for
93
100
  # Session.open (e.g. {"scouting/multicast/interface" => "eth0"}).
94
101
  # Multicast must be allowed on the network (it often is not in
95
102
  # containers and VPNs).
96
- def self.scout(what: %i[router peer], timeout: 1.0, timeout_ms: nil, config: nil)
103
+ def self.scout(what: %i[router peer], timeout: nil, timeout_ms: nil, config: nil)
97
104
  kinds = what == :all ? SCOUT_WHAT.keys : Array(what)
98
105
  mask = kinds.sum do |k|
99
106
  SCOUT_WHAT.fetch(k.to_sym) { raise ArgumentError, "what must be :router, :peer, :client or :all" }
100
107
  end
101
- ms = timeout_ms || (timeout * 1000).round
108
+ ms = ::Asterism.time_ms("Asterism::Zenoh.scout", timeout, timeout_ms, nil, 1000)
102
109
  _scout(mask, ms, config)
103
110
  end
104
111
  end
@@ -1,6 +1,6 @@
1
1
  module Asterism
2
2
  module Zenoh
3
3
  # The gem's version.
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
6
6
  end
@@ -8,3 +8,5 @@
8
8
  require_relative "zenoh/version"
9
9
  require_relative "asterism_zenoh"
10
10
  require_relative "zenoh/values"
11
+ require_relative "zenoh/common"
12
+ require_relative "zenoh/cruby"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asterism-zenoh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katsuhiko Kageyama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-08 00:00:00.000000000 Z
11
+ date: 2026-10-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Sessions, put / subscribe, get / queryable, liveliness and attachments,
14
14
  received by polling. The same Ruby API as Asterism's mruby / PicoRuby binding, plus
@@ -22,6 +22,7 @@ extensions:
22
22
  - ext/asterism_zenoh/extconf.rb
23
23
  extra_rdoc_files: []
24
24
  files:
25
+ - CHANGELOG.md
25
26
  - LICENSE
26
27
  - README.md
27
28
  - ZENOH_C_PIN
@@ -30,6 +31,8 @@ files:
30
31
  - ext/asterism_zenoh/zenoh.c
31
32
  - ext/asterism_zenoh/zenoh_c.rb
32
33
  - lib/asterism/zenoh.rb
34
+ - lib/asterism/zenoh/common.rb
35
+ - lib/asterism/zenoh/cruby.rb
33
36
  - lib/asterism/zenoh/global.rb
34
37
  - lib/asterism/zenoh/values.rb
35
38
  - lib/asterism/zenoh/version.rb