├── web-ui ├── .gitignore ├── .dockerignore ├── src │ ├── assets │ │ ├── logo.png │ │ └── mobility-profile.png │ ├── store │ │ ├── index.js │ │ └── auth-module.js │ ├── main.js │ ├── components │ │ ├── SortIndicator.vue │ │ ├── AccordianComponent.vue │ │ ├── Modal.vue │ │ ├── OnOffSwitch.vue │ │ ├── PageHeader.vue │ │ ├── JsonFileUpload.vue │ │ ├── PaginationControl.vue │ │ └── Authentication.vue │ ├── behaviors │ │ ├── EstimatedTagPopulation.vue │ │ ├── OptionalFeature.vue │ │ ├── OptionalTextInput.vue │ │ ├── OptionalNumberInput.vue │ │ ├── PowerSweeping.vue │ │ ├── TagMemoryReads.vue │ │ ├── RfModeSelect.vue │ │ ├── ChannelFreqs.vue │ │ ├── Triggers.vue │ │ ├── AntennaConfig.vue │ │ ├── Behavior.vue │ │ └── Filtering.vue │ ├── App.vue │ ├── router.js │ ├── tags │ │ └── TagFilterInput.vue │ ├── sensor │ │ └── control │ │ │ ├── Sensor.vue │ │ │ └── Sensors.vue │ └── utils.js ├── jsconfig.json ├── vite.config.js ├── Dockerfile ├── nginx │ ├── nginx.conf │ └── nginx-https.conf ├── package.json └── index.html ├── controller ├── .gitignore ├── .dockerignore ├── config │ ├── events │ │ └── event_config.json │ ├── mqtt │ │ └── mqtt_config.json │ ├── behaviors │ │ ├── Default_Single_Port.json │ │ ├── Default_Single_Port_TID.json │ │ ├── FastScan_Single_Port.json │ │ ├── DeepScan_Single_Port.json │ │ ├── FastScan_Dual_Port.json │ │ ├── Example_Store_Two_Port.json │ │ ├── Example_FIlter_Single_Port_TID.json │ │ └── Example_Store_Four_Port.json │ └── scripts │ │ └── r700setup.sh ├── src │ ├── sensors │ │ ├── run-state.js │ │ ├── personality.js │ │ ├── impinj │ │ │ ├── hostname.js │ │ │ ├── error-response.js │ │ │ ├── system-image.js │ │ │ ├── system-info.js │ │ │ ├── status.js │ │ │ ├── ntp.js │ │ │ ├── mqtt.js │ │ │ ├── region.js │ │ │ ├── event.js │ │ │ ├── rf-mode.js │ │ │ ├── preset.js │ │ │ └── rest-cmd-service.js │ │ ├── antenna-ports.js │ │ ├── router.js │ │ ├── db-model.js │ │ ├── controller.js │ │ └── service.js │ ├── auth │ │ ├── router.js │ │ └── controller.js │ ├── tags │ │ ├── state.js │ │ ├── location.js │ │ ├── router.js │ │ ├── db-tag-stats-model.js │ │ ├── db-tag-model.js │ │ ├── service.js │ │ └── controller.js │ ├── events │ │ ├── router.js │ │ ├── inventory.js │ │ ├── controller.js │ │ └── config.js │ ├── mqtt │ │ ├── router.js │ │ ├── controller.js │ │ ├── config.js │ │ └── service.js │ ├── behaviors │ │ ├── router.js │ │ ├── behavior.js │ │ └── controller.js │ ├── firmware │ │ ├── router.js │ │ └── controller.js │ ├── logger.js │ ├── persist │ │ └── db.js │ ├── app.js │ └── server.js ├── Dockerfile └── package.json ├── images ├── login.png ├── tags.png ├── events.png ├── behaviors.png ├── main-menu.png ├── firmware-upload.png ├── rssi-adjustment.png ├── sensor-control.png ├── sensor-firmware.png ├── behaviors-upload.png ├── config-new-sensor.png ├── tag-state-machine.png ├── architecture-system.png ├── behaviors-create-new.png ├── behaviors-port-config.png ├── sensor-config-add-new.png ├── sensor-control-running.png ├── firmware-upgrade-confirm.png ├── firmware-upgrade-select.png ├── sensor-config-port-info.png ├── architecture-rfid-controller.png ├── firmware-upgrade-sending-request.png ├── firmware-upgrade-installing-bundle.png └── firmware-upgrade-success-rebooting.png ├── docker-compose-https.yml ├── gen-cert.sh ├── LICENSE ├── .env.template ├── .gitignore ├── docker-compose.yml └── run.sh /web-ui/.gitignore: -------------------------------------------------------------------------------- 1 | run/ 2 | -------------------------------------------------------------------------------- /controller/.gitignore: -------------------------------------------------------------------------------- 1 | run/ 2 | -------------------------------------------------------------------------------- /controller/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | run 4 | -------------------------------------------------------------------------------- /web-ui/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | **/node_modules 3 | **/run 4 | **/dist 5 | -------------------------------------------------------------------------------- /images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/login.png -------------------------------------------------------------------------------- /images/tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/tags.png -------------------------------------------------------------------------------- /images/events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/events.png -------------------------------------------------------------------------------- /images/behaviors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/behaviors.png -------------------------------------------------------------------------------- /images/main-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/main-menu.png -------------------------------------------------------------------------------- /images/firmware-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/firmware-upload.png -------------------------------------------------------------------------------- /images/rssi-adjustment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/rssi-adjustment.png -------------------------------------------------------------------------------- /images/sensor-control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/sensor-control.png -------------------------------------------------------------------------------- /images/sensor-firmware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/sensor-firmware.png -------------------------------------------------------------------------------- /web-ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/web-ui/src/assets/logo.png -------------------------------------------------------------------------------- /images/behaviors-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/behaviors-upload.png -------------------------------------------------------------------------------- /images/config-new-sensor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/config-new-sensor.png -------------------------------------------------------------------------------- /images/tag-state-machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/tag-state-machine.png -------------------------------------------------------------------------------- /images/architecture-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/architecture-system.png -------------------------------------------------------------------------------- /images/behaviors-create-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/behaviors-create-new.png -------------------------------------------------------------------------------- /images/behaviors-port-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/behaviors-port-config.png -------------------------------------------------------------------------------- /images/sensor-config-add-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/sensor-config-add-new.png -------------------------------------------------------------------------------- /images/sensor-control-running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/sensor-control-running.png -------------------------------------------------------------------------------- /images/firmware-upgrade-confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/firmware-upgrade-confirm.png -------------------------------------------------------------------------------- /images/firmware-upgrade-select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/firmware-upgrade-select.png -------------------------------------------------------------------------------- /images/sensor-config-port-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/sensor-config-port-info.png -------------------------------------------------------------------------------- /web-ui/src/assets/mobility-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/web-ui/src/assets/mobility-profile.png -------------------------------------------------------------------------------- /images/architecture-rfid-controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/architecture-rfid-controller.png -------------------------------------------------------------------------------- /images/firmware-upgrade-sending-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/firmware-upgrade-sending-request.png -------------------------------------------------------------------------------- /images/firmware-upgrade-installing-bundle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/firmware-upgrade-installing-bundle.png -------------------------------------------------------------------------------- /images/firmware-upgrade-success-rebooting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intel/applications-iot-rfid-sensor-controller/main/images/firmware-upgrade-success-rebooting.png -------------------------------------------------------------------------------- /web-ui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { "@/*": ["./src/*"] } 5 | }, 6 | "include": [ "./src/**/*" ], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /controller/config/events/event_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exitTimeout": 30000, 3 | "posReturnHoldoff": 8640000, 4 | "mobilityProfile": { 5 | "holdoff": 0, 6 | "slope": -0.8, 7 | "threshold": 600 8 | } 9 | } -------------------------------------------------------------------------------- /controller/src/sensors/run-state.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | module.exports = Object.freeze({ 7 | INACTIVE: 'inactive', 8 | ACTIVE: 'active', 9 | }); 10 | -------------------------------------------------------------------------------- /controller/src/sensors/personality.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | module.exports = Object.freeze({ 7 | NONE: 'none', 8 | EXIT: 'exit', 9 | POS: 'pos', 10 | }); 11 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/hostname.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | function getDefault(hostname) { 7 | return { 8 | hostname: hostname 9 | }; 10 | } 11 | 12 | module.exports = { 13 | getDefault 14 | }; 15 | -------------------------------------------------------------------------------- /web-ui/src/store/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | import {createStore} from 'vuex' 7 | import auth from './auth-module' 8 | 9 | export default createStore({ 10 | modules: { 11 | auth 12 | } 13 | }) 14 | 15 | -------------------------------------------------------------------------------- /controller/src/auth/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const router = require('express').Router(); 7 | const controller = require('./controller'); 8 | 9 | router.post('/login', controller.logIn); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /controller/src/tags/state.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | module.exports = Object.freeze({ 7 | UNKNOWN: 'unknown', 8 | PRESENT: 'present', 9 | EXITING: 'exiting', 10 | DEPARTED_EXIT: 'departed_exit', 11 | DEPARTED_POS: 'departed_pos' 12 | }); 13 | -------------------------------------------------------------------------------- /web-ui/src/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | import { createApp } from 'vue' 7 | import App from './App.vue' 8 | import store from './store' 9 | import router from './router' 10 | 11 | createApp(App) 12 | .use(store) 13 | .use(router) 14 | .mount("#app"); 15 | 16 | -------------------------------------------------------------------------------- /controller/src/events/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const router = require('express').Router(); 7 | const controller = require('./controller'); 8 | 9 | router.get('/', controller.get); 10 | router.post('/', controller.upsert); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/error-response.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | function getDefault(message) { 7 | return { 8 | message: message, 9 | invalidPropertyId: '', 10 | detail: '' 11 | }; 12 | } 13 | 14 | module.exports = { 15 | getDefault 16 | }; 17 | -------------------------------------------------------------------------------- /controller/src/tags/location.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | function getDefault(deviceId, antennaPort, antennaName) { 7 | return { 8 | deviceId: deviceId, 9 | antennaPort: antennaPort, 10 | antennaName: antennaName, 11 | }; 12 | } 13 | 14 | module.exports = { 15 | getDefault 16 | }; 17 | -------------------------------------------------------------------------------- /controller/src/sensors/antenna-ports.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | function getDefault() { 7 | return [ 8 | { 9 | antennaPort: 1, 10 | antennaName: '', 11 | facilityId: '', 12 | personality: '', 13 | } 14 | ]; 15 | } 16 | 17 | module.exports = { 18 | getDefault 19 | }; 20 | -------------------------------------------------------------------------------- /controller/src/mqtt/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const router = require('express').Router(); 7 | const controller = require('./controller'); 8 | 9 | router.get('/', controller.getConfig); 10 | router.post('/', controller.postConfig); 11 | router.delete('/', controller.deleteConfig); 12 | 13 | module.exports = router; 14 | -------------------------------------------------------------------------------- /web-ui/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import path from 'path' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | resolve: { 9 | alias: { 10 | '@': path.resolve(__dirname, './src'), 11 | } 12 | }, 13 | server: { 14 | port: 8080, 15 | strictPort: true, 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/system-image.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | function getDefault() { 7 | return { 8 | primaryFirmware: '', 9 | secondaryFirmware: '', 10 | scmRevision: '', 11 | buildDate: '', 12 | buildPlan: '', 13 | devBuild: true 14 | }; 15 | } 16 | 17 | module.exports = { 18 | getDefault 19 | }; 20 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/system-info.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | function getDefault() { 7 | return { 8 | manufacturer: '', 9 | productModel: '', 10 | productDescription: '', 11 | productSku: '', 12 | productHla: '', 13 | pcba: '', 14 | serialNumber: '' 15 | }; 16 | } 17 | 18 | module.exports = { 19 | getDefault 20 | }; 21 | -------------------------------------------------------------------------------- /controller/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Intel Corporation 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | ## see https://hub.docker.com/_/node 5 | FROM node:19-alpine AS base 6 | ENV NODE_ENV=production 7 | WORKDIR /controller 8 | COPY ./package*.json ./ 9 | RUN npm ci && npm cache clean --force 10 | 11 | FROM base AS prod 12 | ENV NODE_ENV=production 13 | WORKDIR /controller 14 | COPY ./src/ ./ 15 | ENTRYPOINT ["node", "./server.js"] 16 | -------------------------------------------------------------------------------- /controller/src/behaviors/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const router = require('express').Router(); 7 | const controller = require('./controller'); 8 | 9 | router.get('/', controller.getAll); 10 | router.get('/:behaviorId', controller.getOne); 11 | router.post('/:behaviorId', controller.upsertOne); 12 | router.delete('/:behaviorId', controller.deleteOne); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /web-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2022 Intel Corporation 3 | # SPDX-License-Identifier: BSD-3-Clause 4 | # 5 | FROM node:19-alpine AS base 6 | WORKDIR /web-ui 7 | COPY package*.json ./ 8 | RUN npm ci && npm cache clean --force 9 | 10 | FROM base AS builder 11 | WORKDIR /web-ui 12 | COPY . . 13 | RUN npm run build 14 | 15 | FROM nginx:stable-alpine as prod 16 | # Remove default nginx static assets 17 | RUN rm -rf /usr/share/nginx/html/* 18 | COPY --from=builder /web-ui/dist /usr/share/nginx/html/ 19 | 20 | -------------------------------------------------------------------------------- /web-ui/src/components/SortIndicator.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 26 | -------------------------------------------------------------------------------- /controller/src/tags/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const controller = require('./controller'); 7 | const router = require('express').Router(); 8 | 9 | router.get('/', controller.getAll); 10 | router.post('/', controller.create); 11 | router.delete('/', controller.deleteBulk); 12 | 13 | router.get('/:epc', controller.getOne); 14 | router.delete('/:epc', controller.deleteOne); 15 | router.get('/:epc/sensors', controller.getTagStats); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /docker-compose-https.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | # 3 | # Copyright (C) 2022 Intel Corporation 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | # 6 | services: 7 | 8 | controller: 9 | volumes: 10 | - rfid-certs:/etc/ssl 11 | environment: 12 | CERT_FILE: /etc/ssl/controller.rfid.com.crt 13 | KEY_FILE: /etc/ssl/controller.rfid.com.key 14 | 15 | web-ui: 16 | volumes: 17 | - rfid-certs:/etc/ssl 18 | - ./web-ui/nginx/nginx-https.conf:/etc/nginx/nginx.conf 19 | 20 | volumes: 21 | rfid-certs: 22 | external: true 23 | 24 | -------------------------------------------------------------------------------- /controller/src/firmware/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const router = require('express').Router(); 7 | const controller = require('./controller'); 8 | 9 | router.get('/images', controller.getImages); 10 | router.post('/images', controller.postImage); 11 | router.delete('/images', controller.deleteImage); 12 | router.get('/sensors/info', controller.getSensorsInfo); 13 | router.get('/sensors/upgrade', controller.getSensorsUpgrade); 14 | router.post('/sensors/upgrade', controller.postSensorsUpgrade); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/status.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Values = Object.freeze({ 7 | UNKNOWN: 'unknown', 8 | IDLE: 'idle', 9 | RUNNING: 'running' 10 | }); 11 | 12 | function getDefault() { 13 | return { 14 | status: Values.UNKNOWN, 15 | time: '', 16 | serialNumber: '', 17 | mqttBrokerConnectionStatus: '', 18 | mqttTlsAuthentication: '', 19 | kafkaClusterConnectionStatus: '', 20 | activePreset: { 21 | id: null, 22 | profile: '' 23 | } 24 | }; 25 | } 26 | 27 | module.exports = { 28 | Values, 29 | getDefault 30 | }; 31 | -------------------------------------------------------------------------------- /controller/src/sensors/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const controller = require('./controller'); 7 | const router = require('express').Router(); 8 | 9 | router.get('/', controller.getAll); 10 | router.post('/', controller.upsertBulk); 11 | router.get('/runstate', controller.getRunState); 12 | router.put('/runstate', controller.putRunState); 13 | router.put('/reboot', controller.rebootAll); 14 | 15 | router.get('/:deviceId', controller.getOne); 16 | router.post('/:deviceId', controller.upsertOne); 17 | router.delete('/:deviceId', controller.deleteOne); 18 | router.put('/:deviceId/reboot', controller.rebootOne); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /controller/config/mqtt/mqtt_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventsTopic": "rfid/events", 3 | "alertsTopic": "rfid/alerts", 4 | "sensorConfig": { 5 | "active": true, 6 | "brokerHostname": "192.168.1.49", 7 | "brokerPort": 1883, 8 | "cleanSession": true, 9 | "clientId": "device_hostname", 10 | "eventBufferSize": 1024, 11 | "eventPerSecondLimit": 1000, 12 | "eventPendingDeliveryLimit": 100, 13 | "eventQualityOfService": 0, 14 | "eventTopic": "rfid/data", 15 | "keepAliveIntervalSeconds": 0, 16 | "username": "", 17 | "password": "", 18 | "willTopic": "rfid/will", 19 | "willMessage": "offline", 20 | "willQualityOfService": 0 21 | } 22 | } -------------------------------------------------------------------------------- /controller/src/sensors/impinj/ntp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Active = { 7 | active: true 8 | }; 9 | 10 | const NtpServer = { 11 | serverId: 1, 12 | server: 'pool.ntp.org', 13 | serverType: 'static' 14 | }; 15 | 16 | const NtpServerString = { 17 | server: 'pool.ntp.org' 18 | }; 19 | 20 | function getDefaultServer() { 21 | return Object.assign({}, NtpServerString); 22 | } 23 | 24 | function getDefault(serverId) { 25 | const ntpServer = Object.assign({}, NtpServer); 26 | ntpServer.serverId = serverId; 27 | return ntpServer; 28 | } 29 | 30 | module.exports = { 31 | Active, 32 | NtpServer, 33 | NtpServerString, 34 | getDefaultServer, 35 | getDefault 36 | }; 37 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/mqtt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | function getDefault() { 7 | // MqttConfiguration has no nested objects so use "Object.assign" 8 | return { 9 | active: true, 10 | brokerHostname: '', 11 | brokerPort: 1883, 12 | cleanSession: true, 13 | clientId: '', 14 | eventBufferSize: 1024, 15 | eventPerSecondLimit: 1000, 16 | eventPendingDeliveryLimit: 100, 17 | eventQualityOfService: 0, 18 | eventTopic: '', 19 | keepAliveIntervalSeconds: 0, 20 | username: '', 21 | password: '', 22 | willTopic: '', 23 | willMessage: '', 24 | willQualityOfService: 0 25 | }; 26 | } 27 | 28 | module.exports = { 29 | getDefault 30 | }; 31 | -------------------------------------------------------------------------------- /gen-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (C) 2022 Intel Corporation 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | # 6 | 7 | name="${1:-server.com}" 8 | key_file=${name}.key 9 | crt_file=${name}.crt 10 | 11 | if [ -f ${key_file} ]; then 12 | echo "${key_file} exists already, not creating new certs" 13 | return 14 | fi 15 | 16 | if [ -f ${crt_file} ]; then 17 | echo "${crt_file} exists already, not creating new certs" 18 | return 19 | fi 20 | 21 | echo "creating ${name} certs in ${PWD}" 22 | 23 | openssl req -x509 \ 24 | -nodes \ 25 | -days 365 \ 26 | -subj "/C=US/ST=AZ/O=RFID Controller Sample/CN=${name}" \ 27 | -addext "subjectAltName=DNS:${name},DNS:localhost,IP:127.0.0.1" \ 28 | -newkey rsa:2048 \ 29 | -keyout ${key_file} \ 30 | -out ${crt_file} 31 | 32 | -------------------------------------------------------------------------------- /web-ui/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | #tcp_nopush on; 25 | 26 | keepalive_timeout 65; 27 | 28 | #gzip on; 29 | 30 | include /etc/nginx/conf.d/*.conf; 31 | } 32 | -------------------------------------------------------------------------------- /controller/src/events/inventory.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Type = Object.freeze({ 7 | UNKNOWN: 'unknown', 8 | ARRIVAL: 'arrival', 9 | DEPARTED: 'departed', 10 | MOVED: 'moved', 11 | CYCLE_COUNT: 'cycle_count' 12 | }); 13 | 14 | function getEvent(sentOn, deviceId) { 15 | return { 16 | sentOn: sentOn, 17 | deviceId: deviceId, 18 | items: [], 19 | }; 20 | } 21 | 22 | function getEventItem(facilityId, 23 | epc, 24 | tid, 25 | type, 26 | timestamp, 27 | location) { 28 | return { 29 | facilityId: facilityId, 30 | epc: epc, 31 | tid: tid, 32 | type: type, 33 | timestamp: timestamp, 34 | location: location 35 | }; 36 | } 37 | 38 | module.exports = { 39 | Type, 40 | getEvent, 41 | getEventItem 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /controller/src/tags/db-tag-stats-model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | module.exports = (sequelize, Sequelize) => { 7 | 8 | return sequelize.define('tagStats', { 9 | epc: { 10 | type: Sequelize.STRING, 11 | allowNull: false, 12 | primaryKey: true 13 | }, 14 | deviceId: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | primaryKey: true 18 | }, 19 | antennaPort: { 20 | type: Sequelize.INTEGER, 21 | allowNull: false, 22 | primaryKey: true 23 | }, 24 | lastRead: { 25 | type: Sequelize.STRING, 26 | }, 27 | meanRssi: { 28 | type: Sequelize.FLOAT, 29 | }, 30 | interval: { 31 | type: Sequelize.FLOAT, 32 | }, 33 | numReads: { 34 | type: Sequelize.INTEGER, 35 | } 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /controller/src/events/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const config = require('./config'); 7 | const logger = require('../logger')('event-controller'); 8 | 9 | async function get(req, res) { 10 | try { 11 | return res.status(200).json(config.getConfig()); 12 | } catch (err) { 13 | logger.error(`get : ${err.toString()}`); 14 | return res.status(500).json({message: err.message}); 15 | } 16 | } 17 | 18 | async function upsert(req, res) { 19 | try { 20 | if (!config.validateConfig(req.body)) { 21 | return res.status(400).json({message: 'invalid configuration'}); 22 | } 23 | config.setConfig(req.body); 24 | return res.status(200).json(req.body); 25 | } catch (err) { 26 | logger.error(`upsert : ${err.toString()}`); 27 | return res.status(500).json({message: err.message}); 28 | } 29 | } 30 | 31 | module.exports = { 32 | get, 33 | upsert 34 | }; 35 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/region.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | module.exports = Object.freeze({ 7 | AUS: 'Australia 920-926 MHz', 8 | BGD: 'Bangladesh 925-927 MHz', 9 | BRA: 'Brazil 902-907 and 915-928 MHz', 10 | CHN: 'China 920-925 MHz', 11 | FCC: 'FCC Part 15.247', 12 | HKG: 'Hong Kong 920-925 MHz', 13 | IDN: 'Indonesia 920-923 MHz', 14 | PRK: 'Korea 917-921 MHz', 15 | LATAM: 'Latin America 902-928 MHz', 16 | MYS: 'Malaysia 919-923 MHz', 17 | NZL: 'New Zealand 921.5-928 MHz', 18 | PRY: 'Paraguay 918-928 MHz', 19 | PER: 'Peru 916-928 MHz', 20 | PHL: 'Philippines 918-920 MHz', 21 | SGP: 'Singapore 920-925 MHz', 22 | ZAF: 'South Africa 915-919 MHz', 23 | TWN: 'Taiwan 922-928 MHz', 24 | THA: 'Thailand 920-925 MHz', 25 | URY: 'Uruguay 916-928 MHz', 26 | VNM1: 'Vietnam 918-923 MHz', 27 | VNM2: 'Vietnam 920-923 MHz', 28 | EULB: 'EU 865-868 MHz', 29 | EUHB: 'EU 915-921 MHz' 30 | }); 31 | 32 | -------------------------------------------------------------------------------- /controller/src/tags/db-tag-model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const TagState = require('./state'); 7 | 8 | module.exports = (sequelize, Sequelize) => { 9 | 10 | return sequelize.define('tag', { 11 | epc: { 12 | type: Sequelize.STRING, 13 | allowNull: false, 14 | primaryKey: true 15 | }, 16 | tid: { 17 | type: Sequelize.STRING 18 | }, 19 | state: { 20 | type: Sequelize.ENUM( 21 | TagState.PRESENT, 22 | TagState.EXITING, 23 | TagState.DEPARTED_EXIT, 24 | TagState.DEPARTED_POS), 25 | defaultValue: TagState.PRESENT 26 | }, 27 | location: { 28 | type: Sequelize.JSON, 29 | }, 30 | facilityId: { 31 | type: Sequelize.STRING, 32 | }, 33 | lastRead: { 34 | type: Sequelize.STRING, 35 | }, 36 | lastArrived: { 37 | type: Sequelize.STRING, 38 | }, 39 | lastMoved: { 40 | type: Sequelize.STRING, 41 | }, 42 | lastDeparted: { 43 | type: Sequelize.STRING, 44 | } 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/EstimatedTagPopulation.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 26 | 27 | 46 | -------------------------------------------------------------------------------- /web-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfid-sensor-web-ui", 3 | "version": "0.0.0", 4 | "author": "Timothy Shockley, John Belstner", 5 | "license": "BSD-3-Clause", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint ./src --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path .gitignore" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.1.3", 13 | "core-js": "^3.25.5", 14 | "lodash": "^4.17.21", 15 | "vue": "^3.2.41", 16 | "vue-router": "^4.1.5", 17 | "vuex": "^4.1.0" 18 | }, 19 | "devDependencies": { 20 | "@rushstack/eslint-patch": "^1.1.4", 21 | "@vitejs/plugin-vue": "^3.1.2", 22 | "@vue/eslint-config-prettier": "^7.0.0", 23 | "eslint": "^8.26.0", 24 | "eslint-plugin-vue": "^9.6.0", 25 | "prettier": "^2.7.1", 26 | "vite": "^3.1.8" 27 | }, 28 | "eslintConfig": { 29 | "root": true, 30 | "env": { 31 | "browser": true, 32 | "node": true 33 | }, 34 | "extends": [ 35 | "eslint:recommended", 36 | "plugin:vue/vue3-essential", 37 | "prettier" 38 | ], 39 | "rules": {} 40 | }, 41 | "browserslist": [ 42 | "> 1%", 43 | "last 2 versions", 44 | "not dead" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /controller/src/mqtt/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const mqttService = require('./service'); 7 | const logger = require('../logger')('mqtt-controller'); 8 | 9 | async function getConfig(req, res) { 10 | try { 11 | return res.status(200).json(mqttService.getConfig()); 12 | } catch (err) { 13 | logger.error(`getting config : ${err.toString()}`); 14 | return res.status(500).json({message: err.message}); 15 | } 16 | } 17 | 18 | async function postConfig(req, res) { 19 | try { 20 | const cfg = await mqttService.setConfig(req.body); 21 | return res.status(200).json(cfg); 22 | } catch (err) { 23 | logger.error(`posting config : ${err.toString()}`); 24 | return res.status(400).json({message: err.message}); 25 | } 26 | } 27 | 28 | async function deleteConfig(req, res) { 29 | try { 30 | const cfg = mqttService.deleteConfig(); 31 | return res.status(200).json(cfg); 32 | } catch (err) { 33 | logger.error(`deleting config : ${err.toString()}`); 34 | return res.status(500).json({message: err.message}); 35 | } 36 | } 37 | 38 | module.exports = { 39 | getConfig, 40 | postConfig, 41 | deleteConfig 42 | }; 43 | -------------------------------------------------------------------------------- /controller/src/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | const winston = require('winston'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const {combine, timestamp, printf} = winston.format; 9 | 10 | const format = printf(msg => { 11 | return `${msg.timestamp} ${msg.level}: ${msg.category}: ${msg.message}`; 12 | }); 13 | 14 | const logger = winston.createLogger({ 15 | level: process.env.LOG_LEVEL || 'info', 16 | format: combine(timestamp(), format), 17 | transports: [ 18 | new winston.transports.Console(), 19 | ], 20 | }); 21 | 22 | if (process.env.LOG_FILE) { 23 | const baseDir = path.dirname(process.env.LOG_FILE); 24 | if (!fs.existsSync(baseDir)) { 25 | try { 26 | fs.mkdirSync(baseDir, {recursive: true}); 27 | logger.info(`logger: created ${baseDir}`); 28 | } catch (err) { 29 | // eslint-disable-next-line no-console 30 | console.error(err); 31 | } 32 | } 33 | logger.add(new winston.transports.File({filename: `${process.env.LOG_FILE}`})); 34 | } 35 | 36 | module.exports = function(category) { 37 | // set the default category of the child 38 | return logger.child({category: category}); 39 | }; 40 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/OptionalFeature.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 27 | 28 | 57 | -------------------------------------------------------------------------------- /web-ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 42 | 43 | 63 | -------------------------------------------------------------------------------- /controller/config/behaviors/Default_Single_Port.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "Default_Single_Port", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 1800, 32 | "rfMode": 100, 33 | "inventorySession": 1, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024 36 | } 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /web-ui/src/components/AccordianComponent.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 47 | 48 | 58 | -------------------------------------------------------------------------------- /controller/config/behaviors/Default_Single_Port_TID.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "Default_Single_Port_TID", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 2700, 32 | "rfMode": 100, 33 | "inventorySession": 1, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024 36 | } 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /web-ui/src/store/auth-module.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | import axios from "axios"; 7 | 8 | export default { 9 | namespaced: true, 10 | state: () => ({ 11 | loggedIn: false, 12 | }), 13 | actions: { 14 | login({ commit }, password) { 15 | return axios 16 | .post("/auth/login", { password: password }) 17 | .then( 18 | rsp => { 19 | if (rsp.data) { 20 | axios.defaults.headers.common['Authorization'] = `Bearer ${rsp.data.token}`; 21 | commit('loginSuccess'); 22 | return Promise.resolve(rsp.data); 23 | } else { 24 | throw new Error("missing login response data") 25 | } 26 | }, 27 | err => { 28 | axios.defaults.headers.common['Authorization'] = ''; 29 | commit('loginFailure'); 30 | return Promise.reject(err); 31 | } 32 | ); 33 | }, 34 | logout({ commit }) { 35 | axios.defaults.headers.common['Authorization'] = ''; 36 | commit('logout'); 37 | }, 38 | }, 39 | mutations: { 40 | loginSuccess(state) { 41 | state.loggedIn = true; 42 | }, 43 | loginFailure(state) { 44 | state.loggedIn = false; 45 | }, 46 | logout(state) { 47 | state.loggedIn = false; 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /web-ui/src/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | import { createRouter, createWebHistory } from "vue-router"; 7 | import Behaviors from "@/behaviors/Behaviors.vue"; 8 | import SensorControl from "@/sensor/control/Sensors.vue"; 9 | import SensorConfig from "@/sensor/config/Sensors.vue"; 10 | import SensorFirmware from "@/sensor/firmware/Firmware.vue"; 11 | import Tags from "@/tags/Tags.vue"; 12 | import Events from "@/events/Events.vue" 13 | 14 | const router = createRouter({ 15 | history: createWebHistory(), 16 | routes: [ 17 | { 18 | path: '/', 19 | redirect: '/behaviors' 20 | }, 21 | { 22 | path: "/behaviors", 23 | name: "Behaviors", 24 | component: Behaviors 25 | }, 26 | { 27 | path: "/sensor/control", 28 | name: "Sensor Control", 29 | component: SensorControl 30 | }, 31 | { 32 | path: "/sensor/config", 33 | name: "Sensor Config", 34 | component: SensorConfig 35 | }, 36 | { 37 | path: "/sensor/firmware", 38 | name: "Sensor Firmware", 39 | component: SensorFirmware 40 | }, 41 | { 42 | path: "/tags", 43 | name: "Tags", 44 | component: Tags 45 | }, 46 | { 47 | path: "/events", 48 | name: "Events", 49 | component: Events 50 | }, 51 | ] 52 | }); 53 | 54 | export default router; 55 | -------------------------------------------------------------------------------- /controller/config/behaviors/FastScan_Single_Port.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "FastScan_Single_Port", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 1500, 32 | "rfMode": 1111, 33 | "inventorySession": 0, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024, 36 | "fastId": "enabled" 37 | } 38 | ] 39 | } 40 | } -------------------------------------------------------------------------------- /controller/src/tags/service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Tag = require('./tag'); 7 | const Sensor = require('../sensors/sensor'); 8 | const mqttService = require('../mqtt/service'); 9 | const logger = require('../logger')('tag-service'); 10 | 11 | mqttService.subscribeData(handleDataMsg); 12 | 13 | async function handleDataMsg(msg) { 14 | const jsonMsg = JSON.parse(msg); 15 | if (jsonMsg.tagInventoryEvent) { 16 | await Tag.onTagInventoryEvent(jsonMsg.hostname, jsonMsg.tagInventoryEvent); 17 | } 18 | if (jsonMsg.inventoryStatusEvent) { 19 | await Sensor.onInventoryStatusEvent(jsonMsg.hostname, jsonMsg.inventoryStatusEvent); 20 | } 21 | } 22 | 23 | let checkDepartedInterval = null; 24 | 25 | async function start() { 26 | try { 27 | await Tag.updateCache(); 28 | checkDepartedInterval = setInterval(Tag.checkForDepartedExit, 1000); 29 | logger.info('started'); 30 | } catch (err) { 31 | err.message = `tag-service start unsuccessful ${err.message}`; 32 | throw err; 33 | } 34 | } 35 | 36 | async function stop() { 37 | try { 38 | clearInterval(checkDepartedInterval); 39 | logger.info('stopped'); 40 | await Tag.persistCache(); 41 | } catch (err) { 42 | err.message = `tag-service stop unsuccessful ${err.message}`; 43 | throw err; 44 | } 45 | } 46 | 47 | module.exports = { 48 | start, 49 | stop 50 | }; 51 | 52 | -------------------------------------------------------------------------------- /web-ui/src/tags/TagFilterInput.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 24 | 25 | 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Intel 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions in binary form must reproduce the above copyright notice, 7 | this list of conditions and the following disclaimer in the documentation and/or 8 | other materials provided with the distribution. 9 | 10 | 2. Neither the name of the copyright holder nor the names of its contributors may 11 | be used to endorse or promote products derived from this software without specific 12 | prior written permission. 13 | 14 | 3. Redistributions of source code must retain the above copyright notice, 15 | this list of conditions and the following disclaimer. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 19 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 25 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /controller/config/behaviors/DeepScan_Single_Port.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "DeepScan_Single_Port", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 2700, 32 | "rfMode": 1110, 33 | "inventorySession": 2, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024, 36 | "powerSweeping": { 37 | "minimumPowerCdbm": 1500, 38 | "stepSizeCdb": 300 39 | } 40 | } 41 | ] 42 | } 43 | } -------------------------------------------------------------------------------- /controller/src/sensors/impinj/event.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const InventoryStatusValues = Object.freeze({ 7 | IDLE: 'idle', 8 | RUNNING: 'running', 9 | ARMED: 'armed' 10 | }); 11 | 12 | const TagInventoryEvent = { 13 | epcHex: '', 14 | tidHex: '', 15 | antennaPort: 0, 16 | antennaName: '', 17 | peakRssiCdbm: 0, 18 | frequency: 0, 19 | transmitPowerCdbm: 0, 20 | lastSeenTime: '', 21 | phaseAngle: 0.0 22 | }; 23 | 24 | const InventoryStatusEvent = { 25 | inventoryStatus: InventoryStatusValues.IDLE 26 | }; 27 | 28 | const CommonEvent = { 29 | timestamp: '', 30 | hostname: '' 31 | }; 32 | 33 | function getCommonEvent(hostname) { 34 | const date = new Date(); 35 | const event = Object.assign({}, CommonEvent); 36 | event.hostname = hostname; 37 | event.timestamp = date.toISOString(); 38 | return event; 39 | } 40 | 41 | function getInventoryStatusEvent(hostname, status) { 42 | const event = getCommonEvent(hostname); 43 | const inventoryStatusEvent = Object.assign({}, InventoryStatusEvent); 44 | inventoryStatusEvent.inventoryStatus = status; 45 | event.inventoryStatusEvent = inventoryStatusEvent; 46 | return event; 47 | } 48 | 49 | function getTagInventoryEvent(hostname) { 50 | const event = getCommonEvent(hostname); 51 | event.tagInventoryEvent = Object.assign({}, TagInventoryEvent); 52 | return event; 53 | } 54 | 55 | module.exports = { 56 | CommonEvent, 57 | InventoryStatusValues, 58 | getInventoryStatusEvent, 59 | getTagInventoryEvent 60 | }; 61 | -------------------------------------------------------------------------------- /web-ui/src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 41 | 42 | 62 | -------------------------------------------------------------------------------- /controller/src/mqtt/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Topic = Object.freeze({ 7 | alerts: 'rfid/alerts', 8 | events: 'rfid/events', 9 | data: 'rfid/data', 10 | will: 'rfid/will', 11 | }); 12 | 13 | function getUpstreamHost() { 14 | return process.env.MQTT_UPSTREAM_HOST || 'localhost'; 15 | } 16 | 17 | function getUpstreamPort() { 18 | return process.env.MQTT_UPSTREAM_PORT || 1883; 19 | } 20 | 21 | function getUpstreamUsername() { 22 | return process.env.MQTT_UPSTREAM_USERNAME || ''; 23 | } 24 | 25 | function getUpstreamPassword() { 26 | return process.env.MQTT_UPSTREAM_PASSWORD || ''; 27 | } 28 | 29 | function getUpstreamUrl() { 30 | const host = getUpstreamHost(); 31 | const port = getUpstreamPort(); 32 | return `mqtt://${host}:${port}`; 33 | } 34 | 35 | function getDownstreamHost() { 36 | return process.env.MQTT_DOWNSTREAM_HOST; 37 | } 38 | 39 | function getDownstreamPort() { 40 | return process.env.MQTT_DOWNSTREAM_PORT || 1883; 41 | } 42 | 43 | function getDownstreamUsername() { 44 | return process.env.MQTT_DOWNSTREAM_USERNAME || ''; 45 | } 46 | 47 | function getDownstreamPassword() { 48 | return process.env.MQTT_DOWNSTREAM_PASSWORD || ''; 49 | } 50 | 51 | function getDownstreamUrl() { 52 | const host = getDownstreamHost(); 53 | const port = getDownstreamPort(); 54 | return `mqtt://${host}:${port}`; 55 | } 56 | 57 | module.exports = { 58 | Topic, 59 | getUpstreamHost, 60 | getUpstreamPort, 61 | getUpstreamUsername, 62 | getUpstreamPassword, 63 | getUpstreamUrl, 64 | getDownstreamHost, 65 | getDownstreamPort, 66 | getDownstreamUsername, 67 | getDownstreamPassword, 68 | getDownstreamUrl, 69 | }; 70 | -------------------------------------------------------------------------------- /controller/src/persist/db.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const {Sequelize, Op} = require('sequelize'); 7 | const logger = require('../logger')('db-client'); 8 | 9 | const sequelize = new Sequelize( 10 | process.env.POSTGRES_DB || 'postgres', 11 | process.env.POSTGRES_USER || 'postgres', 12 | process.env.POSTGRES_PASSWORD, 13 | { 14 | logging: false, 15 | host: process.env.POSTGRES_HOST || 'localhost', 16 | dialect: 'postgres', 17 | define: { 18 | timestamps: false 19 | }, 20 | }); 21 | 22 | 23 | const db = {}; 24 | 25 | db.Sequelize = Sequelize; 26 | db.sequelize = sequelize; 27 | db.Op = Op; 28 | 29 | db.tags = require('../tags/db-tag-model')(sequelize, Sequelize); 30 | db.tagStats = require('../tags/db-tag-stats-model')(sequelize, Sequelize); 31 | db.sensors = require('../sensors/db-model')(sequelize, Sequelize); 32 | 33 | db.start = async function () { 34 | try { 35 | logger.info(`connecting ${sequelize.config.host}:${sequelize.config.port} ` + 36 | `user:${sequelize.config.username} database:${sequelize.config.database}`); 37 | await sequelize.authenticate(); 38 | logger.info('synchronizing'); 39 | await sequelize.sync({force: false}); 40 | logger.info(`started ${sequelize.config.host}`); 41 | } catch (err) { 42 | err.message = `db: start unsuccessful ${err.message}`; 43 | throw err; 44 | } 45 | }; 46 | 47 | db.stop = async function () { 48 | try { 49 | await sequelize.close(); 50 | logger.info(`stopped ${sequelize.config.host}`); 51 | } catch (err) { 52 | err.message = `db: stop unsuccessful ${err.message}`; 53 | throw err; 54 | } 55 | }; 56 | 57 | module.exports = db; 58 | -------------------------------------------------------------------------------- /controller/src/behaviors/behavior.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const fs = require('fs'); 7 | const logger = require('../logger')('behavior'); 8 | 9 | const baseDir = process.env.DIR_CONFIG || './run/config'; 10 | const cfgDir = `${baseDir}/behaviors`; 11 | if (!fs.existsSync(cfgDir)) { 12 | fs.mkdirSync(cfgDir, {recursive: true}); 13 | logger.info(`created ${cfgDir}`); 14 | } 15 | 16 | function getAll() { 17 | const behaviors = []; 18 | try { 19 | const files = fs.readdirSync(cfgDir); 20 | files.forEach(file => { 21 | const behavior = JSON.parse(fs.readFileSync(`${cfgDir}/${file}`, 'utf8')); 22 | behaviors.push(behavior);}); 23 | } catch (err) { 24 | logger.error(`getting all : ${err.toString()}`); 25 | } 26 | return behaviors; 27 | } 28 | 29 | function getOne(behaviorId) { 30 | let behavior = null; 31 | try { 32 | const filenameWithPath = `${cfgDir}/${behaviorId}.json`; 33 | const json = fs.readFileSync(filenameWithPath, 'utf8'); 34 | behavior = JSON.parse(json); 35 | } catch (err) { 36 | logger.error(`getting one : ${err.toString()}`); 37 | } 38 | return behavior; 39 | } 40 | 41 | function upsertOne(behavior) { 42 | const filenameWithPath = `${cfgDir}/${behavior.id}.json`; 43 | fs.writeFileSync(filenameWithPath, JSON.stringify(behavior, null, 4), 'utf8'); 44 | logger.info(`upserted ${filenameWithPath}`); 45 | } 46 | 47 | function deleteOne(behaviorId) { 48 | const filenameWithPath = `${cfgDir}/${behaviorId}.json`; 49 | fs.unlinkSync(filenameWithPath); 50 | logger.info(`deleted ${filenameWithPath}`); 51 | } 52 | 53 | module.exports = { 54 | getAll, 55 | getOne, 56 | upsertOne, 57 | deleteOne 58 | }; 59 | -------------------------------------------------------------------------------- /controller/src/sensors/db-model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Status = require('./impinj/status'); 7 | 8 | module.exports = (sequelize, Sequelize) => { 9 | 10 | const SensorModel = sequelize.define('sensor', { 11 | deviceId: { 12 | type: Sequelize.STRING, 13 | allowNull: false, 14 | primaryKey: true 15 | }, 16 | ip4Address: { 17 | type: Sequelize.STRING, 18 | allowNull: false, 19 | defaultValue: 'unknown' 20 | }, 21 | behaviorId: { 22 | type: Sequelize.STRING 23 | }, 24 | status: { 25 | type: Sequelize.ENUM( 26 | Status.Values.UNKNOWN, 27 | Status.Values.IDLE, 28 | Status.Values.RUNNING), 29 | defaultValue: Status.Values.UNKNOWN 30 | }, 31 | connected: { 32 | type: Sequelize.BOOLEAN, 33 | defaultValue: false 34 | }, 35 | antennaPorts: { 36 | type: Sequelize.JSON 37 | } 38 | }); 39 | 40 | SensorModel.prototype.getAntennaName = function (port) { 41 | for (const ap of this.antennaPorts) { 42 | if (ap.antennaPort === port) { 43 | return ap.antennaName; 44 | } 45 | } 46 | return ''; 47 | }; 48 | 49 | SensorModel.prototype.getFacilityId = function (port) { 50 | for (const ap of this.antennaPorts) { 51 | if (ap.antennaPort === port) { 52 | return ap.facilityId; 53 | } 54 | } 55 | return ''; 56 | }; 57 | 58 | SensorModel.prototype.getPersonality = function (port) { 59 | for (const ap of this.antennaPorts) { 60 | if (ap.antennaPort === port) { 61 | return ap.personality; 62 | } 63 | } 64 | return ''; 65 | }; 66 | 67 | return SensorModel; 68 | }; 69 | -------------------------------------------------------------------------------- /controller/config/behaviors/FastScan_Dual_Port.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "FastScan_Dual_Port", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 2700, 32 | "rfMode": 100, 33 | "inventorySession": 0, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024, 36 | "fastId": "disabled" 37 | }, 38 | { 39 | "antennaPort": 2, 40 | "transmitPowerCdbm": 2700, 41 | "rfMode": 100, 42 | "inventorySession": 0, 43 | "inventorySearchMode": "single-target", 44 | "estimatedTagPopulation": 1024, 45 | "fastId": "disabled" 46 | } 47 | ] 48 | } 49 | } -------------------------------------------------------------------------------- /controller/src/behaviors/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Behavior = require('./behavior'); 7 | const logger = require('../logger')('behavior'); 8 | 9 | async function getAll(req, res) { 10 | try { 11 | const behaviors = Behavior.getAll(); 12 | return res.status(202).json(behaviors); 13 | } catch (err) { 14 | logger.error(`getting all : ${err.toString()}`); 15 | return res.status(500).json({message: err.message}); 16 | } 17 | } 18 | 19 | async function getOne(req, res) { 20 | const behaviorId = req.params.behaviorId; 21 | try { 22 | const behavior = Behavior.getOne(behaviorId); 23 | if (behavior !== null) { 24 | return res.status(202).json(behavior); 25 | } else { 26 | return res.status(400).json({message: `unkown behavior id: ${behaviorId}`}); 27 | } 28 | } catch (err) { 29 | logger.error(`getting one : ${err.toString()}`); 30 | return res.status(500).json({message: err.message}); 31 | } 32 | } 33 | 34 | async function upsertOne(req, res) { 35 | try { 36 | Behavior.upsertOne(req.body); 37 | return res.status(202).json(req.body); 38 | } catch (err) { 39 | logger.error(`upserting one : ${err.toString()}`); 40 | return res.status(400).json({message: err.message}); 41 | } 42 | } 43 | 44 | async function deleteOne(req, res) { 45 | const behaviorId = req.params.behaviorId; 46 | try { 47 | Behavior.deleteOne(behaviorId); 48 | return res.status(202).json({message: 'SUCCESS'}); 49 | } catch (err) { 50 | logger.error(`deleting one : ${err.toString()}`); 51 | return res.status(400).json({message: err.message}); 52 | } 53 | } 54 | 55 | module.exports = { 56 | getAll, 57 | getOne, 58 | upsertOne, 59 | deleteOne 60 | }; 61 | -------------------------------------------------------------------------------- /controller/config/behaviors/Example_Store_Two_Port.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "Example_Store_Two_Port", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 1800, 32 | "rfMode": 1111, 33 | "inventorySession": 0, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024, 36 | "fastId": "enabled" 37 | }, 38 | { 39 | "antennaPort": 2, 40 | "transmitPowerCdbm": 1800, 41 | "rfMode": 1111, 42 | "inventorySession": 3, 43 | "inventorySearchMode": "single-target", 44 | "estimatedTagPopulation": 1024, 45 | "fastId": "enabled", 46 | "receiveSensitivityDbm": -55 47 | } 48 | ] 49 | } 50 | } -------------------------------------------------------------------------------- /web-ui/src/behaviors/OptionalTextInput.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | 35 | 77 | -------------------------------------------------------------------------------- /web-ui/src/components/OnOffSwitch.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 17 | 31 | 92 | -------------------------------------------------------------------------------- /web-ui/nginx/nginx-https.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | access_log /var/log/nginx/access.log main; 15 | sendfile on; 16 | keepalive_timeout 65; 17 | 18 | server { 19 | server_name webui.rfid.com; 20 | listen 80; 21 | listen [::]:80; 22 | location / { 23 | root /usr/share/nginx/html; 24 | index index.html; 25 | try_files $uri $uri/ /index.html; 26 | add_header Content-Security-Policy "frame-ancestors 'none'"; 27 | } 28 | } 29 | server { 30 | server_name web-ui.rfid.com; 31 | listen 443 default_server ssl http2; 32 | listen [::]:443 ssl http2; 33 | ssl_certificate /etc/ssl/web-ui.rfid.com.crt; 34 | ssl_certificate_key /etc/ssl/web-ui.rfid.com.key; 35 | ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; 36 | ssl_prefer_server_ciphers off; 37 | ssl_protocols TLSv1.2 TLSv1.3; 38 | ssl_session_cache shared:le_nginx_SSL:10m; 39 | ssl_session_tickets off; 40 | ssl_session_timeout 1440m; 41 | location / { 42 | root /usr/share/nginx/html; 43 | index index.html; 44 | try_files $uri $uri/ /index.html; 45 | add_header Content-Security-Policy "frame-ancestors 'none'"; 46 | add_header Cache-Control "public, max-age=120s"; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # Postgres Database 2 | # 3 | # POSTGRES_DB=postgres 4 | # POSTGRES_USER=postgres 5 | POSTGRES_PASSWORD= 6 | # 7 | # used by the controller app to connect to the postgress server host 8 | # POSTGRES_HOST=localhost 9 | 10 | # MQTT Broker 11 | # 12 | # Upstream broker is where tag events are sent 13 | # MQTT_UPSTREAM_HOST='localhost' 14 | # MQTT_UPSTREAM_PORT=1883 15 | # MQTT_UPSTREAM_USERNAME='' 16 | # MQTT_UPSTREAM_PASSWORD='' 17 | # 18 | # Downstream broker is where tag reads from sensors arrive 19 | # and !! IMPORTANT !! is sent to the sensors for them to 20 | # publish to, so it must be a host/ip that is reachable from the sensors 21 | MQTT_DOWNSTREAM_HOST= 22 | # MQTT_DOWNSTREAM_PORT=1883 23 | # MQTT_DOWNSTREAM_USERNAME='' 24 | # MQTT_DOWNSTREAM_PASSWORD='' 25 | 26 | # At the time of publishing the controller code, the Impinj sensor certificates 27 | # are generated per sensor upon start up and are self-signed. This causes the TLS 28 | # connections to fail as there is no certificate authority available to establish 29 | # root of trust. Be aware of running in this mode in a production environment! 30 | # 31 | # NOTE: !! Be aware of the implications of running this mode in a production environment !! 32 | # 33 | NODE_TLS_REJECT_UNAUTHORIZED=0 34 | 35 | # this value is the base64 encoded value of 36 | # the impinj username:password and should be set 37 | # appropriately per the configuration of the sensor 38 | # > echo -n $USERNAME:$NEW_PASSWORD | openssl enc -base64 39 | # 40 | # copy the output of the above command as the value of the variable 41 | # NOTE!!: at this time, all sensors MUST use the same username:password combination. 42 | # 43 | IMPINJ_BASIC_AUTH= 44 | 45 | # LOGGING 46 | # 47 | # Log Levels (error, warn, info, verbose, debug, silly) 48 | # source code default is info 49 | # LOG_LEVEL=info 50 | # 51 | # Log file is optional, if not specified, no log files are created. 52 | # LOG_FILE=${PROJECT_DIR}/controller/run/log/controller.log 53 | -------------------------------------------------------------------------------- /controller/config/behaviors/Example_FIlter_Single_Port_TID.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "Example_FIlter_Single_Port_TID", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 2700, 32 | "rfMode": 100, 33 | "inventorySession": 1, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024, 36 | "filtering": { 37 | "filters": [ 38 | { 39 | "action": "include", 40 | "tagMemoryBank": "tid", 41 | "bitOffset": 0, 42 | "mask": "E2801100", 43 | "maskLength": 32 44 | } 45 | ], 46 | "filterLink": "union" 47 | }, 48 | "fastId": "enabled" 49 | } 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /web-ui/index.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | RFID Sensor Controller 26 | 27 | 28 | 32 | 33 | 42 | 43 | 44 | 45 | 48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/rf-mode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Region = require('./region'); 7 | 8 | const lookup = Object.freeze([ 9 | [100, 120, 142, 185, 140, 1110, 1111, 1112], 10 | [201, 221, 240, 284, 242, 1210, 1211, 1212], 11 | [301, 322, 340, 381, 341, 1310, 1311, 1312], 12 | ]); 13 | 14 | const Mode = Object.freeze({ 15 | HighThroughput: 'Mode 0 - High Throughput', 16 | Hybrid: 'Mode 1 - Hybrid', 17 | DenseReaderM4: 'Mode 2 - Dense Reader M4', 18 | DenseReaderM8: 'Mode 3 - Dense Reader M8', 19 | MaxMiller: 'Mode 4/5 - Max Miller', 20 | AutosetDenseReaderDeepScan: 'Mode 1002 - Autoset Dense Reader Deep Scan', 21 | AutosetStaticFast: 'Mode 1003 - Autoset Static Fast', 22 | AutosetStaticDenseReader: 'Mode 1004 - Autoset Static Dense Reader' 23 | }); 24 | 25 | function getRfMode(regionString, modeString) { 26 | 27 | let regionIndex; 28 | let modeIndex; 29 | 30 | switch (regionString) { 31 | case Region.EUHB: 32 | regionIndex = 2; 33 | break; 34 | case Region.EULB: 35 | regionIndex = 1; 36 | break; 37 | default: 38 | regionIndex = 0; 39 | break; 40 | } 41 | switch (modeString) { 42 | case Mode.HighThroughput: 43 | modeIndex = 0; 44 | break; 45 | case Mode.Hybrid: 46 | modeIndex = 1; 47 | break; 48 | case Mode.DenseReaderM4: 49 | modeIndex = 2; 50 | break; 51 | case Mode.DenseReaderM8: 52 | modeIndex = 3; 53 | break; 54 | case Mode.MaxMiller: 55 | modeIndex = 4; 56 | break; 57 | case Mode.AutosetDenseReaderDeepScan: 58 | modeIndex = 5; 59 | break; 60 | case Mode.AutosetStaticFast: 61 | modeIndex = 6; 62 | break; 63 | case Mode.AutosetStaticDenseReader: 64 | modeIndex = 7; 65 | break; 66 | default: 67 | modeIndex = 0; 68 | break; 69 | } 70 | 71 | return lookup[regionIndex][modeIndex]; 72 | } 73 | 74 | module.exports = { 75 | getRfMode, 76 | }; 77 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/OptionalNumberInput.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 37 | 38 | 84 | -------------------------------------------------------------------------------- /web-ui/src/sensor/control/Sensor.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 56 | 57 | 82 | -------------------------------------------------------------------------------- /controller/src/app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const express = require('express'); 7 | const db = require('./persist/db'); 8 | 9 | const authController = require('./auth/controller'); 10 | const sensorService = require('./sensors/service'); 11 | const tagService = require('./tags/service'); 12 | const mqttService = require('./mqtt/service'); 13 | 14 | // Initialize the app 15 | const app = express(); 16 | app.use(express.json()); 17 | app.use(express.urlencoded({extended: true})); 18 | 19 | // Set proper Headers on Backend 20 | app.use((req, res, next) => { 21 | res.header('Access-Control-Allow-Origin', '*'); 22 | res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET, OPTIONS'); 23 | res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 24 | if (req.method === 'OPTIONS') { 25 | return res.status(204).json({}); 26 | } 27 | res.header('Content-Security-Policy', 'frame-ancestors "none"'); 28 | // Disable caching for content files 29 | // was getting 304 on GET against the tags 30 | res.header('Cache-Control', 'no-cache, no-store, must-revalidate'); 31 | res.header('Pragma', 'no-cache'); 32 | res.header('Expires', '0'); 33 | 34 | next(); 35 | }); 36 | 37 | app.use('/api/v01/auth', require('./auth/router')); 38 | app.use(authController.verifyToken); 39 | 40 | app.use('/api/v01/behaviors', require('./behaviors/router')); 41 | app.use('/api/v01/events', require('./events/router')); 42 | app.use('/api/v01/firmware', require('./firmware/router')); 43 | app.use('/api/v01/mqtt', require('./mqtt/router')); 44 | app.use('/api/v01/sensors', require('./sensors/router')); 45 | app.use('/api/v01/tags', require('./tags/router')); 46 | 47 | app.get('/', (req, res) => { 48 | res.send('Hello from the RFID Controller'); 49 | }); 50 | 51 | app.start = async function () { 52 | await db.start(); 53 | await sensorService.start(); 54 | await tagService.start(); 55 | await mqttService.start(); 56 | }; 57 | 58 | app.stop = async function () { 59 | await mqttService.stop(); 60 | await tagService.stop(); 61 | await sensorService.stop(); 62 | await db.stop(); 63 | }; 64 | 65 | module.exports = app; 66 | -------------------------------------------------------------------------------- /controller/src/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const fs = require('fs'); 7 | const http = require('http'); 8 | const https = require('https'); 9 | const logger = require('./logger')('server'); 10 | const app = require('./app.js'); 11 | 12 | const httpPort = 3000; 13 | const httpsPort = 3443; 14 | 15 | let httpServer; 16 | let httpsServer; 17 | let serverState = 'init'; 18 | 19 | async function shutdown(signal) { 20 | if (serverState === 'stopping') { return; } 21 | serverState = 'stopping'; 22 | logger.info(`shutdown: ${signal}`); 23 | if (signal.stack) { 24 | logger.info(signal.stack); 25 | } 26 | if (httpServer) { 27 | httpServer.close(() => { 28 | logger.info('closed httpServer'); 29 | }); 30 | } 31 | if (httpsServer) { 32 | httpsServer.close(() => { 33 | logger.info('closed httpsServer'); 34 | }); 35 | } 36 | await app.stop(); 37 | } 38 | 39 | // ctrl-c 40 | process.on('SIGINT', shutdown); 41 | process.on('SIGQUIT', shutdown); 42 | process.on('SIGTERM', shutdown); 43 | 44 | process.on('uncaughtException', shutdown); 45 | 46 | (async () => { 47 | try { 48 | await app.start(); 49 | serverState = 'starting'; 50 | // HTTP SERVER 51 | httpServer = http.createServer(app); 52 | httpServer.listen(httpPort); 53 | logger.info(`http server listening on ${httpPort}`); 54 | 55 | // HTTPS SERVER 56 | const keyFile = process.env.KEY_FILE || './run/certs/controller.rfid.com.key'; 57 | const certFile = process.env.CERT_FILE || './run/certs/controller.rfid.com.crt'; 58 | if (fs.existsSync(certFile) && fs.existsSync(keyFile)) { 59 | try { 60 | const key = fs.readFileSync(keyFile, 'utf8'); 61 | const cert = fs.readFileSync(certFile, 'utf8'); 62 | httpsServer = https.createServer({key: key, cert: cert}, app); 63 | httpsServer.listen(httpsPort); 64 | logger.info(`https server listening on ${httpsPort}`); 65 | } catch (err) { 66 | logger.error(`failed creating https server ${err.message}`); 67 | } 68 | } else { 69 | logger.warn(`https server disabled - missing key ${certFile} or certificate ${certFile}`); 70 | } 71 | serverState = 'running'; 72 | } catch (error) { 73 | logger.error(error); 74 | } 75 | })(); 76 | 77 | -------------------------------------------------------------------------------- /controller/config/scripts/r700setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # Copyright (C) 2022 Intel Corporation 5 | # SPDX-License-Identifier: BSD-3-Clause 6 | # 7 | echo 'Welcome to the Impinj R700 initial configuration script.' 8 | echo 'This script requires sshpass and openssl to execute the' 9 | echo 'necessary commands. When executed, this script will...' 10 | echo ' 1. Enable the HTTPS network functionality' 11 | echo ' 2. Enable the RESTful command interface' 12 | echo ' 3. Change the root user password from its default' 13 | echo ' 4. Reboot the Impinj R700' 14 | echo ' 5. Calculate the Basic Authorization header value' 15 | echo 16 | 17 | # Install necessary packages 18 | sudo apt install -qq sshpass openssl 19 | echo 20 | 21 | # R700 username 22 | USERNAME=root 23 | 24 | # Default R700 password 25 | OLD_PASSWORD=impinj 26 | 27 | # Prompt for new password 28 | echo 'Please enter a new root user password...' 29 | read NEW_PASSWORD 30 | #NEW_PASSWORD=impinj 31 | echo 32 | 33 | # Prompt for IP Addresses 34 | echo 'Please enter the IP Address of your Impinj R700...' 35 | read IP_ADDRESS 36 | #IP_ADDRESS=192.168.1.34 37 | echo 38 | 39 | # Enable the https interface 40 | echo 'Enabling HTTPS...' 41 | sshpass -p$OLD_PASSWORD ssh root@$IP_ADDRESS config network https enable 42 | 43 | # Enable the RESTful interface 44 | echo 'Enabling RESTful command interface...' 45 | sshpass -p$OLD_PASSWORD ssh root@$IP_ADDRESS config rfid interface rest 46 | 47 | # Update the password 48 | echo 'Updating the root user password...' 49 | sshpass -p$OLD_PASSWORD ssh root@$IP_ADDRESS config access mypasswd $OLD_PASSWORD $NEW_PASSWORD 50 | 51 | # Reboot the reader 52 | echo 'Rebooting the Impinj R700...' 53 | sshpass -p$NEW_PASSWORD ssh root@$IP_ADDRESS reboot 54 | 55 | # Calculate the Auth String for https BASIC Authentication header 56 | IMPINJ_BASIC_AUTH=$(echo -n $USERNAME:$NEW_PASSWORD | openssl enc -base64) 57 | echo 58 | echo 'Your Basic Authorization header value is: Basic '$IMPINJ_BASIC_AUTH 59 | echo 60 | echo 'Please be sure to add the following variable to your .env file...' 61 | echo 'IMPINJ_BASIC_AUTH='$IMPINJ_BASIC_AUTH 62 | echo 63 | 64 | # Create a self signed certificate for the backend API 65 | echo 66 | echo 'Create a self signed certificate for the backend API using openssl...' 67 | openssl genrsa -out key.pem 68 | openssl req -new -key key.pem -out csr.pem 69 | openssl x509 -req -days 365 -in csr.pem -signkey key.pem -out cert.pem 70 | -------------------------------------------------------------------------------- /controller/config/behaviors/Example_Store_Four_Port.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "Example_Store_Four_Port", 3 | "preset": { 4 | "eventConfig": { 5 | "common": { 6 | "hostname": "enabled" 7 | }, 8 | "tagInventory": { 9 | "tagReporting": { 10 | "reportingIntervalSeconds": 0, 11 | "tagCacheSize": 2048, 12 | "antennaIdentifier": "antennaPort", 13 | "tagIdentifier": "epc" 14 | }, 15 | "epc": "disabled", 16 | "epcHex": "enabled", 17 | "tid": "disabled", 18 | "tidHex": "enabled", 19 | "antennaPort": "enabled", 20 | "transmitPowerCdbm": "enabled", 21 | "peakRssiCdbm": "enabled", 22 | "frequency": "enabled", 23 | "pc": "disabled", 24 | "lastSeenTime": "enabled", 25 | "phaseAngle": "enabled" 26 | } 27 | }, 28 | "antennaConfigs": [ 29 | { 30 | "antennaPort": 1, 31 | "transmitPowerCdbm": 2700, 32 | "rfMode": 1111, 33 | "inventorySession": 0, 34 | "inventorySearchMode": "single-target", 35 | "estimatedTagPopulation": 1024, 36 | "fastId": "disabled" 37 | }, 38 | { 39 | "antennaPort": 2, 40 | "transmitPowerCdbm": 2700, 41 | "rfMode": 1111, 42 | "inventorySession": 1, 43 | "inventorySearchMode": "single-target", 44 | "estimatedTagPopulation": 1024, 45 | "fastId": "disabled", 46 | "receiveSensitivityDbm": -65 47 | }, 48 | { 49 | "antennaPort": 3, 50 | "transmitPowerCdbm": 2700, 51 | "rfMode": 1111, 52 | "inventorySession": 3, 53 | "inventorySearchMode": "single-target", 54 | "estimatedTagPopulation": 1024, 55 | "fastId": "disabled", 56 | "receiveSensitivityDbm": -55 57 | }, 58 | { 59 | "antennaPort": 4, 60 | "transmitPowerCdbm": 2700, 61 | "rfMode": 1111, 62 | "inventorySession": 2, 63 | "inventorySearchMode": "single-target", 64 | "estimatedTagPopulation": 1024, 65 | "fastId": "disabled" 66 | } 67 | ] 68 | } 69 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Any certificates 2 | *.pem 3 | *.cert 4 | 5 | # Impinj Reader firmware 6 | *.upgx 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | .env.test 82 | .env.production 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # Serverless directories 106 | .serverless/ 107 | 108 | # FuseBox cache 109 | .fusebox/ 110 | 111 | # DynamoDB Local files 112 | .dynamodb/ 113 | 114 | # TernJS port file 115 | .tern-port 116 | 117 | # Stores VSCode versions used for testing VSCode extensions 118 | .vscode-test 119 | .vscode/ 120 | 121 | # yarn v2 122 | .yarn/cache 123 | .yarn/unplugged 124 | .yarn/build-state.yml 125 | .yarn/install-state.gz 126 | .pnp.* 127 | 128 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/PowerSweeping.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 58 | 59 | 100 | -------------------------------------------------------------------------------- /web-ui/src/components/PageHeader.vue: -------------------------------------------------------------------------------- 1 | --> 8 | 9 | 66 | 100 | 101 | -------------------------------------------------------------------------------- /web-ui/src/components/JsonFileUpload.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | 35 | 118 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | # 3 | # Copyright (C) 2022 Intel Corporation 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | # 6 | # for production, have a volume for the web gui that 7 | # the web-ui will compile into, then set up the controller 8 | # server to serve the static content? 9 | # but the controller API will still need to be secured 10 | # for M2M calls from other use cases so does it matter that 11 | # much? 12 | # 13 | # What certs are needed? 14 | # use a different axios instance on controller to talk to 15 | # sensors without checking certs vs the API calls? 16 | 17 | services: 18 | postgres-db: 19 | image: postgres:alpine 20 | container_name: sensor-controller_postgres-db 21 | network_mode: host 22 | #ports: 23 | # - "5432:5432" 24 | environment: 25 | POSTGRES_DB: ${POSTGRES_DB:-postgres} 26 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 27 | POSTGRES_USER: ${POSTGRES_USER:-postgres} 28 | volumes: 29 | - db-data:/var/lib/postgresql/data 30 | healthcheck: 31 | test: pg_isready -U postgres -h 127.0.0.1 32 | interval: 5s 33 | 34 | mqtt-broker: 35 | image: eclipse-mosquitto:1.6.15 36 | container_name: sensor-controller_mqtt-broker 37 | network_mode: host 38 | #ports: 39 | # - "1883:1883" 40 | # - "9883:9883" 41 | 42 | controller: 43 | build: 44 | context: ./controller/ 45 | image: sensor-controller/controller 46 | container_name: sensor-controller_controller 47 | volumes: 48 | - controller-auth:/controller/run/auth 49 | - controller-config:/controller/run/config 50 | - controller-firmware:/controller/run/firmware 51 | # docker volume logging 52 | - controller-log:/controller/run/log 53 | # host logging - BE SURE DIRECTORY EXISTS 54 | #- ./controller/run/log:/controller/run/log 55 | depends_on: 56 | postgres-db: 57 | condition: service_healthy 58 | mqtt-broker: 59 | condition: service_started 60 | network_mode: host 61 | #ports: 62 | # - "3000:3000" 63 | environment: 64 | DIR_AUTH: /controller/run/auth 65 | DIR_CONFIG: /controller/run/config 66 | DIR_FIRMWARE: /controller/run/firmware 67 | MQTT_DOWNSTREAM_HOST: ${MQTT_DOWNSTREAM_HOST:-localhost} 68 | MQTT_UPSTREAM_HOST: ${MQTT_UPSTREAM_HOST:-localhost} 69 | POSTGRES_HOST: ${POSTGRES_HOST:-localhost} 70 | #MQTT_DOWNSTREAM_HOST: ${MQTT_DOWNSTREAM_HOST:-mqtt-broker} 71 | #MQTT_UPSTREAM_HOST: ${MQTT_UPSTREAM_HOST:-mqtt-broker} 72 | #POSTGRES_HOST: ${POSTGRES_HOST:-postgres-db} 73 | POSTGRES_DB: ${POSTGRES_DB:-postgres} 74 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 75 | POSTGRES_USER: ${POSTGRES_USER:-postgres} 76 | NODE_TLS_REJECT_UNAUTHORIZED: ${NODE_TLS_REJECT_UNAUTHORIZED} 77 | IMPINJ_BASIC_AUTH: ${IMPINJ_BASIC_AUTH} 78 | LOG_LEVEL: ${LOG_LEVEL:-info} 79 | LOG_FILE: ${LOG_FILE:-/controller/run/log/rfid-controller.log} 80 | 81 | web-ui: 82 | build: 83 | context: ./web-ui/ 84 | image: sensor-controller/web-ui 85 | container_name: sensor-controller_web-ui 86 | network_mode: host 87 | #ports: 88 | # - "80:80" 89 | 90 | volumes: 91 | db-data: {} 92 | controller-auth: {} 93 | controller-config: {} 94 | controller-firmware: {} 95 | controller-log: {} 96 | -------------------------------------------------------------------------------- /controller/src/auth/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const bcrypt = require('bcryptjs'); 7 | const fs = require('fs'); 8 | const jwt = require('jsonwebtoken'); 9 | const logger = require('../logger')('auth-controller'); 10 | 11 | const authDir = process.env.DIR_AUTH || './run/auth'; 12 | if (!fs.existsSync(authDir)) { 13 | fs.mkdirSync(authDir, {recursive: true}); 14 | logger.info(`created ${authDir}`); 15 | } 16 | const secretFile = `${authDir}/secret`; 17 | let secret = ''; 18 | try { 19 | secret = fs.readFileSync(secretFile, 'utf8'); 20 | } catch (err) { 21 | secret = require('crypto').randomBytes(48).toString('hex'); 22 | fs.writeFileSync(secretFile, secret, 'utf8'); 23 | logger.info('auth-controller: generated new secret'); 24 | } 25 | 26 | const pwFile = `${authDir}/password`; 27 | const userId = 'admin'; 28 | 29 | function logIn(req, res) { 30 | // is password in the req? 31 | const plainPw = req.body.password; 32 | if (!plainPw) { 33 | res.status(404).json({ message: 'missing password'}); 34 | return; 35 | } 36 | 37 | try { 38 | const hashedPw = fs.readFileSync(pwFile, 'utf8'); 39 | if (bcrypt.compareSync(plainPw, hashedPw)) { 40 | returnLoginSuccess(res); 41 | } else { 42 | res.status(401).json({ message: 'invalid password'}); 43 | } 44 | } catch (err) { 45 | // this catch block is expected on initial 46 | // startup of the app or clearing out the 47 | // password file i.e. password reset 48 | if (isValidFormat(plainPw)) { 49 | savePassword(plainPw); 50 | returnLoginSuccess(res); 51 | } else { 52 | res.status(401).json({ message: 'invalid password'}); 53 | } 54 | } 55 | } 56 | 57 | // be careful! expiresIn is interpreted as seconds if a numeric type 58 | // if a string type with no explicit units "2 days", then milliseconds 59 | // is the default 60 | function returnLoginSuccess(res) { 61 | try { 62 | const userToken = jwt.sign({userId: userId}, secret, {expiresIn: 3600}, null); 63 | logger.info(`login success : token ${userToken}`); 64 | res.status(200).json( 65 | { 66 | userId: userId, 67 | token: userToken, 68 | } 69 | ); 70 | } catch (err) { 71 | logger.error(`login failure : ${err.toString()}`); 72 | return res.status(500).json({message: err.message}); 73 | } 74 | } 75 | 76 | function isValidFormat(pw) { 77 | return pw.length >= 8; 78 | } 79 | 80 | function savePassword(plainPw) { 81 | bcrypt.hash(plainPw, 10, (err, hashed) => { 82 | if (err) { 83 | logger.error(err.message); 84 | } 85 | if (hashed) { 86 | fs.writeFileSync(pwFile, hashed, 'utf8'); 87 | } 88 | }); 89 | } 90 | 91 | function verifyToken(req, res, next) { 92 | const authHeader = String(req.headers['authorization'] || ''); 93 | if (!authHeader.startsWith('Bearer')) { 94 | return res.status(404).json({ message: 'missing authentication token'}); 95 | } 96 | const token = authHeader.substring(7, authHeader.length); 97 | try { 98 | const decoded = jwt.verify(token, secret, null, null); 99 | req.userId = decoded.id; 100 | next(); 101 | } catch (err) { 102 | logger.error(`verifying token : ${err.toString()}`); 103 | return res.status(401).json({ message: 'unauthorized'}); 104 | } 105 | } 106 | 107 | module.exports = { 108 | logIn, 109 | verifyToken, 110 | }; 111 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/TagMemoryReads.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 82 | 83 | 129 | -------------------------------------------------------------------------------- /controller/src/events/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const fs = require('fs'); 7 | const cloneDeep = require('lodash/cloneDeep'); 8 | const logger = require('../logger')('event-config'); 9 | 10 | const baseDir = process.env.DIR_CONFIG || './run/config'; 11 | const cfgDir = `${baseDir}/events`; 12 | if (!fs.existsSync(cfgDir)) { 13 | fs.mkdirSync(cfgDir, {recursive: true}); 14 | logger.info(`created ${cfgDir}`); 15 | } 16 | const cfgFile = `${cfgDir}/event-config.json`; 17 | let eventCfg; 18 | try { 19 | const json = fs.readFileSync(cfgFile, 'utf8'); 20 | eventCfg = JSON.parse(json); 21 | } catch (error) { 22 | eventCfg = { 23 | exitTimeout: 30000, 24 | posReturnHoldoff: 8640000, 25 | mobilityProfile: { 26 | holdoff: 0, 27 | slope: -0.8, 28 | threshold: 600 29 | } 30 | }; 31 | logger.info('initialized using defaults'); 32 | } 33 | 34 | function getConfig() { 35 | return cloneDeep(eventCfg); 36 | } 37 | 38 | function setConfig(newCfg) { 39 | eventCfg = cloneDeep(newCfg); 40 | fs.writeFileSync(cfgFile, JSON.stringify(eventCfg, null, 4), 'utf8'); 41 | logger.info(`set config : ${JSON.stringify(eventCfg, null, 2)}`); 42 | } 43 | 44 | function validateConfig(cfg) { 45 | try { 46 | if (cfg.exitTimeout < 0) { 47 | return false; 48 | } 49 | if (cfg.posReturnHoldoff < 0) { 50 | return false; 51 | } 52 | if (!cfg.mobilityProfile) { 53 | return false; 54 | } 55 | if (cfg.mobilityProfile.holdoff < 0) { 56 | return false; 57 | } 58 | if (!cfg.mobilityProfile.slope) { 59 | return false; 60 | } 61 | if (!cfg.mobilityProfile.threshold) { 62 | return false; 63 | } 64 | } catch (err) { 65 | logger.error(`validating : ${err.toString()}`); 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | function getExitTimeout() { 72 | return eventCfg.exitTimeout; 73 | } 74 | 75 | function getPosReturnHoldoff() { 76 | return eventCfg.posReturnHoldoff; 77 | } 78 | 79 | function getRssiAdjustment(now, lastRead) { 80 | 81 | /* From the linear equation y = m(x) + b ... 82 | y = RSSI Adjustment 83 | m = RSSI decay rate over time (i.e., slope) 84 | x = Time since last tag read 85 | b = RSSI threshold (i.e., new location must be better than old by b dB) 86 | 87 | The holdoff is how long to wait before applying the RSSI adjustment, 88 | so the equation becomes ... 89 | rssiAdjustment = slope(time - holdoff) + threshold 90 | 91 | Then we bound rssiAdjustment by a (max = threshold) and a (min = -50 dBm) 92 | creating an RSSI Adjustment curve that looks something like this... 93 | 94 | | 95 | | holdoff ms 96 | |---------------\ + threshold dB 97 | | \ 98 | |_________________\_______________________ time in ms 99 | | \ 100 | | \ 101 | | \ 102 | | ------------- -50 dB 103 | | 104 | */ 105 | 106 | let rssiAdjustment; 107 | const profile = eventCfg.mobilityProfile; 108 | const time = (Date.parse(now) - Date.parse(lastRead)); 109 | rssiAdjustment = (profile.slope * (time - profile.holdoff)) + profile.threshold; 110 | rssiAdjustment = Math.max(Math.min(rssiAdjustment, profile.threshold), -5000); 111 | return rssiAdjustment; 112 | } 113 | 114 | module.exports = { 115 | getConfig, 116 | setConfig, 117 | validateConfig, 118 | getExitTimeout, 119 | getPosReturnHoldoff, 120 | getRssiAdjustment, 121 | }; 122 | -------------------------------------------------------------------------------- /controller/src/mqtt/service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Mqtt = require('mqtt'); 7 | const Config = require('./config'); 8 | const logger = require('../logger')('mqtt-service'); 9 | 10 | let upstreamClient; 11 | let downstreamClient; 12 | const dataSubscribers = []; 13 | const willSubscribers = []; 14 | 15 | async function init() { 16 | upstreamClient = Mqtt.connect( 17 | Config.getUpstreamUrl(), 18 | { 19 | clientId: `snsrcntrlup_${Math.random().toString(16).substr(2, 8)}`, 20 | username: Config.getUpstreamUsername(), 21 | password: Config.getUpstreamPassword(), 22 | clean: true 23 | }); 24 | 25 | upstreamClient.on('connect', function () { 26 | logger.info(`upstream CONNECTED : ${Config.getUpstreamUrl()}`); 27 | }); 28 | 29 | upstreamClient.on('error', function (error) { 30 | logger.error(`upstream ERROR : ${error}`); 31 | }); 32 | 33 | upstreamClient.on('message', function (topic, message) { 34 | logger.error(`upstream message not processed : ${topic} : ${message}`); 35 | }); 36 | 37 | downstreamClient = Mqtt.connect( 38 | Config.getDownstreamUrl(), 39 | { 40 | clientId: `snsrcntrldown_${Math.random().toString(16).substr(2, 8)}`, 41 | username: Config.getDownstreamUsername(), 42 | password: Config.getDownstreamPassword(), 43 | clean: true 44 | }); 45 | 46 | downstreamClient.on('connect', function () { 47 | logger.info(`downstream CONNECTED : ${Config.getDownstreamUrl()}`); 48 | downstreamClient.subscribe(Config.Topic.will, {qos: 0}); 49 | downstreamClient.subscribe(Config.Topic.data, {qos: 0}); 50 | }); 51 | 52 | downstreamClient.on('error', function (error) { 53 | logger.error(`downstream ERROR : ${error}`); 54 | downstreamClient.unsubscribe(Config.Topic.data); 55 | downstreamClient.unsubscribe(Config.Topic.will); 56 | }); 57 | 58 | downstreamClient.on('message', function (topic, message) { 59 | switch (topic) { 60 | case Config.Topic.data: 61 | dataSubscribers.forEach((callback) => { 62 | callback(message); 63 | }); 64 | break; 65 | case Config.Topic.will: 66 | willSubscribers.forEach((callback) => { 67 | callback(message); 68 | }); 69 | break; 70 | } 71 | }); 72 | logger.info('started'); 73 | } 74 | 75 | async function start() { 76 | try { 77 | await init(); 78 | } catch (err) { 79 | err.message = `mqtt-client start unsuccessful ${err.message}`; 80 | throw err; 81 | } 82 | } 83 | 84 | async function stop() { 85 | try { 86 | // in some startup failure conditions, 87 | // stop might be called without 88 | // start having been called so check for 89 | if (upstreamClient) { 90 | upstreamClient.end(); 91 | } 92 | if (downstreamClient) { 93 | downstreamClient.end(); 94 | } 95 | logger.info('stopped'); 96 | } catch (err) { 97 | err.message = `mqtt-client stop unsuccessful ${err.message}`; 98 | throw err; 99 | } 100 | } 101 | 102 | function subscribeData(callback) { 103 | dataSubscribers.push(callback); 104 | } 105 | 106 | function subscribeWill(callback) { 107 | willSubscribers.push(callback); 108 | } 109 | 110 | function publish(topic, jsonObj) { 111 | if (upstreamClient.connected) { 112 | upstreamClient.publish(topic, JSON.stringify(jsonObj)); 113 | } 114 | } 115 | 116 | module.exports = { 117 | start, 118 | stop, 119 | publish, 120 | subscribeData, 121 | subscribeWill, 122 | }; 123 | -------------------------------------------------------------------------------- /web-ui/src/components/PaginationControl.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 58 | 136 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/RfModeSelect.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 32 | 33 | 119 | -------------------------------------------------------------------------------- /controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "keywords": [ 3 | "rfid", 4 | "Impinj", 5 | "Intel" 6 | ], 7 | "name": "rfid-sensor-controller", 8 | "description": "Used for configuring and managing a fleet of rfid sensors.", 9 | "version": "1.0.0", 10 | "author": "Timothy Shockley, John Belstner", 11 | "license": "BSD-3-Clause", 12 | "main": "server.js", 13 | "scripts": { 14 | "dev": "nodemon --watch src --verbose --inspect src/server", 15 | "serve": "node src/server", 16 | "test": "jest --detectOpenHandles", 17 | "lint": "eslint ./src --ext .js,.jsx --ignore-path .gitignore" 18 | }, 19 | "nodemonConfig": { 20 | "ignore": [ 21 | "config/*.json", 22 | "firmware/*" 23 | ] 24 | }, 25 | "dependencies": { 26 | "async-lock": "^1.3.2", 27 | "async-mutex": "^0.4.0", 28 | "axios": "^1.1.3", 29 | "bcryptjs": "^2.4.3", 30 | "body-parser": "^1.20.1", 31 | "cors": "^2.8.5", 32 | "dnssd": "^0.4.1", 33 | "express": "^4.18.2", 34 | "form-data": "^4.0.0", 35 | "formidable": "^2.1.1", 36 | "fs": "^0.0.1-security", 37 | "jsonwebtoken": "^9.0.0", 38 | "lodash": "^4.17.21", 39 | "mqtt": "^4.3.7", 40 | "pg": "^8.8.0", 41 | "pg-hstore": "^2.3.4", 42 | "sequelize": "^6.29.0", 43 | "winston": "^3.8.2" 44 | }, 45 | "devDependencies": { 46 | "eslint": "^8.26.0", 47 | "eslint-config-airbnb-base": "^15.0.0", 48 | "eslint-plugin-import": "^2.26.0", 49 | "nodemon": "^2.0.20" 50 | }, 51 | "eslintConfig": { 52 | "root": true, 53 | "env": { 54 | "node": true, 55 | "commonjs": true, 56 | "es2021": true, 57 | "jest": true 58 | }, 59 | "extends": [ 60 | "eslint:recommended" 61 | ], 62 | "parserOptions": { 63 | "ecmaVersion": "latest" 64 | }, 65 | "rules": { 66 | "arrow-spacing": "error", 67 | "curly": "error", 68 | "eqeqeq": "warn", 69 | "indent": [ 70 | "error", 71 | 2, 72 | { 73 | "SwitchCase": 1 74 | } 75 | ], 76 | "keyword-spacing": "error", 77 | "max-len": [ 78 | "error", 79 | { 80 | "code": 100 81 | } 82 | ], 83 | "max-lines": [ 84 | "warn", 85 | { 86 | "max": 500 87 | } 88 | ], 89 | "multiline-ternary": [ 90 | "error", 91 | "always-multiline" 92 | ], 93 | "no-confusing-arrow": "error", 94 | "no-console": "warn", 95 | "no-constant-condition": "warn", 96 | "no-duplicate-imports": "error", 97 | "no-invalid-this": "error", 98 | "no-mixed-operators": "error", 99 | "no-mixed-spaces-and-tabs": "warn", 100 | "no-multiple-empty-lines": [ 101 | "error", 102 | { 103 | "max": 2, 104 | "maxEOF": 1 105 | } 106 | ], 107 | "no-return-assign": "error", 108 | "no-undef": "error", 109 | "no-unused-expressions": [ 110 | "error", 111 | { 112 | "allowTernary": true 113 | } 114 | ], 115 | "no-unused-vars": [ 116 | "warn", 117 | { 118 | "argsIgnorePattern": "req|res|next|__" 119 | } 120 | ], 121 | "no-useless-concat": "error", 122 | "no-useless-return": "error", 123 | "no-var": "error", 124 | "no-whitespace-before-property": "error", 125 | "nonblock-statement-body-position": "error", 126 | "object-property-newline": [ 127 | "error", 128 | { 129 | "allowAllPropertiesOnSameLine": true 130 | } 131 | ], 132 | "object-shorthand": "off", 133 | "prefer-const": "error", 134 | "prefer-template": "warn", 135 | "quotes": [ 136 | "error", 137 | "single" 138 | ], 139 | "semi": "error", 140 | "semi-spacing": "error", 141 | "space-before-blocks": "error", 142 | "space-in-parens": "error", 143 | "space-infix-ops": "error", 144 | "space-unary-ops": "error" 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /web-ui/src/components/Authentication.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 48 | 49 | 141 | -------------------------------------------------------------------------------- /controller/src/tags/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const db = require('../persist/db'); 7 | const Tag = require('./tag'); 8 | const logger = require('../logger')('tag-controller'); 9 | 10 | async function create(req, res) { 11 | try { 12 | const results = await Tag.createBulk(req.body); 13 | return res.status(201).json(results); 14 | } catch (err) { 15 | logger.error(`creating tag : ${err.toString()}`); 16 | return res.status(400).json({message: err.message}); 17 | } 18 | } 19 | 20 | async function getAll(req, res) { 21 | try { 22 | let sortCol = 'epc'; 23 | let sortDir = 'ASC'; 24 | switch (req.query.sortCol) { 25 | case 'EPC': 26 | sortCol = 'epc'; 27 | break; 28 | case 'TID': 29 | sortDir = 'tid'; 30 | break; 31 | case 'State': 32 | sortDir = 'state'; 33 | break; 34 | case 'Location': 35 | sortDir = 'location'; 36 | break; 37 | case 'Facility': 38 | sortDir = 'facilityId'; 39 | break; 40 | case 'LastRead': 41 | sortDir = 'lastRead'; 42 | break; 43 | } 44 | switch (req.query.sortDir) { 45 | case 'ASC': 46 | sortDir = 'ASC'; 47 | break; 48 | case 'DESC': 49 | sortDir = 'DESC'; 50 | break; 51 | } 52 | const filter = {}; 53 | if (req.query.filterEpc) { 54 | const s = sanitizeFilter(req.query.filterEpc); 55 | filter['epc'] = { 56 | [db.Op.like]: s 57 | }; 58 | } 59 | if (req.query.filterTid) { 60 | const s = sanitizeFilter(req.query.filterTid); 61 | filter['tid'] = { 62 | [db.Op.like]: s 63 | }; 64 | } 65 | const params = { 66 | where: filter, 67 | order: [[sortCol, sortDir]], 68 | offset: req.query.offset ? req.query.offset : 0, 69 | limit: req.query.limit ? req.query.limit : null, 70 | }; 71 | const result = await Tag.getAll(params); 72 | return res.status(200).json(result); 73 | } catch (err) { 74 | logger.error(`getting all : ${err.toString()}`); 75 | return res.status(500).json({message: err.message}); 76 | } 77 | } 78 | 79 | function sanitizeFilter(f) { 80 | return f.replace(/[^A-Fa-f0-9%]/g, '').toUpperCase(); 81 | } 82 | 83 | async function getOne(req, res) { 84 | try { 85 | return res.status(200).json(Tag.getOne(req.params.epc)); 86 | } catch (err) { 87 | logger.error(`getting one : ${err.toString()}`); 88 | return res.status(500).json({message: err.message}); 89 | } 90 | } 91 | 92 | async function deleteOne(req, res) { 93 | try { 94 | const tagsCount = await Tag.deleteOne(req.params.epc); 95 | if (tagsCount) { 96 | return res.status(200).json(req.params.epc); 97 | } else { 98 | return res.status(404).json({status: 404, message: `Bad epc: ${ req.params.epc}`}); 99 | } 100 | } catch (err) { 101 | logger.error(`deleting one tag ${req.params.epc}: ${err.toString()}`); 102 | return res.status(500).json({message: err.message}); 103 | } 104 | } 105 | 106 | async function deleteBulk(req, res) { 107 | try { 108 | let epcList; 109 | if (req.body && req.body.tags && Array.isArray(req.body.tags)) { 110 | epcList = req.body.tags; 111 | } 112 | const deletedCount = await Tag.deleteBulk(epcList); 113 | return res.status(200).json({ count: deletedCount }); 114 | } catch (err) { 115 | logger.error(`deleting bulk tags with epcs ${req.body.tags} : ${err.toString()}`); 116 | return res.status(500).json({message: err.message}); 117 | } 118 | } 119 | 120 | async function getTagStats(req, res) { 121 | try { 122 | const reads = await Tag.getStats(req.params.epc); 123 | return res.status(200).json(reads); 124 | } catch (err) { 125 | logger.error(`getting tag stats : ${err.toString()}`); 126 | return res.status(500).json({message: err.message}); 127 | } 128 | } 129 | 130 | module.exports = { 131 | create, 132 | getAll, 133 | getOne, 134 | deleteOne, 135 | deleteBulk, 136 | getTagStats, 137 | }; 138 | 139 | -------------------------------------------------------------------------------- /web-ui/src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | export default { 7 | pushSequentialArrayObj(parent, key, obj, sequentialKey) { 8 | if (!parent[key]) { 9 | parent[key] = []; 10 | } 11 | parent[key].push(obj); 12 | this.setLastToMax(parent[key], sequentialKey); 13 | }, 14 | setLastToGap(arr, key, iniVal, maxVal) { 15 | let i; 16 | if (!key) { return; } 17 | if (arr.length < 2) { return; } 18 | const lastIndex = arr.length - 1; 19 | const existing = []; 20 | for (i = 0; i < lastIndex; i++) { 21 | existing.push(arr[i][key]); 22 | } 23 | for (i = iniVal; i <= maxVal; i++) { 24 | if (!existing.includes(i)) { 25 | arr[lastIndex][key] = i; 26 | break; 27 | } 28 | } 29 | }, 30 | setLastToMax(a, key) { 31 | if (!key) { return; } 32 | if (a.length < 2) { return; } 33 | let max = 0; 34 | let i = 0; 35 | for (; i < a.length - 1; i++) { 36 | max = Math.max(max, a[i][key]); 37 | } 38 | if (a[i][key] <= max) { 39 | a[i][key] = +max + 1; 40 | } 41 | }, 42 | removeArrayObj(parent, key, index) { 43 | if (!parent[key]) { 44 | return; 45 | } 46 | parent[key].splice(index, 1); 47 | if (parent[key].length === 0) { 48 | delete parent[key]; 49 | } 50 | }, 51 | ensureHex(value) { 52 | return value.replace(/[^A-Fa-f0-9]/g, "").toUpperCase(); 53 | }, 54 | validateHex8(value) { 55 | value = value.slice(0, 8); 56 | value = this.ensureHex(value); 57 | return value; 58 | }, 59 | validateHex12(value) { 60 | value = value.slice(0, 12); 61 | value = this.ensureHex(value); 62 | return value; 63 | }, 64 | objectsEqual(o1, o2) { 65 | const keys1 = Object.keys(o1); 66 | const keys2 = Object.keys(o2); 67 | 68 | if (keys1.length !== keys2.length) { 69 | return false; 70 | } 71 | 72 | for (const key of keys1) { 73 | if (o1[key] !== o2[key]) { 74 | if (typeof o1[key] == "object" && typeof o2[key] == "object") { 75 | if (!this.objectsEqual(o1[key], o2[key])) { 76 | return false; 77 | } 78 | } else { 79 | return false; 80 | } 81 | } 82 | } 83 | 84 | return true; 85 | }, 86 | download(name, jsonObj) { 87 | const data = JSON.stringify(jsonObj, null, 2); 88 | const a = document.createElement("a"); 89 | a.download = name + ".json"; 90 | a.href = window.URL.createObjectURL( 91 | new Blob([data], { type: "text/plain" }) 92 | ); 93 | a.dataset.downloadurl = ["text/json", a.download, a.href].join(":"); 94 | a.dispatchEvent( 95 | new MouseEvent("click", { 96 | view: window, 97 | bubbles: true, 98 | cancelable: false, 99 | }) 100 | ); 101 | URL.revokeObjectURL(a.href); 102 | }, 103 | sortSensorById(a, b) { 104 | return a.deviceId > b.deviceId 105 | ? 1 106 | : a.deviceId < b.deviceId 107 | ? -1 108 | : 0; 109 | }, 110 | responseErrorChain(err, messages) { 111 | if (this.errorIs401(err)) { 112 | return; 113 | } 114 | let m = err.message; 115 | if (err.response) { 116 | // The request was made and the server responded with a status code 117 | // that falls out of the range of 2xx 118 | if (err.response.data) { 119 | // the app should respond with json format and property of 'message' 120 | if (err.response.data.message) { 121 | m += ": " + err.response.data.message; 122 | } else { 123 | m += ` ${JSON.stringify(err.response.data)}` 124 | } 125 | } else { 126 | m += ": " + err.response.status + ": " + err.response.statusText; 127 | } 128 | } else { 129 | // if there is no response defined, it might be CORS, or network, or server went down 130 | // which causes CORS preflight to fail when refreshing once the vue app is loaded already. 131 | m += 'Network Error: lost connection, try reloading the page' 132 | } 133 | const i = messages.findIndex(el => el === m); 134 | if(i < 0) { 135 | messages.push(m); 136 | } 137 | }, 138 | errorIs401(err) { 139 | return err.response && err.response.status === 401 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /controller/src/sensors/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Sensor = require('./sensor'); 7 | const Service = require('./service'); 8 | const RunState = require('./run-state'); 9 | const logger = require('../logger')('sensor-controller'); 10 | 11 | async function getAll(req, res) { 12 | try { 13 | const sensors = await Sensor.getAll(); 14 | return res.status(200).json(sensors); 15 | } catch (err) { 16 | logger.error(`getting all : ${err.toString()}`); 17 | return res.status(500).json({message: err.message}); 18 | } 19 | } 20 | 21 | async function getOne(req, res) { 22 | try { 23 | const sensor = await Sensor.getOne(req.params.deviceId); 24 | return res.status(200).json(sensor); 25 | } catch (err) { 26 | logger.error(`getting one : ${err.toString()}`); 27 | return res.status(500).json({message: err.message}); 28 | } 29 | } 30 | 31 | async function upsertBulk(req, res) { 32 | try { 33 | const rsp = { 34 | sensors: [], 35 | stopMessages: [], 36 | startMessages: [], 37 | }; 38 | let msg; 39 | for (const sensor of req.body) { 40 | await Sensor.upsertOne(sensor); 41 | rsp.sensors.push(sensor); 42 | if (sensor.connected) { 43 | // Stop any reading that might be in progress with old config 44 | msg = await Sensor.stop(rsp.sensor); 45 | if (msg) { 46 | rsp.stopMessages.push(msg); 47 | } 48 | if (Service.getRunState() === RunState.ACTIVE) { 49 | msg = await Sensor.start(rsp.sensor); 50 | if (msg) { 51 | rsp.startMessages.push(msg); 52 | } 53 | } 54 | } 55 | } 56 | return res.status(200).json(rsp); 57 | } catch (err) { 58 | logger.error(`upserting bulk : ${err.toString()}`); 59 | return res.status(500).json({message: err.message}); 60 | } 61 | } 62 | 63 | async function upsertOne(req, res) { 64 | try { 65 | const rsp = {sensor: {}}; 66 | rsp.sensor = await Sensor.upsertOne(req.body); 67 | if (rsp.sensor.connected) { 68 | // Stop any reading that might be in progress with old config 69 | rsp.stopMessages = await Sensor.stop(rsp.sensor); 70 | if (Service.getRunState() === RunState.ACTIVE) { 71 | rsp.stopMessages = await Sensor.start(rsp.sensor); 72 | } 73 | } 74 | return res.status(200).json(rsp); 75 | } catch (err) { 76 | logger.error(`upserting one : ${err.toString()}`); 77 | return res.status(500).json({message: err.message}); 78 | } 79 | } 80 | 81 | async function deleteOne(req, res) { 82 | try { 83 | const sensor = await Sensor.deleteOne(req.params.deviceId); 84 | return res.status(200).json(sensor); 85 | } catch (err) { 86 | logger.error(`deleting one : ${err.toString()}`); 87 | return res.status(400).json({message: err.message}); 88 | } 89 | } 90 | 91 | async function rebootAll(req, res) { 92 | try { 93 | const statuses = await Sensor.commandAll('reboot'); 94 | return res.status(202).json(statuses); 95 | } catch (err) { 96 | logger.error(`rebooting all : ${err.toString()}`); 97 | return res.status(500).json({message: err.message}); 98 | } 99 | } 100 | 101 | async function rebootOne(req, res) { 102 | try { 103 | const sensor = await Sensor.getOne(req.params.deviceId); 104 | let status = await Sensor.reboot(sensor); 105 | if (!status) { status = 'Reboot in progress';} 106 | const statusRsp = { deviceId: sensor.deviceId, status: status }; 107 | return res.status(202).json(statusRsp); 108 | } catch (err) { 109 | logger.error(`rebooting one : ${err.toString()}`); 110 | return res.status(400).json({message: 'Bad Request'}); 111 | } 112 | } 113 | 114 | async function getRunState(req, res) { 115 | try { 116 | return res.status(200).json({ runState: Service.getRunState()}); 117 | } catch (err) { 118 | logger.error(`getting run state : ${err.toString()}`); 119 | return res.status(400).json({message: 'Bad Request'}); 120 | } 121 | } 122 | 123 | async function putRunState(req, res) { 124 | try { 125 | const [runState, statuses] = await Service.setRunState(req.body.runState); 126 | return res.status(200).json({ runState: runState, statuses: statuses}); 127 | } catch (err) { 128 | logger.error(`putting run state : ${err.toString()}`); 129 | return res.status(500).json({message: err.message}); 130 | } 131 | } 132 | 133 | module.exports = { 134 | getAll, 135 | getOne, 136 | upsertBulk, 137 | upsertOne, 138 | deleteOne, 139 | rebootAll, 140 | rebootOne, 141 | getRunState, 142 | putRunState, 143 | }; 144 | -------------------------------------------------------------------------------- /controller/src/sensors/service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const Dnssd = require('dnssd'); 7 | const MqttConfig = require('../mqtt/config'); 8 | const MqttService = require('../mqtt/service'); 9 | const RunState = require('./run-state'); 10 | const Sensor = require('./sensor'); 11 | const ipminjMqtt = require('./impinj/mqtt'); 12 | const logger = require('../logger')('sensor-service'); 13 | 14 | MqttService.subscribeWill(handleWillMsg); 15 | 16 | async function handleWillMsg(deviceId) { 17 | await Sensor.onDisconnect(deviceId); 18 | } 19 | 20 | // Looking for all llrp readers 21 | const dnssdServiceType = 'llrp'; 22 | const dnssdBrowser = Dnssd.Browser(Dnssd.tcp(dnssdServiceType)); 23 | dnssdBrowser.on('serviceUp', service => { 24 | void onDnssdEvent('UP', service); 25 | }); 26 | dnssdBrowser.on('serviceDown', service => { 27 | void onDnssdEvent('DOWN', service); 28 | }); 29 | 30 | async function onDnssdEvent(event, service) { 31 | const str = JSON.stringify(service); 32 | const obj = JSON.parse(str); 33 | const hostname = obj.name; 34 | const ip4Address = obj.addresses[0]; 35 | switch (event) { 36 | case 'UP': 37 | await onDnssdServiceUp(hostname, ip4Address); 38 | break; 39 | case 'DOWN': 40 | await onDnssdServiceDown(hostname, ip4Address); 41 | break; 42 | default: 43 | } 44 | } 45 | 46 | async function onDnssdServiceUp(hostname, ip4Address) { 47 | const sensor = await Sensor.onConnect(hostname, ip4Address); 48 | if (!sensor) { return; } 49 | // Stop any rogue reading in progress 50 | await Sensor.stop(sensor); 51 | await Sensor.configureMqtt(sensor, getImpinjMqttCfg(sensor)); 52 | if (runState === RunState.ACTIVE) { 53 | // At this point, any newly created sensors have not yet been configured. 54 | // Once configured, they will be in the database with a valid behavior and 55 | // antennaPorts assigned. 56 | // startSensor will check for the proper conditions 57 | await Sensor.start(sensor); 58 | } 59 | } 60 | 61 | async function onDnssdServiceDown(hostname, ip4Address) { 62 | await Sensor.onDisconnect(hostname, ip4Address); 63 | } 64 | 65 | function getImpinjMqttCfg(sensor) { 66 | const impinjCfg = ipminjMqtt.getDefault(); 67 | impinjCfg.brokerHostname = MqttConfig.getDownstreamHost(); 68 | impinjCfg.brokerPort = MqttConfig.getDownstreamPort(); 69 | impinjCfg.username = MqttConfig.getDownstreamUsername(); 70 | impinjCfg.password = MqttConfig.getDownstreamPassword(); 71 | // tag reads are events for Impinj, but the data stream for us 72 | impinjCfg.eventTopic = MqttConfig.Topic.data; 73 | impinjCfg.willTopic = MqttConfig.Topic.will; 74 | // seems can't set the willMessage message to a json string 75 | // sensorCfg.willMessage = JSON.stringify({sensorId: sensor.deviceId}) 76 | impinjCfg.willMessage = sensor.deviceId; 77 | impinjCfg.clientId = sensor.deviceId.replaceAll('-', ''); 78 | return impinjCfg; 79 | } 80 | 81 | let runState = RunState.ACTIVE; 82 | let statusInterval = null; 83 | 84 | async function start() { 85 | try { 86 | await Sensor.updateCache(); 87 | await syncSensorStatus(); 88 | statusInterval = setInterval(syncSensorStatus, 30000); 89 | logger.info('started syncSensorStatus'); 90 | dnssdBrowser.start(); 91 | logger.info('started dnssdBrowser'); 92 | } catch (err) { 93 | err.message = `sensor-service start unsuccessful ${err.message}`; 94 | throw err; 95 | } 96 | } 97 | 98 | async function stop() { 99 | try { 100 | dnssdBrowser.stop(); 101 | logger.info('stopped dnssdBrowser'); 102 | clearInterval(statusInterval); 103 | logger.info('stopped syncSensorStatus'); 104 | await Sensor.persistCache(); 105 | } catch (err) { 106 | err.message = `sensor-service stop unsuccessful ${err.message}`; 107 | throw err; 108 | } 109 | } 110 | 111 | async function syncSensorStatus() { 112 | const sensors = await Sensor.getAll(); 113 | for (const sensor of sensors) { 114 | await Sensor.synchronizeStatus(sensor); 115 | } 116 | } 117 | 118 | function getRunState() { 119 | return runState; 120 | } 121 | 122 | async function setRunState(nextState) { 123 | logger.info(`setting run state [${runState}] to [${nextState}]`); 124 | let statuses; 125 | switch (nextState) { 126 | case RunState.INACTIVE: 127 | statuses = await Sensor.commandAll('stop'); 128 | break; 129 | case RunState.ACTIVE: 130 | statuses = await Sensor.commandAll('start'); 131 | break; 132 | } 133 | runState = nextState; 134 | return [runState, statuses]; 135 | } 136 | 137 | module.exports = { 138 | start, 139 | stop, 140 | getRunState, 141 | setRunState 142 | }; 143 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/ChannelFreqs.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 62 | 63 | 165 | -------------------------------------------------------------------------------- /web-ui/src/sensor/control/Sensors.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 40 | 41 | 172 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/Triggers.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 87 | 88 | 178 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/AntennaConfig.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 131 | 132 | 181 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/Behavior.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 95 | 96 | 182 | 183 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2022 Intel Corporation 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | # 6 | proj_dir="$( cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 7 | controller_cert_dir="${proj_dir}/controller/run/certs" 8 | web_ui_cert_dir="${proj_dir}/web-ui/run/certs" 9 | 10 | usage() { 11 | echo 12 | echo "-----------------------------------------------------------------" 13 | echo "Usage: ${BASH_SOURCE[0]##*/} [args]" 14 | echo "-----------------------------------------------------------------" 15 | echo " no args ... controller and web-ui only http." 16 | echo " -s, --https" 17 | echo " generates certificates (see -c option)" 18 | echo " controller and web-ui https" 19 | echo " -f, --foreground" 20 | echo " docker-compose in foreground with logs to console" 21 | echo " -l, --logs [controller,web-ui,db,mqtt]" 22 | echo " displays the docker log of the container" 23 | echo " -f option will follow(tail) the log" 24 | echo " -q, --quit" 25 | echo " -c, --certs" 26 | echo " generates self signed certs for controller and web-ui" 27 | echo " ${controller_cert_dir}" 28 | echo " ${web_ui_cert_dir}" 29 | echo " -d, --dev" 30 | echo " only starts the postgres-db and mqtt-broker containers" 31 | echo " -b, --build" 32 | echo " triggers rebuild of the docker images" 33 | echo " --clean" 34 | echo " stops all services and removes the builder images" 35 | echo " --clean-all" 36 | echo " stops all services and removes all sensor-controller images" 37 | echo " -h | --help" 38 | echo "-----------------------------------------------------------------" 39 | exit 255 40 | } 41 | 42 | notify_dependencies() { 43 | echo 44 | echo "Be sure docker and docker-compose are installed" 45 | echo "Unable to continue..." 46 | echo 47 | exit 255 48 | } 49 | 50 | # need docker && docker-compose to be installed 51 | ( command docker -v &> /dev/null && \ 52 | command docker-compose -v &> /dev/null) \ 53 | || notify_dependencies 54 | 55 | 56 | gen_cert() { 57 | sudo docker run -it --rm \ 58 | -v rfid-certs:/certs \ 59 | -v ${proj_dir}/:/tmp/bin/ \ 60 | -w /certs \ 61 | --entrypoint /tmp/bin/gen-cert.sh \ 62 | alpine/openssl ${1} 63 | } 64 | 65 | confirm_certs() { 66 | gen_cert "controller.rfid.com" 67 | gen_cert "web-ui.rfid.com" 68 | } 69 | 70 | clean_builders() { 71 | echo "stopping services ..." 72 | docker-compose down 73 | echo "removing runtime and builder images ..." 74 | sudo docker rmi \ 75 | sensor-controller/web-ui-builder:latest \ 76 | sensor-controller/web-ui:latest \ 77 | sensor-controller/controller:latest 78 | } 79 | 80 | clean_everything() { 81 | clean_builders 82 | echo "removing base (node_modules) images ..." 83 | sudo docker rmi \ 84 | sensor-controller/web-ui-base:latest \ 85 | sensor-controller/controller-base:latest 86 | } 87 | 88 | # parse command line 89 | build=false 90 | clean=false 91 | clean_all=false 92 | run_dev=false 93 | run_https=false 94 | container_log='' 95 | follow_log='' 96 | background_flag='-d' 97 | 98 | while (( "$#" )); do 99 | case "$1" in 100 | -b | --build) build=true ;; 101 | -c | --certs) 102 | confirm_certs 103 | exit ;; 104 | --clean) clean=true ;; 105 | --clean-all) clean_all=true ;; 106 | -d | --dev) run_dev=true ;; 107 | -f | --foreground) 108 | background_flag='' 109 | follow_log='--follow' ;; 110 | --follow) 111 | follow_log='--follow' ;; 112 | -h | --help) usage ;; 113 | -l | --logs) 114 | case "$2" in 115 | 'controller') container_log='sensor-controller_controller' ;; 116 | 'db') container_log='sensor-controller_postgres-db' ;; 117 | 'mqtt') container_log='sensor-controller_mqtt-broker' ;; 118 | 'web-ui') container_log='sensor-controller_web-ui' ;; 119 | '') container_log='sensor-controller_controller' ;; 120 | *) 121 | echo "UNKNOWN LOG OPTION ${1}" 122 | usage ;; 123 | esac ;; 124 | -s | --https) run_https=true ;; 125 | -q | --quit) 126 | docker-compose down 127 | exit ;; 128 | *) 129 | echo 130 | echo "UNKNOWN OPTION: ${1}" 131 | echo 132 | usage ;; 133 | esac 134 | shift 135 | done 136 | 137 | if [[ "${clean}" == true ]]; then 138 | clean_builders 139 | elif [[ "${clean_all}" == true ]]; then 140 | clean_everything 141 | elif [[ "${build}" == true ]]; then 142 | echo "building controller-base" 143 | docker build --target base -t sensor-controller/controller-base:latest ./controller 144 | echo "building controller" 145 | docker build --target prod -t sensor-controller/controller:latest ./controller 146 | echo "building web-ui-base" 147 | docker build --target base -t sensor-controller/web-ui-base:latest ./web-ui 148 | echo "building web-ui-builder" 149 | docker build --target builder -t sensor-controller/web-ui-builder:latest ./web-ui 150 | echo "building web-ui" 151 | docker build --target prod -t sensor-controller/web-ui:latest ./web-ui 152 | elif [[ "${container_log}" != '' ]]; then 153 | docker logs ${container_log} ${follow_log} 154 | elif [[ "${run_dev}" == true ]]; then 155 | echo "running postgres-db and mqtt-broker" 156 | docker-compose up ${background_flag} postgres-db mqtt-broker 157 | elif [[ "${run_https}" == true ]]; then 158 | echo "running https" 159 | confirm_certs 160 | docker-compose \ 161 | -f docker-compose.yml \ 162 | -f docker-compose-https.yml \ 163 | up ${background_flag} 164 | else 165 | echo "running simple" 166 | docker-compose up ${background_flag} 167 | fi 168 | 169 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/preset.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const fs = require('fs'); 7 | 8 | const ToggleString = Object.freeze({ 9 | ENABLED: 'enabled', 10 | DISABLED: 'disabled' 11 | }); 12 | 13 | const AntennaIdentifier = Object.freeze({ 14 | ANTENNA_PORT: 'antennaPort', 15 | ANTENNA_NAME: 'antennaName' 16 | }); 17 | 18 | const TagIdentifier = Object.freeze({ 19 | EPC: 'epc', 20 | TID: 'tid' 21 | }); 22 | 23 | const TagFilterAction = Object.freeze({ 24 | INCLUDE: 'include', 25 | EXCLUDE: 'exclude' 26 | }); 27 | 28 | const TagMemoryBank = Object.freeze({ 29 | EPC: 'epc', 30 | TID: 'tid', 31 | USER: 'user', 32 | RESERVED: 'reserved' 33 | }); 34 | 35 | const FilterVerification = Object.freeze({ 36 | ACTIVE: 'active', 37 | DISABLED: 'disabled' 38 | }); 39 | 40 | const FilterLink = Object.freeze({ 41 | UNION: 'union', 42 | INTERSECTION: 'intersection' 43 | }); 44 | 45 | const InventorySession = Object.freeze({ 46 | SESSION_0: 0, 47 | SESSION_1: 1, 48 | SESSION_2: 2, 49 | SESSION_3: 3 50 | }); 51 | 52 | const InventorySearchMode = Object.freeze({ 53 | SINGLE_TARGET: 'single-target', 54 | DUAL_TARGET: 'dual-target', 55 | SINGLE_TARGET_WITH_TAG_FOCUS: 'single-target-with-tagfocus', 56 | SINGLE_TARGET_B_TO_A: 'single-target-b-to-a', 57 | DUAL_TARGET_WITH_B_TO_A_SELECT: 'dual-target-with-b-to-a-select' 58 | }); 59 | 60 | const GpiTransition = Object.freeze({ 61 | HIGH_TO_LOW: 'high-to-low', 62 | LOW_TO_HIGH: 'low-to-high' 63 | }); 64 | 65 | const CommonEventConfiguration = { 66 | hostname: ToggleString.ENABLED 67 | }; 68 | 69 | const TagReportingConfiguration = { 70 | reportingIntervalSeconds: 0, 71 | tagCacheSize: 2048, 72 | antennaIdentifier: AntennaIdentifier.ANTENNA_PORT, 73 | tagIdentifier: TagIdentifier.EPC 74 | }; 75 | 76 | const TagInventoryEventConfiguration = { 77 | tagReporting: {}, 78 | epc: ToggleString.DISABLED, 79 | epcHex: ToggleString.ENABLED, 80 | tid: ToggleString.DISABLED, 81 | tidHex: ToggleString.ENABLED, 82 | antennaPort: ToggleString.ENABLED, 83 | transmitPowerCdbm: ToggleString.ENABLED, 84 | peakRssiCdbm: ToggleString.ENABLED, 85 | frequency: ToggleString.ENABLED, 86 | pc: ToggleString.DISABLED, 87 | lastSeenTime: ToggleString.ENABLED, 88 | phaseAngle: ToggleString.ENABLED 89 | }; 90 | 91 | const InventoryEventConfiguration = { 92 | common: {}, 93 | tagInventory: {} 94 | }; 95 | 96 | const TagFilter = { 97 | action: TagFilterAction.INCLUDE, 98 | tagMemoryBank: TagMemoryBank.EPC, 99 | bitOffset: 0, 100 | mask: '0123456789ABCDEFabcdef', 101 | maskLength: 0 102 | }; 103 | 104 | const InventoryFilterConfiguration = { 105 | filters: [], 106 | filterLink: FilterLink.UNION, 107 | filterVerification: FilterVerification.DISABLED 108 | }; 109 | 110 | const TransmitPowerSweepConfiguration = { 111 | minimumPowerCdbm: 0, 112 | stepSizeCdb: 0 113 | }; 114 | 115 | const TagAuthentication = { 116 | messageHex: '0123456789ABCDEFabcdef' 117 | }; 118 | 119 | const TagMemoryRead = { 120 | memoryBank: TagMemoryBank.EPC, 121 | wordOffset: 0, 122 | wordCount: 0 123 | }; 124 | 125 | const InventoryAntennaConfiguration = { 126 | antennaPort: 0, 127 | transmitPowerCdbm: 0, 128 | rfMode: 100, 129 | inventorySession: InventorySession.SESSION_0, 130 | inventorySearchMode: InventorySearchMode.SINGLE_TARGET, 131 | estimatedTagPopulation: 2048, 132 | // The following are optional. Add them AS NEEDED! 133 | // antennaName, 134 | // filtering: {}, 135 | // powerSweeping: {}, 136 | // fastId: ToggleString.DISABLED, 137 | // receiveSensitivityDbm: 0, 138 | // tagAuthentication: {}, 139 | // tagMemoryReads: [], 140 | // tagAccessPasswordHex: "0123456789ABCDEFabcdef" 141 | }; 142 | 143 | const GpiTransitionEvent = { 144 | gpi: 0, 145 | transition: GpiTransition.HIGH_TO_LOW 146 | }; 147 | 148 | const InventoryStartTrigger = { 149 | gpiTransitionEvent: {} 150 | }; 151 | 152 | const InventoryStopTrigger = { 153 | gpiTransitionEvent: {} 154 | }; 155 | 156 | const Preset = { 157 | eventConfig: {}, 158 | antennaConfigs: [], 159 | // The following are optional. Add them AS NEEDED! 160 | // channelFrequenciesKHz: [], 161 | // startTriggers: [], 162 | // stopTriggers: [] 163 | }; 164 | 165 | function getDefault(numberOfPorts, powerLevelCdbm) { 166 | const preset = Object.assign({}, Preset); 167 | preset.eventConfig = Object.assign({}, InventoryEventConfiguration); 168 | preset.eventConfig.common = Object.assign({}, CommonEventConfiguration); 169 | preset.eventConfig.tagInventory = Object.assign({}, TagInventoryEventConfiguration); 170 | preset.eventConfig.tagInventory.tagReporting = Object.assign({}, TagReportingConfiguration); 171 | 172 | for (let x = 0; x < numberOfPorts; x++) { 173 | preset.antennaConfigs[x] = Object.assign({}, InventoryAntennaConfiguration); 174 | preset.antennaConfigs[x].antennaPort = (x + 1); 175 | preset.antennaConfigs[x].transmitPowerCdbm = powerLevelCdbm; 176 | } 177 | 178 | return preset; 179 | } 180 | 181 | function getFromBehaviorJsonFile(filename) { 182 | const filenameWithRelativePath = `./config/behaviors/${ filename}`; 183 | try { 184 | const behaviorBuffer = fs.readFileSync(filenameWithRelativePath); 185 | const behavior = JSON.parse(behaviorBuffer.toString()); 186 | return behavior.preset; 187 | } catch (err) { 188 | return err; 189 | } 190 | } 191 | 192 | module.exports = { 193 | ToggleString, 194 | AntennaIdentifier, 195 | TagIdentifier, 196 | TagFilterAction, 197 | TagMemoryBank, 198 | FilterLink, 199 | InventorySession, 200 | InventorySearchMode, 201 | GpiTransition, 202 | CommonEventConfiguration, 203 | TagReportingConfiguration, 204 | TagInventoryEventConfiguration, 205 | InventoryEventConfiguration, 206 | TagFilter, 207 | InventoryFilterConfiguration, 208 | TransmitPowerSweepConfiguration, 209 | TagAuthentication, 210 | TagMemoryRead, 211 | InventoryAntennaConfiguration, 212 | GpiTransitionEvent, 213 | InventoryStartTrigger, 214 | InventoryStopTrigger, 215 | Preset, 216 | getDefault, 217 | getFromBehaviorJsonFile 218 | }; 219 | -------------------------------------------------------------------------------- /controller/src/firmware/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const CmdService = require('../sensors/impinj/rest-cmd-service'); 7 | const formidable = require('formidable'); 8 | const db = require('../persist/db'); 9 | const fs = require('fs'); 10 | const logger = require('../logger')('firmware-controller'); 11 | 12 | const firmwareDir = process.env.DIR_FIRMWARE || './run/firmware'; 13 | if (!fs.existsSync(firmwareDir)) { 14 | fs.mkdirSync(firmwareDir, {recursive: true}); 15 | logger.info(`created ${firmwareDir}`); 16 | } 17 | 18 | function returnError500(deviceId, res, err) { 19 | logger.error(err.toString()); 20 | let msg; 21 | if (err.response) { 22 | msg = `device ${deviceId} ` + 23 | `returned status ${err.response.status} ${err.response.statusText}`; 24 | } else { 25 | msg = err.message; 26 | } 27 | return res.status(500).json({message: msg}); 28 | } 29 | 30 | async function getImages(req, res) { 31 | try { 32 | const fileStats = []; 33 | const filenames = fs.readdirSync(firmwareDir); 34 | for (const fname of filenames) { 35 | const stats = fs.statSync(`${firmwareDir}/${fname}`); 36 | fileStats.push({ 37 | name: fname, 38 | size: stats.size, 39 | mtime: stats.mtime, 40 | }); 41 | } 42 | return res.status(200).json(fileStats); 43 | } catch (err) { 44 | returnError500(null, res, err); 45 | } 46 | } 47 | 48 | async function postImage(req, res) { 49 | try { 50 | let finalPath = ''; 51 | let fileNameUploaded = ''; 52 | const form = new formidable.IncomingForm({}); 53 | form.parse(req); 54 | form.on('fileBegin', function (name, file) { 55 | file.path = `${firmwareDir}/${file.name}`; 56 | finalPath = file.path; 57 | }); 58 | form.on('file', function (name, file) { 59 | logger.info(`image uploaded ${file.name}`); 60 | fileNameUploaded = file.name; 61 | }); 62 | form.on('end', function () { 63 | return res.status(202).json({filename: fileNameUploaded}); 64 | }); 65 | form.on('error', function (err) { 66 | logger.error(`posting image : ${err.message}`); 67 | try { 68 | if (finalPath) { 69 | fs.unlinkSync(finalPath); 70 | } 71 | } catch (err) { 72 | logger.error(`deleting file uploaded with errors : ${err.message}`); 73 | } 74 | return res.status(400).json({message: err.message}); 75 | }); 76 | } catch (err) { 77 | returnError500(null, res, err); 78 | } 79 | } 80 | 81 | async function deleteImage(req, res) { 82 | if (!(req.body.filename)) { 83 | return res.status(400).json({message: 'missing required fields: filename'}); 84 | } 85 | const filePath = `${firmwareDir}/${req.body.filename}`; 86 | if (!fs.existsSync(filePath)) { 87 | return res.status(400).json({message: `unkown firmware file: ${req.body.filename}`}); 88 | } 89 | try { 90 | fs.unlinkSync(filePath); 91 | logger.info(`deleted image ${filePath}`); 92 | return res.status(200).json({filename: req.body.filename}); 93 | } catch (err) { 94 | returnError500(null, res, err); 95 | } 96 | } 97 | 98 | async function getSensorsInfo(req, res) { 99 | try { 100 | const infos = []; 101 | const sensors = await db.sensors.findAll({where: {connected: true,}}); 102 | for (const sensor of sensors) { 103 | try { 104 | const resp = await CmdService.getFirmwareInfo(sensor.ip4Address); 105 | if (resp.data) { 106 | resp.data['deviceId'] = sensor.deviceId; 107 | infos.push(resp.data); 108 | } 109 | } catch (err) { 110 | returnError500(sensor.deviceId, res, err); 111 | return; 112 | } 113 | } 114 | return res.status(200).json(infos); 115 | } catch (err) { 116 | returnError500(null, res, err); 117 | } 118 | } 119 | 120 | async function postSensorsUpgrade(req, res) { 121 | 122 | if (!(req.body.filename) || !(req.body.deviceIds)) { 123 | return res.status(400).json({message: 'missing required fields: filename, deviceIds'}); 124 | } 125 | const filePath = `${firmwareDir}/${req.body.filename}`; 126 | if (!fs.existsSync(filePath)) { 127 | return res.status(404).json({message: `unkown firmware file: ${req.body.filename}`}); 128 | } 129 | try { 130 | const whereClause = { 131 | connected: true, 132 | deviceId: req.body.deviceIds, 133 | }; 134 | const sensors = await db.sensors.findAll({where: whereClause}); 135 | if (sensors.length <= 0) { 136 | const msg = {message: 'no connected sensors found for the provided deviceIds'}; 137 | return res.status(404).json(msg); 138 | } 139 | const deviceIds = []; 140 | for (const sensor of sensors) { 141 | try { 142 | CmdService.postFirmwareUpgrade(sensor.ip4Address, filePath); 143 | deviceIds.push(sensor.deviceId); 144 | logger.info(`posted upgrade : ${sensor.deviceId} : ${sensor.ip4Address}`); 145 | } catch (err) { 146 | logger.error(`posting upgrade : ${sensor.deviceId} : ${sensor.ip4Address} ${err}`); 147 | } 148 | } 149 | return res.status(202).json(deviceIds); 150 | } catch (err) { 151 | returnError500(null, res, err); 152 | } 153 | } 154 | 155 | async function getSensorsUpgrade(req, res) { 156 | try { 157 | const whereClause = { 158 | connected: true 159 | }; 160 | if (req.body.deviceIds) { 161 | whereClause['deviceIds'] = req.body.deviceIds; 162 | } 163 | const sensors = await db.sensors.findAll({where: whereClause}); 164 | const statuses = []; 165 | for (const sensor of sensors) { 166 | try { 167 | const sensorRes = await CmdService.getUpgradeStatus(sensor.ip4Address); 168 | if (sensorRes.data) { 169 | sensorRes.data['deviceId'] = sensor.deviceId; 170 | statuses.push(sensorRes.data); 171 | } 172 | } catch (err) { 173 | returnError500(sensor.deviceId, res, err); 174 | return; 175 | } 176 | } 177 | return res.status(200).json(statuses); 178 | } catch (err) { 179 | returnError500(null, res, err); 180 | } 181 | } 182 | 183 | module.exports = { 184 | getImages, 185 | postImage, 186 | deleteImage, 187 | getSensorsInfo, 188 | getSensorsUpgrade, 189 | postSensorsUpgrade 190 | }; 191 | -------------------------------------------------------------------------------- /controller/src/sensors/impinj/rest-cmd-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Intel Corporation 3 | * SPDX-License-Identifier: BSD-3-Clause 4 | */ 5 | 6 | const axios = require('axios'); 7 | const formData = require('form-data'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const logger = require('../../logger')('rest-cmd-service'); 11 | 12 | const apiVersion = '/api/v1'; 13 | 14 | const Path = Object.freeze({ 15 | status: '/status', 16 | mqtt: '/mqtt', 17 | profiles: '/profiles', 18 | stop: '/profiles/stop', 19 | presets: '/profiles/inventory/presets', 20 | system: '/system', 21 | hostname: '/system/hostname', 22 | image: '/system/image', 23 | upgrade: '/system/image/upgrade', 24 | interfaces: '/system/network/interfaces', 25 | power: '/system/power', 26 | region: '/system/region', 27 | reboot: '/system/reboot', 28 | time: '/system/time', 29 | ntp: '/system/time/ntp', 30 | ntpServers: '/system/time/ntp/servers' 31 | }); 32 | 33 | axios.defaults.headers['Authorization'] = `Basic ${process.env.IMPINJ_BASIC_AUTH}`; 34 | axios.defaults.headers['Content-Type'] = 'application/json'; 35 | axios.defaults.timeout = 2000; 36 | 37 | function buildURL(host, path) { 38 | return `https://${host}${apiVersion}${path}`; 39 | } 40 | 41 | async function getStatus(host) { 42 | return await axios.get(buildURL(host, Path.status)); 43 | } 44 | 45 | async function getMqttSettings(host) { 46 | return await axios.get(buildURL(host, Path.mqtt)); 47 | } 48 | 49 | async function putMqttSettings(host, data) { 50 | return await axios.put(buildURL(host, Path.mqtt), data); 51 | } 52 | 53 | async function getProfiles(host) { 54 | return await axios.get(buildURL(host, Path.profiles)); 55 | } 56 | 57 | async function stopPreset(host) { 58 | return await axios.post(buildURL(host, Path.stop), null); 59 | } 60 | 61 | async function getPresets(host) { 62 | return await axios.get(buildURL(host, Path.presets)); 63 | } 64 | 65 | async function getPreset(host, presetId) { 66 | return await axios.get(buildURL(host, `${Path.presets}/${presetId}`)); 67 | } 68 | 69 | async function putPreset(host, presetId, data) { 70 | return await axios.put(buildURL(host, `${Path.presets}/${presetId}`), data); 71 | } 72 | 73 | async function deletePreset(host, presetId) { 74 | return await axios.delete(buildURL(host, `${Path.presets}/${presetId}`)); 75 | } 76 | 77 | async function startPreset(host, presetId) { 78 | return await axios.post(buildURL(host, `${Path.presets}/${presetId}/start`), null); 79 | } 80 | 81 | async function getSystemInfo(host) { 82 | return await axios.get(buildURL(host, Path.system)); 83 | } 84 | 85 | async function getHostname(host) { 86 | return await axios.get(buildURL(host, Path.hostname)); 87 | } 88 | 89 | async function putHostname(host, data) { 90 | return await axios.put(buildURL(host, Path.hostname), data); 91 | } 92 | 93 | async function getFirmwareInfo(host) { 94 | return await axios.get(buildURL(host, Path.image)); 95 | } 96 | 97 | function postFirmwareUpgrade(host, filePath) { 98 | const fullPath = path.resolve(filePath); 99 | const fileName = fullPath.substring(fullPath.lastIndexOf('/') + 1, fullPath.length); 100 | const form = new formData(); 101 | form.append('upgradeFile', fs.createReadStream(fullPath), fileName); 102 | // using form submit is the only solution that handled the read stream 103 | // in a way that the Impinj sensor could handle. 104 | // Axios worked if the whole file was read into memory first with 105 | // fs.readFileSync() and then using form.getBuffer() for the data. 106 | form.submit({ 107 | protocol: 'https:', 108 | host: host, 109 | path: `${apiVersion}${Path.upgrade}`, 110 | headers: {Authorization: `Basic ${process.env.IMPINJ_BASIC_AUTH}`} 111 | }, function(err, res) { 112 | if (res) { 113 | logger.info(`post firmware upgrade response : ${res}`); 114 | } 115 | if (err) { 116 | logger.error(`post firmware upgrade response : ${err}`); 117 | } 118 | }); 119 | } 120 | 121 | async function getUpgradeStatus(host) { 122 | return await axios.get(buildURL(host, Path.upgrade)); 123 | } 124 | 125 | async function getDcPowerConfig(host) { 126 | return await axios.get(buildURL(host, Path.power)); 127 | } 128 | 129 | async function putDcPowerConfig(host, data) { 130 | return await axios.put(buildURL(host, Path.power), data); 131 | } 132 | 133 | async function getOperatingRegion(host) { 134 | return await axios.get(buildURL(host, Path.region)); 135 | } 136 | 137 | async function putOperatingRegion(host, data) { 138 | return await axios.put(buildURL(host, Path.region), data); 139 | } 140 | 141 | async function postReboot(host) { 142 | return await axios.post(buildURL(host, Path.reboot), null); 143 | } 144 | 145 | async function getSystemTime(host) { 146 | return await axios.get(buildURL(host, Path.time)); 147 | } 148 | 149 | async function putSystemTime(host, data) { 150 | return await axios.put(buildURL(host, Path.time), data); 151 | } 152 | 153 | async function getNtpStatus(host) { 154 | return await axios.get(buildURL(host, Path.ntp)); 155 | } 156 | 157 | async function putNtpActive(host) { 158 | return await axios.put(buildURL(host, Path.ntp), {active: true}); 159 | } 160 | 161 | async function getNtpServers(host) { 162 | return await axios.get(buildURL(host, Path.ntpServers)); 163 | } 164 | 165 | async function postNtpServer(host, ntpHost) { 166 | return await axios.post(buildURL(host, Path.ntpServers), {server: ntpHost}); 167 | } 168 | 169 | async function getNtpServer(host, serverId) { 170 | return await axios.get(buildURL(host, `${Path.ntpServers}/${serverId}`)); 171 | } 172 | 173 | async function deleteNtpServer(host, serverId) { 174 | return await axios.delete(buildURL(host, `${Path.ntpServers}/${serverId}`)); 175 | } 176 | 177 | module.exports = { 178 | getStatus, 179 | getMqttSettings, 180 | putMqttSettings, 181 | getProfiles, 182 | stopPreset, 183 | getPresets, 184 | getPreset, 185 | putPreset, 186 | deletePreset, 187 | startPreset, 188 | getSystemInfo, 189 | getHostname, 190 | putHostname, 191 | getFirmwareInfo, 192 | postFirmwareUpgrade, 193 | getUpgradeStatus, 194 | getDcPowerConfig, 195 | putDcPowerConfig, 196 | getOperatingRegion, 197 | putOperatingRegion, 198 | postReboot, 199 | getSystemTime, 200 | putSystemTime, 201 | getNtpStatus, 202 | putNtpActive, 203 | getNtpServers, 204 | postNtpServer, 205 | getNtpServer, 206 | deleteNtpServer 207 | }; 208 | -------------------------------------------------------------------------------- /web-ui/src/behaviors/Filtering.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 158 | 159 | 241 | --------------------------------------------------------------------------------