ojousima.ruuvi_endpoints.ts
Version:
Experiments with RuuviTags and TS
126 lines (105 loc) • 4.47 kB
text/typescript
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;
};