somfy_sdn 1.0.11 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,387 @@
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("+/discover/set")
98
+ subscribe("+/label/set")
99
+ subscribe("+/control/set")
100
+ subscribe("+/jog-ms/set")
101
+ subscribe("+/jog-pulses/set")
102
+ subscribe("+/position-pulses/set")
103
+ subscribe("+/position-percent/set")
104
+ subscribe("+/ip/set")
105
+ subscribe("+/reset/set")
106
+ subscribe("+/direction/set")
107
+ subscribe("+/up-speed/set")
108
+ subscribe("+/down-speed/set")
109
+ subscribe("+/slow-speed/set")
110
+ subscribe("+/up-limit/set")
111
+ subscribe("+/down-limit/set")
112
+ subscribe("+/groups/set")
113
+ (1..16).each do |ip|
114
+ subscribe("+/ip#{ip}-pulses/set")
115
+ subscribe("+/ip#{ip}-percent/set")
116
+ end
117
+
118
+ publish("$state", "ready")
119
+ end
120
+ end
121
+
122
+ def publish_motor(addr, node_type)
123
+ motor = nil
124
+
125
+ @mqtt.batch_publish do
126
+ publish("#{addr}/$name", addr)
127
+ publish("#{addr}/$type", node_type.to_s)
128
+ properties = %w{
129
+ discover
130
+ label
131
+ state
132
+ control
133
+ jog-ms
134
+ jog-pulses
135
+ position-pulses
136
+ position-percent
137
+ ip
138
+ down-limit
139
+ groups
140
+ last-direction
141
+ } + (1..16).map { |ip| ["ip#{ip}-pulses", "ip#{ip}-percent"] }.flatten
142
+
143
+ unless node_type == :st50ilt2
144
+ properties.concat %w{
145
+ reset
146
+ last-action-source
147
+ last-action-cause
148
+ up-limit
149
+ direction
150
+ up-speed
151
+ down-speed
152
+ slow-speed
153
+ }
154
+ end
155
+
156
+ publish("#{addr}/$properties", properties.join(","))
157
+
158
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
159
+ publish("#{addr}/discover/$datatype", "enum")
160
+ publish("#{addr}/discover/$format", "discover")
161
+ publish("#{addr}/discover/$settable", "true")
162
+ publish("#{addr}/discover/$retained", "false")
163
+
164
+ publish("#{addr}/label/$name", "Node label")
165
+ publish("#{addr}/label/$datatype", "string")
166
+ publish("#{addr}/label/$settable", "true")
167
+
168
+ publish("#{addr}/state/$name", "Current state of the motor")
169
+ publish("#{addr}/state/$datatype", "enum")
170
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(','))
171
+
172
+ publish("#{addr}/control/$name", "Control motor")
173
+ publish("#{addr}/control/$datatype", "enum")
174
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
175
+ publish("#{addr}/control/$settable", "true")
176
+ publish("#{addr}/control/$retained", "false")
177
+
178
+ publish("#{addr}/jog-ms/$name", "Jog motor by ms")
179
+ publish("#{addr}/jog-ms/$datatype", "integer")
180
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
181
+ publish("#{addr}/jog-ms/$unit", "ms")
182
+ publish("#{addr}/jog-ms/$settable", "true")
183
+ publish("#{addr}/jog-ms/$retained", "false")
184
+
185
+ publish("#{addr}/jog-pulses/$name", "Jog motor by pulses")
186
+ publish("#{addr}/jog-pulses/$datatype", "integer")
187
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
188
+ publish("#{addr}/jog-pulses/$unit", "pulses")
189
+ publish("#{addr}/jog-pulses/$settable", "true")
190
+ publish("#{addr}/jog-pulses/$retained", "false")
191
+
192
+ publish("#{addr}/position-percent/$name", "Position (in %)")
193
+ publish("#{addr}/position-percent/$datatype", "integer")
194
+ publish("#{addr}/position-percent/$format", "0:100")
195
+ publish("#{addr}/position-percent/$unit", "%")
196
+ publish("#{addr}/position-percent/$settable", "true")
197
+
198
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
199
+ publish("#{addr}/position-pulses/$datatype", "integer")
200
+ publish("#{addr}/position-pulses/$format", "0:65535")
201
+ publish("#{addr}/position-pulses/$unit", "pulses")
202
+ publish("#{addr}/position-pulses/$settable", "true")
203
+
204
+ publish("#{addr}/ip/$name", "Intermediate Position")
205
+ publish("#{addr}/ip/$datatype", "integer")
206
+ publish("#{addr}/ip/$format", "1:16")
207
+ publish("#{addr}/ip/$settable", "true")
208
+ publish("#{addr}/ip/$retained", "false") if node_type == :st50ilt2
209
+
210
+ publish("#{addr}/down-limit/$name", "Down limit")
211
+ publish("#{addr}/down-limit/$datatype", "integer")
212
+ publish("#{addr}/down-limit/$format", "0:65535")
213
+ publish("#{addr}/down-limit/$unit", "pulses")
214
+ publish("#{addr}/down-limit/$settable", "true")
215
+
216
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
217
+ publish("#{addr}/last-direction/$datatype", "enum")
218
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(','))
219
+
220
+ unless node_type == :st50ilt2
221
+ publish("#{addr}/reset/$name", "Recall factory settings")
222
+ publish("#{addr}/reset/$datatype", "enum")
223
+ publish("#{addr}/reset/$format", Message::SetFactoryDefault::RESET.keys.join(','))
224
+ publish("#{addr}/reset/$settable", "true")
225
+ publish("#{addr}/reset/$retained", "false")
226
+
227
+ publish("#{addr}/last-action-source/$name", "Source of last action")
228
+ publish("#{addr}/last-action-source/$datatype", "enum")
229
+ publish("#{addr}/last-action-source/$format", Message::PostMotorStatus::SOURCE.keys.join(','))
230
+
231
+ publish("#{addr}/last-action-cause/$name", "Cause of last action")
232
+ publish("#{addr}/last-action-cause/$datatype", "enum")
233
+ publish("#{addr}/last-action-cause/$format", Message::PostMotorStatus::CAUSE.keys.join(','))
234
+
235
+ publish("#{addr}/up-limit/$name", "Up limit (always = 0)")
236
+ publish("#{addr}/up-limit/$datatype", "integer")
237
+ publish("#{addr}/up-limit/$format", "0:65535")
238
+ publish("#{addr}/up-limit/$unit", "pulses")
239
+ publish("#{addr}/up-limit/$settable", "true")
240
+
241
+ publish("#{addr}/direction/$name", "Motor rotation direction")
242
+ publish("#{addr}/direction/$datatype", "enum")
243
+ publish("#{addr}/direction/$format", "standard,reversed")
244
+ publish("#{addr}/direction/$settable", "true")
245
+
246
+ publish("#{addr}/up-speed/$name", "Up speed")
247
+ publish("#{addr}/up-speed/$datatype", "integer")
248
+ publish("#{addr}/up-speed/$format", "6:28")
249
+ publish("#{addr}/up-speed/$unit", "RPM")
250
+ publish("#{addr}/up-speed/$settable", "true")
251
+
252
+ publish("#{addr}/down-speed/$name", "Down speed, always = Up speed")
253
+ publish("#{addr}/down-speed/$datatype", "integer")
254
+ publish("#{addr}/down-speed/$format", "6:28")
255
+ publish("#{addr}/down-speed/$unit", "RPM")
256
+ publish("#{addr}/down-speed/$settable", "true")
257
+
258
+ publish("#{addr}/slow-speed/$name", "Slow speed")
259
+ publish("#{addr}/slow-speed/$datatype", "integer")
260
+ publish("#{addr}/slow-speed/$format", "6:28")
261
+ publish("#{addr}/slow-speed/$unit", "RPM")
262
+ publish("#{addr}/slow-speed/$settable", "true")
263
+ end
264
+
265
+ publish("#{addr}/groups/$name", "Group Memberships (comma separated, address must start 0101xx)")
266
+ publish("#{addr}/groups/$datatype", "string")
267
+ publish("#{addr}/groups/$settable", "true")
268
+
269
+ (1..16).each do |ip|
270
+ publish("#{addr}/ip#{ip}-pulses/$name", "Intermediate Position #{ip}")
271
+ publish("#{addr}/ip#{ip}-pulses/$datatype", "integer")
272
+ publish("#{addr}/ip#{ip}-pulses/$format", "0:65535")
273
+ publish("#{addr}/ip#{ip}-pulses/$unit", "pulses")
274
+ publish("#{addr}/ip#{ip}-pulses/$settable", "true")
275
+
276
+ publish("#{addr}/ip#{ip}-percent/$name", "Intermediate Position #{ip}")
277
+ publish("#{addr}/ip#{ip}-percent/$datatype", "integer")
278
+ publish("#{addr}/ip#{ip}-percent/$format", "0:100")
279
+ publish("#{addr}/ip#{ip}-percent/$unit", "%")
280
+ publish("#{addr}/ip#{ip}-percent/$settable", "true")
281
+ end
282
+
283
+ motor = Motor.new(self, addr, node_type)
284
+ @motors[addr] = motor
285
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
286
+ end
287
+
288
+ sdn_addr = Message.parse_address(addr)
289
+ @mutex.synchronize do
290
+ @queues[2].push(MessageAndRetries.new(Message::GetNodeLabel.new(sdn_addr), 5, 2))
291
+ case node_type
292
+ when :st30
293
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorStatus.new(sdn_addr), 5, 2))
294
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorLimits.new(sdn_addr), 5, 2))
295
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorDirection.new(sdn_addr), 5, 2))
296
+ @queues[2].push(MessageAndRetries.new(Message::GetMotorRollingSpeed.new(sdn_addr), 5, 2))
297
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
298
+ when :st50ilt2
299
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorSettings.new(sdn_addr), 5, 2))
300
+ @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorPosition.new(sdn_addr), 5, 2))
301
+ (1..16).each { |ip| @queues[2].push(MessageAndRetries.new(Message::ILT2::GetMotorIP.new(sdn_addr, ip), 5, 2)) }
302
+ end
303
+ (1..16).each { |g| @queues[2].push(MessageAndRetries.new(Message::GetGroupAddr.new(sdn_addr, g), 5, 2)) }
304
+
305
+ @cond.signal
306
+ end
307
+
308
+ motor
309
+ end
310
+
311
+ def touch_group(group_addr)
312
+ group = @groups[Message.print_address(group_addr).gsub('.', '')]
313
+ group&.publish(:motors, group.motors_string)
314
+ end
315
+
316
+ def add_group(addr)
317
+ addr = addr.gsub('.', '')
318
+ group = @groups[addr]
319
+ return group if group
320
+
321
+ @mqtt.batch_publish do
322
+ publish("#{addr}/$name", addr)
323
+ publish("#{addr}/$type", "Shade Group")
324
+ publish("#{addr}/$properties", "discover,control,jog-ms,jog-pulses,position-pulses,position-percent,ip,reset,state,last-direction,motors")
325
+
326
+ publish("#{addr}/discover/$name", "Trigger Motor Discovery")
327
+ publish("#{addr}/discover/$datatype", "enum")
328
+ publish("#{addr}/discover/$format", "discover")
329
+ publish("#{addr}/discover/$settable", "true")
330
+ publish("#{addr}/discover/$retained", "false")
331
+
332
+ publish("#{addr}/control/$name", "Control motors")
333
+ publish("#{addr}/control/$datatype", "enum")
334
+ publish("#{addr}/control/$format", "up,down,stop,wink,next_ip,previous_ip,refresh")
335
+ publish("#{addr}/control/$settable", "true")
336
+ publish("#{addr}/control/$retained", "false")
337
+
338
+ publish("#{addr}/jog-ms/$name", "Jog motors by ms")
339
+ publish("#{addr}/jog-ms/$datatype", "integer")
340
+ publish("#{addr}/jog-ms/$format", "-65535:65535")
341
+ publish("#{addr}/jog-ms/$unit", "ms")
342
+ publish("#{addr}/jog-ms/$settable", "true")
343
+ publish("#{addr}/jog-ms/$retained", "false")
344
+
345
+ publish("#{addr}/jog-pulses/$name", "Jog motors by pulses")
346
+ publish("#{addr}/jog-pulses/$datatype", "integer")
347
+ publish("#{addr}/jog-pulses/$format", "-65535:65535")
348
+ publish("#{addr}/jog-pulses/$unit", "pulses")
349
+ publish("#{addr}/jog-pulses/$settable", "true")
350
+ publish("#{addr}/jog-pulses/$retained", "false")
351
+
352
+ publish("#{addr}/position-pulses/$name", "Position from up limit (in pulses)")
353
+ publish("#{addr}/position-pulses/$datatype", "integer")
354
+ publish("#{addr}/position-pulses/$format", "0:65535")
355
+ publish("#{addr}/position-pulses/$unit", "pulses")
356
+ publish("#{addr}/position-pulses/$settable", "true")
357
+
358
+ publish("#{addr}/position-percent/$name", "Position (in %)")
359
+ publish("#{addr}/position-percent/$datatype", "integer")
360
+ publish("#{addr}/position-percent/$format", "0:100")
361
+ publish("#{addr}/position-percent/$unit", "%")
362
+ publish("#{addr}/position-percent/$settable", "true")
363
+
364
+ publish("#{addr}/ip/$name", "Intermediate Position")
365
+ publish("#{addr}/ip/$datatype", "integer")
366
+ publish("#{addr}/ip/$format", "1:16")
367
+ publish("#{addr}/ip/$settable", "true")
368
+
369
+ publish("#{addr}/state/$name", "State of the motors")
370
+ publish("#{addr}/state/$datatype", "enum")
371
+ publish("#{addr}/state/$format", Message::PostMotorStatus::STATE.keys.join(',') + ",mixed")
372
+
373
+ publish("#{addr}/last-direction/$name", "Direction of last motion")
374
+ publish("#{addr}/last-direction/$datatype", "enum")
375
+ publish("#{addr}/last-direction/$format", Message::PostMotorStatus::DIRECTION.keys.join(',') + ",mixed")
376
+
377
+ publish("#{addr}/motors/$name", "Comma separated motor addresses that are members of this group")
378
+ publish("#{addr}/motors/$datatype", "string")
379
+
380
+ group = @groups[addr] = Group.new(self, addr)
381
+ publish("$nodes", (["FFFFFF"] + @motors.keys.sort + @groups.keys.sort).join(","))
382
+ end
383
+ group
384
+ end
385
+ end
386
+ end
387
+ 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