Skip to content

Commit

Permalink
feat: Added Grid-EYE thermopile integration
Browse files Browse the repository at this point in the history
Common thermopile functionality has been extracted out for easy
integration of other sensors
  • Loading branch information
mKeRix committed Jan 20, 2020
1 parent 2357f83 commit 134c98b
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 49 deletions.
2 changes: 2 additions & 0 deletions config/default.ts
Expand Up @@ -4,12 +4,14 @@ import {ClusterConfig} from '../src/cluster/cluster.config';
import {GlobalConfig} from '../src/config/global.config';
import {HomeAssistantConfig} from '../src/integrations/home-assistant/home-assistant.config';
import {OmronD6tConfig} from '../src/integrations/omron-d6t/omron-d6t.config';
import {GridEyeConfig} from '../src/integrations/grid-eye/grid-eye.config';

const config: AppConfig = {
global: new GlobalConfig(),
cluster: new ClusterConfig(),
bluetoothLowEnergy: new BluetoothLowEnergyConfig(),
omronD6t: new OmronD6tConfig(),
gridEye: new GridEyeConfig(),
homeAssistant: new HomeAssistantConfig(),
};

Expand Down
2 changes: 2 additions & 0 deletions src/config/app.config.ts
Expand Up @@ -3,11 +3,13 @@ import { ClusterConfig } from '../cluster/cluster.config';
import { BluetoothLowEnergyConfig } from '../integrations/bluetooth-low-energy/bluetooth-low-energy.config';
import { HomeAssistantConfig } from '../integrations/home-assistant/home-assistant.config';
import { OmronD6tConfig } from '../integrations/omron-d6t/omron-d6t.config';
import { GridEyeConfig } from '../integrations/grid-eye/grid-eye.config';

export default interface AppConfig {
global: GlobalConfig;
cluster: ClusterConfig;
bluetoothLowEnergy: BluetoothLowEnergyConfig;
omronD6t: OmronD6tConfig;
gridEye: GridEyeConfig;
homeAssistant: HomeAssistantConfig;
}
5 changes: 5 additions & 0 deletions src/integrations/grid-eye/grid-eye.config.ts
@@ -0,0 +1,5 @@
export class GridEyeConfig {
busNumber: number = 1;
address: number = 0x69;
deltaThreshold: number = 2;
}
16 changes: 16 additions & 0 deletions src/integrations/grid-eye/grid-eye.module.ts
@@ -0,0 +1,16 @@
import { DynamicModule, Module } from '@nestjs/common';
import { GridEyeService } from './grid-eye.service';
import { EntitiesModule } from '../../entities/entities.module';
import { ConfigModule } from '../../config/config.module';
import { ScheduleModule } from '@nestjs/schedule';

@Module({})
export default class GridEyeModule {
static forRoot(): DynamicModule {
return {
module: GridEyeModule,
imports: [EntitiesModule, ConfigModule, ScheduleModule.forRoot()],
providers: [GridEyeService]
};
}
}
18 changes: 18 additions & 0 deletions src/integrations/grid-eye/grid-eye.service.spec.ts
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GridEyeService } from './grid-eye.service';

describe('GridEyeService', () => {
let service: GridEyeService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GridEyeService]
}).compile();

service = module.get<GridEyeService>(GridEyeService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
105 changes: 105 additions & 0 deletions src/integrations/grid-eye/grid-eye.service.ts
@@ -0,0 +1,105 @@
/* tslint:disable:no-bitwise */
import {
Injectable,
OnApplicationBootstrap,
OnApplicationShutdown
} from '@nestjs/common';
import i2cBus, { PromisifiedBus } from 'i2c-bus';
import { Entity } from '../../entities/entity.entity';
import { EntitiesService } from '../../entities/entities.service';
import { Sensor } from '../../entities/sensor.entity';
import * as math from 'mathjs';
import { Interval } from '@nestjs/schedule';
import { ThermopileOccupancySensor } from '../../util/thermopile/thermopile-occupancy.sensor';
import { GridEyeConfig } from './grid-eye.config';
import { ConfigService } from '../../config/config.service';

const TEMPERATURE_REGISTER_START = 0x80;
const FRAMERATE_REGISTER = 0x02;

@Injectable()
export class GridEyeService extends ThermopileOccupancySensor
implements OnApplicationBootstrap, OnApplicationShutdown {
private readonly config: GridEyeConfig;
private i2cBus: PromisifiedBus;
private sensor: Entity;

constructor(
private readonly entitiesService: EntitiesService,
private readonly configService: ConfigService
) {
super();
this.config = this.configService.get('gridEye');
}

async onApplicationBootstrap(): Promise<void> {
this.i2cBus = await i2cBus.openPromisified(this.config.busNumber);
this.setRegister(FRAMERATE_REGISTER, 1); // set framerate to 1 FPS -> less noise
this.sensor = this.entitiesService.add(
new Sensor('grideye_occupancy_count', 'GridEYE Occupancy Count')
);
}

async onApplicationShutdown(signal?: string): Promise<void> {
return this.i2cBus.close();
}

@Interval(1000)
async updateState(): Promise<void> {
const coordinates = await this.getCoordinates(this.config.deltaThreshold);

this.sensor.state = coordinates.length;
this.sensor.attributes.coordinates = coordinates;
}

async getPixelTemperatures(): Promise<number[][]> {
const temperatures = [];
for (let i = 0; i < 64; i++) {
temperatures.push(await this.getPixelTemperature(i));
}

return math.reshape(temperatures, [8, 8]) as number[][];
}

async getPixelTemperature(pixelAddr: number): Promise<number> {
const pixelLowRegister = TEMPERATURE_REGISTER_START + 2 * pixelAddr;
let temperature = await this.getRegister(pixelLowRegister, 2);

if (temperature & (1 << 11)) {
temperature &= ~(1 << 11);
temperature = temperature * -1;
}

return temperature * 0.25;
}

async getRegister(register: number, length: number): Promise<number> {
const commandBuffer = Buffer.alloc(1);
commandBuffer.writeUInt8(register, 0);
const resultBuffer = Buffer.alloc(length);

await this.i2cBus.i2cWrite(
this.config.address,
commandBuffer.length,
commandBuffer
);
await this.i2cBus.i2cRead(this.config.address, length, resultBuffer);

const lsb = resultBuffer.readUInt8(0);
const msb = resultBuffer.readUInt8(1);

return (msb << 8) | lsb;
}

async setRegister(register: number, value: number): Promise<void> {
const commandBuffer = Buffer.alloc(2);
commandBuffer.writeUInt8(register, 0);
commandBuffer.writeUInt8(value, 1);

await this.i2cBus.i2cWrite(
this.config.address,
commandBuffer.length,
commandBuffer
);
}
}
59 changes: 11 additions & 48 deletions src/integrations/omron-d6t/omron-d6t.service.ts
Expand Up @@ -11,17 +11,16 @@ import { OmronD6tConfig } from './omron-d6t.config';
import i2cBus, { PromisifiedBus } from 'i2c-bus';
import { Interval } from '@nestjs/schedule';
import * as math from 'mathjs';
import { Pixel } from './pixel.entity';
import { Cluster } from './cluster.entity';
import { Sensor } from '../../entities/sensor.entity';
import { Entity } from '../../entities/entity.entity';
import { I2CError } from './i2c.error';
import { SensorConfig } from '../home-assistant/sensor-config';
import { ThermopileOccupancySensor } from '../../util/thermopile/thermopile-occupancy.sensor';

const TEMPERATURE_COMMAND = 0x4c;

@Injectable()
export class OmronD6tService
export class OmronD6tService extends ThermopileOccupancySensor
implements OnApplicationBootstrap, OnApplicationShutdown {
private readonly config: OmronD6tConfig;
private i2cBus: PromisifiedBus;
Expand All @@ -32,6 +31,7 @@ export class OmronD6tService
private readonly entitiesService: EntitiesService,
private readonly configService: ConfigService
) {
super();
this.config = this.configService.get('omronD6t');
this.logger = new Logger(OmronD6tService.name);
}
Expand All @@ -56,19 +56,12 @@ export class OmronD6tService
}

@Interval(250)
async updateState() {
async updateState(): Promise<void> {
try {
const temperatures = await this.getPixelTemperatures();
const relevantPixels = this.findRelevantPixels(
temperatures as number[][]
);
const clusters = this.clusterPixels(relevantPixels);

this.sensor.state = clusters.length;
this.sensor.attributes.coordinates = clusters.map(cluster => [
cluster.center.x,
cluster.center.y
]);
const coordinates = await this.getCoordinates(this.config.deltaThreshold);

this.sensor.state = coordinates.length;
this.sensor.attributes.coordinates = coordinates;
} catch (e) {
if (e instanceof I2CError) {
this.logger.debug(`Error during I2C communication: ${e.message}`);
Expand All @@ -81,7 +74,7 @@ export class OmronD6tService
}
}

async getPixelTemperatures() {
async getPixelTemperatures(): Promise<number[][]> {
const commandBuffer = Buffer.alloc(1);
const resultBuffer = Buffer.alloc(35);

Expand All @@ -108,10 +101,10 @@ export class OmronD6tService
pixelTemperatures.push(temperature);
}

return math.reshape(pixelTemperatures, [4, 4]);
return math.reshape(pixelTemperatures, [4, 4]) as number[][];
}

checkPEC(buffer: Buffer, pecIndex: number) {
checkPEC(buffer: Buffer, pecIndex: number): boolean {
let crc = this.calculateCRC(0x15);
for (let i = 0; i < pecIndex; i++) {
crc = this.calculateCRC(buffer.readUInt8(i) ^ crc);
Expand All @@ -132,34 +125,4 @@ export class OmronD6tService

return crc[0];
}

findRelevantPixels(data: number[][]): Pixel[] {
const mean = math.mean(data);
const threshold = mean + this.config.deltaThreshold;

const relevantPixels: Pixel[] = [];
for (const [x, row] of data.entries()) {
for (const [y, value] of row.entries()) {
if (value >= threshold) {
relevantPixels.push(new Pixel(x, y, value));
}
}
}

return relevantPixels;
}

clusterPixels(pixels: Pixel[]) {
const clusters: Cluster[] = [];
pixels.forEach(pixel => {
const neighbor = clusters.find(cluster => cluster.isNeighboredTo(pixel));
if (neighbor === undefined) {
clusters.push(new Cluster([pixel]));
} else {
neighbor.pixels.push(pixel);
}
});

return clusters;
}
}
@@ -1,4 +1,4 @@
import { Pixel } from './pixel.entity';
import { Pixel } from './pixel';

export class Cluster {
pixels: Pixel[];
Expand Down
File renamed without changes.
48 changes: 48 additions & 0 deletions src/util/thermopile/thermopile-occupancy.sensor.ts
@@ -0,0 +1,48 @@
import { Pixel } from './pixel';
import * as math from 'mathjs';
import { Cluster } from './cluster';

export abstract class ThermopileOccupancySensor {
abstract async getPixelTemperatures(): Promise<number[][]>;

async getCoordinates(deltaThreshold: number): Promise<number[][]> {
const temperatures = await this.getPixelTemperatures();
const relevantPixels = this.findRelevantPixels(
temperatures as number[][],
deltaThreshold
);
const clusters = this.clusterPixels(relevantPixels);

return clusters.map(cluster => [cluster.center.x, cluster.center.y]);
}

findRelevantPixels(data: number[][], deltaThreshold: number): Pixel[] {
const mean = math.mean(data);
const threshold = mean + deltaThreshold;

const relevantPixels: Pixel[] = [];
for (const [x, row] of data.entries()) {
for (const [y, value] of row.entries()) {
if (value >= threshold) {
relevantPixels.push(new Pixel(x, y, value));
}
}
}

return relevantPixels;
}

clusterPixels(pixels: Pixel[]): Cluster[] {
const clusters: Cluster[] = [];
pixels.forEach(pixel => {
const neighbor = clusters.find(cluster => cluster.isNeighboredTo(pixel));
if (neighbor === undefined) {
clusters.push(new Cluster([pixel]));
} else {
neighbor.pixels.push(pixel);
}
});

return clusters;
}
}

0 comments on commit 134c98b

Please sign in to comment.