From e6372acf3457801e2e23c2a25bcec6067902eb0d Mon Sep 17 00:00:00 2001 From: Patrick McDonagh Date: Mon, 8 Jan 2018 18:26:08 -0600 Subject: [PATCH] Completes correct function for converting 2 bytes to Float16 --- python-driver/lambda.js | 111 ++++++++++++++++++++++++++++ python-driver/prostarsolar.py | 102 +++++-------------------- python-driver/prostarsolar_utils.py | 68 +++++++++++++++++ python-driver/test_utilities.py | 48 ++++++++++++ python-driver/utilities.py | 23 ++++-- 5 files changed, 262 insertions(+), 90 deletions(-) create mode 100644 python-driver/lambda.js create mode 100644 python-driver/prostarsolar_utils.py create mode 100644 python-driver/test_utilities.py diff --git a/python-driver/lambda.js b/python-driver/lambda.js new file mode 100644 index 0000000..cb7f69d --- /dev/null +++ b/python-driver/lambda.js @@ -0,0 +1,111 @@ +var assert = require('assert'); + +var topic = {"ChannelName":"t_amb","DeviceAddress":"c4:93:00:0a:8a:0d:01:99","DeviceType":"prostarsolar","GatewayAddress":"C493000A8A0D","TechName":"prostarsolar_[c4:93:00:0a:8a:0d:01:99]","TenantID":194}; +var payload = {"Timestamp":1515422927,"Value":"0"}; +var now = 1515422927; + + +var lambda = function(topic, payload, now){ + var inpValue = parseInt(payload.Value); + var outValue; + var binaryInput = Array(16).join("0") + inpValue.toString(2); + var binRep = binaryInput.substring(binaryInput.length - 16); + var sign = 1; + var fraction; + if (parseInt(binRep[0]) === 1){ + sign = -1; + } + var exponent = parseInt(binRep.substring(1, 6), 2); + if (exponent === 30.0){ + fraction = parseInt("1".concat(binRep.substring(7, 17)), 2); + } else { + fraction = parseInt(binRep.substring(7, 17), 2); + } + if (exponent === 0){ + outValue = sign * Math.pow(2,-14) * fraction / Math.pow(2.0, 10.0); + } else if (exponent === 0b11111){ + if (fraction == 0){ + outValue = sign * Infinity; + } else { + outValue = NaN; + } + } else { + var frac_part = 1.0 + fraction / Math.pow(2.0, 10.0); + outValue = sign * Math.pow(2 , exponent - 15) * frac_part; + } + + + return outValue; +} + +var intToFloat16 = function(intToConvert){ + var binRep = pad(intToConvert.toString(2), 16, "0"); + var sign = 1; + var fraction; + if (parseInt(binRep[0]) === 1){ + sign = -1; + } + var exponent = parseInt(binRep.substring(1, 6), 2); + if (exponent === 30.0){ + fraction = parseInt("1".concat(binRep.substring(7, 17)), 2); + } else { + fraction = parseInt(binRep.substring(7, 17), 2); + } + + // console.log("Sign: " + sign); + // console.log("exponent: " + exponent + " -- " + binRep.substring(1, 6)); + // console.log("fraction: " + fraction); + + if (exponent === 0){ + return sign * (2 ** -14) * fraction / (2.0 ** 10.0); + } else if (exponent === 0b11111){ + if (fraction == 0){ + return sign * Infinity; + } else { + return NaN; + } + } else { + var frac_part = 1.0 + fraction / (2.0 ** 10.0); + return sign * (2 ** (exponent - 15)) * frac_part; + } +} + +var pad = function(start, numChars, padWith){ + var fullString = Array(numChars).join(padWith) + start; + return fullString.substring(fullString.length - 16); +} + +var test_values = [ + [0b0000000000000000, 0.], + [0b1000000000000000, -0.], + [0b0011110000000000, 1], + [0b0011110000000001, 1.0009765625], + [0b1011110000000001, -1.0009765625], + [0b1100000000000000, -2], + [0b0100000000000000, 2], + [0b0111101111111111, 65504.], + [0b1111101111111111, -65504.], + [0b0000010000000000, 6.10352e-5], + [0b0000001111111111, 6.09756e-5], + [0b0000000000000001, 5.96046e-8], + [0b0111110000000000, Infinity], + [0b1111110000000000, -Infinity], + [0b0011010101010101, 0.333251953125] +] + + +describe('intToFloat16', function(){ + for (var i = 0; i < test_values.length; i++){ + var testInput = test_values[i][0]; + var expectedVal = test_values[i][1]; + + + payload.Value = testInput; + // var testVal = intToFloat16(testInput); + var testVal = lambda(topic, payload, now); + + it("should return " + test_values[i][1] + " for " + test_values[i][0].toString(2) + " : " + testVal, function(){ + assert.equal(testVal, expectedVal); + }) + } +}) diff --git a/python-driver/prostarsolar.py b/python-driver/prostarsolar.py index 2d923a0..7ea3a11 100644 --- a/python-driver/prostarsolar.py +++ b/python-driver/prostarsolar.py @@ -2,11 +2,10 @@ import threading from device_base import deviceBase -from Channel import ModbusChannel import persistence -from utilities import get_public_ip_address, int_to_float16 +from utilities import get_public_ip_address +import prostarsolar_utils import time - import minimalmodbus import minimalmodbusM1 @@ -16,68 +15,10 @@ minimalmodbusM1.STOPBITS = 2 _ = None -def charge_state(inp_state): - """Map function for charge state.""" - states = { - 0: "Start", - 1: "Night Check", - 2: "Disconnect", - 3: "Night", - 4: "Fault", - 5: "Bulk", - 6: "Absorption", - 7: "Float", - 8: "Equalize" - } - if inp_state in range(0, 9): - return states[inp_state] - else: - return inp_state - - -def array_faults(inp_array_faults): - """Form a string for the array_faults.""" - fault_string = "" - faults = { - 0: "Overcurrent Phase 1", - 1: "FETs Shorted", - 2: "Software Bug", - 3: "Battery HVD (High Voltage Disconnect)", - 4: "Array HVD (High Voltage Disconnect)", - 5: "EEPROM Setting Edit (reset required)", - 6: "RTS Shorted", - 7: "RTS was valid now disconnected", - 8: "Local temp. sensor failed", - 9: "Battery LVD (Low Voltage Disconect)", - 10: "DIP Switch Changed (excl. DIP 8)", - 11: "Processor Supply Fault" - } - - bit_string = ("0" * 16 + "{0:b}".format(inp_array_faults))[-16:] - for i in range(0, 12): - if int(bit_string[i]) == 1: - fault_string += faults[i] + ", " - if fault_string: - return fault_string[:-2] - else: - return "None" - - # GLOBAL VARIABLES WATCHDOG_SEND_PERIOD = 3600 # Seconds, the longest amount of time before sending the watchdog status PLC_IP_ADDRESS = "192.168.1.10" -CHANNELS = [ - ModbusChannel("adc_ia", 17, "REAL", 0.5, 3600, transformFn=int_to_float16), - ModbusChannel("adc_vbterm", 18, "REAL", 0.5, 3600, transformFn=int_to_float16), - ModbusChannel("adc_va", 19, "REAL", 0.5, 3600, transformFn=int_to_float16), - ModbusChannel("adc_vl", 20, "REAL", 0.5, 3600, transformFn=int_to_float16), - ModbusChannel("adc_il", 22, "REAL", 0.5, 3600, transformFn=int_to_float16), - ModbusChannel("t_amb", 28, "REAL", 2.0, 3600, transformFn=int_to_float16), - ModbusChannel("vb_min_daily", 65, "REAL", 2.0, 3600, transformFn=int_to_float16), - ModbusChannel("vb_max_daily", 66, "REAL", 2.0, 3600, transformFn=int_to_float16), - ModbusChannel('charge_state', 33, "STRING", 1, 3600, transformFn=charge_state), - ModbusChannel('array_fault', 34, "STRING", 1, 3600, transformFn=array_faults) -] +CHANNELS = prostarsolar_utils.CHANNELS # Load the channels from the utils program # PERSISTENCE FILE persist = persistence.load() @@ -116,17 +57,8 @@ class start(threading.Thread, deviceBase): public_ip_address = get_public_ip_address() self.sendtodbDev(1, 'public_ip_address', public_ip_address, 0, 'prostarsolar') - # watchdog = self.prostarsolar_watchdog() - # self.sendtodbDev(1, 'watchdog', watchdog, 0, 'prostarsolar') - # watchdog_send_timestamp = time.time() - connected_to_485 = False - while connected_to_485 is False: - connected_to_485 = self.mcu.set485Baud(9600) - - serial_485 = self.mcu.rs485 - instrument_485 = minimalmodbusM1.Instrument(21, serial_485) - instrument_485.address = 21 + instrument_485 = self.connect_to_485(prostarsolar_utils.BAUDRATE, prostarsolar_utils.NODEADDRESS) send_loops = 0 watchdog_loops = 0 @@ -137,20 +69,14 @@ class start(threading.Thread, deviceBase): for chan in CHANNELS: try: - val = chan.read(instrument_485.read_register(chan.register_number, functioncode=4)) + val = chan.read(instrument_485.read_registers(chan.register_number, chan.channel_size, functioncode=4)) if chan.check(val, self.forceSend): self.sendtodbDev(1, chan.mesh_name, chan.value, 0, 'prostarsolar') time.sleep(0.1) except IOError as e: print("prostarsolar IO Error: {}".format(e)) print("Attempting to reconnect to rs485 device") - connected_to_485 = False - while connected_to_485 is False: - connected_to_485 = self.mcu.set485Baud(9600) - - serial_485 = self.mcu.rs485 - instrument_485 = minimalmodbusM1.Instrument(21, serial_485) - instrument_485.address = 21 + instrument_485 = self.connect_to_485(prostarsolar_utils.BAUDRATE, prostarsolar_utils.NODEADDRESS) except Exception as e: print("prostarsolar Non-IO Error: {}".format(e)) @@ -165,11 +91,6 @@ class start(threading.Thread, deviceBase): watchdog_loops += 1 if (watchdog_loops >= watchdog_check_after): - # test_watchdog = self.prostarsolar_watchdog() - # if not test_watchdog == watchdog or (time.time() - watchdog_send_timestamp) > WATCHDOG_SEND_PERIOD: - # self.sendtodbDev(1, 'watchdog', test_watchdog, 0, 'prostarsolar') - # watchdog = test_watchdog - test_public_ip = get_public_ip_address() if not test_public_ip == public_ip_address: self.sendtodbDev(1, 'public_ip_address', test_public_ip, 0, 'prostarsolar') @@ -177,6 +98,17 @@ class start(threading.Thread, deviceBase): watchdog_loops = 0 time.sleep(10) + def connect_to_485(self, baudrate, nodeaddress): + """Connect to the RS485 device.""" + connected_to_485 = False + while connected_to_485 is False: + connected_to_485 = self.mcu.set485Baud(baudrate) + + serial_485 = self.mcu.rs485 + instrument_485 = minimalmodbusM1.Instrument(nodeaddress, serial_485) + instrument_485.address = nodeaddress + return instrument_485 + def prostarsolar_sync(self, name, value): """Sync all data from the driver.""" self.forceSend = True diff --git a/python-driver/prostarsolar_utils.py b/python-driver/prostarsolar_utils.py new file mode 100644 index 0000000..de4ad4d --- /dev/null +++ b/python-driver/prostarsolar_utils.py @@ -0,0 +1,68 @@ +"""Provide utilities for reading prostar solar panels.""" + +from utilities import int_to_float16 +from Channel import ModbusChannel + + +def charge_state(inp_state): + """Map function for charge state.""" + states = { + 0: "Start", + 1: "Night Check", + 2: "Disconnect", + 3: "Night", + 4: "Fault", + 5: "Bulk", + 6: "Absorption", + 7: "Float", + 8: "Equalize" + } + if inp_state in range(0, 9): + return states[inp_state] + else: + return inp_state + + +def array_faults(inp_array_faults): + """Form a string for the array_faults.""" + fault_string = "" + faults = { + 0: "Overcurrent Phase 1", + 1: "FETs Shorted", + 2: "Software Bug", + 3: "Battery HVD (High Voltage Disconnect)", + 4: "Array HVD (High Voltage Disconnect)", + 5: "EEPROM Setting Edit (reset required)", + 6: "RTS Shorted", + 7: "RTS was valid now disconnected", + 8: "Local temp. sensor failed", + 9: "Battery LVD (Low Voltage Disconect)", + 10: "DIP Switch Changed (excl. DIP 8)", + 11: "Processor Supply Fault" + } + + bit_string = ("0" * 16 + "{0:b}".format(inp_array_faults))[-16:] + for i in range(0, 12): + if int(bit_string[i]) == 1: + fault_string += faults[i] + ", " + if fault_string: + return fault_string[:-2] + else: + return "None" + + +CHANNELS = [ + ModbusChannel("adc_ia", 17, "REAL", 0.5, 3600, transformFn=int_to_float16), + ModbusChannel("adc_vbterm", 18, "REAL", 0.5, 3600, transformFn=int_to_float16), + ModbusChannel("adc_va", 19, "REAL", 0.5, 3600, transformFn=int_to_float16), + ModbusChannel("adc_vl", 20, "REAL", 0.5, 3600, transformFn=int_to_float16), + ModbusChannel("adc_il", 22, "REAL", 0.5, 3600, transformFn=int_to_float16), + ModbusChannel("t_amb", 28, "REAL", 2.0, 3600, transformFn=int_to_float16), + ModbusChannel("vb_min_daily", 65, "REAL", 2.0, 3600, transformFn=int_to_float16), + ModbusChannel("vb_max_daily", 66, "REAL", 2.0, 3600, transformFn=int_to_float16), + ModbusChannel('charge_state', 33, "STRING", 1, 3600, transformFn=charge_state), + ModbusChannel('array_fault', 34, "STRING", 1, 3600, transformFn=array_faults) +] + +NODEADDRESS = 21 # Default node address +BAUDRATE = 9600 # Default Baud Rate diff --git a/python-driver/test_utilities.py b/python-driver/test_utilities.py new file mode 100644 index 0000000..493d5da --- /dev/null +++ b/python-driver/test_utilities.py @@ -0,0 +1,48 @@ +from utilities import int_to_float16 +import struct +from math import copysign, frexp, isinf, isnan, trunc + +NEGATIVE_INFINITY = b'\x00\xfc' +POSITIVE_INFINITY = b'\x00\x7c' +POSITIVE_ZERO = b'\x00\x00' +NEGATIVE_ZERO = b'\x00\x80' +# exp=2**5-1 and significand non-zero +EXAMPLE_NAN = struct.pack('