From e7c398bf93697e18763e7dc88c49c05123a07dde Mon Sep 17 00:00:00 2001 From: Jack Hsieh <97704544+chengweih001@users.noreply.github.com> Date: Fri, 16 Jun 2023 11:56:03 -0700 Subject: [PATCH] CO2 meter extension - a sample for using WebHID on extension service worker. (#921) * Initial README.md file * fix design doc link * Update README.md adding scheib * Update README.md * test * update README.md with alvinjiooo * add a ascii fish in README.md * Update README.md * skeloton extension * Add original co2meter.html * add buttons to settings page * Initial storage module stub. * Add storage module reference from settings page. * Fix settings JS to be module compatible. * Add stubs for settings storage writing * Call saveCO2Value() from onInputReport in co2meter.html * background page to read CO2 periodically based on the stored interval * verion 1.0 /module/co2_meter.js * Refactor settings.html and hook up CO2 driver * update v2 of /module/co2_meter.js which access all methods from CO2Meter class * Fix co2_meter import in background.js * background page to show CO2 meter disconnected icon * v2.1 update /module/co2_meter.js add support for virtual meter and connection listener also rquestPermission * v2.1 update /module/co2_meter.js * Import idb-keyval indexedDB library into storage.js, with trivial usage. * background page handle disconnect connect device * update v2.2 /module/co2_meter.js make non-static and add guarding flag for exccessive reading * update v2.3 /module/co2_meter.js await device to close before open device in * update v2.4 /module/co2_meter.js change to return reject promise instead of Error in * Add third-party/idb-keyval, use to set/get interval. * Simplify settings and storage. * handle background page device disconnect/connect * Rename *_script.js files to only *.js. * Add temp reading * Add chart to popup.html with chartjs etc. * Update extension tool tip to show connected/disconnected status. * use indexedDB to store CO2 reading * Renaming items in background.js Name methods more clearly for what they are doing. Collapse some code when possible to be inline. * add in Storage * add support for storing temperature and query temperature in range * background page boardcasting reading updated * Refactor chart out of popup into chart.html for iframing * Continued: chart out of popup into chart.html for iframing * Use message channel for message broadcasting and move common constant to constant.js * Work in progress for data into chart. * Use set for clients in background page * Add promise to storage constructor. * Change IDB version number due to schema change * Populate chart data. * check store name before creating when version changed * increase popup window and iframe size * Update chart upon new data. * Add calibration period and refactor inputReport * Use flexbox to layout pages. * move internal and temperature unit into setting store * move dbInitialized into transaction oncomplete event * remove reading out interval and temp unit log * Remove co2meter.html * Add .map files for third party code to remove devtools warnings. * Display chart in Fahrenheit * remove idb-keyvalue * prettier storage.js and add comments to public methods * Chart style: time axis respects time of data, no data points * Show dialog on the chart when device is disconnected * Add Example Data Button * Close dialog button * Implement toggling between Celsius / Fahrenheit * polish co2_meter.js and remove virtual device and change default interval to 30 sec * Change small icon to CO2 text * change reading interval default to 30 secs which helps the chart update faster for fresh load case * Fix lint error * Clean up console.log * Add chrome.alarm example * remove keep alive for reading alarm * refactor to keep connection open * add README.md * strip down * addressed comments * add link to README.md --------- Co-authored-by: Vincent Scheib Co-authored-by: Alvin Ji <111466895+alvinjiooo@users.noreply.github.com> Co-authored-by: Alvin Ji Co-authored-by: Alpaca Jam --- functional-samples/sample.co2meter/README.md | 29 ++++ .../sample.co2meter/background.js | 116 +++++++++++++ .../sample.co2meter/co2-state-iframe.html | 22 +++ .../sample.co2meter/co2-state-iframe.js | 94 ++++++++++ .../sample.co2meter/images/icon128.png | Bin 0 -> 17218 bytes .../sample.co2meter/images/icon32.png | Bin 0 -> 402 bytes .../sample.co2meter/images/icon32.psd | Bin 0 -> 45475 bytes .../images/icon32_disconnected.png | Bin 0 -> 482 bytes .../sample.co2meter/main-page.html | 19 ++ .../sample.co2meter/main-page.js | 24 +++ .../sample.co2meter/manifest.json | 21 +++ .../sample.co2meter/modules/co2_meter.js | 163 ++++++++++++++++++ .../sample.co2meter/modules/constant.js | 21 +++ .../sample.co2meter/modules/icon.js | 29 ++++ functional-samples/sample.co2meter/popup.html | 14 ++ functional-samples/sample.co2meter/popup.js | 21 +++ 16 files changed, 573 insertions(+) create mode 100644 functional-samples/sample.co2meter/README.md create mode 100644 functional-samples/sample.co2meter/background.js create mode 100644 functional-samples/sample.co2meter/co2-state-iframe.html create mode 100644 functional-samples/sample.co2meter/co2-state-iframe.js create mode 100644 functional-samples/sample.co2meter/images/icon128.png create mode 100644 functional-samples/sample.co2meter/images/icon32.png create mode 100644 functional-samples/sample.co2meter/images/icon32.psd create mode 100644 functional-samples/sample.co2meter/images/icon32_disconnected.png create mode 100644 functional-samples/sample.co2meter/main-page.html create mode 100644 functional-samples/sample.co2meter/main-page.js create mode 100644 functional-samples/sample.co2meter/manifest.json create mode 100644 functional-samples/sample.co2meter/modules/co2_meter.js create mode 100644 functional-samples/sample.co2meter/modules/constant.js create mode 100644 functional-samples/sample.co2meter/modules/icon.js create mode 100644 functional-samples/sample.co2meter/popup.html create mode 100644 functional-samples/sample.co2meter/popup.js diff --git a/functional-samples/sample.co2meter/README.md b/functional-samples/sample.co2meter/README.md new file mode 100644 index 00000000..a3f357b6 --- /dev/null +++ b/functional-samples/sample.co2meter/README.md @@ -0,0 +1,29 @@ +# **Sample CO₂ Meter Chrome Extension** + +The extension uses [WebHID](https://developer.chrome.com/en/articles/hid/) to access a device for measuring the CO₂ level and temperature in your surroundings. + +## **Testing the extension** + +1. Follow the instructions to load an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). +2. Connect the CO₂ meter (currently it only supports the [CO2Mini Indoor Air Quality Monitor](https://www.co2meter.com/products/co2mini-co2-indoor-air-quality-monitor) from CO2Meter.com). +3. Open the extension popup window and click “Settings” button to go to the settings page. +4. Click the “Grant CO2 meter permission” button and grant the permission to the CO₂ meter. + +Following the above steps, the device connection session to the CO₂ meter will be created when the extension is running. The input reports from the device will be processed and are visble from the popup window or settings page. + +## **Design** + +- [co2_meter.js](modules/co2_meter.js): A CO2 meter device driver layer that uses WebHID to communicate with the device. +- [co2-state-iframe.js](./co2-state-iframe.js): A module to be embedded in a regular page or popup window for showing the current CO2 meter status. It listens for events from the extension service worker, such as meter readings or availability, and renders the results. +- [popup.js](./popup.js): For the extension popup window. It includes [co2-state-iframe.js](./co2-state-iframe.js) and a link to open [main-page.js](./main-page.js). +- [main-page.js](./main-page.js): The settings page for opening a popup to grant permission to the device. It includes [co2-state-iframe.js](./co2-state-iframe.js) as well. +- [background.js](./background.js): The script that runs on the extension service worker. This is the central piece of this extension, and it will: + - Initialize the CO2 meter for starting to generate reading input reports using [co2_meter.js](modules/co2_meter.js). + - Broadcast events (e.g., CO2 readings, CO2 availability) to registered clients (e.g., the popup window). + +## **WebHID limitations in extension service workers** + +WebHID will be officially available to extension service workers in Chrome 115. Before M115, it can be enabled through the flag chrome://flags#enable-web-hid-on-extension-service-worker. However, there are limitations to the support for WebHID in extension service workers: + +- Before M115 with flag enabled, if the service worker is idle for longer than [30 seconds](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/service_worker/service_worker.mojom;l=150;drc=ff468ef351dc107e9bb92635914e3908d763cf29) it may be terminated, closing the device connection session. This limitation will be resolved in M115. +- Device connection events are not fired if the device is plugged or unplugged while the service worker is inactive. We have [crbug.com/1446487](http://crbug.com/1446487) to track the resolution of this limitation. If your extension encounters issues because of this limitation, please leave a comment in the bug about your use case and how the limitation affects your extension. diff --git a/functional-samples/sample.co2meter/background.js b/functional-samples/sample.co2meter/background.js new file mode 100644 index 00000000..6ec17a8a --- /dev/null +++ b/functional-samples/sample.co2meter/background.js @@ -0,0 +1,116 @@ +// Copyright 2023 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +import icon from './modules/icon.js'; +import CO2Meter from './modules/co2_meter.js'; +import { + PERMISSION_GRANTED_MESSAGE, + CO2_METER_UNAVAILABLE, + CO2_METER_AVAILABLE, + NO_CO2_METER_FOR_READING, + NEW_CO2_READING, + NEW_TEMP_READING, + READING_UNKNOWN +} from './modules/constant.js'; + +let clients = new Set(); + +let last_co2_reading = READING_UNKNOWN; +let last_temp_reading = READING_UNKNOWN; + +async function co2MeterConnected() { + broadcastMessage(CO2_METER_AVAILABLE); + icon.setConnected(); + startCO2Reading(); +} + +async function co2MeterDisconnected() { + CO2Meter.stopReading(); + broadcastMessage(CO2_METER_UNAVAILABLE); + icon.setDisconnected(); + last_co2_reading = READING_UNKNOWN; + last_temp_reading = READING_UNKNOWN; + await broadcastMessage(NEW_CO2_READING, last_co2_reading); + await broadcastMessage(NEW_TEMP_READING, last_temp_reading); +} + +async function broadcastMessage(type, data) { + for (const client of clients.values()) { + client.postMessage({ + type: type, + data: data + }); + } +} + +function onPermissionGranted() { + co2MeterConnected(); +} + +async function startCO2Reading() { + try { + await CO2Meter.startReading(); + } catch (e) { + console.log('Exception when startCO2Reading:', e); + if (e === NO_CO2_METER_FOR_READING) { + co2MeterDisconnected(); + } + } +} + +async function OnCO2Reading(co2_reading) { + last_co2_reading = co2_reading; + await broadcastMessage(NEW_CO2_READING, co2_reading); +} + +async function OnTempReading(temp_reading) { + last_temp_reading = temp_reading; + await broadcastMessage(NEW_TEMP_READING, temp_reading); +} + +async function initialize() { + chrome.runtime.onMessage.addListener((message) => { + if (message === PERMISSION_GRANTED_MESSAGE) { + onPermissionGranted(); + broadcastMessage(CO2_METER_AVAILABLE); + } + }); + + chrome.runtime.onConnect.addListener(async function (port) { + port.onDisconnect.addListener(function (port) { + clients.delete(port); + }); + clients.add(port); + await broadcastMessage(NEW_CO2_READING, last_co2_reading); + await broadcastMessage(NEW_TEMP_READING, last_temp_reading); + }); + + await CO2Meter.init( + co2MeterConnected, + co2MeterDisconnected, + OnCO2Reading, + OnTempReading + ); + startCO2Reading(); +} + +if (navigator.hid) { + initialize(); +} else { + console.error( + 'WebHID is not available! Use chrome://flags#enable-web-hid-on-extension-service-worker' + ); +} diff --git a/functional-samples/sample.co2meter/co2-state-iframe.html b/functional-samples/sample.co2meter/co2-state-iframe.html new file mode 100644 index 00000000..27a47630 --- /dev/null +++ b/functional-samples/sample.co2meter/co2-state-iframe.html @@ -0,0 +1,22 @@ + + + +
+
CO2 meter
+
unknown
+
CO2 reading (㏙)
+
unknown
+
Temperature reading (℉)
+
unknown
+
+ + + + +

+ Device is not detected, please make sure the CO2 meter is connected and the + permission is granted! +

+

Permission can be granted in the settings page.

+ +
diff --git a/functional-samples/sample.co2meter/co2-state-iframe.js b/functional-samples/sample.co2meter/co2-state-iframe.js new file mode 100644 index 00000000..eaa1be5c --- /dev/null +++ b/functional-samples/sample.co2meter/co2-state-iframe.js @@ -0,0 +1,94 @@ +// Copyright 2023 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + CO2_METER_UNAVAILABLE, + CO2_METER_AVAILABLE, + NEW_CO2_READING, + NEW_TEMP_READING, + READING_UNKNOWN +} from './modules/constant.js'; +import CO2Meter from './modules/co2_meter.js'; + +window.onload = async () => { + // Register for messages to update chart upon new data readings. + chrome.runtime.connect().onMessage.addListener((msg) => { + switch (msg.type) { + case NEW_CO2_READING: + updateCO2Reading(msg.data); + break; + case NEW_TEMP_READING: + updateTempReading(msg.data); + break; + case CO2_METER_AVAILABLE: + updateCO2MeterStatus(true); + break; + case CO2_METER_UNAVAILABLE: + updateCO2MeterStatus(false); + break; + } + }); + + // Dialog + document.getElementById('closeDialogButton').onclick = () => { + document.getElementById('noDeviceDialog').close(); + }; + + await CO2Meter.init(CO2MeterConnected, CO2MeterDisconnected); + const deviceStatus = await CO2Meter.getDeviceStatus(); + updateCO2MeterStatus(deviceStatus); +}; + +function updateCO2Reading(co2_reading) { + let co2_reading_element = document.getElementById('co2_reading'); + if (co2_reading === READING_UNKNOWN) { + co2_reading_element.textContent = 'unknown'; + } else { + co2_reading_element.textContent = `${co2_reading} \u33d9`; + } +} + +function updateTempReading(temp_reading) { + let temp_reading_element = document.getElementById('temp_reading'); + if (temp_reading === READING_UNKNOWN) { + temp_reading_element.textContent = 'unknown'; + } else { + const fahrenheit = CO2Meter.tempReadingToFahrenheit(temp_reading); + temp_reading_element.textContent = `${fahrenheit}\u2109`; + } +} + +function updateCO2MeterStatus(connected) { + let noDeviceDialog = document.getElementById('noDeviceDialog'); + let co2_meter_connected_status = document.getElementById( + 'co2_meter_connected_status' + ); + if (connected) { + noDeviceDialog.close(); + co2_meter_connected_status.textContent = 'connected'; + } else { + if (!noDeviceDialog.open) { + noDeviceDialog.showModal(); + } + co2_meter_connected_status.textContent = 'disconnected'; + } +} + +function CO2MeterConnected() { + updateCO2MeterStatus(true); +} + +function CO2MeterDisconnected() { + updateCO2MeterStatus(false); +} diff --git a/functional-samples/sample.co2meter/images/icon128.png b/functional-samples/sample.co2meter/images/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..029c5a957437ddfbef522ff8fac534269dd275e4 GIT binary patch literal 17218 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 literal 0 HcmV?d00001 diff --git a/functional-samples/sample.co2meter/images/icon32.png b/functional-samples/sample.co2meter/images/icon32.png new file mode 100644 index 0000000000000000000000000000000000000000..901e07dcb22353f0dc3007e4d2659e02e192976d GIT binary patch literal 402 zcmV;D0d4+?P)x#7r=>c4EO+J-2ICy_JZ{VM)56l+BBK$3Y~qO)<5Wh{}O* z#5xY-6$ER^0002pNkl=d?4`2nh!X#KWsP wA)3GJ>)iJ#%zXYZbN!WHVXqJN8TbNw12t|Bqti^Um;e9(07*qoM6N<$f-LT{j{pDw literal 0 HcmV?d00001 diff --git a/functional-samples/sample.co2meter/images/icon32.psd b/functional-samples/sample.co2meter/images/icon32.psd new file mode 100644 index 0000000000000000000000000000000000000000..62d7455c0bd7c612f3e8694f6c37f16ba6e898a5 GIT binary patch literal 45475 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 literal 0 HcmV?d00001 diff --git a/functional-samples/sample.co2meter/images/icon32_disconnected.png b/functional-samples/sample.co2meter/images/icon32_disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..8e2ecf2c27111ed3ba5e612f6feb4ebac97058d8 GIT binary patch literal 482 zcmV<80UiE{P)&zt zRzC&?K?w;!2M1d$EY#7_+1A$D*x1XDm|W;;9A)z#9?%~>icMGg)_3kv`SV9)>n z0Ru@yK~#90-I2*s#4rp*rPy(keVu`@4a*Gt{~sdDF2_?vaf1W8M9XuxBNP3j;KGox zAvZ-~ucxJh21{;lDxcO(HgAEhZv$YRF9x78yE-sf#Y&TWym_MQ%%FBy=8J~E5X%;NXs Y3pRKUs2hABDF6Tf07*qoM6N<$f(kIkdjJ3c literal 0 HcmV?d00001 diff --git a/functional-samples/sample.co2meter/main-page.html b/functional-samples/sample.co2meter/main-page.html new file mode 100644 index 00000000..7e7d4cfd --- /dev/null +++ b/functional-samples/sample.co2meter/main-page.html @@ -0,0 +1,19 @@ + + + CO2 Meter + + + + +
+ +
+ + + diff --git a/functional-samples/sample.co2meter/main-page.js b/functional-samples/sample.co2meter/main-page.js new file mode 100644 index 00000000..c982b2cf --- /dev/null +++ b/functional-samples/sample.co2meter/main-page.js @@ -0,0 +1,24 @@ +// Copyright 2023 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import CO2Meter from './modules/co2_meter.js'; + +window.onload = async () => { + // Permission + // Popup window can't open a permission prompt so we have to use a page instead. + // This issue is being tracked by crbug.com/1349183. + document.getElementById('grantPermissionButton').onclick = () => { + CO2Meter.requestPermission(); + }; +}; diff --git a/functional-samples/sample.co2meter/manifest.json b/functional-samples/sample.co2meter/manifest.json new file mode 100644 index 00000000..f7878e4a --- /dev/null +++ b/functional-samples/sample.co2meter/manifest.json @@ -0,0 +1,21 @@ +{ + "name": "CO2 meter extension", + "description": "An extension that connects to a CO2 meter using WebHID extension service workers", + "version": "1.0", + "manifest_version": 3, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "action": { + "default_icon": { + "32": "images/icon32.png" + }, + "default_title": "Show CO2 meter status", + "default_popup": "popup.html" + }, + "icons": { + "32": "images/icon32.png", + "128": "images/icon128.png" + } +} diff --git a/functional-samples/sample.co2meter/modules/co2_meter.js b/functional-samples/sample.co2meter/modules/co2_meter.js new file mode 100644 index 00000000..b30f5292 --- /dev/null +++ b/functional-samples/sample.co2meter/modules/co2_meter.js @@ -0,0 +1,163 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @filename co2_meter.js + * + * @description CO2Meter provides methods for accessing status and data of a + * CO2 meter. When creating a CO2Meter, it has to await `init()` to finish + * before quering device status. + */ + +import { PERMISSION_GRANTED_MESSAGE } from './constant.js'; + +const key = new Uint8Array([0xc4, 0xc6, 0xc0, 0x92, 0x40, 0x23, 0xdc, 0x96]); + +function KelvinToFahrenheit(k) { + return Math.trunc(((k - 273.15) * 9) / 5 + 32); +} + +class CO2Meter { + constructor() { + this.device = null; + this.connectClientCB = null; + this.disconnectClientCB = null; + this.co2ReadingClientCB = null; + this.tempReadingClientCB = null; + this.connectHandler = this.connectHandler.bind(this); + this.disconnectHandler = this.disconnectHandler.bind(this); + this.onInputReport = this.onInputReport.bind(this); + } + + /** + * @description This function initializes the CO2Meter object. + */ + async init( + connectCallback = null, + disconnectCallback = null, + co2ReadingCallback = null, + tempReadingCallback = null + ) { + this.connectClientCB = connectCallback; + this.disconnectClientCB = disconnectCallback; + this.co2ReadingClientCB = co2ReadingCallback; + this.tempReadingClientCB = tempReadingCallback; + navigator.hid.addEventListener('connect', this.connectHandler); + navigator.hid.addEventListener('disconnect', this.disconnectHandler); + console.log('CO2Meter init() done'); + } + + async startReading() { + if (this.device) { + console.log('CO2 reading has already started!'); + return; + } + const devices = await navigator.hid.getDevices(); + if (devices.length == 0) { + throw 'No CO2 meter for reading!'; + } + this.device = devices[0]; + + try { + await this.device.open(); + await this.device.sendFeatureReport(0, key); + } catch (e) { + console.log('CO2 reading exception:', e); + await this.device.close(); + this.device = null; + throw 'Fail to open CO2 meter for reading!'; + } + + this.device.addEventListener('inputreport', this.onInputReport); + } + + async stopReading() { + if (this.device) { + this.device.removeEventListener('inputreport', this.onInputReport); + await this.device.close(); + this.device = null; + } + } + + onInputReport(report) { + let data = new Uint8Array( + report.data.buffer, + report.data.byteOffset, + report.data.byteLength + ); + + const op = data[0]; + let val = (data[1] << 8) | data[2]; + + if (op == 0x50) { + console.log(`Current CO2 reading is ${val}`); + if (this.co2ReadingClientCB) { + this.co2ReadingClientCB(val); + } + } else if (op == 0x42) { + val = val / 16; + console.log(`Current Temp reading is ${val}`); + if (this.tempReadingClientCB) { + this.tempReadingClientCB(val); + } + } + } + + /** + * @description Request user to grant permission for using CO2 meter. + * The extension currently only support this model: + * https://www.co2meter.com/products/co2mini-co2-indoor-air-quality-monitor + */ + async requestPermission() { + const devices = await navigator.hid.requestDevice({ + filters: [{ vendorId: 1241, productId: 41042 }] + }); + console.log('CO2 meter permission granted!', devices[0]); + chrome.runtime.sendMessage(PERMISSION_GRANTED_MESSAGE); + } + + connectHandler() { + if (this.connectClientCB && typeof this.connectClientCB === 'function') { + this.connectClientCB(); + } + } + + disconnectHandler() { + if (this.device) { + this.device.close(); + } + this.device = null; + if ( + this.disconnectClientCB && + typeof this.disconnectClientCB === 'function' + ) { + this.disconnectClientCB(); + } + } + + /** + * @description Get Device connected status. + * @return {Boolean} + */ + async getDeviceStatus() { + const devices = await navigator.hid.getDevices(); + return devices.length > 0; + } + + tempReadingToFahrenheit(temp_reading) { + return KelvinToFahrenheit(temp_reading); + } +} + +export default new CO2Meter(); diff --git a/functional-samples/sample.co2meter/modules/constant.js b/functional-samples/sample.co2meter/modules/constant.js new file mode 100644 index 00000000..14a50e46 --- /dev/null +++ b/functional-samples/sample.co2meter/modules/constant.js @@ -0,0 +1,21 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const PERMISSION_GRANTED_MESSAGE = 'permission granted'; +export const CO2_METER_UNAVAILABLE = 'co2 meter unavailable'; +export const CO2_METER_AVAILABLE = 'co2 meter available'; +export const NO_CO2_METER_FOR_READING = 'No CO2 meter for reading!'; +export const NEW_CO2_READING = 'new co2 reading'; +export const NEW_TEMP_READING = 'new temperature reading'; +export const READING_UNKNOWN = 'reading unknown'; diff --git a/functional-samples/sample.co2meter/modules/icon.js b/functional-samples/sample.co2meter/modules/icon.js new file mode 100644 index 00000000..4791a85d --- /dev/null +++ b/functional-samples/sample.co2meter/modules/icon.js @@ -0,0 +1,29 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +class Icon { + constructor() {} + + setConnected() { + chrome.action.setTitle({ title: 'Connected' }); + chrome.action.setIcon({ path: { 32: 'images/icon32.png' } }); + } + + setDisconnected() { + chrome.action.setTitle({ title: 'Disconnected' }); + chrome.action.setIcon({ path: { 32: 'images/icon32_disconnected.png' } }); + } +} + +export default new Icon(); diff --git a/functional-samples/sample.co2meter/popup.html b/functional-samples/sample.co2meter/popup.html new file mode 100644 index 00000000..f08339ef --- /dev/null +++ b/functional-samples/sample.co2meter/popup.html @@ -0,0 +1,14 @@ + + + + +
+ +
+ + + diff --git a/functional-samples/sample.co2meter/popup.js b/functional-samples/sample.co2meter/popup.js new file mode 100644 index 00000000..4c75f7b5 --- /dev/null +++ b/functional-samples/sample.co2meter/popup.js @@ -0,0 +1,21 @@ +// Copyright 2023 Google LLC + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// https://www.apache.org/licenses/LICENSE-2.0 + +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +window.onload = async () => { + document.getElementById('mainPageButton').onclick = () => { + window.open('main-page.html', '_blank'); + }; +};