Skip to content

Commit

Permalink
samples/smarthome/local: add support for query intent
Browse files Browse the repository at this point in the history
- add OPC SYSEX message to retrieve pixel colors
- add query handlers for HTTP, TCP, and UDP protocol
- remove commandOnly from cloud SYNC handler
- update QUERY fallback to always return device as online
- update README

Bug: 173669544
Change-Id: Id32d5d667748bc2de845be616cee3a16dc123460
  • Loading branch information
proppy committed Nov 2, 2021
1 parent aabce06 commit 0a827ca
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 11 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ or deploy it to a publicly reacheable URL endpoint.
- In _Develop > Actions > Configure local home SDK_
- Set the *testing URL for Chrome* to the one displayed in the local development server logs.
- Set the *testing URL for Node* to the one displayed in the local development server logs.
- Under _Add capabilities_
- Check *Support local query*.
- Click *Save*

### Deploy to Firebase Hosting
Expand All @@ -190,9 +192,11 @@ npm run deploy --prefix app/ -- --project ${FIREBASE_PROJECT_ID}
```

- Go to the smart home project in the [Actions console](https://console.actions.google.com/)
- In _Develop > Actions > On device testing_ set the development URLs to
- **Chrome**: `http://${FIREBASE_PROJECT_ID}.firebaseapp.com/web/index.html`
- **Node**: `http://${FIREBASE_PROJECT_ID}.firebaseapp.com/node/bundle.js`
- In _Develop > Actions > Configure local home SDK_
- Set the *testing URL for Chrome* to: `http://${FIREBASE_PROJECT_ID}.firebaseapp.com/web/index.html`
- Set the *testing URL for Node* to: `http://${FIREBASE_PROJECT_ID}.firebaseapp.com/node/bundle.js`
- Under _Add capabilities_
- Check *Support local query*.
- Click *Save*

## Test the local execution app
Expand Down
21 changes: 21 additions & 0 deletions app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,25 @@ export class HomeApp {
console.log('EXECUTE response:', executeResponse);
return executeResponse;
}

public queryHandler = (queryRequest: smarthome.IntentFlow.QueryRequest): Promise<smarthome.IntentFlow.QueryResponse> => {
console.log('QUERY request:', queryRequest);
const queryResponse = (() => {
// Infer execution protocol from the first device custom data.
const device = queryRequest.inputs[0].payload.devices[0];
const customData = device.customData as ICustomData;
switch (customData.control_protocol) {
case 'UDP':
return this.appExecutionUdp.queryHandler(queryRequest);
case 'TCP':
return this.appExecutionTcp.queryHandler(queryRequest);
case 'HTTP':
return this.appExecutionHttp.queryHandler(queryRequest);
default:
throw new Error(`Unsupported protocol for QUERY intent requestId ${queryRequest.requestId}: ${customData.control_protocol}`);
}
})();
console.log('QUERY response:', queryResponse);
return queryResponse;
}
}
51 changes: 51 additions & 0 deletions app/execution_http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,55 @@ export class HomeApp {
// Return execution response to smarthome infrastructure.
return executeResponse.build();
}

async queryHandler(queryRequest: smarthome.IntentFlow.QueryRequest): Promise<smarthome.IntentFlow.QueryResponse> {
// TODO(proppy): handle multiple devices.
const device = queryRequest.inputs[0].payload.devices[0];
const customData = device.customData as ICustomData;
const stream = opcStream();
stream.writeMessage(customData.channel,
0xff, // SYSEX
Buffer.from([
0x00, 0x03, // System IDs
0x00, 0x01 // get-pixel-color
]));
const opcMessage = stream.read();
const getPixelColorCommand = new smarthome.DataFlow.HttpRequestData();
getPixelColorCommand.requestId = queryRequest.requestId;
getPixelColorCommand.deviceId = device.id;
getPixelColorCommand.port = customData.port;
getPixelColorCommand.method = smarthome.Constants.HttpOperation.GET;
getPixelColorCommand.path = `/${customData.channel}`
console.debug('HTTP getPixelColorCommand:', getPixelColorCommand);
const getPixelColorResponse = await this.app.getDeviceManager().send(getPixelColorCommand) as smarthome.DataFlow.HttpResponseData;
console.debug('HTTP getPixelColorResponse:', getPixelColorResponse);
if (getPixelColorResponse.httpResponse.statusCode !== 200) {
throw new Error(`Unsupported protocol for OPC get-pixel-color: HTTP`);
}
const opcPayload = Buffer.from(getPixelColorResponse.httpResponse.body as string, 'base64');
console.debug('HTTP opcPayload:', opcPayload);
const opcChannel = opcPayload.readUInt8(0);
const opcCommand = opcPayload.readUInt8(1); // SYSEX
const opcDataSize = opcPayload.readUInt16BE(2);
const opcData = opcPayload.slice(4);
if (opcDataSize !== opcData.length) {
throw new Error(`Unexpected message size: expected: ${opcDataSize} got: ${opcData.length}`);
}
const strand = opcStrand(opcData);
const pixel = strand.getPixel(0); // get first pixel of the strand.
const rgb = pixel[0] << 16 | pixel[1] << 8 | pixel[2];
return {
requestId: queryRequest.requestId,
payload: {
devices: {
[device.id]: {
online: true,
color: {
spectrumRgb: rgb
}
}
}
}
};
}
}
61 changes: 61 additions & 0 deletions app/execution_tcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,65 @@ export class HomeApp {
// Return execution response to smarthome infrastructure.
return executeResponse.build();
}

async queryHandler(queryRequest: smarthome.IntentFlow.QueryRequest): Promise<smarthome.IntentFlow.QueryResponse> {
// TODO(proppy): handle multiple devices.
const device = queryRequest.inputs[0].payload.devices[0];
const customData = device.customData as ICustomData;
const stream = opcStream();
stream.writeMessage(customData.channel,
0xff, // SYSEX
Buffer.from([
0x00, 0x03, // System IDs
0x00, 0x01 // get-pixel-color
]));
const opcMessage = stream.read();
const getPixelColorCommand = new smarthome.DataFlow.TcpRequestData();
getPixelColorCommand.requestId = queryRequest.requestId;
getPixelColorCommand.deviceId = device.id;
getPixelColorCommand.port = customData.port;
getPixelColorCommand.operation = smarthome.Constants.TcpOperation.WRITE;
getPixelColorCommand.data = opcMessage.toString('hex');
console.debug('TCP getPixelColorCommand:', getPixelColorCommand);
const getPixelColorResponse = await this.app.getDeviceManager().send(getPixelColorCommand);
console.debug('TCP getPixelColorResponse:', getPixelColorResponse);

const readHeaderCommand = new smarthome.DataFlow.TcpRequestData();
readHeaderCommand.operation = smarthome.Constants.TcpOperation.READ;
readHeaderCommand.requestId = queryRequest.requestId;
readHeaderCommand.deviceId = device.id;
readHeaderCommand.port = customData.port;
const opcHeaderSize = 4;
readHeaderCommand.bytesToRead = opcHeaderSize + customData.leds * 3;
console.debug('TCP readHeaderCommand:', readHeaderCommand);
const readHeaderResponse = await this.app.getDeviceManager().send(readHeaderCommand) as smarthome.DataFlow.TcpResponseData;
console.debug('TCP readHeaderResponse:', readHeaderResponse);
const opcPayload = Buffer.from(readHeaderResponse.tcpResponse.data, 'hex');
console.debug('TCP opcPayload:', opcPayload);
const opcChannel = opcPayload.readUInt8(0);
const opcCommand = opcPayload.readUInt8(1); // SYSEX
const opcDataSize = opcPayload.readUInt16BE(2);
console.debug('TCP opcDataSize:', opcDataSize);
const opcData = opcPayload.slice(4);
console.debug('TCP opcData:', opcData);
if (opcDataSize !== opcData.length) {
throw new Error(`Unexpected message size: expected: ${opcDataSize} got: ${opcData.length}`);
}
const strand = opcStrand(opcData);
const pixel = strand.getPixel(0); // get first pixel of the strand.
const rgb = pixel[0] << 16 | pixel[1] << 8 | pixel[2];
return {
requestId: queryRequest.requestId,
payload: {
devices: {
[device.id]: {
online: true,
color: {
spectrumRgb: rgb
}
}
}
}
};
}
}
48 changes: 48 additions & 0 deletions app/execution_udp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,52 @@ export class HomeApp {
// Return execution response to smarthome infrastructure.
return executeResponse.build();
}

async queryHandler(queryRequest: smarthome.IntentFlow.QueryRequest): Promise<smarthome.IntentFlow.QueryResponse> {
// TODO(proppy): handle multiple devices.
const device = queryRequest.inputs[0].payload.devices[0];
const customData = device.customData as ICustomData;
const stream = opcStream();
stream.writeMessage(customData.channel,
0xff, // SYSEX
Buffer.from([
0x00, 0x03, // System IDs
0x00, 0x01 // get-pixel-color
]));
const opcMessage = stream.read();
const getPixelColorCommand = new smarthome.DataFlow.UdpRequestData();
getPixelColorCommand.requestId = queryRequest.requestId;
getPixelColorCommand.deviceId = device.id;
getPixelColorCommand.port = customData.port;
getPixelColorCommand.data = opcMessage.toString('hex');
getPixelColorCommand.expectedResponsePackets = 1;
console.debug('UDP getPixelColorCommand:', getPixelColorCommand);
const getPixelColorResponse = await this.app.getDeviceManager().send(getPixelColorCommand) as smarthome.DataFlow.UdpResponseData;
console.debug('UDP getPixelColorResponse:', getPixelColorResponse);
const opcPayload = Buffer.from(getPixelColorResponse.udpResponse.responsePackets![0], 'hex');
console.debug('UDP opcPayload:', opcPayload);
const opcChannel = opcPayload.readUInt8(0);
const opcCommand = opcPayload.readUInt8(1); // SYSEX
const opcDataSize = opcPayload.readUInt16BE(2);
const opcData = opcPayload.slice(4);
if (opcDataSize !== opcData.length) {
throw new Error(`Unexpected message size: expected: ${opcDataSize} got: ${opcData.length}`);
}
const strand = opcStrand(opcData);
const pixel = strand.getPixel(0); // get first pixel of the strand.
const rgb = pixel[0] << 16 | pixel[1] << 8 | pixel[2];
return {
requestId: queryRequest.requestId,
payload: {
devices: {
[device.id]: {
online: true,
color: {
spectrumRgb: rgb
}
}
}
}
};
}
}
1 change: 1 addition & 0 deletions app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ smarthomeApp
.onIdentify(homeApp.identifyHandler)
.onReachableDevices(homeApp.reachableDevicesHandler)
.onExecute(homeApp.executeHandler)
.onQuery(homeApp.queryHandler)
.listen()
.then(() => {
console.log('Ready');
Expand Down
16 changes: 16 additions & 0 deletions device/execution_http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ export function start(port: number, opcHandler: opcDevice.Handler) {
.set('Content-Type', 'text/plain')
.send('OK');
});
server.get('/:channel', (req, res) => {
console.debug(`HTTP: received ${req.method} request.`);
const message: opcDevice.IMessage = {
channel: parseInt(req.params.channel, 10),
command: 0xff, // SYSEX
data: Buffer.from([
0x00, 0x03, // System IDs
0x00, 0x01// get-pixel-color
]),
};
const response = opcHandler.handle(message);
res.status(200)
.set('Content-Type', 'application/octet-stream')
.send(response!.toString('base64'));
console.debug(`HTTP: sent response:`, response);
});

server.listen(port, () => {
console.log(`HTTP control listening on port ${port}`);
Expand Down
6 changes: 5 additions & 1 deletion device/execution_tcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ import * as opcDevice from './opc_device';
export function start(port: number, opcHandler: opcDevice.Handler) {
const server = net.createServer((conn) => {
conn.pipe(opcParser()).on('data', (message: opcDevice.IMessage) => {
opcHandler.handle(message);
const response = opcHandler.handle(message);
if (response !== undefined) {
conn.write(response);
console.debug(`TCP: sent response:`, response);
}
});
});
server.listen(port, () => {
Expand Down
10 changes: 10 additions & 0 deletions device/execution_udp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ export function start(port: number, opcHandler: opcDevice.Handler) {
console.debug(`UDP: from ${rinfo.address} got`, msg);
Readable.from(msg).pipe(opcParser()).on('data', (message: opcDevice.IMessage) => {
opcHandler.handle(message);
const response = opcHandler.handle(message);
if (response !== undefined) {
server.send(response, rinfo.port, rinfo.address, (error) => {
if (error !== null) {
console.error('UDP failed to send OPC command response:', error);
return;
}
console.debug(`UDP: sent response to ${rinfo.address}:${rinfo.port}:`, response);
});
}
});
});
server.on('listening', () => {
Expand Down
16 changes: 15 additions & 1 deletion device/opc_device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export class Handler {
handle(message: IMessage): Buffer|undefined {
console.debug('received command:', message.command, message.data);
switch (message.command) {
case 0: // set-pixel-color
case 0: { // set-pixel-color
// TODO(proppy): implement channel 0 broadcast
if (!this.strands.has(message.channel)) {
console.warn('unknown OPC channel:', message.command);
Expand All @@ -64,6 +64,20 @@ export class Handler {
process.stdout.write('\n');
}
break;
}
case 0xff: { // SYSEX
if (message.data[0] === 0x00 && // System IDs[0]
message.data[1] === 0x03 && // System IDs[1]
message.data[2] === 0x00 && // get-pixel-color[0]
message.data[3] === 0x01) { // get-pixel-color[1]
const stream = opcStream();
stream.writeMessage(message.channel,
0xff, // SYSEX
this.strands.get(message.channel)!.buffer);
return stream.read();
}
break;
}
default:
console.warn('Unsupported OPC command:', message.command);
break;
Expand Down
10 changes: 4 additions & 6 deletions functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,7 @@ app.onSync((body, headers) => {
},
willReportState: false,
attributes: {
colorModel: 'rgb',
commandOnlyColorSetting: true,
colorModel: 'rgb'
},
customData: {
channel: device.channel,
Expand All @@ -81,15 +80,14 @@ app.onSync((body, headers) => {
});
app.onQuery((body, headers) => {
functions.logger.log('Cloud Fulfillment received QUERY');
// Command-only devices do not support state queries
// Always show the devices as online.
return {
requestId: body.requestId,
payload: {
devices: devices.reduce((result, device) => {
result[device.id] = {
status: 'ERROR',
errorCode: 'notSupported',
debugString: `${device.id} is command only`,
status: 'SUCCESS',
online: true
};
return result;
}, {}),
Expand Down

0 comments on commit 0a827ca

Please sign in to comment.