nl-knd_client 0.0.0.pre.usegit
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 +7 -0
- data/.gitignore +25 -0
- data/.rspec +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +53 -0
- data/LICENSE +661 -0
- data/README.md +59 -0
- data/Rakefile +10 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/ext/Makefile +36 -0
- data/ext/front.c +26 -0
- data/ext/front_grid.c +43 -0
- data/ext/kinutils/extconf.rb +8 -0
- data/ext/kinutils/kinutils.c +411 -0
- data/ext/kinutils/unpack.c +212 -0
- data/ext/kinutils/unpack.h +108 -0
- data/ext/overhead.c +26 -0
- data/ext/overhead_grid.c +42 -0
- data/ext/side.c +26 -0
- data/ext/side_grid.c +42 -0
- data/ext/unpacktest.c +69 -0
- data/lib/nl/knd_client.rb +21 -0
- data/lib/nl/knd_client/em_knd_client.rb +904 -0
- data/lib/nl/knd_client/em_knd_command.rb +77 -0
- data/lib/nl/knd_client/simple_knd_client.rb +106 -0
- data/lib/nl/knd_client/version.rb +5 -0
- data/lib/nl/knd_client/zone.rb +227 -0
- data/nl-knd_client.gemspec +37 -0
- metadata +145 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module NL
|
2
|
+
module KndClient
|
3
|
+
# Represents a deferred KND command run by the EventMachine-based client,
|
4
|
+
# EMKndClient. Callbacks will be called with the EMKndCommand object as
|
5
|
+
# their sole parameter.
|
6
|
+
class EMKndCommand
|
7
|
+
include EM::Deferrable
|
8
|
+
attr_reader :linecount, :lines, :name, :message
|
9
|
+
|
10
|
+
# Initializes a deferred 'name' command, removing commas from arguments
|
11
|
+
# (KND command arguments cannot contain commas)
|
12
|
+
def initialize(name, *args)
|
13
|
+
@name = name
|
14
|
+
@args = args
|
15
|
+
@argstring = (args.length > 0 && " #{@args.map {|s| s.to_s.gsub(',', '') if s != nil }.join(',')}") || ''
|
16
|
+
@linecount = 0
|
17
|
+
@lines = []
|
18
|
+
@message = ""
|
19
|
+
|
20
|
+
timeout 10
|
21
|
+
|
22
|
+
log "Command #{@name} Initialized" if EMKndClient.debug_cmd?(@name)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Called by EMKndClient when a success line is received
|
26
|
+
# Returns true if the command is done, false if lines are needed
|
27
|
+
def ok_line(message)
|
28
|
+
@message = message
|
29
|
+
|
30
|
+
log "Command #{@name} OK - #{message}" if EMKndClient.debug_cmd?(@name)
|
31
|
+
|
32
|
+
case @name
|
33
|
+
when "zones", "help"
|
34
|
+
@linecount = message.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
if @linecount == 0
|
38
|
+
succeed self
|
39
|
+
return true
|
40
|
+
end
|
41
|
+
|
42
|
+
return false
|
43
|
+
end
|
44
|
+
|
45
|
+
# Called by EMKndClient when an error line is received
|
46
|
+
def err_line(message)
|
47
|
+
@message = message
|
48
|
+
|
49
|
+
log "Command #{@name} ERR - #{message}" if EMKndClient.debug_cmd?(@name)
|
50
|
+
|
51
|
+
fail self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Called by EMKndClient to add a line
|
55
|
+
# Returns true when enough lines have been received
|
56
|
+
def add_line(line)
|
57
|
+
lines << line
|
58
|
+
@linecount -= 1
|
59
|
+
if @linecount == 0
|
60
|
+
succeed self
|
61
|
+
end
|
62
|
+
return @linecount == 0
|
63
|
+
end
|
64
|
+
|
65
|
+
# Converts the command to a string formatted for sending to knd.
|
66
|
+
def to_s
|
67
|
+
"#{@name}#{@argstring}"
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def log(msg)
|
73
|
+
EMKndClient.log(msg)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module NL
|
4
|
+
module KndClient
|
5
|
+
# A simple KND client that uses a background thread for I/O and works
|
6
|
+
# without EventMachine. This client does not (yet) support all of KND's
|
7
|
+
# features.
|
8
|
+
class SimpleKndClient
|
9
|
+
def initialize(host: 'localhost', port: 14308)
|
10
|
+
@host = host
|
11
|
+
@port = 14308
|
12
|
+
|
13
|
+
@callbacks = {}
|
14
|
+
@zones = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Calls the given block with the current full state of a zone, the type of
|
18
|
+
# command received for a zone, and the updates for the zone. '! DEPTH' is a
|
19
|
+
# special zone name for depth images [HACK].
|
20
|
+
def on_zone(name, &block)
|
21
|
+
@callbacks[name] ||= []
|
22
|
+
@callbacks[name] << block
|
23
|
+
end
|
24
|
+
|
25
|
+
# Removes the given callback from the given zone name. '! DEPTH' is a
|
26
|
+
# special zone name for depth images [HACK].
|
27
|
+
def remove_callback(name, &block)
|
28
|
+
@callbacks[name]&.delete(block)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Connect to KND.
|
32
|
+
def open
|
33
|
+
@socket = TCPSocket.new(@host, @port)
|
34
|
+
@run = true
|
35
|
+
@t = Thread.new do read_loop end
|
36
|
+
@socket.puts('sub')
|
37
|
+
end
|
38
|
+
|
39
|
+
# Close the connection to KND and stop the background thread.
|
40
|
+
def close
|
41
|
+
@run = false
|
42
|
+
@t&.wakeup
|
43
|
+
@t&.kill
|
44
|
+
@socket&.close
|
45
|
+
@t = nil
|
46
|
+
@socket = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
# Waits for and then returns depth data.
|
50
|
+
def get_depth
|
51
|
+
data = nil
|
52
|
+
t = Thread.current
|
53
|
+
|
54
|
+
cb = ->(d) { data = d; t.wakeup }
|
55
|
+
on_zone('! DEPTH', &cb)
|
56
|
+
|
57
|
+
@socket.puts('getdepth')
|
58
|
+
sleep(1)
|
59
|
+
|
60
|
+
raise "Data wasn't set within 1 second" if data.nil?
|
61
|
+
|
62
|
+
data
|
63
|
+
ensure
|
64
|
+
remove_callback('! DEPTH', &cb)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def read_loop
|
70
|
+
while @run do
|
71
|
+
line = @socket.readline
|
72
|
+
|
73
|
+
begin
|
74
|
+
type = line.split(' - ', 2).first
|
75
|
+
|
76
|
+
case type
|
77
|
+
when 'DEL', 'OK'
|
78
|
+
next
|
79
|
+
|
80
|
+
when 'DEPTH'
|
81
|
+
num_bytes = line.gsub(/\A[^0-9]*(\d+)[^0-9]*\z/, '\1').to_i
|
82
|
+
puts "\n\n\n\n\e[1;33m=========== DEPTH line #{num_bytes} ============\n\n\n\n"
|
83
|
+
data = @socket.read(num_bytes)
|
84
|
+
@callbacks['! DEPTH']&.each do |cb|
|
85
|
+
cb.call(data) rescue puts "Error calling depth callback: #{MB::Sound::U.syntax($!.inspect)}"
|
86
|
+
end
|
87
|
+
|
88
|
+
else
|
89
|
+
kvp = line.kin_kvp(symbolize_keys: true)
|
90
|
+
name = kvp[:name] || (raise "No zone name was found")
|
91
|
+
@zones[name] ||= {}
|
92
|
+
@zones[name].merge!(kvp)
|
93
|
+
|
94
|
+
@callbacks[kvp[:name]]&.each do |cb|
|
95
|
+
cb.call(@zones[name]) rescue puts "Error calling callback: #{MB::Sound::U.syntax($!)}\n\t#{MB::Sound::U.syntax($!.backtrace.join("\n\t"))}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
rescue => e
|
100
|
+
puts "Error parsing line '#{line}': #{MB::Sound::U.syntax(e)}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
module NL
|
2
|
+
module KndClient
|
3
|
+
# Represents a spatial zone from KND.
|
4
|
+
class Zone < Hash
|
5
|
+
# Switched to integer millimeters in version 2
|
6
|
+
ZONE_VERSION = 2
|
7
|
+
|
8
|
+
# TODO: This should be a constant, not a class variable
|
9
|
+
@@param_names = {
|
10
|
+
'occupied' => 'Occupied',
|
11
|
+
|
12
|
+
'bright' => 'Brightness',
|
13
|
+
|
14
|
+
'sa' => 'Surface Area',
|
15
|
+
|
16
|
+
'xc' => 'X Center',
|
17
|
+
'yc' => 'Y Center',
|
18
|
+
'zc' => 'Z Center',
|
19
|
+
|
20
|
+
'xmin' => 'World Xmin',
|
21
|
+
'ymin' => 'World Ymin',
|
22
|
+
'zmin' => 'World Zmin',
|
23
|
+
'xmax' => 'World Xmax',
|
24
|
+
'ymax' => 'World Ymax',
|
25
|
+
'zmax' => 'World Zmax',
|
26
|
+
|
27
|
+
'px_xmin' => 'Screen Xmin',
|
28
|
+
'px_ymin' => 'Screen Ymin',
|
29
|
+
'px_zmin' => 'Screen Zmin',
|
30
|
+
'px_xmax' => 'Screen Xmax',
|
31
|
+
'px_ymax' => 'Screen Ymax',
|
32
|
+
'px_zmax' => 'Screen Zmax',
|
33
|
+
|
34
|
+
'pop' => 'Population',
|
35
|
+
'maxpop' => 'Max Population',
|
36
|
+
|
37
|
+
'negate' => 'Inverted',
|
38
|
+
'param' => 'Occupied Parameter',
|
39
|
+
'on_level' => 'Rising Threshold',
|
40
|
+
'off_level' => 'Falling Threshold',
|
41
|
+
'on_delay' => 'Rising Delay',
|
42
|
+
'off_delay' => 'Falling Delay',
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
@@name_params = @@param_names.invert.freeze
|
46
|
+
|
47
|
+
# Merges with the other Hash or Zone, then converts known keys into
|
48
|
+
# their expected types.
|
49
|
+
def merge_zone other
|
50
|
+
merge! other
|
51
|
+
normalize! unless other.is_a? Zone
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# Initializes a zone definition with the given key-value set. If
|
56
|
+
# kvpairs is a string, it is parsed with kin_kvp(). If it is a Hash,
|
57
|
+
# its keys are used as the zone definition.
|
58
|
+
def initialize kvpairs, normalize=true
|
59
|
+
EMKndClient.bench 'Zone.new' do
|
60
|
+
super nil
|
61
|
+
if kvpairs.is_a? String
|
62
|
+
EMKndClient.bench 'Zone.new string' do
|
63
|
+
merge! kvpairs.kin_kvp
|
64
|
+
normalize = false
|
65
|
+
end
|
66
|
+
elsif kvpairs.is_a? Hash
|
67
|
+
EMKndClient.bench 'Zone.new hash' do
|
68
|
+
merge! kvpairs
|
69
|
+
end
|
70
|
+
else
|
71
|
+
raise "kvpairs must be a String or a Hash, not #{kvpairs.class}."
|
72
|
+
end
|
73
|
+
normalize! if normalize
|
74
|
+
self['occupied'] = (self['occupied'] == 1) if self['occupied'].is_a?(Fixnum)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Converts known keys into their expected types.
|
79
|
+
def normalize!
|
80
|
+
EMKndClient.bench 'Zone.normalize!' do
|
81
|
+
if has_key?('version') && self['version'].to_i < 2
|
82
|
+
EMKndClient.log "Converting version #{self['version']} zone to version #{ZONE_VERSION}"
|
83
|
+
self['xmin'] &&= self['xmin'].to_f * 1000.0
|
84
|
+
self['ymin'] &&= self['ymin'].to_f * 1000.0
|
85
|
+
self['zmin'] &&= self['zmin'].to_f * 1000.0
|
86
|
+
self['xmax'] &&= self['xmax'].to_f * 1000.0
|
87
|
+
self['ymax'] &&= self['ymax'].to_f * 1000.0
|
88
|
+
self['zmax'] &&= self['zmax'].to_f * 1000.0
|
89
|
+
end
|
90
|
+
|
91
|
+
self['xmin'] &&= self['xmin'].to_i
|
92
|
+
self['ymin'] &&= self['ymin'].to_i
|
93
|
+
self['zmin'] &&= self['zmin'].to_i
|
94
|
+
self['xmax'] &&= self['xmax'].to_i
|
95
|
+
self['ymax'] &&= self['ymax'].to_i
|
96
|
+
self['zmax'] &&= self['zmax'].to_i
|
97
|
+
self['px_xmin'] &&= self['px_xmin'].to_i
|
98
|
+
self['px_ymin'] &&= self['px_ymin'].to_i
|
99
|
+
self['px_zmin'] &&= self['px_zmin'].to_i
|
100
|
+
self['px_xmax'] &&= self['px_xmax'].to_i
|
101
|
+
self['px_ymax'] &&= self['px_ymax'].to_i
|
102
|
+
self['px_zmax'] &&= self['px_zmax'].to_i
|
103
|
+
self['pop'] &&= self['pop'].to_i
|
104
|
+
self['maxpop'] &&= self['maxpop'].to_i
|
105
|
+
self['xc'] &&= self['xc'].to_i
|
106
|
+
self['yc'] &&= self['yc'].to_i
|
107
|
+
self['zc'] &&= self['zc'].to_i
|
108
|
+
self['sa'] &&= self['sa'].to_i
|
109
|
+
self['bright'] &&= self['bright'].to_i
|
110
|
+
|
111
|
+
if has_key? 'negate'
|
112
|
+
if self['negate'] == 'true' then
|
113
|
+
self['negate'] = true
|
114
|
+
elsif self['negate'] == 'false' then
|
115
|
+
self['negate'] = false
|
116
|
+
elsif self['negate'].respond_to? 'to_i' then
|
117
|
+
self['negate'] = self['negate'].to_i == 1 ? true : false
|
118
|
+
else
|
119
|
+
self['negate'] = !!self['negate']
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
unless self.range('param').include?(self['param'])
|
124
|
+
self.delete 'param'
|
125
|
+
end
|
126
|
+
|
127
|
+
self['on_level'] &&= self['on_level'].to_i
|
128
|
+
self['off_level'] &&= self['off_level'].to_i
|
129
|
+
self['on_delay'] &&= self['on_delay'].to_i
|
130
|
+
self['off_delay'] &&= self['off_delay'].to_i
|
131
|
+
|
132
|
+
if has_key? 'occupied' and self['occupied'].respond_to? 'to_i' then
|
133
|
+
self['occupied'] = self['occupied'].to_i == 1 ? true : false
|
134
|
+
end
|
135
|
+
|
136
|
+
Zone.fix_name!(self['name']) if has_key? 'name'
|
137
|
+
end
|
138
|
+
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
# Computes the range for the given parameter. Returns nil if param is
|
143
|
+
# not a valid zone parameter, the parameter's range is unknown, or the
|
144
|
+
# given parameter has no range.
|
145
|
+
#
|
146
|
+
# TODO: this should probably just be a constant Hash
|
147
|
+
def range param
|
148
|
+
case param
|
149
|
+
when 'xmin', 'xmax'
|
150
|
+
(-KNC_XMAX / 2)..(KNC_XMAX / 2)
|
151
|
+
|
152
|
+
when 'ymin', 'ymax'
|
153
|
+
(-KNC_YMAX / 2)..(KNC_YMAX / 2)
|
154
|
+
|
155
|
+
when 'zmin', 'zmax'
|
156
|
+
0..KNC_ZMAX
|
157
|
+
|
158
|
+
when 'px_xmin', 'px_xmax'
|
159
|
+
0..639
|
160
|
+
|
161
|
+
when 'px_ymin', 'px_ymax'
|
162
|
+
0..479
|
163
|
+
|
164
|
+
when 'px_zmin', 'px_zmax'
|
165
|
+
0..KNC_PXZMAX
|
166
|
+
|
167
|
+
when 'pop'
|
168
|
+
0..self['maxpop']
|
169
|
+
|
170
|
+
when 'maxpop'
|
171
|
+
0..(640 * 480)
|
172
|
+
|
173
|
+
when 'xc'
|
174
|
+
0..1000
|
175
|
+
|
176
|
+
when 'yc'
|
177
|
+
0..1000
|
178
|
+
|
179
|
+
when 'zc'
|
180
|
+
0..1000
|
181
|
+
|
182
|
+
when 'sa'
|
183
|
+
(self['xmax'] - self['xmin']) * (self['ymax'] - self['ymin'])
|
184
|
+
|
185
|
+
when 'bright'
|
186
|
+
0..1000
|
187
|
+
|
188
|
+
when 'negate'
|
189
|
+
[false, true]
|
190
|
+
|
191
|
+
when 'param'
|
192
|
+
['pop', 'sa', 'bright', 'xc', 'yc', 'zc']
|
193
|
+
|
194
|
+
when 'on_level', 'off_level'
|
195
|
+
# TODO: Range depends on param
|
196
|
+
-5000..(640 * 480)
|
197
|
+
|
198
|
+
when 'on_delay', 'off_delay'
|
199
|
+
0..(86400 * 30)
|
200
|
+
|
201
|
+
else
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Returns a hash mapping parameter names to their human-friendly names
|
207
|
+
# (excludes the 'name' parameter).
|
208
|
+
def self.params
|
209
|
+
@@param_names
|
210
|
+
end
|
211
|
+
|
212
|
+
# Returns a hash mapping human-friendly names to parameter names.
|
213
|
+
def self.names
|
214
|
+
@@name_params
|
215
|
+
end
|
216
|
+
|
217
|
+
# Removes unsupported characters from the given name, modifying the string directly
|
218
|
+
def self.fix_name! str
|
219
|
+
# TODO: Support spaces (need to change HTML to use data-zone),
|
220
|
+
# strip or error on non-UTF8 non-printable characters
|
221
|
+
str.delete!(",")
|
222
|
+
str.tr!(" \t\r\n", '_')
|
223
|
+
str
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'lib/nl/knd_client/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "nl-knd_client"
|
5
|
+
spec.version = NL::KndClient::VERSION
|
6
|
+
spec.authors = ["Mike Bourgeous"]
|
7
|
+
spec.email = ["mike@mikebourgeous.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{Client library for Nitrogen Logic's Kinect data server, KND (use Git directly for installation)}
|
10
|
+
spec.homepage = "https://github.com/nitrogenlogic/nl-knd_client"
|
11
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
12
|
+
|
13
|
+
spec.license = 'AGPLv3'
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.extensions = [
|
28
|
+
'ext/kinutils/extconf.rb'
|
29
|
+
]
|
30
|
+
|
31
|
+
spec.add_runtime_dependency 'nl-fast_png'
|
32
|
+
|
33
|
+
spec.add_development_dependency 'rake-compiler'
|
34
|
+
spec.add_development_dependency 'pry'
|
35
|
+
spec.add_development_dependency 'rspec'
|
36
|
+
spec.add_development_dependency 'eventmachine'
|
37
|
+
end
|