somfy_sdn 1.0.12 → 2.1.2

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.
@@ -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