axpert_rs232 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/axpert_commands.rb +543 -0
- data/lib/axpert_constants.rb +151 -0
- data/lib/axpert_rs232.rb +2 -0
- data/lib/voltronic_device_operation.rb +113 -0
- data/lib/voltronic_rs232.rb +75 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: feaf14c95a0bb048162f2de460083f56e1564c9a
|
4
|
+
data.tar.gz: 124bfdfe33531983e742cff13ce5883ca8163d9f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4f69247e57e69cd8bee76208639a0fbd55f94743d85b9282fe7f1a300bbc289e85a6c2ae4859d3e55ab5d2417922a88712bd85465d88ff99b85af690bb53e0a7
|
7
|
+
data.tar.gz: cd36a586e15a3f5421abf9260b63bcd5c7d35c0499876bf6b3adcf57212d635792d766d2f8bd1cefc281ff07f6e93465eaac4d6595f19822135302f09e3ed5b7
|
@@ -0,0 +1,543 @@
|
|
1
|
+
##
|
2
|
+
# A list of known commands
|
3
|
+
#
|
4
|
+
# Commands implemented as constants
|
5
|
+
# Each command includes the actual command to be executed and a parser for the results of the command
|
6
|
+
#
|
7
|
+
# Commands source: http://forums.aeva.asn.au/uploads/293/HS_MS_MSX_RS232_Protocol_20140822_after_current_upgrade.pdf
|
8
|
+
#
|
9
|
+
# @author: Johan van der Vyver
|
10
|
+
module AxpertCommands
|
11
|
+
require 'voltronic_device_operation'
|
12
|
+
OP = ::VoltronicDeviceOperation # :nodoc
|
13
|
+
|
14
|
+
##
|
15
|
+
# Device protocol ID
|
16
|
+
#
|
17
|
+
# Returns:
|
18
|
+
# # An Integer specifying the protocol ID
|
19
|
+
# 30 # Example
|
20
|
+
PROTOCOL_ID = OP.new(command: 'QPI', parser: lambda { |r| Integer(r.data[3..-1]) })
|
21
|
+
|
22
|
+
##
|
23
|
+
# Device serial number
|
24
|
+
#
|
25
|
+
# Returns:
|
26
|
+
# # A String specifying the device serial number
|
27
|
+
# "XXXXXXXXXXXXXX" # Example
|
28
|
+
SERIAL_NUMBER = OP.new(command: 'QID', parser: lambda { |r| r.data[1..-1] })
|
29
|
+
|
30
|
+
##
|
31
|
+
# Main CPU Firmware version
|
32
|
+
#
|
33
|
+
# Returns:
|
34
|
+
# # A String representing the CPU Firmware as a hexidecimal number
|
35
|
+
# "124004.10" # Example
|
36
|
+
#
|
37
|
+
MAIN_CPU_FIRMWARE = OP.new(command: 'QVFW', parser: lambda do |r|
|
38
|
+
r.data[8..-1].split('.').map { |s| s.chars.map { |c| Integer(c, 16).to_s }.flatten.join }.join('.').upcase
|
39
|
+
end)
|
40
|
+
|
41
|
+
##
|
42
|
+
# Other CPU Firmware version
|
43
|
+
#
|
44
|
+
# Returns:
|
45
|
+
# # A String representing the CPU Firmware as a hexidecimal number
|
46
|
+
# "124004.10" # Example
|
47
|
+
OTHER_CPU_FIRMWARE = OP.new(command: 'QVFW2', parser: lambda do |r|
|
48
|
+
r.data[8..-1].split('.').map { |s| s.chars.map { |c| Integer(c, 16).to_s }.flatten.join }.join('.').upcase
|
49
|
+
end)
|
50
|
+
|
51
|
+
##
|
52
|
+
# Device rating information
|
53
|
+
#
|
54
|
+
# Returns:
|
55
|
+
# # A Hash containing device rating information
|
56
|
+
# { grid_voltage: 230.5, grid_current: 11.2, .. }
|
57
|
+
DEVICE_RATING = OP.new(command: 'QPIRI', parser: lambda do |r|
|
58
|
+
r = r.data[1..-1].split(' ')
|
59
|
+
{ grid_voltage: Float(r[0]),
|
60
|
+
grid_current: Float(r[1]),
|
61
|
+
output_voltage: Float(r[2]),
|
62
|
+
output_frequency: Float(r[3]),
|
63
|
+
output_current: Float(r[4]),
|
64
|
+
output_va: Integer(r[5]),
|
65
|
+
output_watts: Integer(r[6]),
|
66
|
+
battery_voltage: Float(r[7]),
|
67
|
+
battery_recharge_voltage: Float(r[8]),
|
68
|
+
battery_under_voltage: Float(r[9]),
|
69
|
+
battery_bulk_charge_voltage: Float(r[10]),
|
70
|
+
battery_float_charge_voltage: Float(r[11]),
|
71
|
+
battery_type: ::AxpertConstants::BATTERY_TYPE[r[12]],
|
72
|
+
maximum_ac_charge_current: Integer(r[13]),
|
73
|
+
maximum_charge_current: Integer(r[14]),
|
74
|
+
input_voltage_sensitivity: CONS::INPUT_VOLTAGE_SENSITIVITY[r[15]],
|
75
|
+
output_source_priority: CONS::OUTPUT_SOURCE_PRIORITY[r[16]],
|
76
|
+
charger_source_priority: CONS::CHARGER_SOURCE_PRIORITY[r[17]],
|
77
|
+
maximum_parallel_units: Integer(r[18]),
|
78
|
+
device_type: CONS::DEVICE_TYPE[r[19]],
|
79
|
+
device_topology: CONS::DEVICE_TOPOLOGY[r[20]],
|
80
|
+
output_mode: CONS::OUTPUT_MODE[r[21]],
|
81
|
+
battery_redischarge_voltage: Float(r[22]),
|
82
|
+
pv_parallel_ok_mode: CONS::PV_PARALLEL_OK_MODE[r[23]],
|
83
|
+
pv_power_balance_mode: CONS::PV_POWER_BALANCE_MODE[r[24]] }
|
84
|
+
end)
|
85
|
+
|
86
|
+
|
87
|
+
##
|
88
|
+
# Device flags
|
89
|
+
#
|
90
|
+
# Returns:
|
91
|
+
# # A Hash containing device flag status as booleans
|
92
|
+
# {enable_buzzer: true/false, nable_bypass_to_utility_on_overload: true/false, ... }
|
93
|
+
DEVICE_FLAGS = OP.new(command: 'QFLAG', parser: lambda do |r|
|
94
|
+
r = r.data[1..-1]
|
95
|
+
lookup = Hash.new { |_, k| raise "The device did not return the status of the flag '#{k}'" }
|
96
|
+
r.scan(/[E][a-z]*/).first.to_s[1..-1].to_s.chars.each { |e| lookup[e.upcase] = true }
|
97
|
+
r.scan(/[D][a-z]*/).first.to_s[1..-1].to_s.chars.each { |d| lookup[d.upcase] = false }
|
98
|
+
{ enable_buzzer: lookup['A'],
|
99
|
+
enable_bypass_to_utility_on_overload: lookup['B'],
|
100
|
+
enable_power_saving: lookup['J'],
|
101
|
+
enable_lcd_timeout_escape_to_default_page: lookup['K'],
|
102
|
+
enable_overload_restart: lookup['U'],
|
103
|
+
enable_over_temperature_restart: lookup['V'],
|
104
|
+
enable_lcd_backlight: lookup['X'],
|
105
|
+
enable_primary_source_interrupt_alarm: lookup['Y'],
|
106
|
+
enable_fault_code_recording: lookup['Z'] }
|
107
|
+
end)
|
108
|
+
|
109
|
+
##
|
110
|
+
# The current device status
|
111
|
+
#
|
112
|
+
# Returns:
|
113
|
+
# # A Hash containing device status information
|
114
|
+
# { grid_voltage: 230.5, grid_current: 11.2, .. }
|
115
|
+
DEVICE_STATUS = OP.new(command: 'QPIGS', parser: lambda do |r|
|
116
|
+
r = r.data[1..-1].split(' ')
|
117
|
+
status = r[16].chars.map { |c| Boolean(c) }
|
118
|
+
{ grid_voltage: Float(r[0]),
|
119
|
+
grid_frequency: Float(r[1]),
|
120
|
+
output_voltage: Float(r[2]),
|
121
|
+
output_frequency: Float(r[3]),
|
122
|
+
output_va: Integer(r[4]),
|
123
|
+
output_watts: Integer(r[5]),
|
124
|
+
output_load_percent: Integer(r[6]),
|
125
|
+
dc_bus_voltage: Integer(r[7]),
|
126
|
+
battery_voltage: Float(r[8]),
|
127
|
+
battery_charge_current: Float(r[9]),
|
128
|
+
battery_capacity_remaining: Integer(r[10]),
|
129
|
+
inverter_heatsink_celsius_temperature: Integer(r[11]),
|
130
|
+
pv_battery_input_current: Integer(r[12]),
|
131
|
+
pv_input_voltage: Float(r[13]),
|
132
|
+
solar_charge_controller_battery_voltage: Float(r[14]),
|
133
|
+
battery_discharge_current: Integer(r[15]),
|
134
|
+
add_sbu_priority_version: status[0],
|
135
|
+
configuration_changed: status[1],
|
136
|
+
solar_charge_controller_firmware_changed: status[2],
|
137
|
+
load_on: status[3],
|
138
|
+
battery_voltage_stable: status[4],
|
139
|
+
charger_enabled: status[5],
|
140
|
+
charging_from_solar_charge_controller: status[6],
|
141
|
+
charging_from_utility: status[7] }
|
142
|
+
end)
|
143
|
+
|
144
|
+
# Device mode
|
145
|
+
#
|
146
|
+
# Returns:
|
147
|
+
# # A symbol denoting the device mode
|
148
|
+
# See AxpertConstants::DEVICE_MODE for constants
|
149
|
+
DEVICE_MODE = OP.new(command: 'QMOD', parser: lambda { |r| CONS::DEVICE_MODE[r.data[1..-1].upcase] })
|
150
|
+
|
151
|
+
##
|
152
|
+
# Device warning status messages
|
153
|
+
#
|
154
|
+
# Returns:
|
155
|
+
# # An Array of Hashes
|
156
|
+
# # Each Hash has the format { description: String description, level: :none/:fault/:warning }
|
157
|
+
# # The level is only :none if a reserved error was returned which may signify an update
|
158
|
+
# # of the parser is needed
|
159
|
+
# [{ description: 'Bus voltage is too high', level: :fault}, ..]
|
160
|
+
DEVICE_WARNING_STATUS = OP.new(command: 'QPIWS', parser: lambda do |r|
|
161
|
+
r = r.data[1..-1].chars.map { |s| Boolean(s) }
|
162
|
+
parse = lambda { |desc, lvl = :none| { description: desc, level: lvl } }
|
163
|
+
errors = []
|
164
|
+
errors << parse.yield('Reserved') if r[0]
|
165
|
+
errors << parse.yield('Inverter fault', :fault) if r[1]
|
166
|
+
errors << parse.yield('Bus voltage is too high', :fault) if r[2]
|
167
|
+
errors << parse.yield('Bus voltage is too low', :fault) if r[3]
|
168
|
+
errors << parse.yield('Bus soft start failed ', :fault) if r[4]
|
169
|
+
errors << parse.yield('LINE_FAIL', :warning) if r[5]
|
170
|
+
errors << parse.yield('Output short circuited', :warning) if r[6]
|
171
|
+
errors << parse.yield('Inverter voltage too low', :fault) if r[7]
|
172
|
+
errors << parse.yield('Output voltage is too high', :fault) if r[8]
|
173
|
+
errors << parse.yield('Over temperature', (r[1] ? :fault : :warning)) if r[9]
|
174
|
+
errors << parse.yield('Fan is locked', (r[1] ? :fault : :warning)) if r[10]
|
175
|
+
errors << parse.yield('Battery voltage is too high', (r[1] ? :fault : :warning)) if r[11]
|
176
|
+
errors << parse.yield('Battery voltage is too low', 'Fault') if r[12]
|
177
|
+
errors << parse.yield('Reserved') if r[13]
|
178
|
+
errors << parse.yield('Battery under shutdown', :warning) if r[14]
|
179
|
+
errors << parse.yield('Reserved') if r[15]
|
180
|
+
errors << parse.yield('Overload', (r[1] ? :fault : :warning)) if r[16]
|
181
|
+
errors << parse.yield('EEPROM fault', :warning) if r[17]
|
182
|
+
errors << parse.yield('Inverter over current', :fault) if r[18]
|
183
|
+
errors << parse.yield('Inverter soft start failed', :fault) if r[19]
|
184
|
+
errors << parse.yield('Self test fail', :fault) if r[20]
|
185
|
+
errors << parse.yield('Over voltage on DC output of inverter', :fault) if r[21]
|
186
|
+
errors << parse.yield('Battery connection is open', :fault) if r[22]
|
187
|
+
errors << parse.yield('Current sensor failed ', :fault) if r[23]
|
188
|
+
errors << parse.yield('Battery short', :fault) if r[24]
|
189
|
+
errors << parse.yield('Power limit', :warning) if r[25]
|
190
|
+
errors << parse.yield('PV voltage high', :warning) if r[26]
|
191
|
+
errors << parse.yield('MPPT overload fault', :warning) if r[27]
|
192
|
+
errors << parse.yield('MPPT overload warning', :warning) if r[28]
|
193
|
+
errors << parse.yield('Battery too low to charge', :warning) if r[29]
|
194
|
+
errors << parse.yield('Reserved') if r[30]
|
195
|
+
errors << parse.yield('Reserved') if r[31]
|
196
|
+
errors
|
197
|
+
end)
|
198
|
+
|
199
|
+
##
|
200
|
+
# The device default settings
|
201
|
+
#
|
202
|
+
# Returns:
|
203
|
+
# # A Hash containing device defaults
|
204
|
+
# { grid_voltage: 230.5, grid_current: 11.2, .. }
|
205
|
+
DEFAULT_SETTINGS = OP.new(command: 'QDI', parser: lambda do |r|
|
206
|
+
r = r.data[1..-1].split(' ')
|
207
|
+
{ output_voltage: Float(r[0]),
|
208
|
+
output_frequency: Float(r[1]),
|
209
|
+
maximum_ac_charge_current: Integer(r[2]),
|
210
|
+
battery_under_voltage: Float(r[3]),
|
211
|
+
battery_float_charge_voltage: Float(r[4]),
|
212
|
+
battery_bulk_charge_voltage: Float(r[5]),
|
213
|
+
battery_recharge_voltage: Float(r[6]),
|
214
|
+
maximum_charge_current: Integer(r[7]),
|
215
|
+
input_voltage_sensitivity: CONS::INPUT_VOLTAGE_SENSITIVITY[r[8]],
|
216
|
+
output_source_priority: CONS::OUTPUT_SOURCE_PRIORITY[r[9]],
|
217
|
+
charger_source_priority: CONS::CHARGER_SOURCE_PRIORITY[r[10]],
|
218
|
+
battery_type: ::AxpertConstants::BATTERY_TYPE[r[11]],
|
219
|
+
enable_buzzer: !Boolean(r[12]),
|
220
|
+
enable_power_saving: Boolean(r[13]),
|
221
|
+
enable_overload_restart: Boolean(r[14]),
|
222
|
+
enable_over_temperature_restart: Boolean(r[15]),
|
223
|
+
enable_lcd_backlight: Boolean(r[16]),
|
224
|
+
enable_primary_source_interrupt_alarm: Boolean(r[17]),
|
225
|
+
enable_fault_code_recording: Boolean(r[18]),
|
226
|
+
enable_bypass_to_utility_on_overload: Boolean(r[19]),
|
227
|
+
enable_lcd_timeout_escape_to_default_page: Boolean(r[20]),
|
228
|
+
output_mode: CONS::OUTPUT_MODE[r[21]],
|
229
|
+
battery_redischarge_voltage: Float(r[22]),
|
230
|
+
pv_parallel_ok_mode: CONS::PV_PARALLEL_OK_MODE[r[23]],
|
231
|
+
pv_power_balance_mode: CONS::PV_POWER_BALANCE_MODE[r[24]] }
|
232
|
+
end)
|
233
|
+
|
234
|
+
##
|
235
|
+
# All the possible input values for the charge current setting
|
236
|
+
#
|
237
|
+
# Returns:
|
238
|
+
# [10, 20, 30] # => Array of Integers, example given
|
239
|
+
ACCEPTED_CHARGE_CURRENT_VALUES = OP.new(command: 'QMCHGCR', parser: lambda do |r|
|
240
|
+
r.data[1..-1].split(' ').map { |s| Integer(s) }
|
241
|
+
end)
|
242
|
+
|
243
|
+
##
|
244
|
+
# All the possible input values for the utility charge current setting
|
245
|
+
#
|
246
|
+
# Returns:
|
247
|
+
# [10, 20, 30] # => Array of Integers, example given
|
248
|
+
ACCEPTED_UTILITY_CHARGE_CURRENT_VALUES = OP.new(command: 'QMUCHGCR', parser: lambda do |r|
|
249
|
+
r.data[1..-1].split(' ').map { |s| Integer(s) }
|
250
|
+
end)
|
251
|
+
|
252
|
+
##
|
253
|
+
# Has device undergone DSP bootstrap
|
254
|
+
#
|
255
|
+
# Returns:
|
256
|
+
# # A Boolean indicating if the device has undergone DSP bootstrap
|
257
|
+
# true # Example
|
258
|
+
DSP_BOOTSTRAP_STATUS = OP.new(command: 'QBOOT', parser: lambda { |r| Boolean(r.data[1..-1]) })
|
259
|
+
|
260
|
+
##
|
261
|
+
# Device parallel output mode status
|
262
|
+
#
|
263
|
+
# Returns:
|
264
|
+
# # A symbol denoting the current device parallel output mode
|
265
|
+
# See AxpertConstants::OUTPUT_MODE for constants
|
266
|
+
OUTPUT_MODE = OP.new(command: 'QOPM', parser: lambda { |r| CONS::OUTPUT_MODE[r.data[2..-1]] })
|
267
|
+
|
268
|
+
##
|
269
|
+
# Parallel device information
|
270
|
+
#
|
271
|
+
# Input: (OPTIONAL, default = 0)
|
272
|
+
# # Default of 0 seems to work for single unit mode
|
273
|
+
# 2 # => parallel_machine_number (Parallel mode only)
|
274
|
+
#
|
275
|
+
# Returns:
|
276
|
+
# # A Hash containing parallel device status information
|
277
|
+
# { grid_voltage: 230.5, grid_current: 11.2, .. }
|
278
|
+
PARALLEL_DEVICE_STATUS = OP.new(command: lambda { |i = 0| "QPGS#{Integer(i)}" }, parser: lambda do |r|
|
279
|
+
r = r.data[1..-1].split(' ')
|
280
|
+
status = r[19].chars
|
281
|
+
{ parallel_number_exists: Boolean(r[0]),
|
282
|
+
serial_number: r[1],
|
283
|
+
device_mode: CONS::DEVICE_MODE[r[2]],
|
284
|
+
fault_code: CONS::FAULT_CODE[r[3]],
|
285
|
+
grid_voltage: Float(r[4]),
|
286
|
+
grid_frequency: Float(r[5]),
|
287
|
+
output_voltage: Float(r[6]),
|
288
|
+
output_frequency: Float(r[7]),
|
289
|
+
output_va: Integer(r[8]),
|
290
|
+
output_watts: Integer(r[9]),
|
291
|
+
load_percentage: Integer(r[10]),
|
292
|
+
battery_voltage: Float(r[11]),
|
293
|
+
battery_charge_current: Integer(r[12]),
|
294
|
+
battery_capacity: Integer(r[13]),
|
295
|
+
pv_input_voltage: Float(r[14]),
|
296
|
+
total_charge_current: Integer(r[15]),
|
297
|
+
total_output_va: Integer(r[16]),
|
298
|
+
total_output_watt: Integer(r[17]),
|
299
|
+
total_load_percentage: Integer(r[18]),
|
300
|
+
solar_charge_controller_enabled: Boolean(status[0]),
|
301
|
+
charging_from_utility: Boolean(status[1]),
|
302
|
+
charging_from_solar_charge_controller: Boolean(status[2]),
|
303
|
+
battery_status: [:normal, :under, :open][Integer("#{status[3]}#{status[4]}")],
|
304
|
+
line_status_ok: !Boolean(status[5]),
|
305
|
+
load_on: Boolean(status[6]),
|
306
|
+
configuration_changed: Boolean(status[7]),
|
307
|
+
output_mode: CONS::OUTPUT_MODE[r[20]],
|
308
|
+
charger_source_priority: CONS::CHARGER_SOURCE_PRIORITY[r[21]],
|
309
|
+
maximum_charge_current: Integer(r[22]),
|
310
|
+
device_maximum_charge_current: Integer(r[23]),
|
311
|
+
pv_input_current: Integer(r[24]),
|
312
|
+
battery_discharge_current: Integer(r[25]) }
|
313
|
+
end)
|
314
|
+
|
315
|
+
##
|
316
|
+
# Reset device to factory defaults
|
317
|
+
#
|
318
|
+
# Returns:
|
319
|
+
# # A Boolean indicating if the device has reset to defaults succesfully
|
320
|
+
# true # Example
|
321
|
+
RESET_TO_DEFAULT = OP.new(command: 'PF', error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
322
|
+
|
323
|
+
##
|
324
|
+
# Set device output frequency
|
325
|
+
#
|
326
|
+
# Input:
|
327
|
+
# 50 # => Any integer is accepted, consult manual for valid values
|
328
|
+
#
|
329
|
+
# Returns:
|
330
|
+
# # A Boolean indicating if the frequency was set succesfully
|
331
|
+
# true # Example
|
332
|
+
SET_OUTPUT_FREQUENCY = OP.new(command: lambda { |input| "F#{Integer(input).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
333
|
+
|
334
|
+
##
|
335
|
+
# Set device output source priority
|
336
|
+
#
|
337
|
+
# Input:
|
338
|
+
# # A symbol donating the output source priority setting
|
339
|
+
# See AxpertConstants::OUTPUT_SOURCE_PRIORITY
|
340
|
+
#
|
341
|
+
# Returns:
|
342
|
+
# # A Boolean indicating if the output priority was set succesfully
|
343
|
+
# true # Example
|
344
|
+
SET_OUTPUT_SOURCE_PRIORITY = OP.new(command: lambda { |i| "POP#{CONS::OUTPUT_SOURCE_PRIORITY.key(i).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
345
|
+
|
346
|
+
##
|
347
|
+
# Set battery re-charge voltage
|
348
|
+
#
|
349
|
+
# Input:
|
350
|
+
# 24.3 # => Any float is accepted, consult manual for valid values
|
351
|
+
#
|
352
|
+
# Returns:
|
353
|
+
# # A Boolean indicating if the recharge voltage was successfully set
|
354
|
+
# true # Example
|
355
|
+
SET_BATTERY_RECHARGE_VOLTAGE = OP.new(command: lambda { |input| "PBCV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
356
|
+
|
357
|
+
##
|
358
|
+
# Set battery re-discharge voltage
|
359
|
+
#
|
360
|
+
# Input:
|
361
|
+
# 24.3 # => Any float is accepted, consult manual for valid values
|
362
|
+
#
|
363
|
+
# Returns:
|
364
|
+
# # A Boolean indicating if the re-discharge voltage was successfully set
|
365
|
+
# true # Example
|
366
|
+
SET_BATTERY_REDISCHARGE_VOLTAGE = OP.new(command: lambda { |input| "PBDV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
367
|
+
|
368
|
+
##
|
369
|
+
# Set device charger priority
|
370
|
+
#
|
371
|
+
# Input:
|
372
|
+
# # (REQUIRED) Parameter 0 => A symbol donating the charger source priority
|
373
|
+
# See AxpertConstants::CHARGER_SOURCE_PRIORITY
|
374
|
+
#
|
375
|
+
# # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
|
376
|
+
# 5 # => Do not pass in this value unless the device is running in parallel mode
|
377
|
+
#
|
378
|
+
# Returns:
|
379
|
+
# # A Boolean indicating if the device charger priority was set successfully
|
380
|
+
# true # Example
|
381
|
+
SET_CHARGER_SOURCE_PRIORITY = OP.new(command: lambda do |mode, parallel_machine_number = nil|
|
382
|
+
if parallel_machine_number.nil?
|
383
|
+
"PPCP#{Integer(parallel_machine_number).to_s}#{CONS::CHARGER_SOURCE_PRIORITY.key(mode).to_s.rjust(2, '0')}"
|
384
|
+
else
|
385
|
+
"PCP#{CONS::CHARGER_SOURCE_PRIORITY.key(mode).to_s.rjust(2, '0')}"
|
386
|
+
end
|
387
|
+
end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
388
|
+
|
389
|
+
##
|
390
|
+
# Set device input voltage sensitivity
|
391
|
+
#
|
392
|
+
# Input:
|
393
|
+
# # A symbol denoting the input voltage sensitivity
|
394
|
+
# See AxpertConstants::INPUT_VOLTAGE_SENSITIVITY
|
395
|
+
#
|
396
|
+
# Returns:
|
397
|
+
# # A Boolean indicating if the device input voltage sensitivity was set successfully
|
398
|
+
# true # Example
|
399
|
+
SET_INPUT_VOLTAGE_SENSITIVITY = OP.new(command: lambda { |i| "PGR#{CONS::INPUT_VOLTAGE_SENSITIVITY.key(i).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
400
|
+
|
401
|
+
##
|
402
|
+
# Set battery type
|
403
|
+
#
|
404
|
+
# Input:
|
405
|
+
# # A symbol denoting the battery type
|
406
|
+
# See AxpertConstants::BATTERY_TYPE
|
407
|
+
#
|
408
|
+
# Returns:
|
409
|
+
# # A Boolean indicating if the device battery type was set successfully
|
410
|
+
# true # Example
|
411
|
+
SET_BATTERY_TYPE = OP.new(command: lambda { |i| "PBT#{CONS::BATTERY_TYPE.key(i).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
412
|
+
|
413
|
+
##
|
414
|
+
# Set device battery cut-off voltage
|
415
|
+
#
|
416
|
+
# Input:
|
417
|
+
# 22.1 # => Any float is accepted, consult manual for valid values
|
418
|
+
#
|
419
|
+
# Returns:
|
420
|
+
# # A Boolean indicating if the battery cut-off voltage was successfully set
|
421
|
+
# true # Example
|
422
|
+
SET_BATTERY_CUTOFF_VOLTAGE = OP.new(command: lambda { |input| "PSDV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
423
|
+
|
424
|
+
##
|
425
|
+
# Set device battery constant charging voltage
|
426
|
+
#
|
427
|
+
# Input:
|
428
|
+
# 28.3 # => Any float is accepted, consult manual for valid values
|
429
|
+
#
|
430
|
+
# Returns:
|
431
|
+
# # A Boolean indicating if the battery constant charging voltage was successfully set
|
432
|
+
# true # Example
|
433
|
+
SET_BATTERY_CONSTANT_CHARGING_VOLTAGE = OP.new(command: lambda { |input| "PCVV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
434
|
+
|
435
|
+
##
|
436
|
+
# Set device battery float charging voltage
|
437
|
+
#
|
438
|
+
# Input:
|
439
|
+
# 26.5 # => Any float is accepted, consult manual for valid values
|
440
|
+
#
|
441
|
+
# Returns:
|
442
|
+
# # A Boolean indicating if the battery float charging was successfully set
|
443
|
+
# true # Example
|
444
|
+
SET_BATTERY_FLOAT_CHARGING_VOLTAGE = OP.new(command: lambda { |input| "PBFT#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
445
|
+
|
446
|
+
##
|
447
|
+
# Set parallel PV OK condition
|
448
|
+
#
|
449
|
+
# Input:
|
450
|
+
# # A input symbol donating the PV Parallel OK condition mode setting
|
451
|
+
# See AxpertConstants::PV_PARALLEL_OK_MODE
|
452
|
+
#
|
453
|
+
# Returns:
|
454
|
+
# # A Boolean indicating if the parallel PV OK condition was set successfully
|
455
|
+
# true # Example
|
456
|
+
SET_PV_PARALLEL_OK_MODE = OP.new(command: lambda { |i| "PPVOKC#{CONS::PV_PARALLEL_OK_MODE.key(i).to_s}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
457
|
+
|
458
|
+
##
|
459
|
+
# Set PV power balance mode
|
460
|
+
#
|
461
|
+
# Input:
|
462
|
+
# # A input symbol donating the PV Power balance mode
|
463
|
+
# See AxpertConstants::PV_POWER_BALANCE_MODE
|
464
|
+
#
|
465
|
+
# Returns:
|
466
|
+
# # A Boolean indicating if the PV power balance mode was set successfully
|
467
|
+
# true # Example
|
468
|
+
SET_PV_POWER_BALANCE_MODE = OP.new(command: lambda { |i| "PSPB#{CONS::PV_POWER_BALANCE_MODE.key(i).to_s}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
469
|
+
|
470
|
+
##
|
471
|
+
# Set the maximum charging current
|
472
|
+
#
|
473
|
+
# Input:
|
474
|
+
# # (REQUIRED) Parameter 0 => Charge current
|
475
|
+
# 30 # => Any integer is accepted, consult manual for valid values
|
476
|
+
#
|
477
|
+
# # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
|
478
|
+
# 5 # => Do not pass in this value unless the device is running in parallel mode
|
479
|
+
#
|
480
|
+
# Returns:
|
481
|
+
# # A Boolean indicating if the maximum charging current was set successfully
|
482
|
+
# true # Example
|
483
|
+
SET_MAXIMUM_CHARGING_CURRENT = OP.new(command: lambda do |current, parallel_machine_number = 0|
|
484
|
+
current = Integer(current)
|
485
|
+
if (current >= 100)
|
486
|
+
"MNCHGC#{Integer(parallel_machine_number).to_s}#{current.to_s.rjust(3, '0')}"
|
487
|
+
else
|
488
|
+
"MCHGC#{Integer(parallel_machine_number).to_s}#{current.to_s.rjust(2, '0')}"
|
489
|
+
end
|
490
|
+
end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
491
|
+
|
492
|
+
#
|
493
|
+
# Set the maximum charging current for utility
|
494
|
+
#
|
495
|
+
# Input:
|
496
|
+
# # (REQUIRED) Parameter 0 => Charge current
|
497
|
+
# 30 # => Any integer is accepted, consult manual for valid values
|
498
|
+
#
|
499
|
+
# # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
|
500
|
+
# 5 # => Do not pass in this value unless the device is running in parallel mode
|
501
|
+
#
|
502
|
+
# Returns:
|
503
|
+
# # A Boolean indicating if the maximum charging current for utility was set successfully
|
504
|
+
# true # Example
|
505
|
+
SET_MAXIMUM_UTILITY_CHARGING_CURRENT = OP.new(command: lambda do |current, parallel_machine_number = 0|
|
506
|
+
"MUCHGC#{Integer(parallel_machine_number).to_s}#{Integer(current).to_s.rjust(2, '0')}"
|
507
|
+
end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
508
|
+
|
509
|
+
##
|
510
|
+
# Set the parallel output mode (or single mode)
|
511
|
+
#
|
512
|
+
# Input:
|
513
|
+
# # (REQUIRED) Parameter 0 => A input symbol donating output mode
|
514
|
+
# See AxpertConstants::OUTPUT_MODE
|
515
|
+
#
|
516
|
+
# # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
|
517
|
+
# 5 # => Do not pass in this value unless the device is running in parallel mode
|
518
|
+
#
|
519
|
+
# Returns:
|
520
|
+
# # A Boolean indicating if the parallel output mode was set successfully
|
521
|
+
# true # Example
|
522
|
+
SET_OUTPUT_MODE = OP.new(command: lambda do |mode, parallel_machine_number = 0|
|
523
|
+
"POPM#{CONS::OUTPUT_MODE.key(mode)}#{Integer(parallel_machine_number).to_s}"
|
524
|
+
end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
|
525
|
+
|
526
|
+
require 'axpert_constants'
|
527
|
+
CONS = ::AxpertConstants # :nodoc:
|
528
|
+
send(:remove_const, :OP)
|
529
|
+
end
|
530
|
+
|
531
|
+
# This is hacky but it is really a must for accurate parsing
|
532
|
+
module ::Kernel # :nodoc:
|
533
|
+
def Boolean(input) # :nodoc:
|
534
|
+
return true if input.equal?(TrueClass)
|
535
|
+
return false if input.equal?(FalseClass)
|
536
|
+
parse = input.to_s.chomp.downcase
|
537
|
+
return true if ('true' == parse) || ('y' == parse) || ('t' == parse) || (1 == (Integer(parse) rescue nil))
|
538
|
+
return false if ('false' == parse) || ('n' == parse) || ('f' == parse) || (0 == (Integer(parse) rescue nil))
|
539
|
+
raise
|
540
|
+
rescue StandardError, ScriptError
|
541
|
+
raise ::ArgumentError.new("invalid value for Boolean(): \"#{input}\"")
|
542
|
+
end
|
543
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
##
|
2
|
+
# A list of constants used by the Axpert devices with a convenient parser
|
3
|
+
#
|
4
|
+
# @author: Johan van der Vyver
|
5
|
+
module AxpertConstants
|
6
|
+
def self.lookup_hash(type, values) # :nodoc:
|
7
|
+
type = type.to_s.strip.freeze
|
8
|
+
values.values.each(&:freeze)
|
9
|
+
lookup = Hash.new do |_, k|
|
10
|
+
if lookup.has_key?(k.to_s)
|
11
|
+
lookup[k.to_s]
|
12
|
+
elsif (k.is_a?(Numeric) && (k.to_i == k) && (lookup.values.count > k))
|
13
|
+
lookup.values[k]
|
14
|
+
elsif lookup.values.include?(k)
|
15
|
+
k
|
16
|
+
else
|
17
|
+
raise ::AxpertConstants::UnknownConstant.new("Unknown #{type} '#{k}'")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
lookup.instance_eval <<-RUBY_CODE
|
21
|
+
def key(input) # :nodoc:
|
22
|
+
parse = super(self[input]) rescue nil
|
23
|
+
parse = (super(input.to_s.to_sym) rescue nil) if parse.nil?
|
24
|
+
parse = (super(input) rescue nil) if parse.nil?
|
25
|
+
return parse unless parse.nil?
|
26
|
+
raise ::AxpertConstants::UnknownConstant.new("Could not find the #{type} constant '\#{input}' in \#{self.values}")
|
27
|
+
end
|
28
|
+
RUBY_CODE
|
29
|
+
lookup.merge!(values).freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# The type of batteries connected to the device
|
34
|
+
# Used to determine the float, bulk charging, re-charge and re-discharge voltage ranges
|
35
|
+
#
|
36
|
+
# :agm # => Absorbent Glass Mat (AGM)
|
37
|
+
# :flooded # => Flooded Cell
|
38
|
+
# :user # => User defined
|
39
|
+
BATTERY_TYPE = lookup_hash('battery type', {'0' => :agm, '1' => :flooded, '2' => :user})
|
40
|
+
|
41
|
+
##
|
42
|
+
# The current device mode
|
43
|
+
#
|
44
|
+
# :power # => Power on
|
45
|
+
# :standby # => Standby
|
46
|
+
# :line # => Line
|
47
|
+
# :battery # => Battery
|
48
|
+
# :fault # => Fault
|
49
|
+
DEVICE_MODE = lookup_hash('device mode', {'P' => :power, 'S' => :standby, 'L' => :line, 'B' => :battery, 'F' => :fault})
|
50
|
+
|
51
|
+
##
|
52
|
+
# The input voltage is monitored to determine if it is within an acceptable range
|
53
|
+
# The sensitivity determines when the unit switches output mode to/from Utility
|
54
|
+
#
|
55
|
+
# :appliance # => Appliance mode sensitivity (see manual for ranges)
|
56
|
+
# :ups # => UPS mode sensitivity (see manual for ranges)
|
57
|
+
INPUT_VOLTAGE_SENSITIVITY = lookup_hash('input voltage sensitivity', {'0' => :appliance, '1' => :ups})
|
58
|
+
|
59
|
+
##
|
60
|
+
# Device output priority
|
61
|
+
#
|
62
|
+
# :utility # => Prefer utility as first output power source
|
63
|
+
# :solar # => Prefer solar as first output power source
|
64
|
+
# :sbu # => Prefer solar first, then battery and utility last as output power source
|
65
|
+
OUTPUT_SOURCE_PRIORITY = lookup_hash('output source priority', {'0' => :utility, '1' => :solar, '2' => :sbu})
|
66
|
+
|
67
|
+
##
|
68
|
+
# Battery charger source priority
|
69
|
+
#
|
70
|
+
# :utility_first # => Charge batteries from utility first
|
71
|
+
# :solar:first # => Charge batteries from solar first,
|
72
|
+
# :solar_and_utility # => Charge batteries from solar & utility
|
73
|
+
# :solar_only # => Charge batteries from solar only
|
74
|
+
CHARGER_SOURCE_PRIORITY = lookup_hash('charger source priority', {'0' => :utility_first, '1' => :solar_first, '2' => :solar_and_utility, '3' => :solar_only})
|
75
|
+
|
76
|
+
##
|
77
|
+
# The type of device
|
78
|
+
#
|
79
|
+
# :grid_tie # => Grid tie device
|
80
|
+
# :off_grid # => Off-Grid device
|
81
|
+
# :hybrid # =>Hybrid device
|
82
|
+
DEVICE_TYPE = lookup_hash('device type', {'00' => :grid_tie, '01' => :off_grid, '10' => :hybrid})
|
83
|
+
|
84
|
+
##
|
85
|
+
# The internal device topology
|
86
|
+
# NOTE: All models make use of a transformer in Inverter mode
|
87
|
+
#
|
88
|
+
# :transformerless # => The device output does not pass through an isolation transformer
|
89
|
+
# :transformer # => The device output does pass through an isolation transformer
|
90
|
+
DEVICE_TOPOLOGY = lookup_hash('device topology', {'0' => :transformerless, '1' => :transformer})
|
91
|
+
|
92
|
+
##
|
93
|
+
# The current output mode of the device
|
94
|
+
#
|
95
|
+
# :single # => The device is running in single mode, single phase output
|
96
|
+
# :parallel # => The device is running in parallel mode (allow multiple units in parallel), single phase output
|
97
|
+
# :phase1 # => The device is running in parallel mode, 3 phase output, set to phase 1 of 3
|
98
|
+
# :phase2 # => The device is running in parallel mode, 3 phase output, set to phase 2 of 3
|
99
|
+
# :phase3 # => The device is running in parallel mode, 3 phase output, set to phase 3 of 3
|
100
|
+
OUTPUT_MODE = lookup_hash('output mode', {'0' => :single, '1' => :parallel, '2' => :phase1, '3' => :phase2, '4' => :phase3})
|
101
|
+
|
102
|
+
##
|
103
|
+
# Only applicable to units running in Parallel!
|
104
|
+
# The required mode for the PV to report OK on the inverter in parallel mode
|
105
|
+
#
|
106
|
+
# :one # => Only one unit needs to report PV is OK
|
107
|
+
# :all # => All units must report that PV is OK
|
108
|
+
PV_PARALLEL_OK_MODE = lookup_hash('PV parallel mode', {'0' => :one, '1' => :all})
|
109
|
+
|
110
|
+
##
|
111
|
+
# The PV power balance mode setting
|
112
|
+
#
|
113
|
+
# :charge # => PV output is limited to battery charge current
|
114
|
+
# :charge_and_load # => PV output will attempt to use enough power for charging the battery and enough to supply the connected load
|
115
|
+
PV_POWER_BALANCE_MODE = lookup_hash('PV power balance mode', {'0' => :charge, '1' => :charge_and_load})
|
116
|
+
|
117
|
+
##
|
118
|
+
# Possible fault codes returned by the device
|
119
|
+
FAULT_CODE = lookup_hash('fault code',
|
120
|
+
{ '00' => 'No faults',
|
121
|
+
'01' => 'Fan is locked',
|
122
|
+
'02' => 'Over temperature',
|
123
|
+
'03' => 'Battery voltage is too high',
|
124
|
+
'04' => 'Battery voltage is too low',
|
125
|
+
'05' => 'Output short circuited/Over temperature',
|
126
|
+
'06' => 'Output voltage is too high',
|
127
|
+
'07' => 'Overload time out',
|
128
|
+
'08' => 'Bus voltage is too high',
|
129
|
+
'09' => 'Bus soft start failed',
|
130
|
+
'11' => 'Main relay failed',
|
131
|
+
'51' => 'Inverter over current',
|
132
|
+
'52' => 'Bus soft start failed',
|
133
|
+
'53' => 'Inverter soft start failed',
|
134
|
+
'54' => 'Self-test failed',
|
135
|
+
'55' => 'Inverter over voltage on DC output',
|
136
|
+
'56' => 'Battery connection is open',
|
137
|
+
'57' => 'Current sensor failed',
|
138
|
+
'58' => 'Output voltage is too low',
|
139
|
+
'60' => 'Inverter negative power',
|
140
|
+
'71' => 'Parallel version different',
|
141
|
+
'71' => 'Output circuit failed',
|
142
|
+
'80' => 'CAN communication failed',
|
143
|
+
'81' => 'Parallel host line lost',
|
144
|
+
'82' => 'Parallel synchronized signal lost',
|
145
|
+
'83' => 'Parallel battery voltage is detected as different',
|
146
|
+
'84' => 'Parallel line voltage or frequency is detected as different',
|
147
|
+
'85' => 'Parallel line input current unbalanced',
|
148
|
+
'86' => 'Parallel output setting is different' })
|
149
|
+
|
150
|
+
class UnknownConstant < RuntimeError; end # :nodoc:
|
151
|
+
end
|
data/lib/axpert_rs232.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
##
|
2
|
+
# A convenient immutable object to encapsulate the logic
|
3
|
+
# of a Voltronic Device operation consisting of:
|
4
|
+
# Command, parameter validation and result parser
|
5
|
+
#
|
6
|
+
# @author: Johan van der Vyver
|
7
|
+
class VoltronicDeviceOperation
|
8
|
+
require 'voltronic_rs232'
|
9
|
+
require 'time'
|
10
|
+
|
11
|
+
def initialize(input, &blk)
|
12
|
+
input = {}.merge(input) rescue (raise ::ArgumentError.new("Expected an input hash"))
|
13
|
+
|
14
|
+
@command = begin
|
15
|
+
as_lambda(input.fetch(:command))
|
16
|
+
rescue ::StandardError => err
|
17
|
+
err = "#{err.class.name.to_s} thrown; #{err.message.to_s}"
|
18
|
+
raise ::ArgumentError.new("Expected :command to be a String with a device command or Proc (#{err})")
|
19
|
+
end
|
20
|
+
|
21
|
+
@error_on_nak = (true == input.fetch(:error_on_nak, true))
|
22
|
+
|
23
|
+
@parser = begin
|
24
|
+
as_lambda(input.fetch(:parser))
|
25
|
+
rescue ::StandardError => err
|
26
|
+
err = "#{err.class.name.to_s} thrown; #{err.message.to_s}"
|
27
|
+
raise ::ArgumentError.new("Expected :parser to be a Proc or Lambda (#{err})")
|
28
|
+
end
|
29
|
+
|
30
|
+
@read_timeout = Integer(input.fetch(:serial_read_timeout_seconds, 2))
|
31
|
+
@write_timeout = Integer(input.fetch(:serial_write_timeout_seconds, 2))
|
32
|
+
|
33
|
+
@termination_character = begin
|
34
|
+
parse = input.fetch(:serial_termination_character, "\r").to_s
|
35
|
+
raise ::ArgumentError.new("Expected :serial_termination_character to be a single character") unless (parse.length == 1)
|
36
|
+
parse.freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Issue a command to the device and parse the output result
|
44
|
+
def issue_command(serial, *args)
|
45
|
+
serial.read_timeout = -1 # Prevent locking on serial
|
46
|
+
|
47
|
+
serial.write(command(*args).bytes)
|
48
|
+
result = begin
|
49
|
+
parse = ''
|
50
|
+
read_timeout = ::Time.now.to_i + @read_timeout # 2 seconds
|
51
|
+
while(true)
|
52
|
+
ch = serial.getc # Retrieve a single character from Serial port
|
53
|
+
if ch.nil?
|
54
|
+
sleep 0.1 # 100ms pause before polling again
|
55
|
+
next
|
56
|
+
end
|
57
|
+
parse += ch
|
58
|
+
break if (@termination_character == ch)
|
59
|
+
raise ::IOError.new("IO read timeout reached, giving up") if (Time.now.to_i > read_timeout)
|
60
|
+
end
|
61
|
+
parse
|
62
|
+
end
|
63
|
+
|
64
|
+
parse_result(result[0..-4])
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Create an VoltronicRS232 object containing a command
|
69
|
+
# and optional parameter to execute on the device
|
70
|
+
def command(*args)
|
71
|
+
RS232_PROTO.new(@command.yield(*args))
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Parse the command output returned from the Voltronic device
|
76
|
+
def parse_result(result)
|
77
|
+
result = RS232_PROTO.new(result)
|
78
|
+
if (@error_on_nak && ('(NAK' == result.data.upcase))
|
79
|
+
raise NAKReceivedError.new("Received NAK from device, this usually means an error occured, an invalid value was supplied or the command is not supported")
|
80
|
+
end
|
81
|
+
@parser.yield(result)
|
82
|
+
rescue ::StandardError, ::ScriptError => err
|
83
|
+
raise err if err.is_a?(NAKReceivedError)
|
84
|
+
err = "#{err.class.name.to_s} thrown; #{err.message.to_s}"
|
85
|
+
raise RS232ParseError.new("Could not parse the result, the output format may have changed (#{err})")
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s # :nodoc:
|
89
|
+
"#{self.class.name.to_s.split('::').last}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def inspect # :nodoc:
|
93
|
+
self.to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def as_lambda(input)
|
99
|
+
if !input.is_a?(Proc)
|
100
|
+
input = begin
|
101
|
+
input_string = input.to_s.chomp.freeze
|
102
|
+
lambda { input_string.to_s.chomp.freeze }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
result = Object.new
|
106
|
+
result.define_singleton_method(:_, &input)
|
107
|
+
result.method(:_).to_proc.freeze
|
108
|
+
end
|
109
|
+
|
110
|
+
RS232_PROTO = ::VoltronicRS232 # :nodoc
|
111
|
+
class NAKReceivedError < ::RuntimeError; end # :nodoc:
|
112
|
+
class RS232ParseError < RuntimeError; end # :nodoc:
|
113
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
##
|
2
|
+
# Simple immutable object representing the Voltronic RS232 protocol
|
3
|
+
#
|
4
|
+
# @author: Johan van der Vyver
|
5
|
+
class VoltronicRS232
|
6
|
+
##
|
7
|
+
# The human readable command to be sent to the device
|
8
|
+
attr_reader :data
|
9
|
+
|
10
|
+
##
|
11
|
+
# The calculated CRC for the data
|
12
|
+
attr_reader :crc
|
13
|
+
|
14
|
+
##
|
15
|
+
# The encoded data that will be transmitted over the wire
|
16
|
+
# Format: <DATA><CRC><CR>
|
17
|
+
attr_reader :bytes
|
18
|
+
|
19
|
+
def initialize(data) #:nodoc:
|
20
|
+
@data = data.to_s.chomp.dup.freeze
|
21
|
+
if (@data != @data.encode(::Encoding.find('ASCII'), {invalid: :replace, undef: :replace, replace: ''}))
|
22
|
+
raise ArgumentError.new("Input data can only be ASCII")
|
23
|
+
end
|
24
|
+
@crc = calculate_crc(data.bytes.to_a).map { |b| b.chr }.join.freeze
|
25
|
+
@bytes = "#{@data}#{@crc}\r".freeze
|
26
|
+
self.freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s #:nodoc:
|
30
|
+
"#{self.class.name.to_s.split('::').last}(data: '#{data}')"
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect #:nodoc:
|
34
|
+
to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other) #:nodoc:
|
38
|
+
return true if self.equal?(other)
|
39
|
+
return false if other.nil?
|
40
|
+
return false unless other.respond_to?(:bytes)
|
41
|
+
(self.bytes == other.bytes)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# CRC calculation source: http://forums.aeva.asn.au/pip4048ms-inverter_topic4332_post53760.html#53760
|
47
|
+
def calculate_crc(pin) #:nodoc:
|
48
|
+
crc, da = 0, 0
|
49
|
+
for index in 0..(pin.length-1)
|
50
|
+
da = byte(byte(crc >> 8) >> 4)
|
51
|
+
crc = short(short(crc << 4) ^ CRC_TABLE[byte(da ^ byte(pin[index] >> 4))])
|
52
|
+
da = byte(byte(crc >> 8) >> 4)
|
53
|
+
crc = short(short(crc << 4) ^ CRC_TABLE[byte(da ^ byte(pin[index] & 0x0f))])
|
54
|
+
end
|
55
|
+
|
56
|
+
crc_low, crc_high = byte(crc & 0x00FF), byte(crc >> 8)
|
57
|
+
crc_low = short(crc_low + 1) if CRC_MOD.include?(crc_low)
|
58
|
+
crc_high = short(crc_high + 1) if CRC_MOD.include?(crc_high)
|
59
|
+
crc = short(short(crc_high << 8) | crc_low)
|
60
|
+
[byte(short(crc >> 8) & 0xff), byte(crc & 0xff)]
|
61
|
+
end
|
62
|
+
|
63
|
+
def byte(input) #:nodoc:
|
64
|
+
(input & 255)
|
65
|
+
end
|
66
|
+
|
67
|
+
def short(input) #:nodoc:
|
68
|
+
(input & 65535)
|
69
|
+
end
|
70
|
+
|
71
|
+
CRC_TABLE = [0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
72
|
+
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef].freeze #:nodoc:
|
73
|
+
|
74
|
+
CRC_MOD = [0x28, 0x0d, 0x0a].freeze #:nodoc:
|
75
|
+
end
|
metadata
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: axpert_rs232
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Johan van der Vyver
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-26 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Simplify communicating with a Voltronics Axpert range inverter
|
14
|
+
email: johan.vdvyver@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/axpert_commands.rb
|
20
|
+
- lib/axpert_constants.rb
|
21
|
+
- lib/voltronic_device_operation.rb
|
22
|
+
- lib/voltronic_rs232.rb
|
23
|
+
- lib/axpert_rs232.rb
|
24
|
+
homepage: http://rubygems.org/gems/axpert_rs232
|
25
|
+
licenses:
|
26
|
+
- MIT
|
27
|
+
metadata: {}
|
28
|
+
post_install_message:
|
29
|
+
rdoc_options: []
|
30
|
+
require_paths:
|
31
|
+
- lib
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
requirements: []
|
43
|
+
rubyforge_project:
|
44
|
+
rubygems_version: 2.0.14
|
45
|
+
signing_key:
|
46
|
+
specification_version: 4
|
47
|
+
summary: Simplify communicating with a Voltronics Axpert range inverter
|
48
|
+
test_files: []
|