axpert_rs232 1.0.0
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.
- 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: []
|