├── 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 |
7 |
8 |
12 |
16 |
17 |
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 |
7 |
25 |
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 |
7 |
8 |
9 |
10 | {{label}}
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
57 |
--------------------------------------------------------------------------------
/web-ui/src/App.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
36 |
37 |
38 |
39 |
43 |
44 |
45 |
46 |
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 |
7 |
8 |
9 |
16 |
17 |
21 |
22 |
23 |
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 |
7 |
11 |
15 |
20 |
21 |
22 |
23 |
24 |
31 |
37 |
38 |
39 |
40 |
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 |
7 |
8 |
9 |
10 | {{label}}
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
77 |
--------------------------------------------------------------------------------
/web-ui/src/components/OnOffSwitch.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
15 |
16 |
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 |
7 |
8 |
9 |
10 | {{label}}
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
33 |
34 |
35 |
36 |
37 |
38 |
84 |
--------------------------------------------------------------------------------
/web-ui/src/sensor/control/Sensor.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
{{ sensor.deviceId }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Status: {{ sensor.status }}
26 |
27 | {{ sensor.behaviorId }}
28 |
29 |
30 |
31 |
36 |
37 |
38 |
39 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
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 |
7 |
8 |
9 |
10 | Power Sweeping
11 |
12 |
18 |
19 |
20 |
21 |
22 |
56 |
57 |
58 |
59 |
100 |
--------------------------------------------------------------------------------
/web-ui/src/components/PageHeader.vue:
--------------------------------------------------------------------------------
1 | -->
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{link.label}}
26 |
31 |
32 |
33 |
34 | {{ this.$route.name }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
55 |
62 |
{{ msg }}
63 |
64 |
65 |
66 |
100 |
101 |
--------------------------------------------------------------------------------
/web-ui/src/components/JsonFileUpload.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
21 |
22 | Select the Json File
23 |
24 |
25 |
30 |
31 |
32 |
33 |
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 |
7 |
8 |
9 |
10 | Tag Memory Reads
11 |
12 |
18 |
19 |
20 |
21 |
22 |
80 |
81 |
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 |
7 |
8 | Items per Page
9 |
21 |
22 |
23 |
30 |
31 |
38 |
39 | Page {{ currentPage }} of {{ totalPages }}
40 |
41 |
48 |
49 |
56 |
57 |
58 |
136 |
--------------------------------------------------------------------------------
/web-ui/src/behaviors/RfModeSelect.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
Rf Mode
9 |
10 |
11 |
12 |
13 |
14 |
23 |
28 |
29 |
30 |
31 |
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 |
7 |
8 |
9 |
14 |
Authentication Required
15 |
{{ errorMsg }}
16 |
20 |
28 |
34 |
35 |
38 |
44 |
45 |
46 |
47 |
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 |
7 |
8 |
9 | Channel Frequencies
10 |
11 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
33 |
41 |
42 |
49 |
50 |
51 | Must be between 1 and 64
52 |
53 |
54 | Already in use
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
165 |
--------------------------------------------------------------------------------
/web-ui/src/sensor/control/Sensors.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
11 |
12 |
13 | Run State
14 |
18 |
19 |
20 |
21 |
22 |
23 |
27 |
32 |
33 |
34 |
{{ cmdStatuses[sensor.deviceId] }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
172 |
--------------------------------------------------------------------------------
/web-ui/src/behaviors/Triggers.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 | {{ label }}
10 |
13 |
21 |
22 |
23 |
85 |
86 |
87 |
88 |
178 |
--------------------------------------------------------------------------------
/web-ui/src/behaviors/AntennaConfig.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 | Port
11 |
12 |
19 |
20 |
21 |
22 |
31 |
32 |
33 |
34 |
44 |
45 |
49 |
50 |
60 |
61 |
65 |
66 |
70 |
71 |
75 |
76 |
81 |
82 |
89 |
90 |
100 |
101 |
108 |
109 |
113 |
114 |
121 |
122 |
129 |
130 |
131 |
132 |
181 |
--------------------------------------------------------------------------------
/web-ui/src/behaviors/Behavior.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 | {{ local.id }}
9 |
10 |
11 |
17 |
18 |
19 |
24 |
25 |
26 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
53 |
54 |
59 |
60 |
64 |
65 |
66 |
67 |
68 | Antenna Configurations
69 |
70 |
77 |
78 |
79 |
80 |
90 |
91 |
92 |
93 |
94 |
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 |
7 |
8 |
9 |
10 | Tag Filtering
11 |
12 |
19 |
20 |
21 |
22 |
Link
23 |
36 |
Verification
37 |
50 |
51 |
52 |
53 |
156 |
157 |
158 |
159 |
241 |
--------------------------------------------------------------------------------