somfy_sdn 1.0.12 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,397 @@
1
+ require 'mqtt'
2
+ require 'uri'
3
+ require 'set'
4
+
5
+ require 'sdn/cli/mqtt/group'
6
+ require 'sdn/cli/mqtt/motor'
7
+ require 'sdn/cli/mqtt/read'
8
+ require 'sdn/cli/mqtt/write'
9
+ require 'sdn/cli/mqtt/subscriptions'
10
+
11
+ module SDN
12
+ module CLI
13
+ class MQTT
14
+ MessageAndRetries = Struct.new(:message, :remaining_retries, :priority)
15
+
16
+ include Read
17
+ include Write
18
+ include Subscriptions
19
+
20
+ WAIT_TIME = 0.25
21
+ BROADCAST_WAIT = 5.0
22
+
23
+ attr_reader :motors, :groups
24
+
25
+ def initialize(port, mqtt_uri, device_id: "somfy", base_topic: "homie", auto_discover: true)
26
+ @base_topic = "#{base_topic}/#{device_id}"
27
+ @mqtt = ::MQTT::Client.new(mqtt_uri)
28
+ @mqtt.set_will("#{@base_topic}/$state", "lost", retain: true)
29
+ @mqtt.connect
30
+
31
+ @motors = {}
32
+ @groups = {}
33
+
34
+ @mutex = Mutex.new
35
+ @cond = ConditionVariable.new
36
+ @queues = [[], [], []]
37
+ @response_pending = false
38
+ @broadcast_pending = false
39
+
40
+ @auto_discover = auto_discover
41
+ @motors_found = true
42
+
43
+ clear_tree(@base_topic)
44
+ publish_basic_attributes
45
+
46
+ @sdn = Client.new(port)
47
+
48
+ read_thread = Thread.new { read }
49
+ write_thread = Thread.new { write }
50
+ @mqtt.get { |packet| handle_message(packet.topic, packet.payload) }
51
+ end
52
+
53
+ def publish(topic, value)
54
+ @mqtt.publish("#{@base_topic}/#{topic}", value, retain: true, qos: 1)
55
+ end
56
+
57
+ def subscribe(topic)
58
+ @mqtt.subscribe("#{@base_topic}/#{topic}")
59
+ end
60
+
61
+ def enqueue(message, queue = 0)
62
+ @mutex.synchronize do
63
+ queue = @queues[queue]
64
+ unless queue.include?(message)
65
+ queue.push(message)
66
+ @cond.signal
67
+ end
68
+ end
69
+ end
70
+
71
+ def clear_tree(topic)
72
+ @mqtt.subscribe("#{topic}/#")
73
+ @mqtt.unsubscribe("#{topic}/#", wait_for_ack: true)
74
+ while !@mqtt.queue_empty?
75
+ packet = @mqtt.get
76
+ @mqtt.publish(packet.topic, nil, retain: true)
77
+ end
78
+ end
79
+
80
+ def publish_basic_attributes
81
+ @mqtt.batch_publish do
82
+ publish("$homie", "4.0.0")
83
+ publish("$name", "Somfy SDN Network")
84
+ publish("$state", "init")
85
+ publish("$nodes", "FFFFFF")
86
+
87
+ publish("FFFFFF/$name", "Broadcast")
88
+ publish("FFFFFF/$type", "sdn")
89
+ publish("FFFFFF/$properties", "discover")
90
+
91
+ publish("FFFFFF/discover/$name", "Trigger Motor Discovery")
92
+ publish("FFFFFF/discover/$datatype", "enum")
93
+ publish("FFFFFF/discover/$format", "discover")
94
+ publish("FFFFFF/discover/$settable", "true")
95
+ publish("FFFFFF/discover/$retained", "false")
96
+
97
+ subscribe_all
98
+
99
+ publish("$state", "ready")
100
+ end
101
+
102
+ @mqtt.on_reconnect do
103
+ subscribe_all
104
+ publish("$state", :init)
105
+ publish("$state", :ready)
106
+ end
107
+ end
108
+
109
+ def subscribe_all
110
+ subscribe("+/discover/set")
111
+ subscribe("+/label/set")
112
+ subscribe("+/control/set")
113
+ subscribe("+/jog-ms/set")
114
+ subscribe("+/jog-pulses/set")
115
+ subscribe("+/position-pulses/set")
116
+ subscribe("+/position-percent/set")
117
+ subscribe("+/ip/set")
118
+ subscribe("+/reset/set")
119
+ subscribe("+/direction/set")
120
+ subscribe("+/up-speed/set")
121
+ subscribe("+/down-speed/set")
122
+ subscribe("+/slow-speed/set")
123
+ subscribe("+/up-limit/set")
124
+ subscribe("+/down-limit/set")
125
+ subscribe("+/groups/set")
126
+ (1..16).each do |ip|
127
+ subscribe("+/ip#{ip}-pulses/set")
128
+ subscribe("+/ip#{ip}-percent/set")
129
+ end
130
+ end
131
+
132
+ def publish_motor(addr, node_type)
133
+ motor = nil
134
+
135
+ @mqtt.batch_publish do
136
+ publish("#{addr}/$name", addr)
137
+ publish("#{addr}/$type", node_type.to_s)
138
+ properties = %w{
139
+ discover
140
+ label
141
+ state
142
+ control
143
+ jog-ms
144
+ jog-pulses
145
+ position-pulses
146
+ position-percent
147
+ ip
148
+ down-limit
149
+ groups
150
+ last-direction
151
+ } + (1..16).map { |ip| ["ip#{ip}-pulses", "ip#{ip}-percent"] }.flatten
152
+
153
+ unless node_type == :st50ilt2
154
+ properties.concat %w{
155
+ reset
156
+ last-action-source
157
+ last-action-cause
158
+ up-limit
159
+ direction
160
+ up-speed
161
+ down-speed
162
+ slow-speed
163
+ }
164
+ end
165
+
166
+ publish("#{addr}/$properties", properties.join(","))
167
+
168
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
169
+ publish("#{addr}/discover/$datatype", "enum")
170
+ publish("#{addr}/discover/$format", "discover")
171
+ publish("#{addr}/discover/$settable", "true")
172
+ publish("#{addr}/discover/$retained", "false")
173
+
174
+ publish("#{addr}/label/$name", "Node label")
175
+ publish("#{addr}/label/$datatype", "string")
176
+ publish("#{addr}/label/$settable", "true")
177
+
178
+ publish("#{addr}/state/$name", "Current state of the motor")
179
+ publish("#{addr}/state/$datatype", "enum")
180
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(','))
181
+
182
+ publish("#{addr}/control/$name", "Control motor")
183
+ publish("#{addr}/control/$datatype", "enum")
184
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
185
+ publish("#{addr}/control/$settable", "true")
186
+ publish("#{addr}/control/$retained", "false")
187
+
188
+ publish("#{addr}/jog-ms/$name", "Jog motor by ms")
189
+ publish("#{addr}/jog-ms/$datatype", "integer")
190
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
191
+ publish("#{addr}/jog-ms/$unit", "ms")
192
+ publish("#{addr}/jog-ms/$settable", "true")
193
+ publish("#{addr}/jog-ms/$retained", "false")
194
+
195
+ publish("#{addr}/jog-pulses/$name", "Jog motor by pulses")
196
+ publish("#{addr}/jog-pulses/$datatype", "integer")
197
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
198
+ publish("#{addr}/jog-pulses/$unit", "pulses")
199
+ publish("#{addr}/jog-pulses/$settable", "true")
200
+ publish("#{addr}/jog-pulses/$retained", "false")
201
+
202
+ publish("#{addr}/position-percent/$name", "Position (in %)")
203
+ publish("#{addr}/position-percent/$datatype", "integer")
204
+ publish("#{addr}/position-percent/$format", "0:100")
205
+ publish("#{addr}/position-percent/$unit", "%")
206
+ publish("#{addr}/position-percent/$settable", "true")
207
+
208
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
209
+ publish("#{addr}/position-pulses/$datatype", "integer")
210
+ publish("#{addr}/position-pulses/$format", "0:65535")
211
+ publish("#{addr}/position-pulses/$unit", "pulses")
212
+ publish("#{addr}/position-pulses/$settable", "true")
213
+
214
+ publish("#{addr}/ip/$name", "Intermediate Position")
215
+ publish("#{addr}/ip/$datatype", "integer")
216
+ publish("#{addr}/ip/$format", "1:16")
217
+ publish("#{addr}/ip/$settable", "true")
218
+ publish("#{addr}/ip/$retained", "false") if node_type == :st50ilt2
219
+
220
+ publish("#{addr}/down-limit/$name", "Down limit")
221
+ publish("#{addr}/down-limit/$datatype", "integer")
222
+ publish("#{addr}/down-limit/$format", "0:65535")
223
+ publish("#{addr}/down-limit/$unit", "pulses")
224
+ publish("#{addr}/down-limit/$settable", "true")
225
+
226
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
227
+ publish("#{addr}/last-direction/$datatype", "enum")
228
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(','))
229
+
230
+ unless node_type == :st50ilt2
231
+ publish("#{addr}/reset/$name", "Recall factory settings")
232
+ publish("#{addr}/reset/$datatype", "enum")
233
+ publish("#{addr}/reset/$format", Message::SetFactoryDefault::RESET.keys.join(','))
234
+ publish("#{addr}/reset/$settable", "true")
235
+ publish("#{addr}/reset/$retained", "false")
236
+
237
+ publish("#{addr}/last-action-source/$name", "Source of last action")
238
+ publish("#{addr}/last-action-source/$datatype", "enum")
239
+ publish("#{addr}/last-action-source/$format", Message::PostMotorStatus::SOURCE.keys.join(','))
240
+
241
+ publish("#{addr}/last-action-cause/$name", "Cause of last action")
242
+ publish("#{addr}/last-action-cause/$datatype", "enum")
243
+ publish("#{addr}/last-action-cause/$format", Message::PostMotorStatus::CAUSE.keys.join(','))
244
+
245
+ publish("#{addr}/up-limit/$name", "Up limit (always = 0)")
246
+ publish("#{addr}/up-limit/$datatype", "integer")
247
+ publish("#{addr}/up-limit/$format", "0:65535")
248
+ publish("#{addr}/up-limit/$unit", "pulses")
249
+ publish("#{addr}/up-limit/$settable", "true")
250
+
251
+ publish("#{addr}/direction/$name", "Motor rotation direction")
252
+ publish("#{addr}/direction/$datatype", "enum")
253
+ publish("#{addr}/direction/$format", "standard,reversed")
254
+ publish("#{addr}/direction/$settable", "true")
255
+
256
+ publish("#{addr}/up-speed/$name", "Up speed")
257
+ publish("#{addr}/up-speed/$datatype", "integer")
258
+ publish("#{addr}/up-speed/$format", "6:28")
259
+ publish("#{addr}/up-speed/$unit", "RPM")
260
+ publish("#{addr}/up-speed/$settable", "true")
261
+
262
+ publish("#{addr}/down-speed/$name", "Down speed, always = Up speed")
263
+ publish("#{addr}/down-speed/$datatype", "integer")
264
+ publish("#{addr}/down-speed/$format", "6:28")
265
+ publish("#{addr}/down-speed/$unit", "RPM")
266
+ publish("#{addr}/down-speed/$settable", "true")
267
+
268
+ publish("#{addr}/slow-speed/$name", "Slow speed")
269
+ publish("#{addr}/slow-speed/$datatype", "integer")
270
+ publish("#{addr}/slow-speed/$format", "6:28")
271
+ publish("#{addr}/slow-speed/$unit", "RPM")
272
+ publish("#{addr}/slow-speed/$settable", "true")
273
+ end
274
+
275
+ publish("#{addr}/groups/$name", "Group Memberships (comma separated, address must start 0101xx)")
276
+ publish("#{addr}/groups/$datatype", "string")
277
+ publish("#{addr}/groups/$settable", "true")
278
+
279
+ (1..16).each do |ip|
280
+ publish("#{addr}/ip#{ip}-pulses/$name", "Intermediate Position #{ip}")
281
+ publish("#{addr}/ip#{ip}-pulses/$datatype", "integer")
282
+ publish("#{addr}/ip#{ip}-pulses/$format", "0:65535")
283
+ publish("#{addr}/ip#{ip}-pulses/$unit", "pulses")
284
+ publish("#{addr}/ip#{ip}-pulses/$settable", "true")
285
+
286
+ publish("#{addr}/ip#{ip}-percent/$name", "Intermediate Position #{ip}")
287
+ publish("#{addr}/ip#{ip}-percent/$datatype", "integer")
288
+ publish("#{addr}/ip#{ip}-percent/$format", "0:100")
289
+ publish("#{addr}/ip#{ip}-percent/$unit", "%")
290
+ publish("#{addr}/ip#{ip}-percent/$settable", "true")
291
+ end
292
+
293
+ motor = Motor.new(self, addr, node_type)
294
+ @motors[addr] = motor
295
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
296
+ end
297
+
298
+ sdn_addr = Message.parse_address(addr)
299
+ @mutex.synchronize do
300
+ @queues[2].push(MessageAndRetries.new(Message::GetNodeLabel.new(sdn_addr), 5, 2))
301
+ case node_type
302
+ when :st30
303
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorStatus.new(sdn_addr), 5, 2))
304
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorLimits.new(sdn_addr), 5, 2))
305
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorDirection.new(sdn_addr), 5, 2))
306
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
307
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
308
+ when :st50ilt2
309
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorSettings.new(sdn_addr), 5, 2))
310
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorPosition.new(sdn_addr), 5, 2))
311
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
312
+ end
313
+ (1..16).each { |g| @queues[2].push(MessageAndRetries.new(Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
314
+
315
+ @cond.signal
316
+ end
317
+
318
+ motor
319
+ end
320
+
321
+ def touch_group(group_addr)
322
+ group = @groups[Message.print_address(group_addr).gsub('.', '')]
323
+ group&.publish(:motors, group.motors_string)
324
+ end
325
+
326
+ def add_group(addr)
327
+ addr = addr.gsub('.', '')
328
+ group = @groups[addr]
329
+ return group if group
330
+
331
+ @mqtt.batch_publish do
332
+ publish("#{addr}/$name", addr)
333
+ publish("#{addr}/$type", "Shade Group")
334
+ publish("#{addr}/$properties", "discover,control,jog-ms,jog-pulses,position-pulses,position-percent,ip,reset,state,last-direction,motors")
335
+
336
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
337
+ publish("#{addr}/discover/$datatype", "enum")
338
+ publish("#{addr}/discover/$format", "discover")
339
+ publish("#{addr}/discover/$settable", "true")
340
+ publish("#{addr}/discover/$retained", "false")
341
+
342
+ publish("#{addr}/control/$name", "Control motors")
343
+ publish("#{addr}/control/$datatype", "enum")
344
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
345
+ publish("#{addr}/control/$settable", "true")
346
+ publish("#{addr}/control/$retained", "false")
347
+
348
+ publish("#{addr}/jog-ms/$name", "Jog motors by ms")
349
+ publish("#{addr}/jog-ms/$datatype", "integer")
350
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
351
+ publish("#{addr}/jog-ms/$unit", "ms")
352
+ publish("#{addr}/jog-ms/$settable", "true")
353
+ publish("#{addr}/jog-ms/$retained", "false")
354
+
355
+ publish("#{addr}/jog-pulses/$name", "Jog motors by pulses")
356
+ publish("#{addr}/jog-pulses/$datatype", "integer")
357
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
358
+ publish("#{addr}/jog-pulses/$unit", "pulses")
359
+ publish("#{addr}/jog-pulses/$settable", "true")
360
+ publish("#{addr}/jog-pulses/$retained", "false")
361
+
362
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
363
+ publish("#{addr}/position-pulses/$datatype", "integer")
364
+ publish("#{addr}/position-pulses/$format", "0:65535")
365
+ publish("#{addr}/position-pulses/$unit", "pulses")
366
+ publish("#{addr}/position-pulses/$settable", "true")
367
+
368
+ publish("#{addr}/position-percent/$name", "Position (in %)")
369
+ publish("#{addr}/position-percent/$datatype", "integer")
370
+ publish("#{addr}/position-percent/$format", "0:100")
371
+ publish("#{addr}/position-percent/$unit", "%")
372
+ publish("#{addr}/position-percent/$settable", "true")
373
+
374
+ publish("#{addr}/ip/$name", "Intermediate Position")
375
+ publish("#{addr}/ip/$datatype", "integer")
376
+ publish("#{addr}/ip/$format", "1:16")
377
+ publish("#{addr}/ip/$settable", "true")
378
+
379
+ publish("#{addr}/state/$name", "State of the motors")
380
+ publish("#{addr}/state/$datatype", "enum")
381
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(',') + ",mixed")
382
+
383
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
384
+ publish("#{addr}/last-direction/$datatype", "enum")
385
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(',') + ",mixed")
386
+
387
+ publish("#{addr}/motors/$name", "Comma separated motor addresses that are members of this group")
388
+ publish("#{addr}/motors/$datatype", "string")
389
+
390
+ group = @groups[addr] = Group.new(self, addr)
391
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
392
+ end
393
+ group
394
+ end
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,234 @@
1
+ require 'curses'
2
+
3
+ module SDN
4
+ module CLI
5
+ class Provisioner
6
+ attr_reader :win, :sdn, :addr, :ns
7
+
8
+ def initialize(port, addr = nil)
9
+ @sdn = Client.new(port)
10
+ @reversed = false
11
+ @pulse_count = 10
12
+
13
+ if addr
14
+ @addr = addr = Message.parse_address(addr)
15
+ else
16
+ puts "Discovering motor..."
17
+ message = sdn.ensure(Message::GetNodeAddr.new)
18
+ puts "Found #{message.node_type}"
19
+ @addr = addr = message.src
20
+ end
21
+
22
+ puts "Preparing to provision motor #{Message.print_address(addr)}"
23
+
24
+ message = sdn.ensure(Message::GetNodeLabel.new(addr))
25
+
26
+ node_type = message.node_type
27
+ @ns = ns = node_type == :st50ilt2 ? Message::ILT2 : Message
28
+
29
+ print "Motor is currently labeled '#{message.label}'; what would you like to change it to (blank to leave alone)? "
30
+ new_label = STDIN.gets
31
+
32
+ unless new_label == "\n"
33
+ new_label.strip!
34
+ sdn.ensure(ns::SetNodeLabel.new(addr, new_label))
35
+ end
36
+
37
+ # make sure some limits exist
38
+ unless ns == Message::ILT2
39
+ limits = sdn.ensure(Message::GetMotorLimits.new(addr))
40
+ if limits.up_limit.nil? || limits.down_limit.nil?
41
+ sdn.ensure(Message::SetMotorLimits.new(addr, :delete, :up))
42
+ sdn.ensure(Message::SetMotorLimits.new(addr, :delete, :down))
43
+ sdn.ensure(Message::SetMotorLimits.new(addr, :current_position, :up))
44
+ sdn.ensure(Message::SetMotorLimits.new(addr, :specified_position, :down, 500))
45
+ end
46
+ end
47
+
48
+ Curses.init_screen
49
+ begin
50
+ Curses.noecho
51
+ Curses.crmode
52
+ Curses.nonl
53
+ Curses.curs_set(0)
54
+ @win = Curses.stdscr
55
+
56
+ process
57
+ rescue => e
58
+ win.setpos(0, 0)
59
+ win.addstr(e.inspect)
60
+ win.addstr("\n")
61
+ win.addstr(e.backtrace.join("\n"))
62
+ win.refresh
63
+ sleep 10
64
+ ensure
65
+ Curses.close_screen
66
+ end
67
+ end
68
+
69
+ def process
70
+ win.keypad = true
71
+ print_help
72
+ refresh
73
+
74
+ loop do
75
+ char = win.getch
76
+ case char
77
+ when 27 # Esc
78
+ stop
79
+ refresh
80
+ when Curses::Key::UP
81
+ if ilt2?
82
+ sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :up_limit))
83
+ else
84
+ sdn.ensure(Message::MoveTo.new(addr, :up_limit))
85
+ end
86
+ wait_for_stop
87
+ when Curses::Key::DOWN
88
+ if ilt2?
89
+ sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :down_limit))
90
+ else
91
+ sdn.ensure(Message::MoveTo.new(addr, :down_limit))
92
+ end
93
+ wait_for_stop
94
+ when Curses::Key::LEFT
95
+ if @pos < @pulse_count
96
+ sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit + @pulse_count - @pos, @pulse_count))
97
+ refresh
98
+ end
99
+ sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :jog_up_pulses, @pulse_count))
100
+ wait_for_stop
101
+ when Curses::Key::RIGHT
102
+ if @limit - @pos < @pulse_count
103
+ sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @pos + @pulse_count, @pos))
104
+ refresh
105
+ end
106
+ sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :jog_down_pulses, @pulse_count))
107
+ wait_for_stop
108
+ when 'u'
109
+ if ilt2?
110
+ sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit - @pos, 0))
111
+ else
112
+ sdn.ensure(Message::SetMotorLimits.new(addr, :current_position, :up))
113
+ end
114
+ refresh
115
+ when 'l'
116
+ if ilt2?
117
+ sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @pos, @pos))
118
+ else
119
+ sdn.ensure(Message::SetMotorLimits.new(addr, :current_position, :down))
120
+ end
121
+ refresh
122
+ when 'r'
123
+ @reversed = !@reversed
124
+ if ilt2?
125
+ sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit, @limit - @pos))
126
+ else
127
+ sdn.ensure(Message::SetMotorDirection.new(addr, @reversed ? :reversed : :standard))
128
+ end
129
+ refresh
130
+ when 'R'
131
+ next unless ilt2?
132
+ @reversed = !@reversed
133
+ sdn.ensure(Message::ILT2::SetMotorSettings.new(addr, reversed_int, @limit, @pos))
134
+ refresh
135
+ when '<'
136
+ @pulse_count /= 2 if @pulse_count > 5
137
+ print_help
138
+ when '>'
139
+ @pulse_count *= 2
140
+ print_help
141
+ when 'q'
142
+ break
143
+ end
144
+ end
145
+ end
146
+
147
+ def print_help
148
+ win.setpos(0, 0)
149
+ win.addstr(<<-INSTRUCTIONS)
150
+ Move the motor. Keys:
151
+ Esc stop movement
152
+ \u2191 go to upper limit
153
+ \u2193 go to lower limit
154
+ \u2190 jog up #{@pulse_count} pulses
155
+ \u2192 jog down #{@pulse_count} pulses
156
+ > increase jog size
157
+ < decrease jog size
158
+ u set upper limit at current position
159
+ l set lower limit at current position
160
+ r reverse motor
161
+ INSTRUCTIONS
162
+
163
+ if ilt2?
164
+ win.addstr("R reverse motor (but leave position alone)\n")
165
+ end
166
+ win.addstr("q quit\n")
167
+ win.refresh
168
+ end
169
+
170
+ def wait_for_stop
171
+ win.setpos(13, 0)
172
+ win.addstr("Moving...\n")
173
+ loop do
174
+ win.nodelay = true
175
+ stop if win.getch == 27 # Esc
176
+ sdn.send(ns::GetMotorPosition.new(addr))
177
+ sdn.receive do |message|
178
+ next unless message.is_a?(ns::PostMotorPosition)
179
+ last_pos = @pos
180
+ @pos = message.position_pulses
181
+ win.setpos(14, 0)
182
+ win.addstr("Position: #{@pos}\n")
183
+
184
+ if last_pos == @pos
185
+ win.setpos(13, 0)
186
+ win.addstr("\n")
187
+ win.nodelay = false
188
+ refresh
189
+ return
190
+ end
191
+ end
192
+ sleep 0.1
193
+
194
+ end
195
+ end
196
+
197
+ def refresh
198
+ pos = sdn.ensure(ns::GetMotorPosition.new(addr))
199
+ @pos = pos.position_pulses
200
+ if ilt2?
201
+ settings = sdn.ensure(Message::ILT2::GetMotorSettings.new(addr))
202
+ @limit = settings.limit
203
+ else
204
+ limits = sdn.ensure(Message::GetMotorLimits.new(addr))
205
+ @limit = limits.down_limit
206
+ direction = sdn.ensure(Message::GetMotorDirection.new(addr))
207
+ @reversed = direction.direction == :reversed
208
+ end
209
+
210
+ win.setpos(14, 0)
211
+ win.addstr("Position: #{@pos}\n")
212
+ win.addstr("Limit: #{@limit}\n")
213
+ win.addstr("Reversed: #{@reversed}\n")
214
+ win.refresh
215
+ end
216
+
217
+ def stop
218
+ if ilt2?
219
+ sdn.ensure(Message::ILT2::SetMotorPosition.new(addr, :stop))
220
+ else
221
+ sdn.ensure(Message::Stop.new(addr))
222
+ end
223
+ end
224
+
225
+ def ilt2?
226
+ ns == Message::ILT2
227
+ end
228
+
229
+ def reversed_int
230
+ @reversed ? 1 : 0
231
+ end
232
+ end
233
+ end
234
+ end