nl-knd_client 0.0.0.pre.usegit

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module NL
2
+ module KndClient
3
+ VERSION = "0.0.0-usegit"
4
+ end
5
+ 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