/** * THIS SOFTWARE IS PROVIDED BY GREENME SAS AND ITS CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ /////////////////////////////////// // Helper class and definitions /////////////////////////////////// var cp437ToUnicode = [0x0000,0x0001,0x0002,0x0003,0x0004,0x0005,0x0006,0x0007,0x0008,0x0009,0x000a,0x000b,0x000c,0x000d,0x000e,0x000f,0x0010,0x0011,0x0012,0x0013,0x0014,0x0015,0x0016,0x0017,0x0018,0x0019,0x001a,0x001b,0x001c,0x001d,0x001e,0x001f,0x0020,0x0021,0x0022,0x0023,0x0024,0x0025,0x0026,0x0027,0x0028,0x0029,0x002a,0x002b,0x002c,0x002d,0x002e,0x002f,0x0030,0x0031,0x0032,0x0033,0x0034,0x0035,0x0036,0x0037,0x0038,0x0039,0x003a,0x003b,0x003c,0x003d,0x003e,0x003f,0x0040,0x0041,0x0042,0x0043,0x0044,0x0045,0x0046,0x0047,0x0048,0x0049,0x004a,0x004b,0x004c,0x004d,0x004e,0x004f,0x0050,0x0051,0x0052,0x0053,0x0054,0x0055,0x0056,0x0057,0x0058,0x0059,0x005a,0x005b,0x005c,0x005d,0x005e,0x005f,0x0060,0x0061,0x0062,0x0063,0x0064,0x0065,0x0066,0x0067,0x0068,0x0069,0x006a,0x006b,0x006c,0x006d,0x006e,0x006f,0x0070,0x0071,0x0072,0x0073,0x0074,0x0075,0x0076,0x0077,0x0078,0x0079,0x007a,0x007b,0x007c,0x007d,0x007e,0x007f,0x00c7,0x00fc,0x00e9,0x00e2,0x00e4,0x00e0,0x00e5,0x00e7,0x00ea,0x00eb,0x00e8,0x00ef,0x00ee,0x00ec,0x00c4,0x00c5,0x00c9,0x00e6,0x00c6,0x00f4,0x00f6,0x00f2,0x00fb,0x00f9,0x00ff,0x00d6,0x00dc,0x00a2,0x00a3,0x00a5,0x20a7,0x0192,0x00e1,0x00ed,0x00f3,0x00fa,0x00f1,0x00d1,0x00aa,0x00ba,0x00bf,0x2310,0x00ac,0x00bd,0x00bc,0x00a1,0x00ab,0x00bb,0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,0x2555,0x2563,0x2551,0x2557,0x255d,0x255c,0x255b,0x2510,0x2514,0x2534,0x252c,0x251c,0x2500,0x253c,0x255e,0x255f,0x255a,0x2554,0x2569,0x2566,0x2560,0x2550,0x256c,0x2567,0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256b,0x256a,0x2518,0x250c,0x2588,0x2584,0x258c,0x2590,0x2580,0x03b1,0x00df,0x0393,0x03c0,0x03a3,0x03c3,0x00b5,0x03c4,0x03a6,0x0398,0x03a9,0x03b4,0x221e,0x03c6,0x03b5,0x2229,0x2261,0x00b1,0x2265,0x2264,0x2320,0x2321,0x00f7,0x2248,0x00b0,0x2219,0x00b7,0x221a,0x207f,0x00b2,0x25a0]; /** * Main class for encoding message. */ class Encoder { MSG_LEN_SET_CALIB = 21; MSG_LEN_SET_CFG = 45; MSG_LEN_SET_DISPLAY = 38; MSG_LEN_RESET = 6; MSG_LEN_SET_VALUE = 10; MSG_LEN_SET_ALERT = 8; RADIOMSG_HEADER1 = 0x65; RADIOMSG_HEADER2 = 0xef; //0xee on devices with fw < 2.5 RADIOCMD_SET_CALIB = 0x04; RADIOCMD_SET_CFG = 0x05; RADIOCMD_REQUEST_CFG = 0x06; RADIOCMD_SET_DISPLAY = 0x07; RADIOCMD_RESET = 0x08; RADIOCMD_SET_VALUE = 0x09; RADIOCMD_SET_ALERT = 0x0A; TOGGLE_ACTION_IMG = 0x00; TOGGLE_ACTION_TEXT = 0x01; POLL_RESPONSE_HAPPY = 0x00; POLL_RESPONSE_YESNO = 0x01; POLL_ACK_OK = 0x00; POLL_ACK_SENT = 0x01; LANG_FR_FR = 0x00; LANG_EN_US = 0x01; LANG_EN_GB = 0x02; LANG_DE_DE = 0x03; LANG_NL_NL = 0x04; LANG_PT_PT = 0x05; LANG_ES_ES = 0x06; MAX_TEXT_LENGTH = 30; MAX_TOGGLE_TEXT_LENGTH = 10; VALUETYPE_TEMP = 1; VALUETYPE_HYGR = 2; VALUETYPE_DBA = 3; VALUETYPE_LUX = 4; VALUETYPE_COLORR = 5; VALUETYPE_COLORG = 6; VALUETYPE_COLORB = 7; VALUETYPE_COLORW = 8; constructor() { } /*** * Calibrate device with deltas or gain to raw electronic measurement. * @param deltaTemp_deg * @param deltaHygr * @param gainLux * @param deltaDBA * @param deltaOctave1 * @param deltaOctave2 * @param deltaOctave3 * @param deltaOctave4 * @param deltaOctave5 * @param deltaOctave6 * @param deltaOctave7 * @param deltaOctave8 * @return */ MakeMsgCalib( deltaTemp_deg, deltaHygr, gainLux, deltaDBA, gainColorR, gainColorG, gainColorB, gainColorW ) { let i= 0; let buffer = new Array(this.MSG_LEN_SET_CALIB).fill(0); i=0; buffer[i] = this.RADIOMSG_HEADER1; i++; buffer[i] = this.RADIOMSG_HEADER2; i++; //cmd buffer[i] = this.RADIOCMD_SET_CALIB; i++; this.WriteInt16ToBuffer((deltaTemp_deg*100), buffer, i); i=i+2; this.WriteInt16ToBuffer((deltaHygr*100), buffer, i); i=i+2; this.WriteU16ToBuffer((gainLux*100), buffer, i); i=i+2; this.WriteInt16ToBuffer((deltaDBA*100), buffer, i); i=i+2; this.WriteU16ToBuffer((gainColorR*100), buffer, i); i=i+2; this.WriteU16ToBuffer((gainColorG*100), buffer, i); i=i+2; this.WriteU16ToBuffer((gainColorB*100), buffer, i); i=i+2; this.WriteInt16ToBuffer((gainColorW*100), buffer, i); i=i+2; //crc let crc = this.Crc16_ccit_false(buffer, i); this.WriteCrc16ToBuffer(crc, buffer, i); i=i+2; let result = {b64:null, hex:null}; let str = ''; for (let i=0; i 8)) return; let i= 0; let buffer = new Array(this.MSG_LEN_SET_VALUE).fill(0); i=0; buffer[i] = this.RADIOMSG_HEADER1; i++; buffer[i] = this.RADIOMSG_HEADER2; i++; //cmd buffer[i] = this.RADIOCMD_SET_VALUE; i++; buffer[i] = sensorType; i++; this.WriteFloatToBuffer(measure, buffer, i); i=i+4; //crc let crc = this.Crc16_ccit_false(buffer, i); this.WriteCrc16ToBuffer(crc, buffer, i); i=i+2; let result = {b64:null, hex:null}; let str = ''; for (let i=0; i 255)?255:shortMsgInterval_min; i++; buffer[i] = (longMsgInterval_min > 255)?255:longMsgInterval_min; i++; buffer [i] = 0; buffer[i] |= showTemp?0x80:0; buffer[i] |= showHygr?0x40:0; buffer[i] |= showLux?0x20:0; buffer[i] |= showNoise?0x10:0; buffer[i] |= showAir?0x08:0; buffer[i] |= showToggle?0x04:0; buffer[i] |= BleBeaconEnabled?0x02:0; buffer[i] |= disableSound?0x01:0; i++; buffer [i] = 0; buffer[i] |= disableVOC?0x80:0; buffer[i] |= toggleModeText?0x40:0; val = (imgToggleLeft>7)?7:imgToggleLeft; buffer[i] |= val << 3; val = (imgToggleRight>7)?7:imgToggleRight; buffer[i] |= val; i++; buffer[i] = 0; val = (imgToggleBack>7)?7:imgToggleBack; buffer[i] |= val << 5; buffer[i] |= extSensorType & 0x1f; i++; let cp437str = this.Utf8TextToCp437(textToggleLeft); for (let j=0; j < cp437str.length; j++) { buffer[i+j] = cp437str[j]; if (j >= this.MAX_TOGGLE_TEXT_LENGTH - 1) break; } i+= this.MAX_TOGGLE_TEXT_LENGTH; cp437str = this.Utf8TextToCp437(textToggleRight); for (let j=0; j < cp437str.length; j++) { buffer[i+j] = cp437str[j]; if (j >= this.MAX_TOGGLE_TEXT_LENGTH - 1) break; } i+= this.MAX_TOGGLE_TEXT_LENGTH; cp437str = this.Utf8TextToCp437(textAcknowledgment); for (let j=0; j < cp437str.length; j++) { buffer[i+j] = cp437str[j]; if (j >= this.MAX_TOGGLE_TEXT_LENGTH - 1) break; } i+= this.MAX_TOGGLE_TEXT_LENGTH; switch (lang) { case "FR_FR": buffer[i] = this.LANG_FR_FR; break; case "EN_US": buffer[i] = this.LANG_EN_US; break; case "EN_GB": buffer[i] = this.LANG_EN_GB; break; case "DE_DE": buffer[i] = this.LANG_DE_DE; break; case "NL_NL": buffer[i] = this.LANG_NL_NL; break; case "PT_PT": buffer[i] = this.LANG_PT_PT; break; case "ES_ES": buffer[i] = this.LANG_ES_ES; break; default: buffer[i] = this.LANG_EN_GB; break; } i++; buffer[i] = eventMode & 0xff; i++; buffer[i] = eventFrom & 0xff; i++; buffer[i] = eventThreshold & 0xff; i++; buffer[i] = eventWindow_s & 0xff; i++; //crc let crc = this.Crc16_ccit_false(buffer, i); this.WriteCrc16ToBuffer(crc, buffer, i); i=i+2; let result = {b64:null, hex:null}; let str = ''; for (let i=0; i= this.MAX_TEXT_LENGTH - 1) break; } i+= this.MAX_TEXT_LENGTH; //crc let crc = this.Crc16_ccit_false(buffer, i); this.WriteCrc16ToBuffer(crc, buffer, i); i=i+2; let result = {b64:null, hex:null}; let str = ''; for (let i=0; i= 0x20) && (j <= 0xA9)) cp437.push(j); break; } } } return cp437; } Base64ToHexString(b64Str) { var str = atob(b64Str); } /** * Calculate CRC16 * @param {*} buffer * @param {*} Size * @returns */ Crc16_ccit_false(buffer, Size) { var crcTable = [ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0, ]; var crc = 0xFFFF; var j, i,c; for (i = 0; i < Size; i++) { c = buffer[i]; j = (c ^ (crc >> 8)) & 0xFF; crc = crcTable[j] ^ (crc << 8); } return crc & 0xffff; } /** * Write CRC value to the end of the byte array. * @param {*} value * @param {*} buffer * @param {*} pos */ WriteCrc16ToBuffer(value, buffer, pos) { let u16 = new Uint16Array([value])[0]; buffer[pos+1] = u16 >> 8; buffer[pos] = u16 & 0x00ff; } /** * Add UINT16 to byte array * @param {*} value * @param {*} buffer * @param {*} pos */ WriteU16ToBuffer(value, buffer, pos) { let u16Array = new Int16Array(1); u16Array[0] = value; let byteArray = new Uint8Array(u16Array.buffer); for (let i=0; i<2; i++) buffer[pos+(1-i)] = byteArray[i]; } /** * Add 16 bit int to byte array * @param {*} value * @param {*} buffer * @param {*} pos */ WriteInt16ToBuffer(value, buffer, pos) { let shortArray = new Int16Array(1); shortArray[0] = value; let byteArray = new Uint8Array(shortArray.buffer); for (let i=0; i<2; i++) buffer[pos+(1-i)] = byteArray[i]; } /** * Add float to byte array. * @param {*} value * @param {*} buffer * @param {*} pos */ WriteFloatToBuffer(value, buffer, pos) { let floatArray = new Float32Array(1); floatArray[0] = value; let byteArray = new Uint8Array(floatArray.buffer); for (let i=0; i<4; i++) buffer[pos+i] = byteArray[i]; } } ///////////////////////////////// // TTN Encode Decode ///////////////////////////////// function decodeUplink(input) { var data = {}; var errors = []; var warnings = []; //constants var MESSAGEV2_SHORT = 5; var MESSAGEV2_FULL = 6; var MESSAGEV2_FEEL = 7; var MESSAGEEXT_NONE = 0; var MESSAGEEXT_CO2ONLY = 1; var MESSAGEEXT_COVONLY = 2; var MESSAGEEXT_COV_CO2 = 3; var MESSAGEEXT_PMONLY = 4; var MESSAGEEXT_COV_PM = 5; var MESSAGEEXT_COV_CO2_PM = 6; var FEEL_UNHAPPY = 1; var FEEL_HAPPY = -1; var FEEL_UNKNOWN = 0; var ValidateMessageSize = function (bytes) { var msgSizes = new Map(); msgSizes[MESSAGEV2_SHORT] = 11; msgSizes[MESSAGEV2_FULL] = 26; msgSizes[MESSAGEV2_FEEL] = 26; var msgExtSizes = new Map(); msgExtSizes[MESSAGEEXT_NONE] = 0; msgExtSizes[MESSAGEEXT_CO2ONLY] = 2; msgExtSizes[MESSAGEEXT_COVONLY] = 2; msgExtSizes[MESSAGEEXT_COV_CO2] = 4; var msgType = bytes[0] & 0x0f; var msgSize = bytes.length; var bodySize = msgSizes[msgType]; if (bodySize === undefined) { return false; } if (msgSize < bodySize) return false; else { var msgExtType = bytes[bodySize - 1]; var extSize = msgExtSizes[msgExtType]; if (extSize === undefined) { return false; } else { if (msgSize == bodySize + extSize) return true; else return false; } } }; var bytes = input.bytes; if (!ValidateMessageSize(bytes)) { errors.push("invalid message size"); } else { var i = 0; //decode header var header = bytes[0]; i++; var val = header & 0xc0; if (val == 0x40) data.lastFeel = FEEL_UNHAPPY; else if (val == 0x80) data.lastFeel = FEEL_HAPPY; else data.lastFeel = FEEL_UNKNOWN; var status = (header & 0x30) >> 4; var messageType = header & 0x0f; //decode code data if ((messageType == MESSAGEV2_SHORT) || (messageType == MESSAGEV2_FULL) || (messageType == MESSAGEV2_FEEL)) { data.temperature = (bytes[i+1]*256 + bytes[i])/100.0; i+=2; data.hygrometry = (bytes[i+1]*256 + bytes[i])/100.0; i+=2; data.noiseMax = bytes[i]/2; i++; data.noiseAvg = bytes[i]/2; i++; data.lux = (bytes[i+1]*256 + bytes[i]); i+=2; } //decode full et feel messages if ((messageType == MESSAGEV2_FULL) || (messageType == MESSAGEV2_FEEL)) { data.lightColorR = bytes[i] & 0xff; i++; data.lightColorG = bytes[i] & 0xff; i++; data.lightColorB = bytes[i] & 0xff; i++; data.lightColorW = (bytes[i+1]*256 + bytes[i]); i=i+2; data.flicker = bytes[i]; i++; data.octave1 = bytes[i]; i++; data.octave2 = bytes[i]; i++; data.octave3 = bytes[i]; i++; data.octave4 = bytes[i]; i++; data.octave5 = bytes[i]; i++; data.octave6 = bytes[i]; i++; data.octave7 = bytes[i]; i++; data.octave8 = bytes[i]; i++; data.octave9 = bytes[i]; i++; } if ((messageType == MESSAGEV2_SHORT) || (messageType == MESSAGEV2_FULL) || (messageType == MESSAGEV2_FEEL)) { //battery level and status: unused i++; //extended messages var extMsgType = bytes[i]; i++; //var extMessageType = extMsgType; if (extMsgType == MESSAGEEXT_CO2ONLY) { data.co2 = bytes[i+1]*256 + bytes[i]; i+=2; } else if (extMsgType == MESSAGEEXT_COVONLY) { data.tvoc = bytes[i+1]*256 + bytes[i]; i+=2; } else if (extMsgType == MESSAGEEXT_COV_CO2) { data.tvoc = bytes[i+1]*256 + bytes[i]; i+=2; data.co2 = bytes[i+1]*256 + bytes[i]; i+=2; } } } return { data: data, warnings: warnings, errors: errors }; } function encodeDownlink(input) { let encoder = new Encoder(); let bytes = null; if (input.msgType == 'MakeMsgCalib') { bytes = encoder.MakeMsgCalib( input.deltaTemp_deg, input.deltaHygr, input.gainLux, input.deltaDBA, input.gainColorR, input.gainColorG, input.gainColorB, input.gainColorW); } else if (input.msgType == 'MakeMsgSetValue') { bytes = encoder.MakeMsgSetValue( input.sensorType, input.measure); } else if (input.msgType == 'MakeMsgConfig') { bytes = encoder.MakeMsgConfig( input.shortMsgInterval_min, input.longMsgInterval_min, input.showTemp, input.showHygr, input.showLux, input.showNoise, input.showAir, input.showToggle, 0, input.disableSound, input.disableVOC, input.toggleModeText, input.imgToggleLeft, input.imgToggleRight, input.imgToggleBack, input.extSensorType, input.textToggleLeft, input.textToggleRight, input.textAcknowledgment, input.lang, 0, 0, 0, 0); } else if (input.msgType == 'MakeMsgDisplay') { bytes = encoder.MakeMsgDisplay( input.endOnToggle, input.endOnReboot, input.expiresAfter_h, input.repeat_every_h, input.responseChoices, input.acknowledgement, input.text); } else { return {errors:['bad message type.']}; } if (bytes == null) { return {errors:['could not encode message.']}; } else { return { // LoRaWAN FPort used for the downlink message fPort: 1, // Encoded bytes bytes: bytes, }; } } function decodeDownlink(input) { return null; }