Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/api/src/app/interfaces/ColorRGBA.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ColorRGBA {
R: number;
G: number;
B: number;
A: number;
}
34 changes: 34 additions & 0 deletions apps/api/src/app/utils/ColorUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ColorRGBA } from '../interfaces/ColorRGBA';

/**
* Convert Hex string to RGB array
* @param hex
Expand All @@ -11,3 +13,35 @@ export const hexToRgb = (hex: string): number[] => {
parseInt(result[3], 16)
] : [0, 0, 0];
}

/**
* Convert RGB array to Hex string
* @param r
* @param g
* @param b
* @returns Hex string
*/
export const rgbToHex = (r: number, g: number, b: number): string => {
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
}

/**
* Calculate the average color for a segment of the payload
* @param segment The segment of the payload
* @returns The average color
*/
export const calculateAverageColor = (segment: ColorRGBA[]): string => {
let r = 0, g = 0, b = 0;

for (const color of segment) {
r += color.R;
g += color.G;
b += color.B;
}

r = Math.round(r / segment.length);
g = Math.round(g / segment.length);
b = Math.round(b / segment.length);

return rgbToHex(r, g, b);
}
7 changes: 7 additions & 0 deletions apps/api/src/app/websocket/websocket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {WebsocketService} from './websocket.service';
import {RoomService} from '../room/room.service';
import {Room} from '../room/Room.model';
import {DeviceService} from '../device/device.service';
import { ColorRGBA } from '../interfaces/ColorRGBA';

@WebSocketGateway(undefined, {cors: true, pingInterval: 5000})
export class WebsocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
Expand Down Expand Up @@ -106,6 +107,12 @@ export class WebsocketGateway implements OnGatewayConnection, OnGatewayDisconnec
return {status: 200, message: 'Device removed'};
}

@SubscribeMessage(WebsocketMessage.IndividualLedControl)
async onIndividualLedControl(client: Socket, body: WebsocketRequest<ColorRGBA[]>): Promise<StandardResponse> {
await this.websocketService.individualLedControl(body.rooms, body.payload);
return {status: 200, message: 'Individual led control sent'};
}

/**
* When a client connects, log its IP address.
* Also set the server instance in the websocketService, so we make sure it is always up-to-date with the current server instance.
Expand Down
140 changes: 140 additions & 0 deletions apps/api/src/app/websocket/websocket.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { RoomService } from '../room/room.service';
import { DeviceService } from '../device/device.service';
import { WebsocketService } from './websocket.service';
import { Test, TestingModule } from '@nestjs/testing';
import { Server } from 'socket.io';
import { ColorRGBA } from '../interfaces/ColorRGBA';
import { WebsocketMessage } from '@angulon/interfaces';
import { Device } from '../device/Device.model';

describe('WebsocketService', () => {
let service: WebsocketService;
let deviceService: DeviceService;
let roomService: RoomService;
let server: Server;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
WebsocketService,
{
provide: DeviceService,
useValue: {
findAll: jest.fn(),
findOne: jest.fn(),
updateOne: jest.fn(),
create: jest.fn(),
},
},
{
provide: RoomService,
useValue: {
findAll: jest.fn(),
getRoomsState: jest.fn(),
updateRoomStateForRoomsSubject: { next: jest.fn() },
},
},
],
}).compile();

service = module.get<WebsocketService>(WebsocketService);
deviceService = module.get<DeviceService>(DeviceService);
roomService = module.get<RoomService>(RoomService);
server = new Server();
service['server'] = server;
});

describe('individualLedControl', () => {
it('should map smaller incoming payload size to the correct payload size for each led in the strip', async () => {
const rooms = ['room1'];
const payload: ColorRGBA[] = [
{ R: 255, G: 255, B: 255, A: 1 },
{ R: 0, G: 0, B: 0, A: 1 },
];

const devices: Device[] = [
{ name: 'device1', ledCount: 30, socketSessionId: 'session1' },
{ name: 'device2', ledCount: 60, socketSessionId: 'session2' },
] as Device[];

service['devicesInRoomCache'].set('room1', devices);

const sendEventToAllLedstripsInRoomSpy = jest.spyOn(service as any, 'sendEventToDevice');

await service.individualLedControl(rooms, payload);

expect(sendEventToAllLedstripsInRoomSpy).toHaveBeenCalledWith(
devices[0],
WebsocketMessage.LedstripIndividualControl,
expect.any(Array)
);

expect(sendEventToAllLedstripsInRoomSpy).toHaveBeenCalledWith(
devices[1],
WebsocketMessage.LedstripIndividualControl,
expect.any(Array)
);


// Check the first call has the desired payload sent to the device
const mappedPayload1 = sendEventToAllLedstripsInRoomSpy.mock.calls[0][2] as string[];
expect(mappedPayload1.length).toBe(30);
// Expect the first 15 elements to be #ffffff
expect(mappedPayload1.slice(0, 15)).toEqual(Array(15).fill('#ffffff'));
// Expect the last 15 elements to be #000000
expect(mappedPayload1.slice(15, 30)).toEqual(Array(15).fill('#000000'));


const mappedPayload2 = sendEventToAllLedstripsInRoomSpy.mock.calls[1][2] as string[];
expect(mappedPayload2.length).toBe(60);
// Expect the first 30 elements to be #ffffff
expect(mappedPayload2.slice(0, 30)).toEqual(Array(30).fill('#ffffff'));
// Expect the last 30 elements to be #000000
expect(mappedPayload2.slice(30, 60)).toEqual(Array(30).fill('#000000'));
});

it('should map larger incoming payload size to the correct payload size for each led in the strip', async () => {
const rooms = ['room1'];
// Create a payload of 100 elements, 50% of which are white and 50% black
const payload: ColorRGBA[] = Array(50).fill({ R: 255, G: 255, B: 255, A: 1 }).concat(Array(50).fill({ R: 0, G: 0, B: 0, A: 1 }));

const devices: Device[] = [
{ name: 'device1', ledCount: 30, socketSessionId: 'session1' },
{ name: 'device2', ledCount: 60, socketSessionId: 'session2' },
] as Device[];

service['devicesInRoomCache'].set('room1', devices);

const sendEventToAllLedstripsInRoomSpy = jest.spyOn(service as any, 'sendEventToDevice');

await service.individualLedControl(rooms, payload);

expect(sendEventToAllLedstripsInRoomSpy).toHaveBeenCalledWith(
devices[0],
WebsocketMessage.LedstripIndividualControl,
expect.any(Array)
);

expect(sendEventToAllLedstripsInRoomSpy).toHaveBeenCalledWith(
devices[1],
WebsocketMessage.LedstripIndividualControl,
expect.any(Array)
);

// Check the first call has the desired payload sent to the device
const mappedPayload1 = sendEventToAllLedstripsInRoomSpy.mock.calls[0][2] as string[];
expect(mappedPayload1.length).toBe(30);
// Expect the first 15 elements to be #ffffff
expect(mappedPayload1.slice(0, 15)).toEqual(Array(15).fill('#ffffff'));
// Expect the last 15 elements to be #000000
expect(mappedPayload1.slice(15, 30)).toEqual(Array(15).fill('#000000'));

const mappedPayload2 = sendEventToAllLedstripsInRoomSpy.mock.calls[1][2] as string[];
expect(mappedPayload2.length).toBe(60);
// Expect the first 30 elements to be #ffffff
expect(mappedPayload2.slice(0, 30)).toEqual(Array(30).fill('#ffffff'));
// Expect the last 30 elements to be #000000
expect(mappedPayload2.slice(30, 60)).toEqual(Array(30).fill('#000000'));
});
})
});
Loading