UNPKG

ojousima.ruuvi_endpoints.ts

Version:
126 lines (105 loc) 4.47 kB
import { AES, ModeOfOperation } from 'aes-js'; import { RuuviTagBroadcast } from './ruuvitagbroadcast'; const dataFormatStart = 0; const dataFormatEnd = dataFormatStart + 1; const versionStart = dataFormatEnd; const versionEnd = versionStart + 1; const versionInvalid = 255; const addressStart = versionEnd; const addressEnd = addressStart + 6; const addressInvalid = 0xffffffffffff; const humidityStart = addressEnd; const humidityEnd = humidityStart + 1; const humidityInvalid = 255; const temperatureStart = humidityEnd; const temperatureEnd = temperatureStart + 2; const temperatureInvalid = -32768; const pressureStart = temperatureEnd; const pressureEnd = pressureStart + 2; const pressureInvalid = 65535; const batteryStart = pressureEnd; const batteryEnd = batteryStart + 2; const sequenceStart = batteryEnd; const sequenceEnd = sequenceStart + 2; const sequenceInvalid = 0xffff; /* * @brief Parse FE data * * Throws error if header byte is not @c 0xFE or version byte is not @c 0x02 * * @param data complete manufacturer data to be parsed, starting from header byte * @return Ruuvi data payload */ export const dffeparser = (data: Uint8Array): RuuviTagBroadcast => { if (data.length < addressEnd - 1 || 0xfe !== data[0]) { throw new Error('Not DF5 data'); } if (0x02 !== data[versionStart]) { throw new Error('Unsupported version ' + data[versionStart].toString(16)); } const robject: RuuviTagBroadcast = new RuuviTagBroadcast(); robject.dataFormat = data[dataFormatStart]; let humidity = data[humidityStart]; humidity /= 2; // scale robject.humidityRh = humidity; const temperatureBytes = data.slice(temperatureStart, temperatureEnd); let temperature = temperatureBytes[0]; // Full degrees temperature += temperatureBytes[1] / 100.0; // Decimals if (temperature > 128) { // Ruuvi format, sign bit + value temperature = temperature - 128; temperature = 0 - temperature; } robject.temperatureC = temperature; const pressureBytes = data.slice(pressureStart, pressureEnd); // uint16_t pascals let pressure = pressureBytes[0] * 256 + pressureBytes[1]; pressure += 50000; // Ruuvi format robject.pressurePa = pressure; const batteryBytes = data.slice(batteryStart, batteryEnd); // millivolts const battery = batteryBytes[0] * 256 + batteryBytes[1]; robject.batteryVoltageV = battery / 1000.0; const sequenceBytes = data.slice(sequenceStart, sequenceEnd); const sequence = sequenceBytes[0] * 256 + sequenceBytes[1]; robject.measurementSequence = sequenceInvalid === sequence ? null : sequence; const addressBytes = data.slice(addressStart, addressEnd); // Sorry, but lint says so. const address = addressBytes[0] * 256 * 256 * 256 * 256 * 256 + addressBytes[1] * 256 * 256 * 256 * 256 + addressBytes[2] * 256 * 256 * 256 + addressBytes[3] * 256 * 256 + addressBytes[4] * 256 + addressBytes[5]; robject.mac = addressInvalid === address ? null : address; return robject; }; /* * @brief Unencrypt data with the basepassword and unique password. * * For example data version 2 has default password of "ruuvi.com\0" + device mac. * Convert ASCII (NOT UTF-8!) string of "ruuvi.com\0" to Uint8Array and use it as a basepw * Add device mac as unique pw. Returns complete data in unencrypted form as Uint8Array * * @param data complete manufacturer data to be unencrypted, starting from header byte * @param basepw: Base password used in encryption * @param unitpw: Unit-specific password used in encryption. * @return unencrypted FE data. * @warning Be careful when converting strings to Uint8Array to avoid UTF-8 encoded values. */ export const dffeunencrypter = (data: Uint8Array, basepw: Uint8Array, unitpw: Uint8Array): Uint8Array => { if (16 !== basepw.length + unitpw.length) { throw new Error( 'Invalid base+unit password length. Expected 16, got ' + (basepw.length + unitpw.length).toString(), ); } const key: Uint8Array = new Uint8Array(basepw.length + unitpw.length); key.set(basepw); key.set(unitpw, basepw.length); const aesEcb = new ModeOfOperation.ecb(key); // All fe data starts with header + version, followed by encrypted 16-byte payload const encryptedBytes: Uint8Array = data.subarray(2, 2 + 16); // Since electronic codebook does not store state, we can // reuse the same instance. const decryptedBytes: Uint8Array = aesEcb.decrypt(encryptedBytes); return decryptedBytes; };