asterism 0.7.0 → 0.8.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.
data/mrblib/status.rb ADDED
@@ -0,0 +1,198 @@
1
+ # An application's status: a small Hash of its own that changes while it
2
+ # runs (how many tables are open, who is seated, a battery level), told by
3
+ # the application itself.
4
+ #
5
+ # Asterism.status(tables: 3, humans: 2) # sets those fields, keeps the others
6
+ # Asterism.status({ "mahjong1" => {...} }) # the whole status (a Hash)
7
+ # Asterism.status # => {"tables" => 3, ...} or nil
8
+ # Asterism.status_of("games", "games") # another application's, or nil
9
+ #
10
+ # Key: asterism/<node>/<app>/@status (docs/protocol.md 8). The application
11
+ # answers queries there with its status, and puts it on the same key when
12
+ # it changes (only then: the same status again puts nothing), at connect
13
+ # and after a lost connection comes back. An application that never set a
14
+ # status declares nothing there and answers nothing.
15
+ #
16
+ # The value is a MessagePack map with str keys; the values are any values
17
+ # of the protocol nested at most MAX_DEPTH levels below the map, and the
18
+ # whole map packs into MAX_BYTES at most. It is not the profile
19
+ # (profile.rb): a profile says once what a node is; a status changes.
20
+ module Asterism
21
+ module Status
22
+ KEY = "@status"
23
+ MAX_BYTES = 1024 # the packed map
24
+ MAX_DEPTH = 4 # levels of arrays and maps below the map
25
+
26
+ # A received status, cleaned: the entries with a non-empty str key; nil
27
+ # when it is not a map or packs into more than MAX_BYTES.
28
+ def self.check(h)
29
+ return nil unless h.is_a?(Hash)
30
+ out = {}
31
+ h.each { |k, v| out[k] = v if k.is_a?(String) && !k.empty? }
32
+ begin
33
+ return nil if ::MessagePack.pack(out).bytesize > MAX_BYTES
34
+ rescue
35
+ return nil
36
+ end
37
+ out
38
+ end
39
+
40
+ # [node, app, status] of a notice put on asterism/<node>/<app>/@status
41
+ # (key and payload as a subscriber gets them), or nil when it is not one.
42
+ def self.parse(key, payload)
43
+ parts = key.to_s.split("/")
44
+ return nil unless parts.size == 4 && parts[0] == ROOT && parts[3] == KEY
45
+ st = check(Codec.unpack(payload))
46
+ st ? [parts[1], parts[2], st] : nil
47
+ end
48
+
49
+ # The checks of Asterism.status: a copy with String keys, or
50
+ # ArgumentError with the reason.
51
+ # @api private
52
+ def self.check!(h)
53
+ raise ArgumentError, "status: expected a Hash, got #{h.class}" unless h.is_a?(Hash)
54
+ out = {}
55
+ h.each do |k, v|
56
+ key = k.is_a?(Symbol) ? k.to_s : k
57
+ unless key.is_a?(String) && !key.empty?
58
+ raise ArgumentError, "status: the keys are Strings or Symbols (not empty), got #{k.inspect}"
59
+ end
60
+ raise ArgumentError, "status #{key}: nested deeper than #{MAX_DEPTH} levels" if too_deep?(v, 1)
61
+ begin
62
+ out[key] = Codec.check(v)
63
+ rescue EncodeError => e
64
+ raise ArgumentError, "status #{key}: #{e.message}"
65
+ end
66
+ end
67
+ n = Codec.pack(out).bytesize
68
+ raise ArgumentError, "status: #{n} bytes packed (#{MAX_BYTES} at most)" if n > MAX_BYTES
69
+ out
70
+ end
71
+
72
+ # @api private
73
+ def self.too_deep?(v, level)
74
+ if v.is_a?(Array) || v.is_a?(Hash)
75
+ return true if level > MAX_DEPTH
76
+ if v.is_a?(Array)
77
+ v.each { |e| return true if too_deep?(e, level + 1) }
78
+ else
79
+ v.each { |k, e| return true if too_deep?(k, level + 1) || too_deep?(e, level + 1) }
80
+ end
81
+ end
82
+ false
83
+ end
84
+ end
85
+
86
+ # This application's status. With a Hash, it is the whole status; with
87
+ # keywords, those fields change (nil removes one) and the others stay;
88
+ # both: the keywords on top of the Hash. Returns the status. Without
89
+ # arguments, returns it (nil when none was set). Raises ArgumentError for
90
+ # a status it does not accept (see Asterism::Status).
91
+ #
92
+ # Kept across connect and close (it belongs to the process, or the VM on
93
+ # a board). While connected, a change is put on asterism/<node>/<app>/@status;
94
+ # setting the same status again puts nothing.
95
+ def self.status(h = nil, **fields)
96
+ if h.nil? && fields.empty?
97
+ return @status ? @status.dup : nil
98
+ end
99
+ base = h.nil? ? (@status ? @status.dup : {}) : Status.check!(h)
100
+ fields.each do |k, v|
101
+ if v.nil?
102
+ base.delete(k.to_s)
103
+ else
104
+ base[k.to_s] = v
105
+ end
106
+ end
107
+ @status = Status.check!(base)
108
+ tell_status(false)
109
+ @status.dup
110
+ end
111
+
112
+ # Another application's status (Asterism::Status.check has cleaned it),
113
+ # or nil when it does not answer in time (no status set, an older
114
+ # version of Asterism, an application that is gone). timeout: in seconds
115
+ # (or timeout_ms:), 2.0 by default. Waits like a call (polling, answering
116
+ # calls meanwhile); raises Disconnected when not connected. This
117
+ # application's own status comes without the network.
118
+ def self.status_of(node, app, timeout: nil, timeout_ms: nil)
119
+ f = status_async(node, app, timeout, timeout_ms)
120
+ begin
121
+ raw = f.raw_value
122
+ rescue Disconnected
123
+ raise
124
+ rescue Error
125
+ return nil # no answer in time, or not MessagePack
126
+ end
127
+ Status.check(raw)
128
+ end
129
+
130
+ # @api private
131
+ def self.status_async(node, app, timeout, timeout_ms)
132
+ node = check_name("node", node)
133
+ app = check_name("app", app)
134
+ ms = time_ms("Asterism.status_of", timeout, timeout_ms, nil, DEFAULT_TIMEOUT_MS)
135
+ path = "#{node}/#{app}/#{Status::KEY}"
136
+ if @session && node == @node && app == @app
137
+ return Future.answered(path, "status", Codec.pack(@status)) if @status
138
+ return Future.answered(path, "status", Codec.pack(nil))
139
+ end
140
+ begin
141
+ g = session!.get("#{ROOT}/#{path}", timeout_ms: ms)
142
+ rescue Asterism::Zenoh::Error => e
143
+ lost_if_closed(e.message)
144
+ raise Disconnected, e.message
145
+ end
146
+ Future.new(path, "status", g, now_ms + ms, ms)
147
+ end
148
+
149
+ # The applications alive now ("<node>/<app>", from their liveliness
150
+ # tokens; this one included) whose path matches the pattern (* is one
151
+ # chunk, ** any number).
152
+ def self.apps(pattern = "*/*")
153
+ list = []
154
+ return list unless @session
155
+ own = "#{@node}/#{@app}"
156
+ list << own if glob?(pattern, own)
157
+ @apps.each_key { |path| list << path if path != own && glob?(pattern, path) }
158
+ list
159
+ end
160
+
161
+ # Puts the status on its key while connected: when it changed since the
162
+ # last put of this connection, or always with force (a connection that
163
+ # came back). Declares the status queryable with the first put.
164
+ # @api private
165
+ def self.tell_status(force)
166
+ s = @session
167
+ return unless s && @status
168
+ bytes = Codec.pack(@status)
169
+ return if !force && !@status_put.nil? && @status_put == bytes
170
+ key = "#{ROOT}/#{@node}/#{@app}/#{Status::KEY}"
171
+ @status_queryable ||= s.queryable(key, depth: 4)
172
+ s.put(key, bytes)
173
+ @status_put = bytes
174
+ rescue Asterism::Zenoh::Error
175
+ nil # the next poll notices a lost connection
176
+ end
177
+
178
+ # @api private
179
+ def self.answer_status
180
+ qa = @status_queryable
181
+ return unless qa && qa.pending > 0
182
+ queries = qa.each_pending
183
+ i = 0
184
+ while i < queries.size
185
+ q = queries[i]
186
+ i += 1
187
+ begin
188
+ q.reply("#{ROOT}/#{@node}/#{@app}/#{Status::KEY}", Codec.pack(@status || {}))
189
+ ensure
190
+ begin
191
+ q.finish
192
+ rescue Asterism::Zenoh::Error
193
+ # the session went away meanwhile
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asterism
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katsuhiko Kageyama
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-12 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asterism-zenoh
@@ -16,14 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.4.0
19
+ version: '0.4'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.4.1
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - "~>"
25
28
  - !ruby/object:Gem::Version
26
- version: 0.4.0
29
+ version: '0.4'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.4.1
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: msgpack
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -42,7 +48,8 @@ description: Proxies for objects exposed by other Ruby processes and boards, a R
42
48
  2 node speaking rmw_zenoh's wire format, CDR and generated message types. Pure Ruby;
43
49
  the same files are the mrbgem for mruby / PicoRuby.
44
50
  email:
45
- executables: []
51
+ executables:
52
+ - asterism
46
53
  extensions: []
47
54
  extra_rdoc_files: []
48
55
  files:
@@ -138,8 +145,15 @@ files:
138
145
  - data/msgs/visualization_msgs/msg/MarkerArray.rb
139
146
  - data/msgs/visualization_msgs/msg/MeshFile.rb
140
147
  - data/msgs/visualization_msgs/msg/UVCoordinate.rb
148
+ - exe/asterism
141
149
  - lib/asterism.rb
150
+ - lib/asterism/cli.rb
151
+ - lib/asterism/cli/allow.rb
152
+ - lib/asterism/cli/literal.rb
153
+ - lib/asterism/cli/mcp.rb
154
+ - lib/asterism/cli/survey.rb
142
155
  - lib/asterism/cruby.rb
156
+ - lib/asterism/cruby/credentials.rb
143
157
  - lib/asterism/cruby/objects.rb
144
158
  - lib/asterism/cruby/ros.rb
145
159
  - lib/asterism/cruby/runner.rb
@@ -147,10 +161,12 @@ files:
147
161
  - lib/asterism/version.rb
148
162
  - mrblib/asterism.rb
149
163
  - mrblib/cdr.rb
164
+ - mrblib/credentials.rb
150
165
  - mrblib/future.rb
151
166
  - mrblib/profile.rb
152
167
  - mrblib/proxy.rb
153
168
  - mrblib/ros.rb
169
+ - mrblib/status.rb
154
170
  - tools/asterism_msggen.rb
155
171
  homepage: https://github.com/ruby-asterism/asterism
156
172
  licenses: