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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/README.md +208 -8
- data/exe/asterism +8 -0
- data/lib/asterism/cli/allow.rb +53 -0
- data/lib/asterism/cli/literal.rb +132 -0
- data/lib/asterism/cli/mcp.rb +272 -0
- data/lib/asterism/cli/survey.rb +204 -0
- data/lib/asterism/cli.rb +424 -0
- data/lib/asterism/cruby/credentials.rb +97 -0
- data/lib/asterism/cruby/objects.rb +72 -10
- data/lib/asterism/cruby/ros.rb +1 -1
- data/lib/asterism/cruby/zenoh.rb +13 -2
- data/lib/asterism/version.rb +1 -1
- data/lib/asterism.rb +2 -2
- data/mrblib/asterism.rb +242 -24
- data/mrblib/credentials.rb +197 -0
- data/mrblib/profile.rb +1 -1
- data/mrblib/status.rb +198 -0
- metadata +22 -6
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.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katsuhiko Kageyama
|
|
8
8
|
autorequire:
|
|
9
|
-
bindir:
|
|
9
|
+
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
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
|
|
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
|
|
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:
|