├── Firmware ├── release │ ├── VERSION │ ├── .gitignore │ ├── changelog.md │ ├── templates │ │ └── changelog.md │ └── config.json ├── boards │ └── nrf52_pca10040.conf ├── src │ ├── init.h │ ├── n2_offload.h │ ├── flash.h │ ├── util.h │ ├── version.h │ ├── util.c │ ├── priorities.h │ ├── config_message.h │ ├── messagebuffer.h │ ├── config.h │ ├── init.c │ ├── gps.h │ ├── i2c_config.h │ ├── spi_config.h │ ├── fota_tlv.h │ ├── chipcap2.h │ ├── max14830.h │ ├── pinout.h │ ├── spi_config.c │ ├── gpio.h │ ├── i2c_config.c │ ├── fota.h │ ├── comms.h │ ├── flash.c │ ├── fota_tlv.c │ ├── nmealib.h │ ├── at_commands.h │ ├── chipcap2.c │ ├── gps_cache.h │ ├── gps_std.h │ └── opc_n3.h ├── .gitignore ├── nrf52_pca10040.overlay ├── Makefile ├── CMakeLists.txt ├── Kconfig ├── prj.conf └── README.md ├── server ├── pkg │ ├── pipeline │ │ ├── test │ │ │ ├── doc.go │ │ │ └── pipeline_test.go │ │ ├── pipeline.go │ │ ├── pipelog │ │ │ └── pipelog.go │ │ ├── root.go │ │ ├── persist │ │ │ └── persist.go │ │ ├── circular │ │ │ └── circularbuffer.go │ │ ├── calculate │ │ │ ├── calculate_test.go │ │ │ └── calculate.go │ │ ├── pipemqtt │ │ │ └── mqtt.go │ │ └── stream │ │ │ ├── client.go │ │ │ └── broker.go │ ├── aqpb │ │ └── doc.go │ ├── listener │ │ ├── listener.go │ │ ├── udplistener │ │ │ └── udplistener.go │ │ └── hordelistener │ │ │ └── hordelistener.go │ ├── api │ │ ├── api_test.go │ │ ├── stream_handler.go │ │ └── api.go │ ├── opts │ │ └── opts.go │ ├── store │ │ ├── sqlitestore │ │ │ ├── sqlitestore.go │ │ │ ├── cal.go │ │ │ └── schema.go │ │ └── store.go │ └── model │ │ ├── util.go │ │ ├── calc_test.go │ │ └── calc.go ├── doc │ ├── 03-use.md │ ├── aqs.pdf │ ├── 00-index.md │ ├── build-doc.sh │ ├── 04-sensors.md │ ├── 01-introduction.md │ └── 02-build.md ├── .gitignore ├── cmd │ └── aq │ │ ├── util.go │ │ ├── main.go │ │ ├── list.go │ │ ├── import.go │ │ ├── calibration.go │ │ ├── show.go │ │ └── fetch.go ├── go.mod ├── calibration-data │ ├── 16-000204.json │ ├── 16-000205.json │ ├── 16-000206.json │ ├── 16-000207.json │ ├── 16-000208.json │ ├── 16-000158.json │ ├── 16-000160.json │ ├── 16-000161.json │ ├── 16-000164.json │ ├── 16-000167.json │ ├── 16-000168.json │ ├── 16-000170.json │ ├── 16-000172.json │ ├── 16-000174.json │ ├── 16-000175.json │ ├── 16-000176.json │ ├── 16-000179.json │ ├── 16-000181.json │ ├── 16-000159.json │ ├── 16-000162.json │ ├── 16-000163.json │ ├── 16-000165.json │ ├── 16-000166.json │ ├── 16-000169.json │ ├── 16-000171.json │ ├── 16-000173.json │ ├── 16-000177.json │ ├── 16-000178.json │ ├── 16-000180.json │ └── 16-000182.json ├── Makefile └── README.md ├── .west └── config ├── images ├── AFE.jpg ├── OPC-N3.jpg ├── power.jpg ├── EE-NBIoT-02.jpg └── controller.jpg ├── hardware ├── powerboard │ ├── HighPowerBoard_v2.PrjPCBStructure │ ├── HighPowerBoard_v2.PcbDoc │ └── HighPowerBoard_v2.SchDoc └── ee-10-sensorboard │ ├── EE-06-main-board.PrjPCBStructure │ ├── TK_EE06.PcbDoc │ └── 2_EE-10_and_SIM_AFE_Interface.SchDoc ├── enclosure ├── images │ ├── node1.jpg │ └── node2.jpg ├── assembly │ ├── OPC-N3.f3d │ ├── assembly-2-v10.f3z │ ├── Alphasense-A4-AFE-3-way.f3d │ └── TP-electric-3310-217-0600.f3d ├── STL │ ├── PLA_AFE_spacer.stl │ ├── PLA_OPC_hood_a.stl │ ├── PLA_OPC_hood_b.stl │ ├── PLA_PCB_bracket.stl │ ├── PP_Gas_intake.stl │ ├── PLA_Sensor_frame.stl │ ├── PLA_Particle_intake_a.stl │ ├── PLA_Particle_intake_b.stl │ └── PP_AFE_mounting_block.stl └── README.md ├── .gitignore ├── common └── protobuf │ ├── aqconfig.options │ ├── aqconfig.proto │ └── aq.proto ├── manifest └── west.yml ├── calibration └── afe3 │ └── json │ ├── 16-000204.json │ ├── 16-000205.json │ ├── 16-000206.json │ ├── 16-000207.json │ ├── 16-000208.json │ ├── 16-000175.json │ ├── 16-000158.json │ ├── 16-000159.json │ ├── 16-000161.json │ ├── 16-000162.json │ ├── 16-000163.json │ ├── 16-000165.json │ ├── 16-000166.json │ ├── 16-000167.json │ ├── 16-000168.json │ ├── 16-000169.json │ ├── 16-000173.json │ ├── 16-000176.json │ ├── 16-000178.json │ ├── 16-000179.json │ ├── 16-000180.json │ ├── 16-000181.json │ ├── 16-000182.json │ ├── 16-000160.json │ ├── 16-000164.json │ ├── 16-000170.json │ ├── 16-000171.json │ ├── 16-000172.json │ ├── 16-000174.json │ └── 16-000177.json └── README.md /Firmware/release/VERSION: -------------------------------------------------------------------------------- 1 | 0.1.6 -------------------------------------------------------------------------------- /Firmware/boards/nrf52_pca10040.conf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Firmware/release/.gitignore: -------------------------------------------------------------------------------- 1 | archives 2 | -------------------------------------------------------------------------------- /server/pkg/pipeline/test/doc.go: -------------------------------------------------------------------------------- 1 | package pipelinetest 2 | -------------------------------------------------------------------------------- /Firmware/src/init.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | void init_board(void); 4 | -------------------------------------------------------------------------------- /server/doc/03-use.md: -------------------------------------------------------------------------------- 1 | # AQS Usage 2 | 3 | ** Usage doc ** 4 | 5 | \newpage 6 | 7 | -------------------------------------------------------------------------------- /Firmware/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | aq_fota.pem 3 | mcuboot 4 | .DS_Store 5 | .vscode/ 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Firmware/release/changelog.md: -------------------------------------------------------------------------------- 1 | ## v{{ .Version }}: {{ .Name }} 2 | 3 | This is the first version 4 | -------------------------------------------------------------------------------- /Firmware/src/n2_offload.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define NEXT_FREE_PORT 6000 4 | 5 | void wait_for_sockets(); -------------------------------------------------------------------------------- /.west/config: -------------------------------------------------------------------------------- 1 | [manifest] 2 | path = manifest 3 | [zephyr] 4 | base = deps/zephyr 5 | base-prefer = configfile 6 | -------------------------------------------------------------------------------- /images/AFE.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/images/AFE.jpg -------------------------------------------------------------------------------- /hardware/powerboard/HighPowerBoard_v2.PrjPCBStructure: -------------------------------------------------------------------------------- 1 | Record=TopLevelDocument|FileName=HighPowerBoard_v2.SchDoc 2 | -------------------------------------------------------------------------------- /images/OPC-N3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/images/OPC-N3.jpg -------------------------------------------------------------------------------- /images/power.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/images/power.jpg -------------------------------------------------------------------------------- /server/doc/aqs.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/server/doc/aqs.pdf -------------------------------------------------------------------------------- /images/EE-NBIoT-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/images/EE-NBIoT-02.jpg -------------------------------------------------------------------------------- /images/controller.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/images/controller.jpg -------------------------------------------------------------------------------- /enclosure/images/node1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/images/node1.jpg -------------------------------------------------------------------------------- /enclosure/images/node2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/images/node2.jpg -------------------------------------------------------------------------------- /hardware/ee-10-sensorboard/EE-06-main-board.PrjPCBStructure: -------------------------------------------------------------------------------- 1 | Record=TopLevelDocument|FileName=2_EE-10_and_SIM_AFE_Interface.SchDoc 2 | -------------------------------------------------------------------------------- /enclosure/assembly/OPC-N3.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/assembly/OPC-N3.f3d -------------------------------------------------------------------------------- /enclosure/STL/PLA_AFE_spacer.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PLA_AFE_spacer.stl -------------------------------------------------------------------------------- /enclosure/STL/PLA_OPC_hood_a.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PLA_OPC_hood_a.stl -------------------------------------------------------------------------------- /enclosure/STL/PLA_OPC_hood_b.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PLA_OPC_hood_b.stl -------------------------------------------------------------------------------- /enclosure/STL/PLA_PCB_bracket.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PLA_PCB_bracket.stl -------------------------------------------------------------------------------- /enclosure/STL/PP_Gas_intake.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PP_Gas_intake.stl -------------------------------------------------------------------------------- /enclosure/STL/PLA_Sensor_frame.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PLA_Sensor_frame.stl -------------------------------------------------------------------------------- /enclosure/assembly/assembly-2-v10.f3z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/assembly/assembly-2-v10.f3z -------------------------------------------------------------------------------- /Firmware/src/flash.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | int write_firmware_block(const uint8_t *data, uint16_t data_len, bool first_block, bool last_block, size_t total_size); -------------------------------------------------------------------------------- /enclosure/STL/PLA_Particle_intake_a.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PLA_Particle_intake_a.stl -------------------------------------------------------------------------------- /enclosure/STL/PLA_Particle_intake_b.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PLA_Particle_intake_b.stl -------------------------------------------------------------------------------- /enclosure/STL/PP_AFE_mounting_block.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/STL/PP_AFE_mounting_block.stl -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | data 3 | dist 4 | cmd/listen/listen 5 | cmd/aq/aq 6 | .telenor-nbiot 7 | aq.db 8 | aq.db* 9 | logs 10 | *.bz2 11 | scripts 12 | -------------------------------------------------------------------------------- /server/pkg/aqpb/doc.go: -------------------------------------------------------------------------------- 1 | // Package aqpb ... 2 | //go:generate protoc -I=../../../common/protobuf/ --go_out=. ../../../common/protobuf/aq.proto 3 | package aqpb 4 | -------------------------------------------------------------------------------- /hardware/ee-10-sensorboard/TK_EE06.PcbDoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/hardware/ee-10-sensorboard/TK_EE06.PcbDoc -------------------------------------------------------------------------------- /hardware/powerboard/HighPowerBoard_v2.PcbDoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/hardware/powerboard/HighPowerBoard_v2.PcbDoc -------------------------------------------------------------------------------- /hardware/powerboard/HighPowerBoard_v2.SchDoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/hardware/powerboard/HighPowerBoard_v2.SchDoc -------------------------------------------------------------------------------- /enclosure/assembly/Alphasense-A4-AFE-3-way.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/assembly/Alphasense-A4-AFE-3-way.f3d -------------------------------------------------------------------------------- /enclosure/assembly/TP-electric-3310-217-0600.f3d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/enclosure/assembly/TP-electric-3310-217-0600.f3d -------------------------------------------------------------------------------- /server/pkg/listener/listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | // Listener interface 4 | type Listener interface { 5 | Start() error 6 | Shutdown() 7 | WaitForShutdown() 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Things added by west-managed repos 2 | deps/ 3 | modules/ 4 | tools/ 5 | build/ 6 | Firmware/mcuboot/ 7 | Firmware/.apikey 8 | Firmware/nanopb/ 9 | .DS_Store 10 | .vscode/ 11 | -------------------------------------------------------------------------------- /Firmware/src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef _UTIL_H_ 2 | #define _UTIL_H_ 3 | 4 | // atoll is not supported out of the box in zephyr. We'll roll our own... 5 | // uint64_t atoll(char *number_string); 6 | 7 | #endif -------------------------------------------------------------------------------- /hardware/ee-10-sensorboard/2_EE-10_and_SIM_AFE_Interface.SchDoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExploratoryEngineering/air-quality-sensor-node/HEAD/hardware/ee-10-sensorboard/2_EE-10_and_SIM_AFE_Interface.SchDoc -------------------------------------------------------------------------------- /server/cmd/aq/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | // msToTime converts milliseconds since epoch to time.Time 6 | func msToTime(t int64) time.Time { 7 | return time.Unix(t/int64(1000), (t%int64(1000))*int64(1000000)) 8 | } 9 | -------------------------------------------------------------------------------- /Firmware/src/version.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef AQ_VERSION 4 | #define AQ_VERSION "a.b.c" 5 | #endif 6 | #ifndef AQ_NAME 7 | #define AQ_NAME "noname" 8 | #endif 9 | #ifndef AQ_COMMITHASH 10 | #define AQ_COMMITHASH "0102030405" 11 | #endif 12 | -------------------------------------------------------------------------------- /common/protobuf/aqconfig.options: -------------------------------------------------------------------------------- 1 | aqconfig.setapn.apn1 max_size:64 2 | aqconfig.setapn.apn2 max_size:64 3 | aqconfig.setapn.apn3 max_size:64 4 | aqconfig.setapn.coap1 max_size:128 5 | aqconfig.setapn.coap2 max_size:128 6 | aqconfig.setapn.coap3 max_size:128 -------------------------------------------------------------------------------- /Firmware/src/util.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | 5 | uint64_t atoll(char *number_string) 6 | { 7 | uint64_t retval; 8 | int i; 9 | 10 | retval = 0; 11 | for (; *number_string; number_string++) { 12 | retval = 10*retval + (*number_string - '0'); 13 | } 14 | return retval; 15 | } 16 | */ -------------------------------------------------------------------------------- /server/doc/00-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "AQS Documentation" 3 | titlepage: true 4 | toc-own-page: true 5 | code-block-font-size: \tiny 6 | hyphenate: false 7 | listings-disable-line-numbers: true 8 | header-includes: 9 | - \newcommand{\sectionbreak}{\clearpage} 10 | - \usepackage[utf8x]{inputenc} 11 | --- 12 | -------------------------------------------------------------------------------- /Firmware/src/priorities.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // UART thread has priority 5 | #define MAX_THREAD_PRIORITY (-CONFIG_NUM_COOP_PRIORITIES + 2) 6 | // ...then the modem comm thread 7 | #define URC_THREAD_PRIORITY (-CONFIG_NUM_COOP_PRIORITIES + 1) 8 | #define GPS_THREAD_PRIORITY (-CONFIG_NUM_COOP_PRIORITIES + 3) 9 | -------------------------------------------------------------------------------- /server/doc/build-doc.sh: -------------------------------------------------------------------------------- 1 | pandoc --pdf-engine=xelatex \ 2 | --from markdown \ 3 | --template eisvogel \ 4 | -o aqs.pdf \ 5 | --toc \ 6 | --listings \ 7 | 00-index.md \ 8 | 01-introduction.md \ 9 | 02-build.md \ 10 | 03-use.md \ 11 | 04-sensors.md 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /server/pkg/pipeline/pipeline.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 5 | ) 6 | 7 | // Pipeline defines the interface of processing pipeline elements. 8 | type Pipeline interface { 9 | Publish(m *model.Message) error 10 | AddNext(pe Pipeline) 11 | Next() Pipeline 12 | } 13 | -------------------------------------------------------------------------------- /Firmware/release/templates/changelog.md: -------------------------------------------------------------------------------- 1 | ## v{{ .Version }}: {{ .Name }} 2 | 3 | ### Features 4 | 5 | [TODO: Write new features] 6 | 7 | ### API 8 | 9 | [TODO: Changes to the API] 10 | 11 | ### Command line 12 | 13 | [TODO: Command line changes] 14 | 15 | ### Other 16 | 17 | [TODO: Write other changes here] 18 | 19 | Commit hash: {{ .CommitHash }} 20 | -------------------------------------------------------------------------------- /Firmware/release/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "..", 3 | "name": "aqsensor", 4 | "committerEmail": "stalehd@gmail.com", 5 | "committerName": "Staale Dahl", 6 | "targets": [ 7 | "nrf52" 8 | ], 9 | "files": [ 10 | { 11 | "id": "bin", 12 | "name": "build/zephyr/zephyr.signed.bin", 13 | "target": "nrf52" 14 | } 15 | ], 16 | "templates": [] 17 | } 18 | -------------------------------------------------------------------------------- /common/protobuf/aqconfig.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package aqconfig; 4 | 5 | option go_package = ".;aqconfig"; 6 | 7 | message ping { 8 | uint32 id = 1; 9 | uint64 deviceid = 2; 10 | uint32 timestamp = 3; 11 | } 12 | 13 | message setapn { 14 | uint32 id = 1; 15 | uint32 timestamp = 2; 16 | string apn1 = 3; 17 | string apn2 = 4; 18 | string apn3 = 5; 19 | string coap1 = 6; 20 | string coap2 = 7; 21 | string coap3 = 8; 22 | } -------------------------------------------------------------------------------- /enclosure/README.md: -------------------------------------------------------------------------------- 1 | # Air quality sensor node 2 | 3 | (work in progress) 4 | 5 | GPS enabled NB-IoT Sensor node for sampling NO2, NO, O3 and particulate matter. 6 | 7 | # Internal structure (preliminary) 8 | 9 | ![node1](https://github.com/ExploratoryEngineering/air-quality-sensor-node/blob/master/enclosure/images/node1.jpg) 10 | 11 | ![node2](https://github.com/ExploratoryEngineering/air-quality-sensor-node/blob/master/enclosure/images/node2.jpg) 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /server/cmd/aq/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/opts" 7 | "github.com/jessevdk/go-flags" 8 | ) 9 | 10 | var options opts.Opts 11 | var parser = flags.NewParser(&options, flags.Default) 12 | 13 | func main() { 14 | if _, err := parser.Parse(); err != nil { 15 | if flagsErr, ok := err.(*flags.Error); ok && flagsErr.Type == flags.ErrHelp { 16 | os.Exit(0) 17 | } else { 18 | os.Exit(1) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Firmware/nrf52_pca10040.overlay: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: Apache-2.0 */ 2 | 3 | &i2c0 { 4 | status = "okay"; 5 | compatible = "nordic,nrf-twi"; 6 | sda-pin = <27>; 7 | scl-pin = <26>; 8 | clock-frequency = ; 9 | }; 10 | 11 | &spi1 { 12 | status = "okay"; 13 | compatible = "nordic,nrf-spi"; 14 | mosi-pin = <23>; 15 | sck-pin = <25>; 16 | miso-pin = <24>; 17 | }; 18 | 19 | &uart0 { 20 | status = "okay"; 21 | compatible = "nordic,nrf-uart"; 22 | current-speed = <9600>; 23 | tx-pin = <7>; 24 | rx-pin = <8>; 25 | }; 26 | -------------------------------------------------------------------------------- /server/pkg/api/api_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // Just make sure that the server starts and terminates. 13 | func TestAPISimple(t *testing.T) { 14 | tempLogDir, err := ioutil.TempDir("", "testlog") 15 | assert.Nil(t, err) 16 | defer os.RemoveAll(tempLogDir) 17 | 18 | log.Print(tempLogDir) 19 | 20 | s := New(&ServerConfig{ 21 | ListenAddr: ":0", 22 | AccessLogDir: tempLogDir, 23 | }) 24 | assert.NotNil(t, s) 25 | s.Start() 26 | s.Shutdown() 27 | } 28 | -------------------------------------------------------------------------------- /server/pkg/api/stream_handler.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/websocket" 8 | ) 9 | 10 | var upgrader = websocket.Upgrader{ 11 | ReadBufferSize: 1024, 12 | WriteBufferSize: 1024, 13 | // We explicitly want people to be able to connect from anywhere 14 | CheckOrigin: func(r *http.Request) bool { return true }, 15 | } 16 | 17 | func (s *Server) streamHandler(w http.ResponseWriter, r *http.Request) { 18 | conn, err := upgrader.Upgrade(w, r, nil) 19 | if err != nil { 20 | log.Println(err) 21 | return 22 | } 23 | 24 | s.broker.AddConnection(conn) 25 | } 26 | -------------------------------------------------------------------------------- /Firmware/src/config_message.h: -------------------------------------------------------------------------------- 1 | #ifndef _CONFIG_MESSAGE_ 2 | #define _CONFIG_MESSAGE_ 3 | 4 | #include 5 | #include "config.h" 6 | 7 | #define FALLBACK_APN "telenor.iot" 8 | #define FALLBACK_IP "88.99.192.151" 9 | 10 | typedef struct _apn_config { 11 | char apn1[64]; 12 | char apn2[64]; 13 | char apn3[64]; 14 | char apn4[64]; 15 | char coap1[128]; 16 | char coap2[128]; 17 | char coap3[128]; 18 | char coap4[128]; 19 | } apn_config; 20 | 21 | int decode_config_message(uint8_t * buf, int size); 22 | int encode_ping(char * buffer, int buffer_size, int * encoded_length); 23 | 24 | 25 | #endif // _CONFIG_MESSAGE_ -------------------------------------------------------------------------------- /Firmware/src/messagebuffer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "opc_n3.h" 5 | #include "gps_cache.h" 6 | #include "chipcap2.h" 7 | #include "ads124s08.h" 8 | 9 | typedef struct 10 | { 11 | gps_fix_t gps_fix; 12 | CC2_SAMPLE cc2_sample; 13 | OPC_SAMPLE opc_sample; 14 | AFE3_SAMPLE afe3_sample; 15 | s64_t uptime; 16 | 17 | } SENSOR_NODE_MESSAGE; 18 | 19 | /** 20 | * @brief Encode message 21 | */ 22 | size_t mb_encode(SENSOR_NODE_MESSAGE *msg, char *buf, size_t max); 23 | 24 | /** 25 | * @brief Dump the message (debug) 26 | */ 27 | void mb_dump_message(SENSOR_NODE_MESSAGE *msg); 28 | 29 | /** 30 | * @brief Hex dump of the messagebuffer (debug) 31 | */ 32 | void mb_hex_dump_message(char *buf, size_t len); 33 | -------------------------------------------------------------------------------- /server/pkg/opts/opts.go: -------------------------------------------------------------------------------- 1 | // Package opts contains the options that are common to app commands 2 | package opts 3 | 4 | // Opts contains command line options 5 | type Opts struct { 6 | HordeCollection string `short:"c" long:"collection" description:"Horde collection id to listen to" default:"17dh0cf43jg007" value-name:""` 7 | 8 | // Database options 9 | DBFilename string `short:"d" long:"db" description:"Data storage file" default:"aq.db" value-name:""` 10 | 11 | // Directory for calibration data 12 | CalibrationDataDir string `short:"m" long:"cal-data-dir" description:"Directory where calibration data is picked up" default:"calibration-data" value-name:""` 13 | 14 | // Verbose 15 | Verbose bool `short:"v" long:"verbose"` 16 | } 17 | -------------------------------------------------------------------------------- /Firmware/src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef _CONFIG_H_ 2 | #define _CONFIG_H_ 3 | 4 | #define APN_QUERY_RETRY_COUNT 30 5 | #define APN_RETRY_DELAY_MS 3000 6 | 7 | #define PING_RETRIES 30 8 | #define PING_RETRY_DELAY_MS 3000 9 | 10 | #define NVS_APN_COUNT 4 11 | 12 | // Default COAP parameters 13 | #define DEFAULT_FOTA_COAP_REPORT_PATH "u" 14 | #define DEFAULT_FOTA_COAP_UPDATE_PATH "fw" 15 | #define DEFAULT_FOTA_COAP_PORT 5683 16 | 17 | // Force reboot after 2 hours. This wil trigger a new scan for active APNs 18 | #define UPTIME_FORCE_REBOOT_LIMIT_SECONDS 60 * 120 19 | 20 | #include "config_message.h" 21 | #include "aqconfig.pb.h" 22 | 23 | void init_config_nvs(); 24 | void select_active_apn(); 25 | bool select_ping_apn(); 26 | int save_apn_config(); 27 | 28 | #endif // _CONFIG_H_ -------------------------------------------------------------------------------- /Firmware/src/init.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define LOG_LEVEL CONFIG_EE06_LOG_LEVEL 5 | LOG_MODULE_REGISTER(INIT); 6 | 7 | #include "init.h" 8 | #include "gpio.h" 9 | #include "spi_config.h" 10 | #include "i2c_config.h" 11 | #include "ads124s08.h" 12 | 13 | void init_board() 14 | { 15 | LOG_INF("Initialising board"); 16 | 17 | if (NULL == get_GPIO_device()) 18 | { 19 | LOG_ERR("Unable to initialize GPIO device"); 20 | k_fatal_halt(1); 21 | } 22 | 23 | if (NULL == get_I2C_device()) 24 | { 25 | LOG_ERR("Unable to initialize I2C device"); 26 | k_fatal_halt(2); 27 | } 28 | if (NULL == get_SPI_device()) 29 | { 30 | LOG_ERR("Unable to initialize SPI device"); 31 | k_fatal_halt(3); 32 | } 33 | ADS124S08_init(); 34 | ADS124S08_begin(); 35 | } -------------------------------------------------------------------------------- /Firmware/src/gps.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | #include "gps_cache.h" 19 | 20 | void gps_init(); 21 | 22 | void gps_get_sample(gps_fix_t *msg); -------------------------------------------------------------------------------- /Firmware/src/i2c_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | #include 21 | 22 | #define I2C_DEV "I2C_0" 23 | 24 | struct device *get_I2C_device(); 25 | -------------------------------------------------------------------------------- /manifest/west.yml: -------------------------------------------------------------------------------- 1 | west: 2 | url: https://github.com/zephyrproject-rtos/west 3 | revision: v0.6.0 4 | 5 | manifest: 6 | defaults: 7 | remote: zephyrproject-rtos 8 | 9 | remotes: 10 | - name: nanopb 11 | url-base: https://github.com/nanopb 12 | - name: zephyrproject-rtos 13 | url-base: https://github.com/zephyrproject-rtos 14 | - name: mcuboot 15 | url-base: https://github.com/JuulLabs-OSS 16 | 17 | projects: 18 | - name: nanopb 19 | path: Firmware/nanopb 20 | revision: nanopb-0.4.1 21 | remote: nanopb 22 | - name: zephyr 23 | path: deps/zephyr 24 | remote: zephyrproject-rtos 25 | revision: v2.1.0 26 | west-commands: scripts/west-commands.yml 27 | import: true 28 | - name: mcuboot 29 | remote: mcuboot 30 | path: Firmware/mcuboot 31 | revision: v1.4.0 32 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ExploratoryEngineering/air-quality-sensor-node/server 2 | 3 | go 1.13 4 | 5 | require github.com/telenordigital/nbiot-go v0.0.0-20200302123245-033df8c1f27d 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go v1.33.5 9 | github.com/davecgh/go-spew v1.1.1 // indirect 10 | github.com/eclipse/paho.mqtt.golang v1.2.0 11 | github.com/golang/protobuf v1.4.0 12 | github.com/gorilla/handlers v1.4.2 13 | github.com/gorilla/mux v1.7.4 14 | github.com/gorilla/websocket v1.4.0 15 | github.com/jessevdk/go-flags v1.4.0 16 | github.com/jmoiron/sqlx v1.2.0 17 | github.com/kr/pretty v0.1.0 // indirect 18 | github.com/mattn/go-sqlite3 v1.9.0 19 | github.com/sgreben/piecewiselinear v0.0.0-20200103140426-2cdc2ca8c19b 20 | github.com/stretchr/testify v1.5.1 21 | google.golang.org/protobuf v1.21.0 22 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /Firmware/src/spi_config.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #ifndef _SPI_CONFIG_H_ 18 | #define _SPI_CONFIG_H_ 19 | 20 | #include 21 | 22 | #define SPI_BUF_SIZE 256 23 | 24 | struct device *get_SPI_device(); 25 | 26 | #define SPI_DEV "SPI_1" 27 | 28 | #endif // _SPI_CONFIG_H_ -------------------------------------------------------------------------------- /calibration/afe3/json/16-000204.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2g", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2021-01-21T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000204", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212251117", 9 | "sensor2Serial": "214251013", 10 | "sensor3Serial": "130200417", 11 | "AFECalDate": "2021-01-21T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 309, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 296, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.73, 18 | "sensor1WESensitivity": 0.199, 19 | "sensor2WEe": 273, 20 | "sensor2WE0": 5, 21 | "sensor2AEe": 263, 22 | "sensor2AE0": 6, 23 | "sensor2PCBGain": -0.73, 24 | "sensor2WESensitivity": 0.338, 25 | "sensor3WEe": 277, 26 | "sensor3WE0": 26, 27 | "sensor3AEe": 275, 28 | "sensor3AE0": 19, 29 | "sensor3PCBGain": 0.80, 30 | "sensor3WESensitivity": 0.406 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000205.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2j", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2021-01-21T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000205", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212251119", 9 | "sensor2Serial": "214530262", 10 | "sensor3Serial": "130200415", 11 | "AFECalDate": "2021-01-21T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 304, 14 | "sensor1WE0": -5, 15 | "sensor1AEe": 299, 16 | "sensor1AE0": 0, 17 | "sensor1PCBGain": -0.73, 18 | "sensor1WESensitivity": 0.208, 19 | "sensor2WEe": 267, 20 | "sensor2WE0": 1, 21 | "sensor2AEe": 262, 22 | "sensor2AE0": 4, 23 | "sensor2PCBGain": -0.73, 24 | "sensor2WESensitivity": 0.314, 25 | "sensor3WEe": 255, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 266, 28 | "sensor3AE0": 16, 29 | "sensor3PCBGain": 0.80, 30 | "sensor3WESensitivity": 0.423 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000206.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2i", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2021-01-21T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000206", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212251118", 9 | "sensor2Serial": "214530263", 10 | "sensor3Serial": "130200418", 11 | "AFECalDate": "2021-01-21T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 302, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 290, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.73, 18 | "sensor1WESensitivity": -0.190, 19 | "sensor2WEe": 262, 20 | "sensor2WE0": 6, 21 | "sensor2AEe": 278, 22 | "sensor2AE0": 7, 23 | "sensor2PCBGain": -0.73, 24 | "sensor2WESensitivity": 0.328, 25 | "sensor3WEe": 270, 26 | "sensor3WE0": 39, 27 | "sensor3AEe": 272, 28 | "sensor3AE0": 34, 29 | "sensor3PCBGain": 0.80, 30 | "sensor3WESensitivity": 0.395 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000207.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2f", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2021-01-21T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000207", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212251116", 9 | "sensor2Serial": "214251014", 10 | "sensor3Serial": "130200416", 11 | "AFECalDate": "2021-01-21T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 304, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 307, 16 | "sensor1AE0": 1, 17 | "sensor1PCBGain": -0.73, 18 | "sensor1WESensitivity": 0.214, 19 | "sensor2WEe": 262, 20 | "sensor2WE0": 7, 21 | "sensor2AEe": 267, 22 | "sensor2AE0": 8, 23 | "sensor2PCBGain": -0.73, 24 | "sensor2WESensitivity": 0.307, 25 | "sensor3WEe": 264, 26 | "sensor3WE0": 29, 27 | "sensor3AEe": 278, 28 | "sensor3AE0": 31, 29 | "sensor3PCBGain": 0.80, 30 | "sensor3WESensitivity": 0.416 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000208.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2h", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2021-01-21T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000208", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212251115", 9 | "sensor2Serial": "214531326", 10 | "sensor3Serial": "130200414", 11 | "AFECalDate": "2021-01-21T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 303, 14 | "sensor1WE0": -5, 15 | "sensor1AEe": 295, 16 | "sensor1AE0": -1, 17 | "sensor1PCBGain": -0.73, 18 | "sensor1WESensitivity": 0.212, 19 | "sensor2WEe": 269, 20 | "sensor2WE0": 11, 21 | "sensor2AEe": 272, 22 | "sensor2AE0": 3, 23 | "sensor2PCBGain": -0.73, 24 | "sensor2WESensitivity": 0.315, 25 | "sensor3WEe": 262, 26 | "sensor3WE0": 38, 27 | "sensor3AEe": 263, 28 | "sensor3AE0": 28, 29 | "sensor3PCBGain": 0.80, 30 | "sensor3WESensitivity": 0.423 31 | } -------------------------------------------------------------------------------- /Firmware/Makefile: -------------------------------------------------------------------------------- 1 | KEYFILE := aq_fota.pem 2 | 3 | all: 4 | west build --board nrf52_pca10040 5 | west sign -t imgtool -- --key $(KEYFILE) 6 | 7 | clean: 8 | rm -fR build 9 | 10 | flash: 11 | west build --board nrf52_pca10040 12 | west sign -t imgtool -- --key $(KEYFILE) 13 | west flash --hex-file build/zephyr/zephyr.signed.hex 14 | 15 | # Generate a key. This will generate a NEW KEY for the image. If you have 16 | # deployed with a different key the images will not be recognized by the 17 | # existing firmware. 18 | fwkey: 19 | imgtool keygen -k $(KEYFILE) -t rsa-2048 20 | 21 | build_mcuboot: 22 | git clone https://github.com/JuulLabs-OSS/mcuboot && \ 23 | cd mcuboot/boot/zephyr && \ 24 | git checkout v1.4.0 && \ 25 | sed -i -e 's+root-rsa-2048.pem+../aq_fota.pem+g' prj.conf 26 | 27 | install_mcuboot: 28 | cd mcuboot/boot/zephyr && \ 29 | west build --board nrf52_pca10040 && \ 30 | west flash 31 | 32 | clean_mcuboot: 33 | rm -fR mcuboot 34 | -------------------------------------------------------------------------------- /server/calibration-data/16-000204.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2g", 3 | "sysID": 357518080254188, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2021-01-21T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000204", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212251117", 10 | "sensor2Serial": "214251013", 11 | "sensor3Serial": "130200417", 12 | "AFECalDate": "2021-01-21T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 309, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 296, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.73, 19 | "sensor1WESensitivity": 0.199, 20 | "sensor2WEe": 273, 21 | "sensor2WE0": 5, 22 | "sensor2AEe": 263, 23 | "sensor2AE0": 6, 24 | "sensor2PCBGain": -0.73, 25 | "sensor2WESensitivity": 0.338, 26 | "sensor3WEe": 277, 27 | "sensor3WE0": 26, 28 | "sensor3AEe": 275, 29 | "sensor3AE0": 19, 30 | "sensor3PCBGain": 0.80, 31 | "sensor3WESensitivity": 0.406 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000205.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2j", 3 | "sysID": 357518080254238, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2021-01-21T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000205", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212251119", 10 | "sensor2Serial": "214530262", 11 | "sensor3Serial": "130200415", 12 | "AFECalDate": "2021-01-21T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 304, 15 | "sensor1WE0": -5, 16 | "sensor1AEe": 299, 17 | "sensor1AE0": 0, 18 | "sensor1PCBGain": -0.73, 19 | "sensor1WESensitivity": 0.208, 20 | "sensor2WEe": 267, 21 | "sensor2WE0": 1, 22 | "sensor2AEe": 262, 23 | "sensor2AE0": 4, 24 | "sensor2PCBGain": -0.73, 25 | "sensor2WESensitivity": 0.314, 26 | "sensor3WEe": 255, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 266, 29 | "sensor3AE0": 16, 30 | "sensor3PCBGain": 0.80, 31 | "sensor3WESensitivity": 0.423 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000206.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2i", 3 | "sysID": 357518080253628, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2021-01-21T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000206", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212251118", 10 | "sensor2Serial": "214530263", 11 | "sensor3Serial": "130200418", 12 | "AFECalDate": "2021-01-21T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 302, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 290, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.73, 19 | "sensor1WESensitivity": -0.190, 20 | "sensor2WEe": 262, 21 | "sensor2WE0": 6, 22 | "sensor2AEe": 278, 23 | "sensor2AE0": 7, 24 | "sensor2PCBGain": -0.73, 25 | "sensor2WESensitivity": 0.328, 26 | "sensor3WEe": 270, 27 | "sensor3WE0": 39, 28 | "sensor3AEe": 272, 29 | "sensor3AE0": 34, 30 | "sensor3PCBGain": 0.80, 31 | "sensor3WESensitivity": 0.395 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000207.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2f", 3 | "sysID": 357518080254592, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2021-01-21T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000207", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212251116", 10 | "sensor2Serial": "214251014", 11 | "sensor3Serial": "130200416", 12 | "AFECalDate": "2021-01-21T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 304, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 307, 17 | "sensor1AE0": 1, 18 | "sensor1PCBGain": -0.73, 19 | "sensor1WESensitivity": 0.214, 20 | "sensor2WEe": 262, 21 | "sensor2WE0": 7, 22 | "sensor2AEe": 267, 23 | "sensor2AE0": 8, 24 | "sensor2PCBGain": -0.73, 25 | "sensor2WESensitivity": 0.307, 26 | "sensor3WEe": 264, 27 | "sensor3WE0": 29, 28 | "sensor3AEe": 278, 29 | "sensor3AE0": 31, 30 | "sensor3PCBGain": 0.80, 31 | "sensor3WESensitivity": 0.416 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000208.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "2f3a11687f7a2h", 3 | "sysID": 357518080274657, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2021-01-21T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000208", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212251115", 10 | "sensor2Serial": "214531326", 11 | "sensor3Serial": "130200414", 12 | "AFECalDate": "2021-01-21T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 303, 15 | "sensor1WE0": -5, 16 | "sensor1AEe": 295, 17 | "sensor1AE0": -1, 18 | "sensor1PCBGain": -0.73, 19 | "sensor1WESensitivity": 0.212, 20 | "sensor2WEe": 269, 21 | "sensor2WE0": 11, 22 | "sensor2AEe": 272, 23 | "sensor2AE0": 3, 24 | "sensor2PCBGain": -0.73, 25 | "sensor2WESensitivity": 0.315, 26 | "sensor3WEe": 262, 27 | "sensor3WE0": 38, 28 | "sensor3AEe": 263, 29 | "sensor3AE0": 28, 30 | "sensor3PCBGain": 0.80, 31 | "sensor3WESensitivity": 0.423 32 | } -------------------------------------------------------------------------------- /Firmware/src/fota_tlv.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define FIRMWARE_VER_ID 1 8 | #define MODEL_NUMBER_ID 2 9 | #define SERIAL_NUMBER_ID 3 10 | #define CLIENT_MANUFACTURER_ID 4 11 | 12 | #define HOST_ID 1 13 | #define PORT_ID 2 14 | #define PATH_ID 3 15 | #define AVAILABLE_ID 4 16 | 17 | size_t encode_tlv_string(uint8_t *buf, uint8_t id, const uint8_t *str); 18 | 19 | /** 20 | * @brief return the next ID in the buffer 21 | */ 22 | uint8_t tlv_id(const uint8_t *buf, size_t idx); 23 | 24 | /** 25 | * @brief return the next ID in the buffer 26 | */ 27 | int decode_tlv_string(const uint8_t *buf, size_t *idx, char *str); 28 | 29 | /** 30 | * @brief return the next ID in the buffer 31 | */ 32 | int decode_tlv_uint32(const uint8_t *buf, size_t *idx, uint32_t *val); 33 | 34 | /** 35 | * @brief return the next ID in the buffer 36 | */ 37 | int decode_tlv_bool(const uint8_t *buf, size_t *idx, bool *val); -------------------------------------------------------------------------------- /calibration/afe3/json/16-000175.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89l", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000175", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890139", 9 | "sensor2Serial": "214890247", 10 | "sensor3Serial": "130020152", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 297, 14 | "sensor1WE0": -7, 15 | "sensor1AEe": 318, 16 | "sensor1AE0": -3, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.2029999941587448, 19 | "sensor2WEe": 433, 20 | "sensor2WE0": -2, 21 | "sensor2AEe": 421, 22 | "sensor2AE0": -1, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4259999990463257, 25 | "sensor3WEe": 258, 26 | "sensor3WE0": 23, 27 | "sensor3AEe": 258, 28 | "sensor3AE0": 23, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.421999990940094 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000158.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg87n", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000158", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890140", 9 | "sensor2Serial": "214890253", 10 | "sensor3Serial": "130020134", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 294, 14 | "sensor1WE0": -2, 15 | "sensor1AEe": 310, 16 | "sensor1AE0": -1, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.21400000154972076, 19 | "sensor2WEe": 421, 20 | "sensor2WE0": -1, 21 | "sensor2AEe": 409, 22 | "sensor2AE0": 0, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.41999998688697815, 25 | "sensor3WEe": 262, 26 | "sensor3WE0": 18, 27 | "sensor3AEe": 283, 28 | "sensor3AE0": 18, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.3720000088214874 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000159.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88e", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000159", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890149", 9 | "sensor2Serial": "214890252", 10 | "sensor3Serial": "130020139", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 303, 14 | "sensor1WE0": -3, 15 | "sensor1AEe": 309, 16 | "sensor1AE0": -1, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.18000000715255737, 19 | "sensor2WEe": 424, 20 | "sensor2WE0": -3, 21 | "sensor2AEe": 415, 22 | "sensor2AE0": -2, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.39500001072883606, 25 | "sensor3WEe": 267, 26 | "sensor3WE0": 19, 27 | "sensor3AEe": 271, 28 | "sensor3AE0": 19, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.3959999978542328 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000161.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88i", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000161", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890154", 9 | "sensor2Serial": "214890251", 10 | "sensor3Serial": "130020141", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 307, 14 | "sensor1WE0": -3, 15 | "sensor1AEe": 311, 16 | "sensor1AE0": -3, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20999999344348907, 19 | "sensor2WEe": 428, 20 | "sensor2WE0": -2, 21 | "sensor2AEe": 423, 22 | "sensor2AE0": -1, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.460999995470047, 25 | "sensor3WEe": 281, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 266, 28 | "sensor3AE0": 21, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.36500000953674316 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000162.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88k", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000162", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890153", 9 | "sensor2Serial": "214890254", 10 | "sensor3Serial": "130020135", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 310, 14 | "sensor1WE0": -2, 15 | "sensor1AEe": 298, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20800000429153442, 19 | "sensor2WEe": 418, 20 | "sensor2WE0": -2, 21 | "sensor2AEe": 408, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.43700000643730164, 25 | "sensor3WEe": 260, 26 | "sensor3WE0": 22, 27 | "sensor3AEe": 278, 28 | "sensor3AE0": 22, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.38999998569488525 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000163.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg887", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000163", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890146", 9 | "sensor2Serial": "214890249", 10 | "sensor3Serial": "130020137", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 292, 14 | "sensor1WE0": -6, 15 | "sensor1AEe": 309, 16 | "sensor1AE0": -6, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.17000000178813934, 19 | "sensor2WEe": 407, 20 | "sensor2WE0": -3, 21 | "sensor2AEe": 417, 22 | "sensor2AE0": -2, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4129999876022339, 25 | "sensor3WEe": 258, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 269, 28 | "sensor3AE0": 21, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.41600000858306885 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000165.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg899", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000165", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890157", 9 | "sensor2Serial": "214890258", 10 | "sensor3Serial": "130020142", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 311, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 300, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20399999618530273, 19 | "sensor2WEe": 413, 20 | "sensor2WE0": -5, 21 | "sensor2AEe": 418, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.38499999046325684, 25 | "sensor3WEe": 263, 26 | "sensor3WE0": 28, 27 | "sensor3AEe": 270, 28 | "sensor3AE0": 28, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.3790000081062317 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000166.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89b", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000166", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890144", 9 | "sensor2Serial": "214890260", 10 | "sensor3Serial": "130020138", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 296, 14 | "sensor1WE0": -2, 15 | "sensor1AEe": 305, 16 | "sensor1AE0": -1, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20000000298023224, 19 | "sensor2WEe": 422, 20 | "sensor2WE0": -1, 21 | "sensor2AEe": 411, 22 | "sensor2AE0": -2, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.41999998688697815, 25 | "sensor3WEe": 261, 26 | "sensor3WE0": 17, 27 | "sensor3AEe": 257, 28 | "sensor3AE0": 17, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.39399999380111694 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000167.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg77n", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000167", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890145", 9 | "sensor2Serial": "214890245", 10 | "sensor3Serial": "130020133", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 296, 14 | "sensor1WE0": -7, 15 | "sensor1AEe": 294, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.210999995470047, 19 | "sensor2WEe": 408, 20 | "sensor2WE0": 1, 21 | "sensor2AEe": 412, 22 | "sensor2AE0": 1, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4099999964237213, 25 | "sensor3WEe": 268, 26 | "sensor3WE0": 16, 27 | "sensor3AEe": 269, 28 | "sensor3AE0": 16, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.37700000405311584 31 | } 32 | -------------------------------------------------------------------------------- /calibration/afe3/json/16-000168.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg897", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000168", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890147", 9 | "sensor2Serial": "214890244", 10 | "sensor3Serial": "130020136", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 292, 14 | "sensor1WE0": -1, 15 | "sensor1AEe": 303, 16 | "sensor1AE0": -1, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.2150000035762787, 19 | "sensor2WEe": 425, 20 | "sensor2WE0": -3, 21 | "sensor2AEe": 424, 22 | "sensor2AE0": -2, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4230000078678131, 25 | "sensor3WEe": 270, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 253, 28 | "sensor3AE0": 21, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.40700000524520874 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000169.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg889", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000169", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890136", 9 | "sensor2Serial": "214890263", 10 | "sensor3Serial": "130020129", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 302, 14 | "sensor1WE0": -1, 15 | "sensor1AEe": 301, 16 | "sensor1AE0": -1, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.2290000021457672, 19 | "sensor2WEe": 417, 20 | "sensor2WE0": -4, 21 | "sensor2AEe": 416, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.40299999713897705, 25 | "sensor3WEe": 251, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 265, 28 | "sensor3AE0": 21, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.41200000047683716 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000173.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg885", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000173", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890138", 9 | "sensor2Serial": "214890058", 10 | "sensor3Serial": "130020130", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 312, 14 | "sensor1WE0": -5, 15 | "sensor1AEe": 295, 16 | "sensor1AE0": -3, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20499999821186066, 19 | "sensor2WEe": 426, 20 | "sensor2WE0": -6, 21 | "sensor2AEe": 413, 22 | "sensor2AE0": -4, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.36399999260902405, 25 | "sensor3WEe": 255, 26 | "sensor3WE0": 24, 27 | "sensor3AEe": 265, 28 | "sensor3AE0": 24, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.3970000147819519 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000176.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88g", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000176", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890160", 9 | "sensor2Serial": "214890246", 10 | "sensor3Serial": "130020153", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 312, 14 | "sensor1WE0": -3, 15 | "sensor1AEe": 327, 16 | "sensor1AE0": -8, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.2199999988079071, 19 | "sensor2WEe": 435, 20 | "sensor2WE0": -5, 21 | "sensor2AEe": 429, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.3959999978542328, 25 | "sensor3WEe": 257, 26 | "sensor3WE0": 25, 27 | "sensor3AEe": 256, 28 | "sensor3AE0": 25, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.4090000092983246 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000178.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg881", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000178", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890143", 9 | "sensor2Serial": "214890248", 10 | "sensor3Serial": "130020145", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 294, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 307, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.18299999833106995, 19 | "sensor2WEe": 425, 20 | "sensor2WE0": -5, 21 | "sensor2AEe": 426, 22 | "sensor2AE0": -1, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4129999876022339, 25 | "sensor3WEe": 267, 26 | "sensor3WE0": 17, 27 | "sensor3AEe": 267, 28 | "sensor3AE0": 17, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.41499999165534973 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000179.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89h", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000179", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890152", 9 | "sensor2Serial": "214890243", 10 | "sensor3Serial": "130020151", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 304, 14 | "sensor1WE0": -5, 15 | "sensor1AEe": 305, 16 | "sensor1AE0": 0, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20100000500679016, 19 | "sensor2WEe": 404, 20 | "sensor2WE0": -6, 21 | "sensor2AEe": 411, 22 | "sensor2AE0": -5, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.3269999921321869, 25 | "sensor3WEe": 263, 26 | "sensor3WE0": 18, 27 | "sensor3AEe": 265, 28 | "sensor3AE0": 18, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.38999998569488525 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000180.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89j", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000180", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890141", 9 | "sensor2Serial": "214890250", 10 | "sensor3Serial": "130020149", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 303, 14 | "sensor1WE0": -2, 15 | "sensor1AEe": 309, 16 | "sensor1AE0": -1, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.17599999904632568, 19 | "sensor2WEe": 408, 20 | "sensor2WE0": -5, 21 | "sensor2AEe": 411, 22 | "sensor2AE0": -4, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.3889999985694885, 25 | "sensor3WEe": 268, 26 | "sensor3WE0": 20, 27 | "sensor3AEe": 271, 28 | "sensor3AE0": 20, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.37400001287460327 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000181.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88b", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000181", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890155", 9 | "sensor2Serial": "214890242", 10 | "sensor3Serial": "130020148", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 299, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 299, 16 | "sensor1AE0": -3, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.22599999606609344, 19 | "sensor2WEe": 408, 20 | "sensor2WE0": -6, 21 | "sensor2AEe": 412, 22 | "sensor2AE0": -1, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4339999854564667, 25 | "sensor3WEe": 282, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 260, 28 | "sensor3AE0": 21, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.3930000066757202 31 | } -------------------------------------------------------------------------------- /calibration/afe3/json/16-000182.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89n", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000182", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890137", 9 | "sensor2Serial": "214890060", 10 | "sensor3Serial": "130020150", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 297, 14 | "sensor1WE0": -3, 15 | "sensor1AEe": 287, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20200000703334808, 19 | "sensor2WEe": 412, 20 | "sensor2WE0": -12, 21 | "sensor2AEe": 409, 22 | "sensor2AE0": -4, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.35100001096725464, 25 | "sensor3WEe": 244, 26 | "sensor3WE0": 19, 27 | "sensor3AEe": 276, 28 | "sensor3AE0": 19, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.38199999928474426 31 | } -------------------------------------------------------------------------------- /Firmware/src/chipcap2.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | #include 19 | 20 | typedef struct 21 | { 22 | float RH; 23 | float Temp_C; 24 | } CC2_SAMPLE; 25 | 26 | #define CHIPCAP2_ADDRESS 0x28 27 | 28 | #define CHIPCAP2_NORMAL_OPERATION_MODE 0x80 29 | 30 | int cc2_init(); 31 | void cc2_sample_data(); 32 | void cc2_get_sample(CC2_SAMPLE *msg); -------------------------------------------------------------------------------- /calibration/afe3/json/16-000160.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg781", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000160", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890159", 9 | "sensor2Serial": "214890256", 10 | "sensor3Serial": "130020128", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 311, 14 | "sensor1WE0": -3, 15 | "sensor1AEe": 312, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.20100000500679016, 19 | "sensor2WEe": 416, 20 | "sensor2WE0": -1, 21 | "sensor2AEe": 412, 22 | "sensor2AE0": -2, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.3919999897480011, 25 | "sensor3WEe": 264, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 266, 28 | "sensor3AE0": 21, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.4180000126361847 31 | } 32 | -------------------------------------------------------------------------------- /calibration/afe3/json/16-000164.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg783", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000164", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890151", 9 | "sensor2Serial": "214890261", 10 | "sensor3Serial": "130020140", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 291, 14 | "sensor1WE0": -2, 15 | "sensor1AEe": 303, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.19599999487400055, 19 | "sensor2WEe": 422, 20 | "sensor2WE0": -6, 21 | "sensor2AEe": 423, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.421999990940094, 25 | "sensor3WEe": 261, 26 | "sensor3WE0": 20, 27 | "sensor3AEe": 277, 28 | "sensor3AE0": 20, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.4059999883174896 31 | } 32 | -------------------------------------------------------------------------------- /calibration/afe3/json/16-000170.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg77j", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000170", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890142", 9 | "sensor2Serial": "214890257", 10 | "sensor3Serial": "130020146", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 299, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 307, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.1720000058412552, 19 | "sensor2WEe": 412, 20 | "sensor2WE0": -4, 21 | "sensor2AEe": 408, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4000000059604645, 25 | "sensor3WEe": 262, 26 | "sensor3WE0": 20, 27 | "sensor3AEe": 260, 28 | "sensor3AE0": 20, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.3869999945163727 31 | } 32 | -------------------------------------------------------------------------------- /calibration/afe3/json/16-000171.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg785", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000171", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890156", 9 | "sensor2Serial": "214890255", 10 | "sensor3Serial": "130020132", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 305, 14 | "sensor1WE0": -7, 15 | "sensor1AEe": 298, 16 | "sensor1AE0": -3, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.1940000057220459, 19 | "sensor2WEe": 423, 20 | "sensor2WE0": -5, 21 | "sensor2AEe": 417, 22 | "sensor2AE0": -4, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.367000013589859, 25 | "sensor3WEe": 262, 26 | "sensor3WE0": 21, 27 | "sensor3AEe": 263, 28 | "sensor3AE0": 21, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.4020000100135803 31 | } 32 | -------------------------------------------------------------------------------- /calibration/afe3/json/16-000172.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg77l", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000172", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890158", 9 | "sensor2Serial": "214890259", 10 | "sensor3Serial": "130020143", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 308, 14 | "sensor1WE0": -4, 15 | "sensor1AEe": 303, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.18700000643730164, 19 | "sensor2WEe": 414, 20 | "sensor2WE0": -2, 21 | "sensor2AEe": 423, 22 | "sensor2AE0": 0, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.4449999928474426, 25 | "sensor3WEe": 259, 26 | "sensor3WE0": 17, 27 | "sensor3AEe": 250, 28 | "sensor3AE0": 17, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.3659999966621399 31 | } 32 | -------------------------------------------------------------------------------- /calibration/afe3/json/16-000174.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg6n4", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000174", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890148", 9 | "sensor2Serial": "214890262", 10 | "sensor3Serial": "130020144", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 312, 14 | "sensor1WE0": -5, 15 | "sensor1AEe": 316, 16 | "sensor1AE0": -5, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.2029999941587448, 19 | "sensor2WEe": 411, 20 | "sensor2WE0": -4, 21 | "sensor2AEe": 411, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.3630000054836273, 25 | "sensor3WEe": 271, 26 | "sensor3WE0": 19, 27 | "sensor3AEe": 256, 28 | "sensor3AE0": 19, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.40799999237060547 31 | } 32 | -------------------------------------------------------------------------------- /calibration/afe3/json/16-000177.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89f", 3 | "collectionID": "17dh0cf43jg007", 4 | "from": "2019-12-13T00:00:00Z", 5 | "circuitType": "01", 6 | "afeSerial": "16-000177", 7 | "afeType": "810-0019", 8 | "sensor1Serial": "212890150", 9 | "sensor2Serial": "214890059", 10 | "sensor3Serial": "130020147", 11 | "AFECalDate": "2019-12-13T00:00:00Z", 12 | "vt20Offset": 0.3195, 13 | "sensor1WEe": 304, 14 | "sensor1WE0": -6, 15 | "sensor1AEe": 301, 16 | "sensor1AE0": -2, 17 | "sensor1PCBGain": -0.7300000190734863, 18 | "sensor1WESensitivity": 0.164000004529953, 19 | "sensor2WEe": 424, 20 | "sensor2WE0": -11, 21 | "sensor2AEe": 425, 22 | "sensor2AE0": -3, 23 | "sensor2PCBGain": -0.7300000190734863, 24 | "sensor2WESensitivity": 0.36899998784065247, 25 | "sensor3WEe": 275, 26 | "sensor3WE0": 1822, 27 | "sensor3AEe": 265, 28 | "sensor3AE0": 1822, 29 | "sensor3PCBGain": 0.800000011920929, 30 | "sensor3WESensitivity": 0.4129999876022339 31 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Air quality sensor node 2 | 3 | (work in progress) 4 | 5 | GPS enabled NB-IoT Sensor node for sampling NO2, NO, O3 and particulate matter. 6 | 7 | ## OPC-N3 particle sensor 8 | ![OPC-N3 particle sensor](https://github.com/ExploratoryEngineering/air-quality-sensor-node/raw/master/images/OPC-N3.jpg) 9 | 10 | ## AFE-3 Analog frontend for NO2, NO and O3 electrochemical sensors 11 | ![Analog frontend and gas sensors](https://github.com/ExploratoryEngineering/air-quality-sensor-node/raw/master/images/AFE.jpg) 12 | 13 | ## Vehicle power board 14 | ![Power board](https://github.com/ExploratoryEngineering/air-quality-sensor-node/raw/master/images/power.jpg) 15 | 16 | ## EE-NBIoT-02 module 17 | ![NB-IoT module](https://github.com/ExploratoryEngineering/air-quality-sensor-node/raw/master/images/EE-NBIoT-02.jpg) 18 | 19 | ## Controller board with nrf52, GPS and ADC 20 | ![Controller board](https://github.com/ExploratoryEngineering/air-quality-sensor-node/raw/master/images/controller.jpg) 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/calibration-data/16-000158.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg87n", 3 | "sysID": 357518080249378, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000158", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890140", 10 | "sensor2Serial": "214890253", 11 | "sensor3Serial": "130020134", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 294, 15 | "sensor1WE0": -2, 16 | "sensor1AEe": 310, 17 | "sensor1AE0": -1, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.21400000154972076, 20 | "sensor2WEe": 421, 21 | "sensor2WE0": -1, 22 | "sensor2AEe": 409, 23 | "sensor2AE0": 0, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.41999998688697815, 26 | "sensor3WEe": 262, 27 | "sensor3WE0": 18, 28 | "sensor3AEe": 283, 29 | "sensor3AE0": 18, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.3720000088214874 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000160.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg781", 3 | "sysID": 357518080249352, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000160", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890159", 10 | "sensor2Serial": "214890256", 11 | "sensor3Serial": "130020128", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 311, 15 | "sensor1WE0": -3, 16 | "sensor1AEe": 312, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20100000500679016, 20 | "sensor2WEe": 416, 21 | "sensor2WE0": -1, 22 | "sensor2AEe": 412, 23 | "sensor2AE0": -2, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.3919999897480011, 26 | "sensor3WEe": 264, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 266, 29 | "sensor3AE0": 21, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.4180000126361847 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000161.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88i", 3 | "sysID": 357518080233208, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000161", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890154", 10 | "sensor2Serial": "214890251", 11 | "sensor3Serial": "130020141", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 307, 15 | "sensor1WE0": -3, 16 | "sensor1AEe": 311, 17 | "sensor1AE0": -3, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20999999344348907, 20 | "sensor2WEe": 428, 21 | "sensor2WE0": -2, 22 | "sensor2AEe": 423, 23 | "sensor2AE0": -1, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.460999995470047, 26 | "sensor3WEe": 281, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 266, 29 | "sensor3AE0": 21, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.36500000953674316 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000164.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg783", 3 | "sysID": 357518080249428, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000164", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890151", 10 | "sensor2Serial": "214890261", 11 | "sensor3Serial": "130020140", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 291, 15 | "sensor1WE0": -2, 16 | "sensor1AEe": 303, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.19599999487400055, 20 | "sensor2WEe": 422, 21 | "sensor2WE0": -6, 22 | "sensor2AEe": 423, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.421999990940094, 26 | "sensor3WEe": 261, 27 | "sensor3WE0": 20, 28 | "sensor3AEe": 277, 29 | "sensor3AE0": 20, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.4059999883174896 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000167.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg77n", 3 | "sysID": 357518080233232, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000167", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890145", 10 | "sensor2Serial": "214890245", 11 | "sensor3Serial": "130020133", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 296, 15 | "sensor1WE0": -7, 16 | "sensor1AEe": 294, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.210999995470047, 20 | "sensor2WEe": 408, 21 | "sensor2WE0": 1, 22 | "sensor2AEe": 412, 23 | "sensor2AE0": 1, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4099999964237213, 26 | "sensor3WEe": 268, 27 | "sensor3WE0": 16, 28 | "sensor3AEe": 269, 29 | "sensor3AE0": 16, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.37700000405311584 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000168.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg897", 3 | "sysID": 357517080142765, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000168", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890147", 10 | "sensor2Serial": "214890244", 11 | "sensor3Serial": "130020136", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 292, 15 | "sensor1WE0": -1, 16 | "sensor1AEe": 303, 17 | "sensor1AE0": -1, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.2150000035762787, 20 | "sensor2WEe": 425, 21 | "sensor2WE0": -3, 22 | "sensor2AEe": 424, 23 | "sensor2AE0": -2, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4230000078678131, 26 | "sensor3WEe": 270, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 253, 29 | "sensor3AE0": 21, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.40700000524520874 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000170.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg77j", 3 | "sysID": 357518080249493, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000170", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890142", 10 | "sensor2Serial": "214890257", 11 | "sensor3Serial": "130020146", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 299, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 307, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.1720000058412552, 20 | "sensor2WEe": 412, 21 | "sensor2WE0": -4, 22 | "sensor2AEe": 408, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4000000059604645, 26 | "sensor3WEe": 262, 27 | "sensor3WE0": 20, 28 | "sensor3AEe": 260, 29 | "sensor3AE0": 20, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.3869999945163727 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000172.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg77l", 3 | "sysID": 357518080231574, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000172", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890158", 10 | "sensor2Serial": "214890259", 11 | "sensor3Serial": "130020143", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 308, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 303, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.18700000643730164, 20 | "sensor2WEe": 414, 21 | "sensor2WE0": -2, 22 | "sensor2AEe": 423, 23 | "sensor2AE0": 0, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4449999928474426, 26 | "sensor3WEe": 259, 27 | "sensor3WE0": 17, 28 | "sensor3AEe": 250, 29 | "sensor3AE0": 17, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.3659999966621399 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000174.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg6n4", 3 | "sysID": 357518080231251, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000174", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890148", 10 | "sensor2Serial": "214890262", 11 | "sensor3Serial": "130020144", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 312, 15 | "sensor1WE0": -5, 16 | "sensor1AEe": 316, 17 | "sensor1AE0": -5, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.2029999941587448, 20 | "sensor2WEe": 411, 21 | "sensor2WE0": -4, 22 | "sensor2AEe": 411, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.3630000054836273, 26 | "sensor3WEe": 271, 27 | "sensor3WE0": 19, 28 | "sensor3AEe": 256, 29 | "sensor3AE0": 19, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.40799999237060547 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000175.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89l", 3 | "sysID": 357518080254303, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000175", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890139", 10 | "sensor2Serial": "214890247", 11 | "sensor3Serial": "130020152", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 297, 15 | "sensor1WE0": -7, 16 | "sensor1AEe": 318, 17 | "sensor1AE0": -3, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.2029999941587448, 20 | "sensor2WEe": 433, 21 | "sensor2WE0": -2, 22 | "sensor2AEe": 421, 23 | "sensor2AE0": -1, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4259999990463257, 26 | "sensor3WEe": 258, 27 | "sensor3WE0": 23, 28 | "sensor3AEe": 258, 29 | "sensor3AE0": 23, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.421999990940094 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000176.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88g", 3 | "sysID": 357518080251069, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000176", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890160", 10 | "sensor2Serial": "214890246", 11 | "sensor3Serial": "130020153", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 312, 15 | "sensor1WE0": -3, 16 | "sensor1AEe": 327, 17 | "sensor1AE0": -8, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.2199999988079071, 20 | "sensor2WEe": 435, 21 | "sensor2WE0": -5, 22 | "sensor2AEe": 429, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.3959999978542328, 26 | "sensor3WEe": 257, 27 | "sensor3WE0": 25, 28 | "sensor3AEe": 256, 29 | "sensor3AE0": 25, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.4090000092983246 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000179.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89h", 3 | "sysID": 357518080269251, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000179", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890152", 10 | "sensor2Serial": "214890243", 11 | "sensor3Serial": "130020151", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 304, 15 | "sensor1WE0": -5, 16 | "sensor1AEe": 305, 17 | "sensor1AE0": 0, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20100000500679016, 20 | "sensor2WEe": 404, 21 | "sensor2WE0": -6, 22 | "sensor2AEe": 411, 23 | "sensor2AE0": -5, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.3269999921321869, 26 | "sensor3WEe": 263, 27 | "sensor3WE0": 18, 28 | "sensor3AEe": 265, 29 | "sensor3AE0": 18, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.38999998569488525 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000181.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88b", 3 | "sysID": 357518080233174, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000181", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890155", 10 | "sensor2Serial": "214890242", 11 | "sensor3Serial": "130020148", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 299, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 299, 17 | "sensor1AE0": -3, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.22599999606609344, 20 | "sensor2WEe": 408, 21 | "sensor2WE0": -6, 22 | "sensor2AEe": 412, 23 | "sensor2AE0": -1, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4339999854564667, 26 | "sensor3WEe": 282, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 260, 29 | "sensor3AE0": 21, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.3930000066757202 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000159.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88e", 3 | "sysID": 357518080272776, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000159", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890149", 10 | "sensor2Serial": "214890252", 11 | "sensor3Serial": "130020139", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 303, 15 | "sensor1WE0": -3, 16 | "sensor1AEe": 309, 17 | "sensor1AE0": -1, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.18000000715255737, 20 | "sensor2WEe": 424, 21 | "sensor2WE0": -3, 22 | "sensor2AEe": 415, 23 | "sensor2AE0": -2, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.39500001072883606, 26 | "sensor3WEe": 267, 27 | "sensor3WE0": 19, 28 | "sensor3AEe": 271, 29 | "sensor3AE0": 19, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.3959999978542328 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000162.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg88k", 3 | "sysID": 357518080254154, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000162", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890153", 10 | "sensor2Serial": "214890254", 11 | "sensor3Serial": "130020135", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 310, 15 | "sensor1WE0": -2, 16 | "sensor1AEe": 298, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20800000429153442, 20 | "sensor2WEe": 418, 21 | "sensor2WE0": -2, 22 | "sensor2AEe": 408, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.43700000643730164, 26 | "sensor3WEe": 260, 27 | "sensor3WE0": 22, 28 | "sensor3AEe": 278, 29 | "sensor3AE0": 22, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.38999998569488525 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000163.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg887", 3 | "sysID": 357518080251044, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000163", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890146", 10 | "sensor2Serial": "214890249", 11 | "sensor3Serial": "130020137", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 292, 15 | "sensor1WE0": -6, 16 | "sensor1AEe": 309, 17 | "sensor1AE0": -6, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.17000000178813934, 20 | "sensor2WEe": 407, 21 | "sensor2WE0": -3, 22 | "sensor2AEe": 417, 23 | "sensor2AE0": -2, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4129999876022339, 26 | "sensor3WEe": 258, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 269, 29 | "sensor3AE0": 21, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.41600000858306885 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000165.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg899", 3 | "sysID": 357518080253404, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000165", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890157", 10 | "sensor2Serial": "214890258", 11 | "sensor3Serial": "130020142", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 311, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 300, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20399999618530273, 20 | "sensor2WEe": 413, 21 | "sensor2WE0": -5, 22 | "sensor2AEe": 418, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.38499999046325684, 26 | "sensor3WEe": 263, 27 | "sensor3WE0": 28, 28 | "sensor3AEe": 270, 29 | "sensor3AE0": 28, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.3790000081062317 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000166.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89b", 3 | "sysID": 357518080272800, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000166", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890144", 10 | "sensor2Serial": "214890260", 11 | "sensor3Serial": "130020138", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 296, 15 | "sensor1WE0": -2, 16 | "sensor1AEe": 305, 17 | "sensor1AE0": -1, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20000000298023224, 20 | "sensor2WEe": 422, 21 | "sensor2WE0": -1, 22 | "sensor2AEe": 411, 23 | "sensor2AE0": -2, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.41999998688697815, 26 | "sensor3WEe": 261, 27 | "sensor3WE0": 17, 28 | "sensor3AEe": 257, 29 | "sensor3AE0": 17, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.39399999380111694 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000169.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg889", 3 | "sysID": 357518080252224, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000169", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890136", 10 | "sensor2Serial": "214890263", 11 | "sensor3Serial": "130020129", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 302, 15 | "sensor1WE0": -1, 16 | "sensor1AEe": 301, 17 | "sensor1AE0": -1, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.2290000021457672, 20 | "sensor2WEe": 417, 21 | "sensor2WE0": -4, 22 | "sensor2AEe": 416, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.40299999713897705, 26 | "sensor3WEe": 251, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 265, 29 | "sensor3AE0": 21, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.41200000047683716 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000171.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg785", 3 | "sysID": 357518080229701, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000171", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890156", 10 | "sensor2Serial": "214890255", 11 | "sensor3Serial": "130020132", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 305, 15 | "sensor1WE0": -7, 16 | "sensor1AEe": 298, 17 | "sensor1AE0": -3, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.1940000057220459, 20 | "sensor2WEe": 423, 21 | "sensor2WE0": -5, 22 | "sensor2AEe": 417, 23 | "sensor2AE0": -4, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.367000013589859, 26 | "sensor3WEe": 262, 27 | "sensor3WE0": 21, 28 | "sensor3AEe": 263, 29 | "sensor3AE0": 21, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.4020000100135803 32 | } 33 | -------------------------------------------------------------------------------- /server/calibration-data/16-000173.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg885", 3 | "sysID": 357518080233158, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000173", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890138", 10 | "sensor2Serial": "214890058", 11 | "sensor3Serial": "130020130", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 312, 15 | "sensor1WE0": -5, 16 | "sensor1AEe": 295, 17 | "sensor1AE0": -3, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20499999821186066, 20 | "sensor2WEe": 426, 21 | "sensor2WE0": -6, 22 | "sensor2AEe": 413, 23 | "sensor2AE0": -4, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.36399999260902405, 26 | "sensor3WEe": 255, 27 | "sensor3WE0": 24, 28 | "sensor3AEe": 265, 29 | "sensor3AE0": 24, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.3970000147819519 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000177.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89f", 3 | "sysID": 357518080251150, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000177", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890150", 10 | "sensor2Serial": "214890059", 11 | "sensor3Serial": "130020147", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 304, 15 | "sensor1WE0": -6, 16 | "sensor1AEe": 301, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.164000004529953, 20 | "sensor2WEe": 424, 21 | "sensor2WE0": -11, 22 | "sensor2AEe": 425, 23 | "sensor2AE0": -3, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.36899998784065247, 26 | "sensor3WEe": 275, 27 | "sensor3WE0": 1822, 28 | "sensor3AEe": 265, 29 | "sensor3AE0": 1822, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.4129999876022339 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000178.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg881", 3 | "sysID": 357518080274665, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000178", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890143", 10 | "sensor2Serial": "214890248", 11 | "sensor3Serial": "130020145", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 294, 15 | "sensor1WE0": -4, 16 | "sensor1AEe": 307, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.18299999833106995, 20 | "sensor2WEe": 425, 21 | "sensor2WE0": -5, 22 | "sensor2AEe": 426, 23 | "sensor2AE0": -1, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.4129999876022339, 26 | "sensor3WEe": 267, 27 | "sensor3WE0": 17, 28 | "sensor3AEe": 267, 29 | "sensor3AE0": 17, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.41499999165534973 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000180.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89j", 3 | "sysID": 357518080245079, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000180", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890141", 10 | "sensor2Serial": "214890250", 11 | "sensor3Serial": "130020149", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 303, 15 | "sensor1WE0": -2, 16 | "sensor1AEe": 309, 17 | "sensor1AE0": -1, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.17599999904632568, 20 | "sensor2WEe": 408, 21 | "sensor2WE0": -5, 22 | "sensor2AEe": 411, 23 | "sensor2AE0": -4, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.3889999985694885, 26 | "sensor3WEe": 268, 27 | "sensor3WE0": 20, 28 | "sensor3AEe": 271, 29 | "sensor3AE0": 20, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.37400001287460327 32 | } -------------------------------------------------------------------------------- /server/calibration-data/16-000182.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceID": "17dh0cf43jg89n", 3 | "sysID": 357518080249345, 4 | "collectionID": "17dh0cf43jg007", 5 | "from": "2019-12-13T00:00:00Z", 6 | "circuitType": "01", 7 | "afeSerial": "16-000182", 8 | "afeType": "810-0019", 9 | "sensor1Serial": "212890137", 10 | "sensor2Serial": "214890060", 11 | "sensor3Serial": "130020150", 12 | "AFECalDate": "2019-12-13T00:00:00Z", 13 | "vt20Offset": 0.3195, 14 | "sensor1WEe": 297, 15 | "sensor1WE0": -3, 16 | "sensor1AEe": 287, 17 | "sensor1AE0": -2, 18 | "sensor1PCBGain": -0.7300000190734863, 19 | "sensor1WESensitivity": 0.20200000703334808, 20 | "sensor2WEe": 412, 21 | "sensor2WE0": -12, 22 | "sensor2AEe": 409, 23 | "sensor2AE0": -4, 24 | "sensor2PCBGain": -0.7300000190734863, 25 | "sensor2WESensitivity": 0.35100001096725464, 26 | "sensor3WEe": 244, 27 | "sensor3WE0": 19, 28 | "sensor3AEe": 276, 29 | "sensor3AE0": 19, 30 | "sensor3PCBGain": 0.800000011920929, 31 | "sensor3WESensitivity": 0.38199999928474426 32 | } -------------------------------------------------------------------------------- /Firmware/src/max14830.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #ifndef _MAX14830_h_ 18 | #define _MAX14830_h_ 19 | 20 | #include 21 | 22 | #define EE_NBIOT_01_ADDRESS 0x61 23 | 24 | #define RX_BUFFER_SIZE 256 25 | 26 | typedef void (*max_char_callback_t)(uint8_t data); 27 | 28 | void init_max14830(); 29 | 30 | int max_send(const uint8_t *buffer, uint8_t len); 31 | 32 | void max_init(max_char_callback_t cb); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /server/pkg/pipeline/pipelog/pipelog.go: -------------------------------------------------------------------------------- 1 | package pipelog 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 7 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/opts" 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 9 | ) 10 | 11 | // Log is a pipeline processor that logs incoming messages 12 | type Log struct { 13 | next pipeline.Pipeline 14 | opts *opts.Opts 15 | } 16 | 17 | // New creates new instance of Log pipeline element 18 | func New(opts *opts.Opts) *Log { 19 | return &Log{ 20 | opts: opts, 21 | } 22 | } 23 | 24 | // Publish ... 25 | func (p *Log) Publish(m *model.Message) error { 26 | log.Printf("Message: device='%s' messageID=%d packetSize=%d", m.DeviceID, m.ID, m.PacketSize) 27 | 28 | if p.next != nil { 29 | return p.next.Publish(m) 30 | } 31 | return nil 32 | } 33 | 34 | // AddNext ... 35 | func (p *Log) AddNext(pe pipeline.Pipeline) { 36 | p.next = pe 37 | } 38 | 39 | // Next ... 40 | func (p *Log) Next() pipeline.Pipeline { 41 | return p.next 42 | } 43 | -------------------------------------------------------------------------------- /Firmware/src/pinout.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #ifndef _PINOUT_H_ 18 | #define _PINOUT_H_ 19 | 20 | #define CS_ADC 28 21 | #define CS_OPC 29 22 | #define CS_SX1276 22 23 | 24 | #define ADC_RESET 30 25 | #define ADC_SYNC 31 26 | 27 | #define GPS_FORCE_ON 17 28 | #define GPS_TX 8 29 | #define GPS_RX 07 30 | #define GPS_RESET 6 31 | #define GPS_WAKE_UP 5 32 | 33 | #define MAX14830_IRQ 3 34 | #define MAX14830_RESET 4 35 | 36 | #define EE_NBIOT_01_RESET 2 37 | 38 | #endif 39 | 40 | -------------------------------------------------------------------------------- /server/pkg/pipeline/root.go: -------------------------------------------------------------------------------- 1 | package pipeline 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 7 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/opts" 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store" 9 | ) 10 | 11 | // ErrEmptyPipeline indicates that the Root pipeline has no next 12 | // element. Which makes it kind of useless. 13 | var ErrEmptyPipeline = errors.New("Empty pipeline, has no next element") 14 | 15 | // Root is the root handler for pipelines. 16 | type Root struct { 17 | next Pipeline 18 | opts *opts.Opts 19 | db store.Store 20 | } 21 | 22 | // New creates a new Root instance 23 | func New(opts *opts.Opts, db store.Store) *Root { 24 | return &Root{ 25 | opts: opts, 26 | db: db, 27 | } 28 | } 29 | 30 | // Publish ... 31 | func (p *Root) Publish(m *model.Message) error { 32 | if p.next != nil { 33 | return p.next.Publish(m) 34 | } 35 | return nil 36 | } 37 | 38 | // AddNext ... 39 | func (p *Root) AddNext(pe Pipeline) { 40 | p.next = pe 41 | } 42 | 43 | // Next ... 44 | func (p *Root) Next() Pipeline { 45 | return p.next 46 | } 47 | -------------------------------------------------------------------------------- /Firmware/src/spi_config.c: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include "spi_config.h" 20 | 21 | #define LOG_LEVEL CONFIG_EESPI_LOG_LEVEL 22 | #include 23 | LOG_MODULE_REGISTER(EESPI); 24 | 25 | static struct device *spi_dev; 26 | 27 | struct device *get_SPI_device() 28 | { 29 | if (!spi_dev) 30 | { 31 | spi_dev = device_get_binding(SPI_DEV); 32 | if (!spi_dev) 33 | { 34 | LOG_ERR("Device driver not found"); 35 | return NULL; 36 | } 37 | } 38 | 39 | return spi_dev; 40 | } -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(GOPATH),) 2 | GOPATH := $(HOME)/go 3 | endif 4 | 5 | all: test lint vet build 6 | 7 | clean: 8 | @find . -name "*-wal" -delete 9 | @find . -name "*-shm" -delete 10 | @rm -f bin/*.linux 11 | 12 | build: aq 13 | 14 | aq: 15 | @cd cmd/aq && go build -o ../../bin/aq 16 | 17 | check: lint vet staticcheck revive 18 | 19 | lint: 20 | @golint ./... 21 | 22 | vet: 23 | @go vet ./... 24 | 25 | staticcheck: 26 | @staticcheck ./... 27 | 28 | revive: 29 | @revive ./... 30 | 31 | test: 32 | @go test ./... 33 | 34 | test_verbose: 35 | @go test ./... -v 36 | 37 | test_race: 38 | @go test ./... -race 39 | 40 | test_all: test_cover test_race 41 | 42 | benchmark: 43 | cd output && go test -bench . 44 | 45 | gen: 46 | @go generate ./... 47 | 48 | 49 | # Here be dragons 50 | dep_install: 51 | (cd $(GOPATH); go get -u github.com/golang/protobuf/protoc-gen-go) 52 | (cd $(GOPATH); go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway) 53 | (cd $(GOPATH); go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger) 54 | (cd $(GOPATH); go get -u github.com/golang/protobuf/protoc-gen-go) 55 | (cd $(GOPATH); go get -u github.com/favadi/protoc-go-inject-tag) 56 | -------------------------------------------------------------------------------- /Firmware/src/gpio.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #ifndef _GPIO_H_ 18 | #define _GPIO_H_ 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #define GPIO_DRV_NAME "GPIO_NRF_P0" 25 | 26 | // For ADS124s08 code 27 | #define LOW 0 28 | #define HIGH 1 29 | int digitalWrite( u32_t pin, u32_t value ); 30 | 31 | 32 | struct device * get_GPIO_device(); 33 | 34 | int ConfigureOutputPin(u32_t pin); 35 | int ConfigureInputPin(u32_t pin); 36 | int UnSelect(u32_t pin); 37 | void UnselectAllSPI(); 38 | int Select(u32_t pin); 39 | void gps_reset(); 40 | 41 | #endif // _GPIO_H_ -------------------------------------------------------------------------------- /server/pkg/store/sqlitestore/sqlitestore.go: -------------------------------------------------------------------------------- 1 | package sqlitestore 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "log" 7 | 8 | "github.com/jmoiron/sqlx" 9 | _ "github.com/mattn/go-sqlite3" // Load sqlite3 driver 10 | ) 11 | 12 | // SqliteStore implements the store interface with Sqlite 13 | type SqliteStore struct { 14 | mu sync.Mutex 15 | db *sqlx.DB 16 | } 17 | 18 | // New creates new Store backed by SQLite3 19 | func New(dbFile string) (*SqliteStore, error) { 20 | var databaseFileExisted = false 21 | if _, err := os.Stat(dbFile); err == nil { 22 | databaseFileExisted = true 23 | } 24 | 25 | // Turn on write-ahead log journaling annd turn off mutex 26 | // since we don't trust this to work anyway. 27 | cs := dbFile + "?" + "_journal=WAL&_mutex=no" 28 | 29 | d, err := sqlx.Open("sqlite3", cs) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if err = d.Ping(); err != nil { 35 | return nil, err 36 | } 37 | 38 | if !databaseFileExisted { 39 | log.Printf("Creating database schema in %s", dbFile) 40 | createSchema(d, cs) 41 | } 42 | 43 | return &SqliteStore{db: d}, nil 44 | } 45 | 46 | // Close ... 47 | func (s *SqliteStore) Close() error { 48 | s.mu.Lock() 49 | defer s.mu.Unlock() 50 | return s.db.Close() 51 | } 52 | -------------------------------------------------------------------------------- /Firmware/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | cmake_minimum_required(VERSION 3.13.1) 4 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/nanopb/extra) 5 | include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) 6 | project(aqnode) 7 | 8 | # For nanoPB 9 | find_package(Nanopb REQUIRED) 10 | include_directories(${NANOPB_INCLUDE_DIRS}) 11 | nanopb_generate_cpp(PROTO_SRCS PROTO_HDRS ../common/protobuf/aq.proto) 12 | nanopb_generate_cpp(PROTO_SRCS PROTO_HDRS ../common/protobuf/aqconfig.proto) 13 | include_directories(${CMAKE_CURRENT_BINARY_DIR}) 14 | add_custom_target(generate_proto_sources DEPENDS ${PROTO_SRCS} ${PROTO_HDRS}) 15 | set_source_files_properties(${PROTO_SRCS} ${PROTO_HDRS} 16 | PROPERTIES GENERATED TRUE) 17 | 18 | FILE(GLOB app_sources src/*.c) 19 | target_sources(app PRIVATE ${app_sources} ${PROTO_SRCS} ${PROTO_HDRS}) 20 | 21 | set(CURDIR ${CMAKE_CURRENT_LIST_DIR} ) 22 | 23 | EXEC_PROGRAM(reto ARGS -r ${CURDIR} version OUTPUT_VARIABLE aq_version) 24 | EXEC_PROGRAM(reto ARGS -r ${CURDIR} hashname OUTPUT_VARIABLE aq_name) 25 | 26 | add_definitions(-DAQ_VERSION="${aq_version}" -DAQ_NAME="${aq_name}") 27 | 28 | message( "Current dir: ${CURDIR}" ) 29 | message( "Version is ${aq_version}" ) 30 | message( "Name is ${aq_name}" ) 31 | 32 | -------------------------------------------------------------------------------- /Firmware/Kconfig: -------------------------------------------------------------------------------- 1 | module = MAIN 2 | module-str = MAIN 3 | source "subsys/logging/Kconfig.template.log_config" 4 | 5 | source "Kconfig.zephyr" 6 | 7 | module = COMMS 8 | module-str = COMMS 9 | source "subsys/logging/Kconfig.template.log_config" 10 | 11 | module = CHIPCAP2 12 | module-str = CHIPCAP2 13 | source "subsys/logging/Kconfig.template.log_config" 14 | 15 | module = MAX14830 16 | module-str = MAX14830 17 | source "subsys/logging/Kconfig.template.log_config" 18 | 19 | module = OPCN3 20 | module-str = OPCN3 21 | source "subsys/logging/Kconfig.template.log_config" 22 | 23 | module = GPS 24 | module-str = GPS 25 | source "subsys/logging/Kconfig.template.log_config" 26 | 27 | module = ADSL 28 | module-str = ADSL 29 | source "subsys/logging/Kconfig.template.log_config" 30 | 31 | module = SARAN2 32 | module-str = SARAN2 33 | source "subsys/logging/Kconfig.template.log_config" 34 | 35 | module = EEGPIO 36 | module-str = EEGPIO 37 | source "subsys/logging/Kconfig.template.log_config" 38 | 39 | module = EEI2C 40 | module-str = EEI2C 41 | source "subsys/logging/Kconfig.template.log_config" 42 | 43 | module = EESPI 44 | module-str = EESPI 45 | source "subsys/logging/Kconfig.template.log_config" 46 | 47 | module = FOTA 48 | module-str = FOTA 49 | source "subsys/logging/Kconfig.template.log_config" 50 | -------------------------------------------------------------------------------- /server/pkg/pipeline/persist/persist.go: -------------------------------------------------------------------------------- 1 | package persist 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 7 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/opts" 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store" 10 | ) 11 | 12 | // Persist is a pipeline processor that persists incoming messages 13 | type Persist struct { 14 | db store.Store 15 | next pipeline.Pipeline 16 | opts *opts.Opts 17 | } 18 | 19 | // New creates new Persist pipeline element 20 | func New(opts *opts.Opts, db store.Store) *Persist { 21 | return &Persist{ 22 | opts: opts, 23 | db: db, 24 | } 25 | } 26 | 27 | // Publish ... 28 | func (p *Persist) Publish(m *model.Message) error { 29 | id, err := p.db.PutMessage(m) 30 | if err != nil { 31 | log.Printf("Error logging message: %v", err) 32 | } else { 33 | // Populate with storage ID 34 | m.ID = id 35 | } 36 | 37 | if p.next != nil { 38 | return p.next.Publish(m) 39 | } 40 | return nil 41 | } 42 | 43 | // AddNext ... 44 | func (p *Persist) AddNext(pe pipeline.Pipeline) { 45 | p.next = pe 46 | } 47 | 48 | // Next ... 49 | func (p *Persist) Next() pipeline.Pipeline { 50 | return p.next 51 | } 52 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Trondheim Kommune Air Quality Server 2 | 3 | The Air Quality Server provides a simple tool for receiving real time 4 | data from the Trondheim Kommune Air Quality Sensors. It supports 5 | fetching 6 | 7 | ## Installing 8 | 9 | You need to install the `protoc` protocol compiler for Google 10 | protobuffers. 11 | 12 | On OSX: 13 | 14 | brew install protobuf 15 | 16 | On Linux: 17 | 18 | sudo apt install protobuf-compiler 19 | 20 | ## Building 21 | 22 | When you build for the first time you need to make sure you have 23 | `github.com/golang/protobuf/protoc-gen-go` installed. You can install 24 | this by issuing the following command 25 | 26 | make dep_install 27 | 28 | The you can simply run 29 | 30 | make 31 | 32 | This should build the AQ server. The binary will be in `bin/aq`. We 33 | have not provided an install target in the Makefile since we cannot 34 | really assume anything about where you would want it installed. One 35 | recommendation is to place the `aq` binary in `/usr/local/bin/aq`, but 36 | that is just a suggestion. 37 | 38 | ## Running AQ 39 | 40 | In order to run AQ you will need an API key and an API endpoint. 41 | 42 | **This is currently subject to change so documentation for this will 43 | turn up when we have things landed. This shouldn't take more than at 44 | most a few weeks. ** 45 | 46 | 47 | -------------------------------------------------------------------------------- /Firmware/src/i2c_config.c: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #include 18 | #include "i2c_config.h" 19 | #include 20 | #include 21 | #include 22 | 23 | #define LOG_LEVEL CONFIG_EEI2C_LOG_LEVEL 24 | #include 25 | LOG_MODULE_REGISTER(EEI2C); 26 | 27 | static struct device *i2c_dev = NULL; 28 | 29 | struct device *get_I2C_device() 30 | { 31 | if (!i2c_dev) 32 | { 33 | i2c_dev = device_get_binding(I2C_DEV); 34 | if (!i2c_dev) 35 | { 36 | LOG_ERR("Device driver not found"); 37 | return NULL; 38 | } 39 | if (i2c_configure(i2c_dev, I2C_SPEED_SET(I2C_SPEED_STANDARD))) 40 | { 41 | LOG_ERR("Configuration failed"); 42 | return NULL; 43 | } 44 | } 45 | return i2c_dev; 46 | } 47 | -------------------------------------------------------------------------------- /Firmware/src/fota.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "config.h" 4 | 5 | #include "version.h" 6 | // This is the reported manufacturer reported by the LwM2M client. It is an 7 | // arbitrary string and will be exposed through the Horde API. 8 | #define CLIENT_MANUFACTURER "Exploratory Engineering" 9 | 10 | // This is the model number reported by the LwM2M client. It is an arbitrary 11 | // string and will be exposed by the Horde API. 12 | #define CLIENT_MODEL_NUMBER "EE-AQ-2.0" 13 | 14 | // This is the serial number reported by the LwM2M client. If you have some 15 | // kind of serial number available you can use that, otherwise the IMEI (the 16 | // ID for the cellular modem) or IMSI (The ID of the SIM in use) 17 | #define CLIENT_SERIAL_NUMBER AQ_NAME 18 | 19 | // This is the version of the firmware. This must match the versions set on the 20 | // images uploaded via the Horde API (at https://api.nbiot.engineering/) 21 | #define CLIENT_FIRMWARE_VER AQ_VERSION 22 | 23 | typedef struct 24 | { 25 | char host[25]; 26 | uint32_t port; 27 | char path[25]; 28 | bool scheduled_update; 29 | } simple_fota_response_t; 30 | 31 | /** 32 | * @brief Initialize FOTA 33 | */ 34 | bool fota_init(); 35 | 36 | /** 37 | * @brief Run a FOTA upgrade 38 | */ 39 | bool fota_run(); 40 | 41 | /** 42 | * @brief Wait for response on a socket fd 43 | */ 44 | bool wait_for_response(int sock); 45 | 46 | int fota_report_version(simple_fota_response_t *resp); -------------------------------------------------------------------------------- /Firmware/src/comms.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * @brief Callback for receive notifications. 5 | */ 6 | typedef void (*recv_callback_t)(int fd, size_t bytes); 7 | 8 | /** 9 | * @brief Set callback function for new data notifications. This function is 10 | * called whenever a +NSOMNI message is received from the modem. 11 | * @note Only a single callback can be registered. 12 | */ 13 | void receive_callback(recv_callback_t receive_cb); 14 | 15 | /** 16 | * @brief Initialize communications 17 | */ 18 | void modem_init(void); 19 | 20 | /** 21 | * @brief Writes a string to the modem. 22 | * @param *cmd: The string to send 23 | */ 24 | void modem_write(const char *cmd); 25 | 26 | /** 27 | * @brief Read a single character from the modem. 28 | */ 29 | bool modem_read(uint8_t *b, int32_t timeout); 30 | 31 | /** 32 | * @brief check if modem is ready and online (ie check if there's an assigned IP address) 33 | */ 34 | bool modem_is_ready(); 35 | 36 | /** 37 | * Restart modem 38 | */ 39 | void modem_restart(); 40 | 41 | /** 42 | * Configure modem 43 | */ 44 | void modem_configure(); 45 | 46 | /** 47 | * Restart modem (in a more network friendly way than modem_restart()) 48 | * NOTE: This function has to be tested while moving between cells - and maybe also renamed at some point. 49 | */ 50 | void modem_restart_without_triggering_network_signalling_storm_but_hopefully_picking_up_the_correct_cell___maybe(); 51 | 52 | void get_IMEI(); -------------------------------------------------------------------------------- /server/doc/04-sensors.md: -------------------------------------------------------------------------------- 1 | # Sensors 2 | 3 | ## Alphasense OPC-N3 4 | 5 | For measuring amount of particulate matter present in the air, we use 6 | the 7 | [OPC-N3](http://www.alphasense.com/index.php/products/optical-particle-counter/) 8 | sensor from [Alphasense](http://www.alphasense.com/). This sensor can 9 | measure particles from 0.35μm to 40 μm and it will report these in 24 10 | ranges or "bins". The particle size ranges for each bin can be found 11 | in the table on the next page. 12 | 13 | \newpage 14 | | bin | from (µm) | to (µm) | 15 | |----:|----------:|--------:| 16 | | 0 | 0.35 | 0.46 | 17 | | 1 | 0.46 | 0.66 | 18 | | 2 | 0.66 | 1.0 | 19 | | 3 | 1.0 | 1.3 | 20 | | 4 | 1.3 | 1.7 | 21 | | 5 | 1.7 | 2.3 | 22 | | 6 | 2.3 | 3.0 | 23 | | 7 | 3.0 | 4.0 | 24 | | 8 | 4.0 | 5.2 | 25 | | 9 | 5.2 | 6.5 | 26 | | 10 | 6.5 | 8.0 | 27 | | 11 | 8.0 | 10.0 | 28 | | 12 | 10.0 | 12.0 | 29 | | 13 | 12.0 | 14.0 | 30 | | 14 | 14.0 | 16.0 | 31 | | 15 | 16.0 | 18.0 | 32 | | 16 | 18.0 | 20.0 | 33 | | 17 | 20.0 | 22.0 | 34 | | 18 | 22.0 | 25.0 | 35 | | 19 | 25.0 | 28.0 | 36 | | 20 | 28.0 | 31.0 | 37 | | 21 | 31.0 | 34.0 | 38 | | 22 | 34.0 | 37.0 | 39 | | 23 | 37.0 | 40.0 | 40 | 41 | \newpage 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /server/pkg/pipeline/circular/circularbuffer.go: -------------------------------------------------------------------------------- 1 | package circular 2 | 3 | import ( 4 | "container/ring" 5 | "sync" 6 | 7 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 9 | ) 10 | 11 | // Buffer is a ring-buffer holding the last N messages received. 12 | type Buffer struct { 13 | ring *ring.Ring 14 | mu sync.Mutex 15 | next pipeline.Pipeline 16 | } 17 | 18 | // New creates a new ring buffer pipeline element. 19 | func New(size int) *Buffer { 20 | return &Buffer{ 21 | ring: ring.New(size), 22 | } 23 | } 24 | 25 | // Publish adds a message to the ring buffer 26 | func (c *Buffer) Publish(m *model.Message) error { 27 | c.mu.Lock() 28 | defer c.mu.Unlock() 29 | 30 | c.ring.Value = m 31 | c.ring = c.ring.Next() 32 | 33 | if c.next != nil { 34 | return c.next.Publish(m) 35 | } 36 | return nil 37 | } 38 | 39 | // GetContents returns the contents of the circular buffer 40 | func (c *Buffer) GetContents() []*model.Message { 41 | c.mu.Lock() 42 | defer c.mu.Unlock() 43 | 44 | var messages []*model.Message 45 | 46 | c.ring.Do(func(p interface{}) { 47 | if p != nil { 48 | messages = append(messages, p.(*model.Message)) 49 | } 50 | }) 51 | return messages 52 | } 53 | 54 | // AddNext adds a Pipeline element after this pipeline element 55 | func (c *Buffer) AddNext(pe pipeline.Pipeline) { 56 | c.next = pe 57 | } 58 | 59 | // Next returns the next pipeline element 60 | func (c *Buffer) Next() pipeline.Pipeline { 61 | return c.next 62 | } 63 | -------------------------------------------------------------------------------- /Firmware/src/flash.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | LOG_MODULE_REGISTER(FLASH, CONFIG_FOTA_LOG_LEVEL); 9 | 10 | #define FLASH_BANK0_ID DT_FLASH_AREA_IMAGE_0_ID 11 | #define FLASH_BANK1_ID DT_FLASH_AREA_IMAGE_1_ID 12 | #define FLASH_BANK_SIZE DT_FLASH_AREA_IMAGE_1_SIZE 13 | 14 | static struct flash_img_context dfu_ctx; 15 | 16 | // This function is more or less an identical copy to the Foundries.io flash write 17 | // code. 18 | int write_firmware_block(const uint8_t *data, const uint16_t data_len, bool first_block, bool last_block, size_t total_size) 19 | { 20 | int ret = 0; 21 | 22 | if (total_size > FLASH_BANK_SIZE) 23 | { 24 | LOG_ERR("Artifact file size too big (%d)", total_size); 25 | return -EINVAL; 26 | } 27 | 28 | if (!data_len) 29 | { 30 | LOG_ERR("Data len is zero, nothing to write."); 31 | return -EINVAL; 32 | } 33 | 34 | /* Erase bank 1 before starting the write process */ 35 | if (first_block) 36 | { 37 | flash_img_init(&dfu_ctx); 38 | LOG_DBG("Download firmware started, erasing second bank"); 39 | ret = boot_erase_img_bank(FLASH_BANK1_ID); 40 | if (ret != 0) 41 | { 42 | LOG_ERR("Failed to erase flash bank 1: %d", ret); 43 | return ret; 44 | } 45 | LOG_DBG("Finished erasing bank 1"); 46 | } 47 | 48 | return flash_img_buffered_write(&dfu_ctx, (uint8_t *)data, data_len, last_block); 49 | } 50 | -------------------------------------------------------------------------------- /Firmware/src/fota_tlv.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "fota_tlv.h" 5 | 6 | LOG_MODULE_REGISTER(FOTA_TLV, CONFIG_FOTA_LOG_LEVEL); 7 | 8 | size_t encode_tlv_string(uint8_t *buf, uint8_t id, const uint8_t *str) 9 | { 10 | size_t ret = 0; 11 | buf[ret++] = id; 12 | buf[ret++] = strlen(str); 13 | for (uint8_t i = 0; i < strlen(str); i++) 14 | { 15 | buf[ret++] = str[i]; 16 | } 17 | return ret; 18 | } 19 | 20 | inline uint8_t tlv_id(const uint8_t *buf, size_t idx) 21 | { 22 | return buf[idx]; 23 | } 24 | 25 | int decode_tlv_string(const uint8_t *buf, size_t *idx, char *str) 26 | { 27 | int len = (int)buf[(*idx)++]; 28 | int i = 0; 29 | for (i = 0; i < len; i++) 30 | { 31 | str[i] = buf[(*idx)++]; 32 | } 33 | str[i] = 0; 34 | return 0; 35 | } 36 | 37 | int decode_tlv_uint32(const uint8_t *buf, size_t *idx, uint32_t *val) 38 | { 39 | size_t len = (size_t)buf[(*idx)++]; 40 | if (len != 4) 41 | { 42 | LOG_ERR("uint32 in TLV buffer isn't 4 bytes"); 43 | return -1; 44 | } 45 | *val = 0; 46 | *val += (buf[(*idx)++] << 24); 47 | *val += (buf[(*idx)++] << 16); 48 | *val += (buf[(*idx)++] << 8); 49 | *val += (buf[(*idx)++]); 50 | return 0; 51 | } 52 | 53 | int decode_tlv_bool(const uint8_t *buf, size_t *idx, bool *val) 54 | { 55 | size_t len = (size_t)buf[(*idx)++]; 56 | if (len != 1) 57 | { 58 | LOG_ERR("bool in TLV buffer isn't 1 byte"); 59 | return -1; 60 | } 61 | 62 | *val = (buf[(*idx)++] == 1); 63 | return 0; 64 | } -------------------------------------------------------------------------------- /server/cmd/aq/list.go: -------------------------------------------------------------------------------- 1 | // The parse command is mostly there to check that we can parse the 2 | // CSV file(s) and visually check that a calibration file looks okay. 3 | // 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store/sqlitestore" 11 | ) 12 | 13 | // ListCommand defines the command line parameters for list command 14 | type ListCommand struct { 15 | Offset int `short:"o" long:"offset" description:"offset from lowest id" default:"0"` 16 | Limit int `short:"l" long:"limit" description:"Number of entries to show" default:"100"` 17 | } 18 | 19 | func init() { 20 | parser.AddCommand("list", 21 | "List calibration data", 22 | "List calibration data", 23 | &ListCommand{}) 24 | } 25 | 26 | // Execute runs the list command 27 | func (a *ListCommand) Execute(args []string) error { 28 | db, err := sqlitestore.New(options.DBFilename) 29 | defer db.Close() 30 | 31 | cals, err := db.ListCals(a.Offset, a.Limit) 32 | if err != nil { 33 | log.Fatalf("Unable to list calibration data: %v", err) 34 | } 35 | 36 | if len(cals) == 0 { 37 | log.Printf("no entries to list") 38 | return nil 39 | } 40 | 41 | fmt.Print("\n---------------------------------------------------------------------------\n") 42 | fmt.Print(" ID CollectionID DeviceID AFE Serial ValidFrom\n") 43 | fmt.Print("---------------------------------------------------------------------------\n") 44 | for _, cal := range cals { 45 | fmt.Printf(" %4d %14s %14s %10s %20s\n", cal.ID, cal.CollectionID, cal.DeviceID, cal.AFESerial, cal.ValidFrom.Format(layout)) 46 | } 47 | fmt.Print("---------------------------------------------------------------------------\n\n") 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /server/pkg/pipeline/calculate/calculate_test.go: -------------------------------------------------------------------------------- 1 | package calculate 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var cals = []model.Cal{ 12 | model.Cal{DeviceID: "foo", SysID: 1, ID: 3, ValidFrom: time.Date(2002, 1, 30, 0, 0, 0, 0, time.UTC)}, 13 | model.Cal{DeviceID: "foo", SysID: 1, ID: 2, ValidFrom: time.Date(2001, 1, 30, 0, 0, 0, 0, time.UTC)}, 14 | model.Cal{DeviceID: "foo", SysID: 1, ID: 1, ValidFrom: time.Date(2000, 1, 30, 0, 0, 0, 0, time.UTC)}, 15 | } 16 | 17 | func TestFindCacheEntry(t *testing.T) { 18 | c := &Calculate{} 19 | c.populateCache(cals) 20 | 21 | assert.Equal(t, int64(1), c.findCacheEntry(1, ms(time.Date(2000, 2, 30, 0, 0, 0, 0, time.UTC))).ID) 22 | assert.Equal(t, int64(2), c.findCacheEntry(1, ms(time.Date(2001, 2, 30, 0, 0, 0, 0, time.UTC))).ID) 23 | assert.Equal(t, int64(3), c.findCacheEntry(1, ms(time.Date(2003, 1, 1, 0, 0, 0, 0, time.UTC))).ID) 24 | 25 | // We want the oldest entry if the data precedes calibration data. 26 | // This is defined behavior but not necessarily smart behavior. 27 | assert.Equal(t, int64(1), c.findCacheEntry(1, ms(time.Date(1999, 2, 30, 0, 0, 0, 0, time.UTC))).ID) 28 | 29 | // Check for exact coincidence 30 | assert.Equal(t, int64(1), c.findCacheEntry(1, ms(time.Date(2000, 1, 30, 0, 0, 0, 0, time.UTC))).ID) 31 | assert.Equal(t, int64(2), c.findCacheEntry(1, ms(time.Date(2001, 1, 30, 0, 0, 0, 0, time.UTC))).ID) 32 | assert.Equal(t, int64(3), c.findCacheEntry(1, ms(time.Date(2003, 1, 30, 0, 0, 0, 0, time.UTC))).ID) 33 | } 34 | 35 | // Convert time.Time to milliseconds since epoch 36 | func ms(t time.Time) int64 { 37 | return t.UnixNano() / int64(time.Millisecond) 38 | } 39 | -------------------------------------------------------------------------------- /server/pkg/listener/udplistener/udplistener.go: -------------------------------------------------------------------------------- 1 | package udplistener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 11 | ) 12 | 13 | // UDPListener listens for UDP packets on a given listenAddress 14 | type UDPListener struct { 15 | listenAddress string 16 | bufferSize int 17 | ctx context.Context 18 | pipeline pipeline.Pipeline 19 | doneChan chan error 20 | quit chan bool 21 | } 22 | 23 | // New creates a new UDPListener instance 24 | func New(listenAddress string, bufferSize int, pipeline pipeline.Pipeline) *UDPListener { 25 | return &UDPListener{ 26 | listenAddress: listenAddress, 27 | bufferSize: bufferSize, 28 | ctx: context.Background(), 29 | pipeline: pipeline, 30 | doneChan: make(chan error), 31 | quit: make(chan bool), 32 | } 33 | } 34 | 35 | // Start UDPListener instance 36 | func (u *UDPListener) Start() error { 37 | pc, err := net.ListenPacket("udp", u.listenAddress) 38 | if err != nil { 39 | return fmt.Errorf("Failed to listen to %s: %v", u.listenAddress, err) 40 | } 41 | 42 | buffer := make([]byte, u.bufferSize) 43 | go func() { 44 | defer pc.Close() 45 | for { 46 | n, addr, err := pc.ReadFrom(buffer) 47 | if err != nil { 48 | u.doneChan <- err 49 | return 50 | } 51 | 52 | pb, err := model.ProtobufFromData(buffer[:n]) 53 | if err != nil { 54 | log.Printf("Error decoding packet from %v into pb: %v", addr, err) 55 | continue 56 | } 57 | 58 | m := model.MessageFromProtobuf(pb) 59 | m.PacketSize = n 60 | u.pipeline.Publish(m) 61 | 62 | } 63 | }() 64 | 65 | return nil 66 | } 67 | 68 | // Shutdown initiates shutdown of the UDPListener 69 | func (u *UDPListener) Shutdown() { 70 | log.Printf("UDPListener: Shutdown not implemented") 71 | } 72 | 73 | // WaitForShutdown waits for the UDP listener to shut down 74 | func (u *UDPListener) WaitForShutdown() { 75 | <-u.quit 76 | } 77 | -------------------------------------------------------------------------------- /server/pkg/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 7 | ) 8 | 9 | // ErrCalExists indicates that calibration entry already exists 10 | var ErrCalExists = errors.New("Calibration entry already exists") 11 | 12 | // Store defines the persistence interface. 13 | type Store interface { 14 | // ############################################################ 15 | // Calibration 16 | // ############################################################ 17 | 18 | // PutCal adds a new calibration entry. Note that the device 19 | // referred to must already exist in the database. 20 | PutCal(c *model.Cal) (int64, error) 21 | 22 | // GetCal gets a calibration entry by id 23 | GetCal(id int64) (*model.Cal, error) 24 | 25 | // DeleteCal deletes calibration entry. 26 | DeleteCal(id int64) error 27 | 28 | // ListCal lists calibration data ordered by DeviceID and ValidFrom in ascending order. 29 | ListCals(offset int, limit int) ([]model.Cal, error) 30 | 31 | // ListCalForDevice lists calibration data for device ordered by 32 | // validFrom date in descending order. 33 | ListCalsForDevice(deviceID string) ([]model.Cal, error) 34 | 35 | // ############################################################ 36 | // Message 37 | // ############################################################ 38 | 39 | // PutMessage adds a new message to database 40 | PutMessage(m *model.Message) (int64, error) 41 | 42 | // GetMessage gets a message by id 43 | GetMessage(id int64) (*model.Message, error) 44 | 45 | // ListMessages pages through messages. Messages are sorted in descending order by ReceivedTime. 46 | ListMessages(offset int, limit int) ([]model.Message, error) 47 | 48 | // ListMessagesByDate lists messages by date [from:to> 49 | ListMessagesByDate(from int64, to int64) ([]model.Message, error) 50 | 51 | // ListDeviceMessagesByDate lists messages by device and date [from:to> 52 | ListDeviceMessagesByDate(deviceID string, from int64, to int64) ([]model.Message, error) 53 | 54 | // Close the database 55 | Close() error 56 | } 57 | -------------------------------------------------------------------------------- /server/pkg/pipeline/pipemqtt/mqtt.go: -------------------------------------------------------------------------------- 1 | // Package pipemqtt is a very simplistic example of how to forward messages to 2 | // an MQTT server. If you want to use this for production you should 3 | // rewrite it for more robustness. 4 | // 5 | package pipemqtt 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | "time" 12 | 13 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 14 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 15 | mqtt "github.com/eclipse/paho.mqtt.golang" 16 | ) 17 | 18 | // MQTTStream ... 19 | type MQTTStream struct { 20 | client mqtt.Client 21 | topicPrefix string 22 | next pipeline.Pipeline 23 | } 24 | 25 | // New ... 26 | func New(clientID string, password string, address string, topicPrefix string) *MQTTStream { 27 | opts := createCLientOptions(clientID, password, address) 28 | client := mqtt.NewClient(opts) 29 | token := client.Connect() 30 | 31 | for !token.WaitTimeout(3 * time.Second) { 32 | } 33 | if err := token.Error(); err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | return &MQTTStream{ 38 | client: client, 39 | topicPrefix: topicPrefix, 40 | } 41 | 42 | } 43 | 44 | func createCLientOptions(clientID string, password string, address string) *mqtt.ClientOptions { 45 | opts := mqtt.NewClientOptions() 46 | opts.AddBroker(fmt.Sprintf("tcp://%s", address)) 47 | opts.SetClientID(clientID) 48 | opts.SetPassword(password) 49 | return opts 50 | } 51 | 52 | // Publish ... 53 | func (p *MQTTStream) Publish(m *model.Message) error { 54 | json, err := json.Marshal(m) 55 | if err == nil { 56 | topic := fmt.Sprintf("%s/%s", p.topicPrefix, m.DeviceID) 57 | token := p.client.Publish(topic, 0, false, json) 58 | if token.Error() != nil { 59 | log.Printf("Error publishing to MQTT: %v", token.Error()) 60 | } 61 | 62 | if !token.WaitTimeout(10 * time.Millisecond) { 63 | log.Printf("MQTT publish timed out") 64 | } 65 | } 66 | 67 | if p.next != nil { 68 | return p.next.Publish(m) 69 | } 70 | return nil 71 | } 72 | 73 | // AddNext ... 74 | func (p *MQTTStream) AddNext(pe pipeline.Pipeline) { 75 | p.next = pe 76 | } 77 | 78 | // Next ... 79 | func (p *MQTTStream) Next() pipeline.Pipeline { 80 | return p.next 81 | } 82 | -------------------------------------------------------------------------------- /server/cmd/aq/import.go: -------------------------------------------------------------------------------- 1 | // The import command imports calibration data from CSV file into the database. 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "io/ioutil" 7 | "log" 8 | 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store/sqlitestore" 11 | ) 12 | 13 | // ImportCommand defines the command line parameters for import command. 14 | type ImportCommand struct { 15 | CalFetch bool `short:"f" long:"fetch-cal" description:"Fetch calibration data from network"` 16 | CalURL string `short:"u" long:"cal-url" description:"Distribution URL for calibration data" default:""` 17 | } 18 | 19 | const ( 20 | layout = "2006-01-02T15:04:05.000Z" 21 | ) 22 | 23 | func init() { 24 | parser.AddCommand( 25 | "import", 26 | "Import a calibration data", 27 | "The import command imports a the calibration data to the database", 28 | &ImportCommand{}) 29 | } 30 | 31 | // Execute runs the import command. 32 | func (a *ImportCommand) Execute(args []string) error { 33 | if len(args) < 1 { 34 | log.Fatalf("Please provide name of JSON file(s)") 35 | } 36 | 37 | a.importFiles(args) 38 | 39 | return nil 40 | } 41 | 42 | func (a *ImportCommand) importFiles(files []string) { 43 | db, err := sqlitestore.New(options.DBFilename) 44 | if err != nil { 45 | log.Fatalf("Unable to open or create database: %v", err) 46 | } 47 | defer db.Close() 48 | 49 | for _, fileName := range files { 50 | data, err := ioutil.ReadFile(fileName) 51 | if err != nil { 52 | log.Printf("Cannot read %s, skipping: %v", fileName, err) 53 | continue 54 | } 55 | 56 | var cal model.Cal 57 | err = json.Unmarshal(data, &cal) 58 | if err != nil { 59 | log.Printf("Cannot unmarshal %s, skipping: %v", fileName, err) 60 | continue 61 | } 62 | 63 | // Do some validation 64 | if cal.DeviceID == "" { 65 | log.Printf("DeviceID is not set in %s, skipping", fileName) 66 | continue 67 | } 68 | 69 | // Override CollectionID if parameter is non-empty 70 | if options.HordeCollection != "" { 71 | cal.CollectionID = options.HordeCollection 72 | } 73 | 74 | id, err := db.PutCal(&cal) 75 | if err != nil { 76 | log.Fatalf("Unable to import calibration entry %s into database: %v", fileName, err) 77 | } 78 | 79 | log.Printf("Imported %s, CollectionID='%s', deviceID='%s', ID='%d'", fileName, cal.CollectionID, cal.DeviceID, id) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Firmware/src/nmealib.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | 18 | #ifndef NMEALIB_H 19 | #define NMEALIB_H 20 | 21 | #include 22 | #include 23 | 24 | /** 25 | * Maximum size of NMEA buffer 26 | */ 27 | #define MAX_NMEA_BUFFER 254 28 | 29 | /** 30 | * Preamble character. 31 | */ 32 | #define NMEA_PREAMBLE '$' 33 | 34 | /** 35 | * Checksum character 36 | */ 37 | #define NMEA_CHECKSUM '*' 38 | 39 | /** 40 | * Field separator 41 | */ 42 | 43 | #define FIELD_SEPARATOR ',' 44 | 45 | #define CR '\r' 46 | #define LF '\n' 47 | 48 | /** 49 | * The maximum number of fields in a sentence. The number might be a bit 50 | * conservative if you use all 255 bytes and/or skip a lot of fields. 51 | * There's probably a proprietary sentence out there that will break this 52 | * but we're only going to process GGA/GSV/GLL and so on. 53 | */ 54 | #define MAX_NMEA_FIELDS 64 55 | 56 | typedef struct { 57 | uint8_t* fields[MAX_NMEA_FIELDS]; /* array of fields */ 58 | uint8_t field_count; /* number of fields */ 59 | uint8_t talker[3]; /* Sentence talker or 'P' if proprietary */ 60 | uint8_t* type; /* sentence type */ 61 | } nmea_sentence_t; 62 | 63 | /** 64 | * Add checksum and CRLF to NMEA sentence, overwriting the end. Assumes that 65 | * either the sentence is 0-terminated, uses another checksum or with CRLF. 66 | * Will explode if there isn't enough room in the buffer for checksum and CRLF. 67 | */ 68 | void nmea_end(uint8_t* buffer); 69 | 70 | /** 71 | * Parse NMEA sentence in buffer. This will modify the original buffer and 72 | * insert zero terminators into it. 73 | * 74 | * Returns false if the buffer doesn't contain a valid NMEA sentence. 75 | */ 76 | bool nmea_parse(uint8_t* buffer, nmea_sentence_t* output); 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /server/doc/01-introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The Air Quality Server (AQS) is a lightweight tool that can be used to 4 | receive, store and serve air quality data from the third generation 5 | air quality sensor from Exploratory Engineering. It can both be run 6 | as a more permanent server in a datacenter in order to receive, 7 | process, store and pass on data, but it is also designed for ad-hoc 8 | experimentation by researchers and integrators. Some care has been 9 | taken in order to make both scenarios possible. It should be trivial 10 | to download, build and run the server on your personal machine, as 11 | well as running it in a datacenter. 12 | 13 | ## Data reception 14 | 15 | Currently the server supports two different ways of receiving data - 16 | you can either configure it to connect to IoT-GW/Horde and have it 17 | listen to the data stream belonging to a given *collection*. 18 | 19 | We are also in the process of developing a solution that will allow 20 | the server to use MIC as the source of messages, but this is not 21 | supported at the time of writing. 22 | 23 | For experimentation purposes it is also possible to use a UDP-based 24 | protocol for injecting messages into a running server. 25 | 26 | ## Database storage 27 | 28 | The AQS makes use of a local SQLite 3 database where it stores 29 | calibration data as well as the messages (datapoints) it has received 30 | from the network. We chose to use SQLite 3 as the database as a 31 | convenience because the data rates and amounts of data are fairly 32 | modest for the number of sensors we are dealing with. At some later 33 | point we may support PostgreSQL so that the Air Quality Server can 34 | deal with higher volumes. 35 | 36 | The choice of embedding the database was made to make life simpler for 37 | researchers and people who simply want to experiment with the system, 38 | as there is no need to set up and manage a separate database system. 39 | Everything is taken care of by the server. The database itself is 40 | located in a single file, which makes it easy to copy the data around 41 | and perform ad-hoc queries on it using the `sqlite3` command line 42 | utility. 43 | 44 | ## Open source 45 | 46 | The AQS is published under the Apache 2.0 license, and you are 47 | encouraged to contribute to its development. Bug reports and feature 48 | requests filed through the issue tracking on the Github project are 49 | welcome. Pull requests with code are even more welcome. The Git repository is located at 50 | [https://github.com/exploratoryengineering/air-quality-sensor-node](https://github.com/exploratoryengineering/air-quality-sensor-node). 51 | 52 | \newpage 53 | -------------------------------------------------------------------------------- /Firmware/src/at_commands.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define AT_OK 0 8 | #define AT_ERROR -1 9 | #define AT_TIMEOUT -2 10 | 11 | /** 12 | * @brief Decode AT+NSORF response. The buffer is read in multiple chunks from 13 | * the modem. 14 | * @return -1 for ERROR response, -2 for timeout, number of bytes decoded otherwise 15 | * @note Will swallow URCs and call the appropriate callbacks. The lenght of 16 | * the buffer must fit the number of bytes that is returned (it's set in 17 | * the NSORF command) 18 | */ 19 | int atnsorf_decode(int *sockfd, char *ip, int *port, uint8_t *data, size_t *received, size_t *remaining); 20 | 21 | /** 22 | * @brief Decode AT+CGPADDR response. 23 | * @return 0 for OK, -1 for ERROR response, -2 for timeout, lenght of address string otherwise 24 | * @note Will swallow URCs and call appropriate callbacks. Address might be "0" 25 | */ 26 | int atcgpaddr_decode(char *address, size_t *len); 27 | 28 | /** 29 | * @brief Decode AT+NSOCR response. Reads until OK or ERROR is received. 30 | * @return socket file descriptor for modem, -1 for ERROR response, -2 for timeout 31 | * @note Will swallow URCs and call the appropriate callbacks 32 | */ 33 | int atnsocr_decode(int *sockfd); 34 | 35 | /** 36 | * @brief Decode AT+NSOCL response. Reads until OK or ERROR is received. 37 | * @return 0 for OK, -1 for ERROR 38 | * @note Will swallow URCs and call the appropriate callbacks 39 | */ 40 | int atnsocl_decode(); 41 | 42 | /** 43 | * @brief Decode AT+NSOST response. Reads until OK or ERROR is received. 44 | * @return 0 for OK, -1 for ERROR, -2 for timeout 45 | * @note Will swallow URCs and call the appropriate callbacks 46 | */ 47 | int atnsost_decode(int *sock_fd, size_t *sent); 48 | 49 | /** 50 | * @brief Reads response from AT+NRB command. Reads until OK or ERROR is received. 51 | * @return 0 for OK, -1 for ERROR response, -2 for timeout, -3 for invalid input 52 | * @note Will swallow URCs and call the appropriate callbacks 53 | */ 54 | int atnrb_decode(); 55 | 56 | /** 57 | * @brief decode AT+CIMI response from modem 58 | * @note buffer should have enough room for IMSIs (22 chars) 59 | */ 60 | int atcimi_decode(char *imsi); 61 | 62 | /** 63 | * @brief decode AT+GCSN=1 response from modem 64 | * @note buffer should have enough room for IMEI (22 chars) 65 | */ 66 | int atcgsn_decode(char *imei); 67 | 68 | /** 69 | * @brief decode AT+CPSMS response from modem. 70 | */ 71 | int atcpsms_decode(); 72 | 73 | /** 74 | * @brief generic decode function. Waits for OK, ERROR or timeout 75 | */ 76 | int at_generic_decode(); -------------------------------------------------------------------------------- /Firmware/prj.conf: -------------------------------------------------------------------------------- 1 | # Basics 2 | CONFIG_DEBUG=y 3 | 4 | # Logging 5 | CONFIG_LOG=y 6 | CONFIG_LOG_IMMEDIATE=n 7 | CONFIG_LOG_STRDUP_BUF_COUNT=30 8 | CONFIG_MAIN_LOG_LEVEL_DBG=y 9 | CONFIG_GPS_LOG_LEVEL_INF=y 10 | CONFIG_MAX14830_LOG_LEVEL_INF=y 11 | CONFIG_EEGPIO_LOG_LEVEL_INF=y 12 | CONFIG_CHIPCAP2_LOG_LEVEL_INF=y 13 | CONFIG_ADSL_LOG_LEVEL_INF=y 14 | CONFIG_COMMS_LOG_LEVEL_INF=y 15 | CONFIG_OPCN3_LOG_LEVEL_INF=y 16 | CONFIG_EEI2C_LOG_LEVEL_INF=y 17 | CONFIG_EESPI_LOG_LEVEL_INF=y 18 | CONFIG_FOTA_LOG_LEVEL_DBG=y 19 | CONFIG_SARAN2_LOG_LEVEL_INF=y 20 | 21 | CONFIG_COAP_LOG_LEVEL_DBG=y 22 | CONFIG_NET_LOG=y 23 | 24 | # Console 25 | CONFIG_CONSOLE=y 26 | CONFIG_STDOUT_CONSOLE=y 27 | CONFIG_RTT_CONSOLE=y 28 | CONFIG_UART_CONSOLE=n 29 | CONFIG_USE_SEGGER_RTT=y 30 | CONFIG_HAS_SEGGER_RTT=y 31 | 32 | # GPIO config 33 | CONFIG_GPIO_AS_PINRESET=n 34 | CONFIG_GPIO=y 35 | 36 | # I2C config 37 | CONFIG_I2C=y 38 | CONFIG_I2C_NRFX=y 39 | CONFIG_I2C_0=y 40 | 41 | # SPI config 42 | CONFIG_SPI=y 43 | CONFIG_SPI_1=y 44 | CONFIG_SPI_LOG_LEVEL_WRN=y 45 | 46 | # UART setup 47 | CONFIG_SERIAL=y 48 | CONFIG_UART_INTERRUPT_DRIVEN=y 49 | 50 | # System 51 | CONFIG_HW_STACK_PROTECTION=y 52 | CONFIG_HEAP_MEM_POOL_SIZE=16384 53 | CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=8192 54 | CONFIG_NEWLIB_LIBC=y 55 | CONFIG_NUM_COOP_PRIORITIES=10 56 | CONFIG_NUM_PREEMPT_PRIORITIES=10 57 | CONFIG_MAIN_STACK_SIZE=16384 58 | 59 | # MCUBoot and flash - needed by LwM2M FOTA 60 | CONFIG_BOOTLOADER_MCUBOOT=y 61 | CONFIG_FLASH_PAGE_LAYOUT=y 62 | CONFIG_FLASH=y 63 | CONFIG_IMG_MANAGER=y 64 | CONFIG_MCUBOOT_IMG_MANAGER=y 65 | CONFIG_MPU_ALLOW_FLASH_WRITE=y 66 | CONFIG_REBOOT=y 67 | 68 | # Non-volatile storage (for config parameters) 69 | CONFIG_NVS=y 70 | CONFIG_NVS_LOG_LEVEL_DBG=y 71 | 72 | # This is enabled by default but we don't need it 73 | CONFIG_CHARACTER_FRAMEBUFFER=n 74 | 75 | # Ring buffer -used when reading UART and processing commands from 76 | # the modem 77 | CONFIG_RING_BUFFER=y 78 | 79 | # Network config 80 | CONFIG_NET_BUF_RX_COUNT=1 81 | CONFIG_NET_BUF_TX_COUNT=1 82 | CONFIG_NET_IPV4=n 83 | CONFIG_NET_IPV6=n 84 | CONFIG_NET_L2_DUMMY=y 85 | CONFIG_NET_NATIVE_UDP=n 86 | CONFIG_NET_NATIVE=n 87 | CONFIG_NET_OFFLOAD=y 88 | CONFIG_NET_RX_STACK_SIZE=512 89 | CONFIG_NET_SOCKETS_OFFLOAD=y 90 | CONFIG_NET_SOCKETS_POSIX_NAMES=y 91 | CONFIG_NET_SOCKETS=y 92 | CONFIG_NET_STATISTICS=n 93 | CONFIG_NET_TX_STACK_SIZE=512 94 | CONFIG_NET_UDP=y 95 | CONFIG_NETWORKING=y 96 | 97 | # CoAP config - needed by LwM2M 98 | CONFIG_COAP=y 99 | #CONFIG_COAP_EXTENDED_OPTIONS_LEN_VALUE=17 100 | #CONFIG_COAP_EXTENDED_OPTIONS_LEN=y 101 | #CONFIG_COAP_INIT_ACK_TIMEOUT_MS=20000 102 | 103 | CONFIG_WATCHDOG=y 104 | 105 | -------------------------------------------------------------------------------- /Firmware/src/chipcap2.c: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #include 18 | #include "chipcap2.h" 19 | #include 20 | #include 21 | #include 22 | #include "i2c_config.h" 23 | #include "messagebuffer.h" 24 | 25 | // Note: The Chipcap sensor is onlys used for monitoring ambient temperature and humidity in the controller housing 26 | // The particle and gas sensors will provide temperature and RH data for the environmentl measurements. 27 | 28 | #define LOG_LEVEL CONFIG_CHIPCAP2_LOG_LEVEL 29 | #include 30 | LOG_MODULE_REGISTER(CHIPCAP2); 31 | 32 | #define CC2_VALID_DATA (0x00) 33 | #define CC2_STALE_DATA (0x01) 34 | 35 | int cc2_init() 36 | { 37 | uint8_t NORMAL_OPERATION_MODE[] = {CHIPCAP2_NORMAL_OPERATION_MODE, 0, 0}; 38 | return i2c_write(get_I2C_device(), NORMAL_OPERATION_MODE, 3, CHIPCAP2_ADDRESS); 39 | } 40 | 41 | static CC2_SAMPLE current; 42 | 43 | void cc2_sample_data() 44 | { 45 | struct device *i2c_device = get_I2C_device(); 46 | uint8_t rxBuffer[] = {0, 0, 0, 0}; 47 | int err = i2c_read(i2c_device, rxBuffer, 4, CHIPCAP2_ADDRESS); 48 | if (0 != err) 49 | { 50 | LOG_ERR("Unable to get Chipcap2 sensor reading. i2c_read failed with error: %d", err); 51 | return; 52 | } 53 | 54 | // uint8_t status = rxBuffer[0] >> 6; 55 | float RH_H = (rxBuffer[0] & 0b00111111); 56 | float RH_L = rxBuffer[1]; 57 | current.RH = ((RH_H * 256 + RH_L) / 16384) * 100; 58 | float TempC_H = rxBuffer[2]; 59 | float TempC_L = rxBuffer[3] >> 4; 60 | current.Temp_C = ((TempC_H * 64 + TempC_L) / 16384) * 165 - 40; 61 | 62 | // Send a new measurement request after reading. Cannot be sent before first read 63 | // uint8_t measurement_request = 1; 64 | 65 | err = i2c_write(i2c_device, NULL, 0, CHIPCAP2_ADDRESS); 66 | if (0 != err) 67 | { 68 | LOG_ERR("Unable to send measurement request to Chipcap2 sensor. i2c_write failed with error: %d", err); 69 | } 70 | } 71 | 72 | void cc2_get_sample(CC2_SAMPLE *msg) 73 | { 74 | memcpy(msg, ¤t, sizeof(current)); 75 | } -------------------------------------------------------------------------------- /Firmware/src/gps_cache.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #ifndef _GPS_CACHE_H_ 18 | #define _GPS_CACHE_H_ 19 | // Cache functions for GPS fix. The fix is updated at a set rate from the 20 | // receiver and accessing the latest fix is simplified by caching the latest 21 | // result. 22 | #include 23 | #include 24 | 25 | #include "gps_std.h" 26 | 27 | // Fix information; aggregates all of the relevant fix into a single struct 28 | typedef struct 29 | { 30 | float timestamp; 31 | float latitude; 32 | float longitude; 33 | float altitude; 34 | float hdop; 35 | float vdop; 36 | float pdop; 37 | uint8_t fix_quality; 38 | uint8_t gps_svs; 39 | uint8_t glo_svs; 40 | uint8_t waas_svs; 41 | bool moving; 42 | float track_angle; 43 | } gps_fix_t; 44 | 45 | typedef struct 46 | { 47 | uint8_t max_time; 48 | uint8_t min_time; 49 | uint8_t last_time; 50 | uint16_t timeouts; 51 | uint16_t samples; 52 | uint32_t total_time; 53 | } gps_fix_stats_t; 54 | 55 | // Check if fix is valid 56 | bool gps_fix_is_valid(const gps_fix_t *fix); 57 | 58 | // Get the latest fix. Returns false if no fix is available. 59 | bool gps_get_fix(gps_fix_t *fix); 60 | 61 | // Update fix with GGA data (internal?) 62 | void gps_update_gga(gps_gga_t *gga); 63 | 64 | // Update fix with GSA data (internal?) 65 | void gps_update_gsa(gps_gsa_t *gsa); 66 | 67 | // Update fix with RMC data (internal?) 68 | void gps_update_rmc(gps_rmc_t *rmc); 69 | 70 | void gps_update_vtg(gps_vtg_t *vtg); 71 | 72 | // Add sample point 73 | void gps_add_fix_stat(const int seconds_to_fix); 74 | 75 | void gps_add_timeout_stat(); 76 | 77 | // Get fix statistics 78 | void gps_get_fix_stat(gps_fix_stats_t *statcp); 79 | 80 | // Reset fix cache 81 | void gps_reset_cache(void); 82 | 83 | typedef struct imu_data_s 84 | { 85 | float accel_x; 86 | float accel_y; 87 | float accel_z; 88 | float mag_x; 89 | float mag_y; 90 | float mag_z; 91 | float temperature; 92 | } imu_data_t; 93 | 94 | void imu_update_accel(float x, float y, float z); 95 | 96 | void imu_update_temp(float temperature); 97 | 98 | void imu_update_mag(float x, float y, float z); 99 | 100 | void imu_get_data(imu_data_t *data); 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /server/cmd/aq/calibration.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 12 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store" 13 | sqlite3 "github.com/mattn/go-sqlite3" 14 | ) 15 | 16 | // DefaultFilenamePattern is the default pattern to be used for 17 | // reading the calibration data files. 18 | const DefaultFilenamePattern = "*.json" 19 | 20 | // checkForNewCalibrationData fetches the calibration data from the 21 | // distribution point in S3 and attempts to insert it into the 22 | // database. If it already exists the database will just reject it 23 | // with a constraint error. 24 | func loadCalibrationData(db store.Store, dir string) error { 25 | cals := make(map[uint64]*model.Cal) 26 | pattern := DefaultFilenamePattern 27 | 28 | f := func(path string, info os.FileInfo, err error) error { 29 | if info.IsDir() { 30 | return nil 31 | } 32 | 33 | match, err := filepath.Match(pattern, info.Name()) 34 | if err != nil { 35 | log.Printf("Error matching '%s': %v", pattern, err) 36 | return err 37 | } 38 | 39 | if !match { 40 | log.Printf("SKIP '%s', no match for %s", info.Name(), pattern) 41 | return nil 42 | } 43 | 44 | data, err := ioutil.ReadFile(path) 45 | if err != nil { 46 | log.Printf("SKIP '%s', error reading : %v", path, err) 47 | return err 48 | } 49 | 50 | var cal model.Cal 51 | err = json.Unmarshal(data, &cal) 52 | if err != nil { 53 | log.Printf("SKIP '%s' unable to parse : %v", path, err) 54 | return err 55 | } 56 | 57 | // Check if SysID is present. If it is not the calibration 58 | // file cannot be used and will be skipped. 59 | if cal.SysID == 0 { 60 | log.Printf("SKIP '%s' Not a valid calibration file: SysID missing", path) 61 | return nil 62 | } 63 | 64 | cals[cal.SysID] = &cal 65 | return nil 66 | } 67 | 68 | dirInfo, err := os.Stat(dir) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | if !dirInfo.IsDir() { 74 | return fmt.Errorf("'%s' is not a directory", dir) 75 | } 76 | 77 | err = filepath.Walk(dir, f) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | newCalibrationSets := 0 83 | calibrationSetCount := 0 84 | 85 | for _, v := range cals { 86 | calibrationSetCount++ 87 | _, err := db.PutCal(v) 88 | if err != nil { 89 | e, ok := err.(sqlite3.Error) 90 | if !(ok && e.Code == sqlite3.ErrConstraint) { 91 | log.Printf("Error loading calibration data: %v", err) 92 | } 93 | } else { 94 | log.Printf("Adding calibration data for %s", v.DeviceID) 95 | newCalibrationSets++ 96 | } 97 | } 98 | 99 | log.Printf("Read %d calibration files from %s and found %d new sets.", calibrationSetCount, dir, newCalibrationSets) 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /server/doc/02-build.md: -------------------------------------------------------------------------------- 1 | # Building AQS 2 | 3 | ## Check out source from Github 4 | 5 | The AQS is hosted on Github at and you can clone it by issuing the 6 | command: 7 | 8 | git clone https://github.com/ExploratoryEngineering/air-quality-sensor-node.git 9 | 10 | This will copy not only the server, but also the hardware designs for 11 | the sensor, the firmware source code for the device, and the server. 12 | In order to build the server change directory to the `server` 13 | directory. 14 | 15 | ## Fetch dependencies 16 | 17 | - Go 1.14 or newer 18 | - Protobuf Compiler version 3.0.0 or newer 19 | - Protobuf Code generator for Go 20 | - SQLite3 libraries 21 | 22 | ### Installing Go 23 | 24 | For information on how to install Go, please refer to 25 | https://golang.org/doc/install. 26 | 27 | 28 | ### Installing Protobuf 29 | 30 | To install the protobuf compiler on macOS the easiest route is to use 31 | Homebrew and install it with: 32 | 33 | brew install protobuf 34 | 35 | or on Linux 36 | 37 | apt install protobuf-compiler 38 | 39 | You then need to install the protobuf code generator for Go, which you 40 | can install with the command, which should work on all platforms Go 41 | has been ported to: 42 | 43 | go install google.golang.org/protobuf/cmd/protoc-gen-go 44 | 45 | ### Installing libsqlite3 46 | 47 | On macOS you can install the latest SQLite version (including 48 | libraries) using 49 | 50 | brew install sqlite 51 | 52 | On Linux you can do this with 53 | 54 | apt install libsqlite3-dev 55 | 56 | 57 | ## Building AQS with `make` 58 | 59 | We use `make` to build the server. Per default the `Makefile` is set 60 | up to build the server for macOS. You can either edit the `Makefile` 61 | and change the default values for `GOOS` and `GOARCH` to fit the 62 | platform you are building on, or you can specify these variables on 63 | the command line. 64 | 65 | For instance in order to build for Linux you would 66 | 67 | GOOS=linux GOARCH=amd64 make 68 | 69 | This should produce a `bin` directory containing a runnable binary 70 | called `ac`. To test that you have succeeded in building it you can 71 | run `bin/aq` and it should produce output that looks something like 72 | this: 73 | 74 | $ bin/aq -h 75 | Usage: 76 | aq [OPTIONS] 77 | 78 | Application Options: 79 | -c, --collection= Horde collection id to listen to (default: 17dh0cf43jg007) 80 | -d, --db= Data storage file (default: aq.db) 81 | -v, --verbose 82 | 83 | Help Options: 84 | -h, --help Show this help message 85 | 86 | Available commands: 87 | convert Convert calibration data 88 | fetch Fetch historical data 89 | import Import a calibration data 90 | list List calibration data 91 | rm Remove calibration data 92 | run Run server 93 | show Show calibration data for device 94 | 95 | \newpage 96 | -------------------------------------------------------------------------------- /server/pkg/model/util.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/aqpb" 5 | "github.com/golang/protobuf/proto" 6 | ) 7 | 8 | // MessageFromProtobuf takes a Sample protobuffer and returns a 9 | // Message 10 | func MessageFromProtobuf(s *aqpb.Sample) *Message { 11 | return &Message{ 12 | // Board fields 13 | SysID: s.Sysid, 14 | FirmwareVersion: s.FirmwareVersion, 15 | Uptime: s.Uptime, 16 | BoardTemp: s.BoardTemp, 17 | BoardRelHumidity: s.BoardRelHumidity, 18 | Status: s.Status, 19 | 20 | // GPS fields 21 | GPSTimeStamp: s.GpsTimestamp, 22 | Lat: s.Lat, 23 | Lon: s.Lon, 24 | Alt: s.Alt, 25 | 26 | // AFE3 fields 27 | Sensor1Work: s.Sensor_1Work, 28 | Sensor1Aux: s.Sensor_1Aux, 29 | Sensor2Work: s.Sensor_2Work, 30 | Sensor2Aux: s.Sensor_2Aux, 31 | Sensor3Work: s.Sensor_3Work, 32 | Sensor3Aux: s.Sensor_3Aux, 33 | AFE3TempRaw: s.Afe3TempRaw, 34 | 35 | // OPC-N3 fields 36 | OPCPMA: s.OpcPmA, 37 | OPCPMB: s.OpcPmB, 38 | OPCPMC: s.OpcPmC, 39 | PM1: s.Pm1, 40 | PM10: s.Pm10, 41 | PM25: s.Pm25, 42 | OPCSamplePeriod: uint16(s.OpcSamplePeriod), 43 | OPCSampleFlowRate: uint16(s.OpcSampleFlowRate), 44 | OPCTemp: uint16(s.OpcTemp), 45 | OPCHum: uint16(s.OpcHum), 46 | OPCFanRevcount: uint16(s.OpcFanRevcount), 47 | OPCLaserStatus: uint16(s.OpcLaserStatus), 48 | OPCSampleValid: uint8(s.OpcSampleValid), 49 | OPCBin0: uint16(s.OpcBin_0), 50 | OPCBin1: uint16(s.OpcBin_1), 51 | OPCBin2: uint16(s.OpcBin_2), 52 | OPCBin3: uint16(s.OpcBin_3), 53 | OPCBin4: uint16(s.OpcBin_4), 54 | OPCBin5: uint16(s.OpcBin_5), 55 | OPCBin6: uint16(s.OpcBin_6), 56 | OPCBin7: uint16(s.OpcBin_7), 57 | OPCBin8: uint16(s.OpcBin_8), 58 | OPCBin9: uint16(s.OpcBin_9), 59 | OPCBin10: uint16(s.OpcBin_10), 60 | OPCBin11: uint16(s.OpcBin_11), 61 | OPCBin12: uint16(s.OpcBin_12), 62 | OPCBin13: uint16(s.OpcBin_13), 63 | OPCBin14: uint16(s.OpcBin_14), 64 | OPCBin15: uint16(s.OpcBin_15), 65 | OPCBin16: uint16(s.OpcBin_16), 66 | OPCBin17: uint16(s.OpcBin_17), 67 | OPCBin18: uint16(s.OpcBin_18), 68 | OPCBin19: uint16(s.OpcBin_19), 69 | OPCBin20: uint16(s.OpcBin_20), 70 | OPCBin21: uint16(s.OpcBin_21), 71 | OPCBin22: uint16(s.OpcBin_22), 72 | OPCBin23: uint16(s.OpcBin_23), 73 | } 74 | } 75 | 76 | // ProtobufFromData unmarshals a protobuffer from a byte slice 77 | func ProtobufFromData(buf []byte) (*aqpb.Sample, error) { 78 | pb := aqpb.Sample{} 79 | return &pb, proto.Unmarshal(buf, &pb) 80 | } 81 | 82 | // DataFromProtobuf marshals a aqpb.Sample into a byte slice 83 | func DataFromProtobuf(pb *aqpb.Sample) ([]byte, error) { 84 | return proto.Marshal(pb) 85 | } 86 | -------------------------------------------------------------------------------- /server/pkg/pipeline/stream/client.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | type client struct { 12 | conn *websocket.Conn 13 | send chan []byte 14 | broker *Broker 15 | } 16 | 17 | const ( 18 | sendQueueLen = 256 19 | maxMessageSize = 256 20 | pongWait = 60 * time.Second 21 | pingPeriod = (pongWait * 9) / 10 22 | writeWait = 10 * time.Second 23 | ) 24 | 25 | var ( 26 | newline = []byte{'\n'} 27 | space = []byte{' '} 28 | ) 29 | 30 | func newClient(conn *websocket.Conn, broker *Broker) *client { 31 | c := &client{ 32 | conn: conn, 33 | send: make(chan []byte, sendQueueLen), 34 | broker: broker, 35 | } 36 | 37 | go c.readLoop() 38 | go c.writeLoop() 39 | 40 | return c 41 | } 42 | 43 | func (c *client) readLoop() { 44 | defer func() { 45 | c.broker.unregister <- c 46 | c.conn.Close() 47 | }() 48 | 49 | c.conn.SetReadLimit(maxMessageSize) 50 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 51 | 52 | c.conn.SetPongHandler(func(string) error { 53 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 54 | return nil 55 | }) 56 | 57 | for { 58 | _, message, err := c.conn.ReadMessage() 59 | if err != nil { 60 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 61 | log.Printf("Websocket error: %v", err) 62 | } 63 | break 64 | } 65 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 66 | 67 | // Since we do not currently handle incoming messages we just log them 68 | log.Printf("Incoming message from [%v]: '%s'", c.conn.RemoteAddr(), message) 69 | } 70 | } 71 | 72 | func (c *client) writeLoop() { 73 | ticker := time.NewTicker(pingPeriod) 74 | defer func() { 75 | ticker.Stop() 76 | c.conn.Close() 77 | }() 78 | 79 | for { 80 | select { 81 | case message, ok := <-c.send: 82 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 83 | if !ok { 84 | // The broker closed the channel. 85 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 86 | return 87 | } 88 | 89 | w, err := c.conn.NextWriter(websocket.TextMessage) 90 | if err != nil { 91 | log.Printf("Error writing to websocket [%v]: %v", c.conn.RemoteAddr(), err) 92 | return 93 | } 94 | w.Write(message) 95 | 96 | // Add queued chat messages to the current websocket message. 97 | n := len(c.send) 98 | for i := 0; i < n; i++ { 99 | w.Write(newline) 100 | w.Write(<-c.send) 101 | } 102 | 103 | if err := w.Close(); err != nil { 104 | log.Printf("Error closing websocket writer for [%v]: %v", c.conn.RemoteAddr(), err) 105 | return 106 | } 107 | case <-ticker.C: 108 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 109 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 110 | log.Printf("Error writing ping message to [%v]: %v", c.conn.RemoteAddr(), err) 111 | return 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /server/pkg/store/sqlitestore/cal.go: -------------------------------------------------------------------------------- 1 | package sqlitestore 2 | 3 | import ( 4 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 5 | ) 6 | 7 | // PutCal ... 8 | func (s *SqliteStore) PutCal(c *model.Cal) (int64, error) { 9 | s.mu.Lock() 10 | defer s.mu.Unlock() 11 | 12 | r, err := s.db.NamedExec(` 13 | INSERT INTO cal 14 | ( 15 | device_id, 16 | sysid, 17 | collection_id, 18 | valid_from, 19 | afe_serial, 20 | circuit_type, 21 | afe_type, 22 | sensor1_serial, 23 | sensor2_serial, 24 | sensor3_serial, 25 | afe_cal_date, 26 | vt20_offset, 27 | sensor1_we_e, 28 | sensor1_we_0, 29 | sensor1_ae_e, 30 | sensor1_ae_0, 31 | sensor1_pcb_gain, 32 | sensor1_we_sensitivity, 33 | sensor2_we_e, 34 | sensor2_we_0, 35 | sensor2_ae_e, 36 | sensor2_ae_0, 37 | sensor2_pcb_gain, 38 | sensor2_we_sensitivity, 39 | sensor3_we_e, 40 | sensor3_we_0, 41 | sensor3_ae_e, 42 | sensor3_ae_0, 43 | sensor3_pcb_gain, 44 | sensor3_we_sensitivity 45 | ) 46 | VALUES( 47 | :device_id, 48 | :sysid, 49 | :collection_id, 50 | :valid_from, 51 | :afe_serial, 52 | :circuit_type, 53 | :afe_type, 54 | :sensor1_serial, 55 | :sensor2_serial, 56 | :sensor3_serial, 57 | :afe_cal_date, 58 | :vt20_offset, 59 | :sensor1_we_e, 60 | :sensor1_we_0, 61 | :sensor1_ae_e, 62 | :sensor1_ae_0, 63 | :sensor1_pcb_gain, 64 | :sensor1_we_sensitivity, 65 | :sensor2_we_e, 66 | :sensor2_we_0, 67 | :sensor2_ae_e, 68 | :sensor2_ae_0, 69 | :sensor2_pcb_gain, 70 | :sensor2_we_sensitivity, 71 | :sensor3_we_e, 72 | :sensor3_we_0, 73 | :sensor3_ae_e, 74 | :sensor3_ae_0, 75 | :sensor3_pcb_gain, 76 | :sensor3_we_sensitivity) 77 | `, c) 78 | 79 | if err != nil { 80 | return -1, err 81 | } 82 | 83 | return r.LastInsertId() 84 | } 85 | 86 | // GetCal ... 87 | func (s *SqliteStore) GetCal(id int64) (*model.Cal, error) { 88 | s.mu.Lock() 89 | defer s.mu.Unlock() 90 | 91 | var c model.Cal 92 | err := s.db.QueryRowx("SELECT * FROM cal WHERE id = ?", id).StructScan(&c) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return &c, nil 97 | } 98 | 99 | // DeleteCal ... 100 | func (s *SqliteStore) DeleteCal(id int64) error { 101 | s.mu.Lock() 102 | defer s.mu.Unlock() 103 | 104 | _, err := s.db.Exec("DELETE FROM cal WHERE id = ?", id) 105 | return err 106 | } 107 | 108 | // ListCals ... 109 | func (s *SqliteStore) ListCals(offset int, limit int) ([]model.Cal, error) { 110 | s.mu.Lock() 111 | defer s.mu.Unlock() 112 | 113 | var cals []model.Cal 114 | err := s.db.Select(&cals, "SELECT * FROM cal ORDER BY device_id, valid_from ASC LIMIT ? OFFSET ?", limit, offset) 115 | return cals, err 116 | } 117 | 118 | // ListCalsForDevice ... 119 | func (s *SqliteStore) ListCalsForDevice(deviceID string) ([]model.Cal, error) { 120 | s.mu.Lock() 121 | defer s.mu.Unlock() 122 | 123 | var cals []model.Cal 124 | err := s.db.Select(&cals, "SELECT * FROM cal WHERE device_id = ? ORDER BY valid_from DESC", deviceID) 125 | return cals, err 126 | } 127 | -------------------------------------------------------------------------------- /Firmware/src/gps_std.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | // Standard GPS sentence decoding. GGA and GSA is the most relevant alternatives 18 | // to decode. GSV, RMC and GSV plus ZDA might be relevant later 19 | #pragma once 20 | 21 | #include 22 | 23 | #include "nmealib.h" 24 | 25 | #define M_PI 3.14159265359 26 | 27 | #define DEG_TO_RAD(x) ((x) * (M_PI / 180.0f)) 28 | #define RAD_TO_DEG(x) ((x) * (180.0f / M_PI)) 29 | 30 | #define FIX_Q_INVALID 0 31 | #define FIX_Q_GPS 1 32 | #define FIX_Q_DGPS 2 33 | #define FIX_Q_PPS 3 34 | #define FIX_Q_RTK 4 35 | #define FIX_Q_FLOAT_RTK 5 36 | #define FIX_Q_ESTIMATED 6 37 | #define FIX_Q_MANUAL 7 38 | #define FIX_Q_SIM 8 39 | 40 | #define FIX_NONE 1 41 | #define FIX_2D 2 42 | #define FIX_3D 3 43 | 44 | #define MAX_GSA_PRNS 12 45 | 46 | // GGA: Essiential fix data 47 | typedef struct 48 | { 49 | float timestamp; // time (HHMMSS.fff) 50 | float latitude; // latitude (positive: north, negative: south) 51 | float longitude; // longitude (positive: east, negative: west) 52 | uint8_t fix_quality; // fix quality, see constants 53 | uint8_t num_svs; // number of satellites 54 | float hdop; // height dilution of precision 55 | float alt; // altitude (over sea level), meters 56 | float geoid_height; // height over geoid (usually WGS84), meters 57 | } gps_gga_t; 58 | 59 | // GSA: GPS DOP and active satellites 60 | typedef struct 61 | { 62 | uint8_t auto_selection; // auto selection 3D fix ('A') or manual ('M') 63 | uint8_t fix_type; // Fix type, see constants 64 | uint8_t prns[MAX_GSA_PRNS]; // PRNs for satellites used (max 12) 65 | float pdop; // precision dilution of position 66 | float vdop; // vertical dilution of precision 67 | } gps_gsa_t; 68 | 69 | typedef struct 70 | { 71 | float speed; // Speed over the ground in knots 72 | bool active; // Void / Active 73 | bool moving; // defined as "speed > 20 km/h" 74 | float track_angle; 75 | } gps_rmc_t; 76 | 77 | typedef struct 78 | { 79 | float true_track; 80 | float magnetic_track; 81 | float ground_speed_kts; 82 | float ground_speed_kmh; 83 | } gps_vtg_t; 84 | 85 | // Decode GGA sentence. 86 | void nmea_decode_gga(const nmea_sentence_t *param, gps_gga_t *output); 87 | 88 | // Decode GSA sentence. 89 | void nmea_decode_gsa(const nmea_sentence_t *param, gps_gsa_t *output); 90 | 91 | // Decode RMC sentence. 92 | void nmea_decode_rmc(const nmea_sentence_t *param, gps_rmc_t *output); 93 | 94 | // Decode VTG sentence 95 | void nmea_decode_vtg(const nmea_sentence_t *param, gps_vtg_t *output); -------------------------------------------------------------------------------- /server/pkg/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | "path" 9 | "time" 10 | 11 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/circular" 12 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/stream" 13 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store" 14 | "github.com/gorilla/handlers" 15 | "github.com/gorilla/mux" 16 | ) 17 | 18 | // Server represents the webserver state 19 | type Server struct { 20 | db store.Store 21 | broker *stream.Broker 22 | circularBuffer *circular.Buffer 23 | listenAddr string 24 | readTimeout time.Duration 25 | writeTimeout time.Duration 26 | httpServer http.Server 27 | accessLogDir string 28 | } 29 | 30 | // ServerConfig represents the webserver configuration 31 | type ServerConfig struct { 32 | DB store.Store 33 | Broker *stream.Broker 34 | CircularBuffer *circular.Buffer 35 | ListenAddr string 36 | AccessLogDir string 37 | } 38 | 39 | const ( 40 | defaultReadTimeout = (15 * time.Second) 41 | defaultWriteTimeout = (30 * time.Second) 42 | defaultShutdownTimeout = (10 * time.Second) 43 | accessLogFileMode = 0644 44 | ) 45 | 46 | // New creates a new webserver instance 47 | func New(config *ServerConfig) *Server { 48 | return &Server{ 49 | db: config.DB, 50 | broker: config.Broker, 51 | circularBuffer: config.CircularBuffer, 52 | listenAddr: config.ListenAddr, 53 | readTimeout: defaultReadTimeout, 54 | writeTimeout: defaultWriteTimeout, 55 | accessLogDir: config.AccessLogDir, 56 | } 57 | } 58 | 59 | // Start starts the webserver. Does not block. 60 | func (s *Server) Start() { 61 | // Create router 62 | m := mux.NewRouter().StrictSlash(true) 63 | m.HandleFunc("/stream", s.streamHandler).Methods("GET") 64 | 65 | // Set up access logging 66 | if _, err := os.Stat(s.accessLogDir); os.IsNotExist(err) { 67 | log.Printf("Creating access log directory: %s", s.accessLogDir) 68 | err := os.MkdirAll(s.accessLogDir, os.ModePerm) 69 | if err != nil { 70 | log.Fatalf("Unable to create directory for access log '%s': %v", s.accessLogDir, err) 71 | } 72 | } 73 | accessLogFileName := path.Join(s.accessLogDir, time.Now().Format("2006-01-02-access-log")) 74 | accessLogFile, err := os.OpenFile(accessLogFileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, accessLogFileMode) 75 | if err != nil { 76 | log.Fatalf("Unable to create access log: %v", err) 77 | } 78 | 79 | // Set up webserver 80 | server := &http.Server{ 81 | Handler: handlers.ProxyHeaders(handlers.CombinedLoggingHandler(accessLogFile, m)), 82 | Addr: s.listenAddr, 83 | WriteTimeout: s.readTimeout, 84 | ReadTimeout: s.writeTimeout, 85 | } 86 | 87 | log.Printf("Webserver listening to '%s'", s.listenAddr) 88 | go func() { 89 | log.Printf("Webserver terminated: '%v'", server.ListenAndServe()) 90 | }() 91 | } 92 | 93 | // Shutdown shuts down the webserver 94 | func (s *Server) Shutdown() { 95 | ctx, cancel := context.WithTimeout(context.Background(), defaultShutdownTimeout) 96 | defer cancel() 97 | 98 | err := s.httpServer.Shutdown(ctx) 99 | if err != nil { 100 | log.Printf("Webserver shutdown error: %v", err) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /server/pkg/model/calc_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // TODO(borud): we need sensible testdata from at least a couple of 11 | // sensors so we can ensure the calculation is correct and that we get 12 | // some sensible values. Part of the testdata should come from 13 | // flooding the AFE3 sensors with calibration mixtures. 14 | // 15 | var calTest = &Cal{ 16 | // value from deviceID '17dh0cf43jg6n4'. Measured by Thomas Langås 17 | // in the lab under relatively stable conditions. Can use 0.32 as 18 | // default VT20Offset if we lack measured offset value. 19 | Vt20Offset: 0.3195, 20 | 21 | Sensor1WEe: 312, 22 | Sensor1WE0: -5, 23 | Sensor1AEe: 316, 24 | Sensor1AE0: -5, 25 | Sensor1PCBGain: -0.73, 26 | Sensor1WESensitivity: 0.203, 27 | 28 | Sensor2WEe: 411, 29 | Sensor2WE0: -4, 30 | Sensor2AEe: 411, 31 | Sensor2AE0: -3, 32 | Sensor2PCBGain: -0.73, 33 | Sensor2WESensitivity: 0.363, 34 | 35 | Sensor3WEe: 271, 36 | Sensor3WE0: 19, 37 | Sensor3AEe: 256, 38 | Sensor3AE0: 23, 39 | Sensor3PCBGain: 0.8, 40 | Sensor3WESensitivity: 0.408, 41 | } 42 | var messageTest = &Message{ 43 | Sensor1Work: 519656, 44 | Sensor1Aux: 522293, 45 | 46 | Sensor2Work: 697336, 47 | Sensor2Aux: 682847, 48 | 49 | Sensor3Work: 443429, 50 | Sensor3Aux: 431900, 51 | 52 | AFE3TempRaw: 540375, 53 | } 54 | 55 | func TestCalculateSensorValues(t *testing.T) { 56 | CalculateSensorValues(messageTest, calTest) 57 | } 58 | 59 | func TestAFE3LUTs(t *testing.T) { 60 | // Ensure the lookup tables are correct 61 | assert.Equal(t, afe3Luts["CO-A4"].LUT, []float64{1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.76, -0.76 - 0.76}) 62 | assert.Equal(t, afe3Luts["CO2-B4"].LUT, []float64{-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -3.8 - 3.8 - 3.8}) 63 | assert.Equal(t, afe3Luts["NO-A4"].LUT, []float64{1.48, 1.48, 1.48, 1.48, 1.48, 2.02, 1.72, 1.72, 1.72}) 64 | assert.Equal(t, afe3Luts["NO-B4"].LUT, []float64{1.04, 1.04, 1.04, 1.04, 1.04, 1.82, 2.0, 2.0, 2.0}) 65 | assert.Equal(t, afe3Luts["NO2-A4"].LUT, []float64{1.09, 1.09, 1.09, 1.09, 1.09, 1.35, 3.0, 3.0, 3.0}) 66 | assert.Equal(t, afe3Luts["NO2-B4"].LUT, []float64{0.76, 0.76, 0.76, 0.76, 0.76, 0.68, 0.23, 0.23, 0.23}) 67 | assert.Equal(t, afe3Luts["SO2-A4"].LUT, []float64{1.15, 1.15, 1.15, 1.15, 1.15, 1.82, 3.93, 3.93, 3.93}) 68 | assert.Equal(t, afe3Luts["SO2-B4"].LUT, []float64{0.96, 0.96, 0.96, 0.96, 0.96, 1.34, 1.10, 1.10, 1.10}) 69 | assert.Equal(t, afe3Luts["O3-A4"].LUT, []float64{0.75, 0.75, 0.75, 0.75, 1.28, 1.28, 1.28, 1.28}) 70 | assert.Equal(t, afe3Luts["O3-B4"].LUT, []float64{0.77, 0.77, 0.77, 0.77, 1.56, 1.56, 1.56, 2.85}) 71 | } 72 | 73 | func TestInterpolation(t *testing.T) { 74 | // Should be good enough 75 | const tolerance = 0.000000001 76 | 77 | compare := func(x, y float64) bool { 78 | diff := math.Abs(x - y) 79 | mean := math.Abs(x+y) / 2.0 80 | return (diff / mean) < tolerance 81 | } 82 | 83 | // perform a simple test to check that the spline matching is 84 | // within reasonable limits for at least the spline points 85 | for k, v := range afe3Luts { 86 | f := correctionFuncFromName(k) 87 | assert.NotNil(t, f) 88 | 89 | for i := 0; i < len(v.LUT); i++ { 90 | assert.True(t, compare(v.LUT[i], f(afe3LutTemperatures[i]))) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /server/pkg/listener/hordelistener/hordelistener.go: -------------------------------------------------------------------------------- 1 | package hordelistener 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "log" 7 | "time" 8 | 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/opts" 11 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 12 | "github.com/telenordigital/nbiot-go" 13 | ) 14 | 15 | // ErrNoHordeCollection indicates that the Horde collectionID is an empty string 16 | var ErrNoHordeCollection = errors.New("No Horde CollectionID specified, please set -c or --collection command line option") 17 | 18 | // HordeListener connects to Horde and listens for messages from a 19 | // particular Collection 20 | type HordeListener struct { 21 | pipeline pipeline.Pipeline 22 | collectionID string 23 | doneChan chan error 24 | quit chan bool 25 | client *nbiot.Client 26 | opts *opts.Opts 27 | } 28 | 29 | // New creates a new HordeListener instance 30 | func New(opts *opts.Opts, pipeline pipeline.Pipeline) *HordeListener { 31 | return &HordeListener{ 32 | opts: opts, 33 | pipeline: pipeline, 34 | collectionID: opts.HordeCollection, 35 | doneChan: make(chan error), 36 | quit: make(chan bool), 37 | } 38 | } 39 | 40 | const reconnectDelay = 5 * time.Second 41 | 42 | // Start HordeListener instance 43 | func (h *HordeListener) Start() error { 44 | if h.collectionID == "" { 45 | return ErrNoHordeCollection 46 | } 47 | 48 | c, err := nbiot.New() 49 | if err != nil { 50 | return err 51 | } 52 | h.client = c 53 | 54 | go func() { 55 | for { 56 | stream, err := h.client.CollectionOutputStream(h.collectionID) 57 | if err != nil { 58 | log.Fatal("Error connecting to Horde: ", err) 59 | } 60 | 61 | log.Printf("Connected. Starting Horde listening loop") 62 | for { 63 | data, err := stream.Recv() 64 | if err == io.EOF { 65 | h.doneChan <- err 66 | break 67 | } 68 | if err != nil { 69 | h.doneChan <- err 70 | } 71 | 72 | pb, err := model.ProtobufFromData(data.Payload) 73 | if err != nil { 74 | log.Printf("Failed to decode protobuffer len=%d: %v", len(data.Payload), err) 75 | continue 76 | } 77 | 78 | m := model.MessageFromProtobuf(pb) 79 | if m == nil { 80 | log.Printf("Unable to create Message from protobuf") 81 | continue 82 | } 83 | 84 | m.DeviceID = data.Device.ID 85 | m.ReceivedTime = data.Received 86 | m.PacketSize = len(data.Payload) 87 | 88 | // TODO(borud): This is a good place to check if a device 89 | // is already known and inject it into the database. 90 | 91 | if h.opts.Verbose { 92 | log.Printf("Accepted packet from Horde %v", m) 93 | } 94 | h.pipeline.Publish(m) 95 | } 96 | log.Printf("Lost connection to Horde. Will wait for %v to reconnect", reconnectDelay) 97 | time.Sleep(reconnectDelay) 98 | log.Printf("Reconnecting to Horde") 99 | } 100 | }() 101 | return nil 102 | } 103 | 104 | // Shutdown initiates shutdown of the UDPListener 105 | func (h *HordeListener) Shutdown() { 106 | log.Printf("UDPListener: Shutdown not implemented") 107 | } 108 | 109 | // WaitForShutdown waits for the UDP listener to shut down 110 | func (h *HordeListener) WaitForShutdown() { 111 | <-h.quit 112 | } 113 | -------------------------------------------------------------------------------- /server/pkg/pipeline/test/pipeline_test.go: -------------------------------------------------------------------------------- 1 | package pipelinetest 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 7 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/opts" 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/calculate" 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/circular" 11 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/persist" 12 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/pipelog" 13 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store/sqlitestore" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var testMessage = &model.Message{ 18 | DeviceID: "my-device-id", 19 | PacketSize: 123, 20 | Sensor1Work: 519656, 21 | Sensor1Aux: 522293, 22 | 23 | Sensor2Work: 697336, 24 | Sensor2Aux: 682847, 25 | 26 | Sensor3Work: 443429, 27 | Sensor3Aux: 431900, 28 | 29 | AFE3TempRaw: 540375, 30 | } 31 | 32 | var testCal = &model.Cal{ 33 | DeviceID: "my-device-id", 34 | // value from deviceID '17dh0cf43jg6n4'. Measured by Thomas Langås 35 | // in the lab under relatively stable conditions. Can use 0.32 as 36 | // default VT20Offset if we lack measured offset value. 37 | Vt20Offset: 0.3195, 38 | 39 | Sensor1WEe: 312, 40 | Sensor1WE0: -5, 41 | Sensor1AEe: 316, 42 | Sensor1AE0: -5, 43 | Sensor1PCBGain: -0.73, 44 | Sensor1WESensitivity: 0.203, 45 | 46 | Sensor2WEe: 411, 47 | Sensor2WE0: -4, 48 | Sensor2AEe: 411, 49 | Sensor2AE0: -3, 50 | Sensor2PCBGain: -0.73, 51 | Sensor2WESensitivity: 0.363, 52 | 53 | Sensor3WEe: 271, 54 | Sensor3WE0: 19, 55 | Sensor3AEe: 256, 56 | Sensor3AE0: 23, 57 | Sensor3PCBGain: 0.8, 58 | Sensor3WESensitivity: 0.408, 59 | } 60 | 61 | func TestRoot(t *testing.T) { 62 | db, err := sqlitestore.New(":memory:") 63 | assert.Nil(t, err) 64 | assert.NotNil(t, db) 65 | 66 | opts := &opts.Opts{ 67 | HordeCollection: "abc", 68 | DBFilename: ":memory:", 69 | } 70 | 71 | root := pipeline.New(opts, db) 72 | assert.NotNil(t, root) 73 | 74 | root.Publish(testMessage) 75 | } 76 | 77 | func TestPipeline(t *testing.T) { 78 | db, err := sqlitestore.New(":memory:") 79 | assert.Nil(t, err) 80 | assert.NotNil(t, db) 81 | 82 | _, err = db.PutCal(testCal) 83 | assert.Nil(t, err) 84 | 85 | opts := &opts.Opts{ 86 | HordeCollection: "abc", 87 | DBFilename: ":memory:", 88 | } 89 | 90 | root := pipeline.New(opts, db) 91 | calculate := calculate.New(opts, db) 92 | persist := persist.New(opts, db) 93 | logger := pipelog.New(opts) 94 | circular := circular.New(10) 95 | 96 | root.AddNext(calculate) 97 | calculate.AddNext(persist) 98 | persist.AddNext(logger) 99 | logger.AddNext(circular) 100 | 101 | // Make defensive copy 102 | msg := *testMessage 103 | 104 | // Ensure the message is modified (new values are calculated) 105 | assert.Equal(t, testMessage, &msg) 106 | root.Publish(&msg) 107 | assert.NotEqual(t, testMessage, &msg) 108 | 109 | // Make sure that message has been persisted 110 | m, err := db.GetMessage(msg.ID) 111 | assert.Nil(t, err) 112 | assert.Equal(t, msg.ID, m.ID) 113 | 114 | // Make sure there's at least one message in the circular buffer 115 | msgs := circular.GetContents() 116 | assert.NotNil(t, msgs) 117 | assert.Equal(t, 1, len(msgs)) 118 | } 119 | -------------------------------------------------------------------------------- /server/cmd/aq/show.go: -------------------------------------------------------------------------------- 1 | // The show command shows an entire calibration entry. 2 | package main 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store/sqlitestore" 11 | ) 12 | 13 | // ShowCommand defines the command line parameters for show command 14 | type ShowCommand struct { 15 | AsJSON bool `short:"j" long:"json" description:"Format calibration entry as JSON"` 16 | } 17 | 18 | func init() { 19 | parser.AddCommand("show", 20 | "Show calibration data for device", 21 | "Show calibration data for device, optionally formatting it as JSON", 22 | &ShowCommand{}) 23 | } 24 | 25 | // Execute runs the list command 26 | func (a *ShowCommand) Execute(args []string) error { 27 | db, err := sqlitestore.New(options.DBFilename) 28 | if err != nil { 29 | return err 30 | } 31 | defer db.Close() 32 | 33 | for _, deviceID := range args { 34 | cals, err := db.ListCalsForDevice(deviceID) 35 | if err != nil { 36 | log.Fatalf("Unable to find calibration data for '%s': %v", deviceID, err) 37 | } 38 | 39 | if a.AsJSON { 40 | return showAsJSON(cals) 41 | } 42 | return showAsText(cals) 43 | } 44 | return nil 45 | } 46 | 47 | func showAsJSON(cals []model.Cal) error { 48 | json, err := json.MarshalIndent(cals, "", " ") 49 | if err != nil { 50 | return err 51 | } 52 | 53 | fmt.Printf("\n%s\n", json) 54 | return nil 55 | } 56 | 57 | func showAsText(cals []model.Cal) error { 58 | for _, cal := range cals { 59 | fmt.Print("--------------------------------------------------\n") 60 | fmt.Printf(" ID : %d\n", cal.ID) 61 | fmt.Printf(" DeviceID : %s\n", cal.DeviceID) 62 | fmt.Printf(" CollectionID : %s\n", cal.CollectionID) 63 | fmt.Printf(" ValidFrom : %s\n", cal.ValidFrom) 64 | fmt.Printf(" CircuitType : %s\n", cal.CircuitType) 65 | fmt.Printf(" AFESerial : %s\n", cal.AFESerial) 66 | fmt.Printf(" AFEType : %s\n", cal.AFEType) 67 | fmt.Printf(" Sensor1Serial : %s\n", cal.Sensor1Serial) 68 | fmt.Printf(" Sensor2Serial : %s\n", cal.Sensor2Serial) 69 | fmt.Printf(" Sensor3Serial : %s\n", cal.Sensor3Serial) 70 | fmt.Printf(" AFECalDate : %s\n", cal.AFECalDate) 71 | fmt.Printf(" Vt20Offset : %f\n", cal.Vt20Offset) 72 | 73 | fmt.Printf(" Sensor1WEe : %d\n", cal.Sensor1WEe) 74 | fmt.Printf(" Sensor1WE0 : %d\n", cal.Sensor1WE0) 75 | fmt.Printf(" Sensor1AEe : %d\n", cal.Sensor1AEe) 76 | fmt.Printf(" Sensor1AE0 : %d\n", cal.Sensor1WE0) 77 | fmt.Printf(" Sensor1PCBGain : %f\n", cal.Sensor1PCBGain) 78 | fmt.Printf(" Sensor1WESensitivity : %f\n", cal.Sensor1WESensitivity) 79 | 80 | fmt.Printf(" Sensor2WEe : %d\n", cal.Sensor2WEe) 81 | fmt.Printf(" Sensor2WE0 : %d\n", cal.Sensor2WE0) 82 | fmt.Printf(" Sensor2AEe : %d\n", cal.Sensor2AEe) 83 | fmt.Printf(" Sensor2AE0 : %d\n", cal.Sensor2AE0) 84 | fmt.Printf(" Sensor2PCBGain : %f\n", cal.Sensor2PCBGain) 85 | fmt.Printf(" Sensor2WESensitivity : %f\n", cal.Sensor2WESensitivity) 86 | 87 | fmt.Printf(" Sensor3WEe : %d\n", cal.Sensor3WEe) 88 | fmt.Printf(" Sensor3WE0 : %d\n", cal.Sensor3WE0) 89 | fmt.Printf(" Sensor3AEe : %d\n", cal.Sensor3AEe) 90 | fmt.Printf(" Sensor3AE0 : %d\n", cal.Sensor3AE0) 91 | fmt.Printf(" Sensor3PCBGain : %f\n", cal.Sensor3PCBGain) 92 | fmt.Printf(" Sensor3WESensitivity : %f\n", cal.Sensor3WESensitivity) 93 | fmt.Print("--------------------------------------------------\n") 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /server/pkg/pipeline/stream/broker.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "time" 7 | 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 10 | "github.com/gorilla/websocket" 11 | ) 12 | 13 | // Broker represents the message broker used for streaming data 14 | // messages to clients. 15 | type Broker struct { 16 | clients map[*client]bool 17 | broadcast chan []byte 18 | register chan *client 19 | unregister chan *client 20 | list chan *listRequest 21 | next pipeline.Pipeline 22 | quit chan bool 23 | 24 | listBlockedCounter int64 25 | clientDroppedCounter int64 26 | clientRegisterCounter int64 27 | clientUnRegisterCounter int64 28 | } 29 | 30 | type listRequest struct { 31 | responseChannel chan *client 32 | } 33 | 34 | // NewBroker creates a new Broker instance. 35 | func NewBroker() *Broker { 36 | b := &Broker{ 37 | clients: make(map[*client]bool), 38 | broadcast: make(chan []byte, 64), 39 | register: make(chan *client), 40 | unregister: make(chan *client), 41 | list: make(chan *listRequest, 10), 42 | quit: make(chan bool), 43 | } 44 | 45 | go b.mainLoop() 46 | 47 | return b 48 | } 49 | 50 | func (b *Broker) mainLoop() { 51 | for { 52 | select { 53 | case message := <-b.broadcast: 54 | for client := range b.clients { 55 | select { 56 | case client.send <- message: 57 | default: 58 | delete(b.clients, client) 59 | close(client.send) 60 | b.clientDroppedCounter++ 61 | } 62 | } 63 | 64 | case client := <-b.register: 65 | b.clients[client] = true 66 | b.clientRegisterCounter++ 67 | log.Printf("CONNECT WebSocket from %v", client.conn.RemoteAddr()) 68 | 69 | case client := <-b.unregister: 70 | if _, ok := b.clients[client]; ok { 71 | delete(b.clients, client) 72 | close(client.send) 73 | b.clientUnRegisterCounter++ 74 | log.Printf("DISCONNECT WebSocket from %v", client.conn.RemoteAddr()) 75 | } 76 | 77 | case listRequest := <-b.list: 78 | for client := range b.clients { 79 | select { 80 | case listRequest.responseChannel <- client: 81 | default: 82 | // Indicate that responseChannel blocked 83 | b.listBlockedCounter++ 84 | } 85 | } 86 | close(listRequest.responseChannel) 87 | 88 | case <-b.quit: 89 | return 90 | } 91 | } 92 | } 93 | 94 | // AddConnection adds a new connection to the message broker. 95 | func (b *Broker) AddConnection(conn *websocket.Conn) { 96 | b.register <- newClient(conn, b) 97 | } 98 | 99 | // ListClients lists clients connected via websocket streamer 100 | func (b Broker) ListClients() []string { 101 | request := &listRequest{ 102 | responseChannel: make(chan *client), 103 | } 104 | 105 | b.list <- request 106 | 107 | clients := []string{} 108 | 109 | for { 110 | select { 111 | case c, ok := <-request.responseChannel: 112 | if !ok { 113 | return clients 114 | } 115 | clients = append(clients, c.conn.RemoteAddr().String()) 116 | 117 | case <-time.After(100 * time.Millisecond): 118 | return clients 119 | } 120 | } 121 | } 122 | 123 | // Publish ... 124 | func (b *Broker) Publish(m *model.Message) error { 125 | json, err := json.Marshal(m) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | b.broadcast <- json 131 | 132 | if b.next != nil { 133 | return b.next.Publish(m) 134 | } 135 | return nil 136 | } 137 | 138 | // AddNext ... 139 | func (b *Broker) AddNext(pe pipeline.Pipeline) { 140 | b.next = pe 141 | } 142 | 143 | // Next ... 144 | func (b *Broker) Next() pipeline.Pipeline { 145 | return b.next 146 | } 147 | -------------------------------------------------------------------------------- /server/pkg/model/calc.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/sgreben/piecewiselinear" 5 | ) 6 | 7 | type sensorLut struct { 8 | LUT []float64 9 | } 10 | 11 | var ( 12 | afe3ScalingFactor = float64(0.0000005960464478) // named "lsb" in datasheet. 13 | 14 | // Temperatures used in the LUT 15 | afe3LutTemperatures = []float64{-30.0, -20.0, -10.0, 0.0, 10.0, 20.0, 30.0, 40.0, 50.0} 16 | 17 | // Lookup tables according to Appendix 1 of Alphasense Application 18 | // Note AAN 803. Included the whole table although we only need 3 19 | // entries in case we will use any of these sensors in the future. 20 | afe3Luts = map[string]sensorLut{ 21 | "CO-A4": sensorLut{LUT: []float64{1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -0.76, -0.76 - 0.76}}, 22 | "CO2-B4": sensorLut{LUT: []float64{-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -3.8 - 3.8 - 3.8}}, 23 | "NO-A4": sensorLut{LUT: []float64{1.48, 1.48, 1.48, 1.48, 1.48, 2.02, 1.72, 1.72, 1.72}}, 24 | "NO-B4": sensorLut{LUT: []float64{1.04, 1.04, 1.04, 1.04, 1.04, 1.82, 2.0, 2.0, 2.0}}, 25 | "NO2-A4": sensorLut{LUT: []float64{1.09, 1.09, 1.09, 1.09, 1.09, 1.35, 3.0, 3.0, 3.0}}, 26 | "NO2-B4": sensorLut{LUT: []float64{0.76, 0.76, 0.76, 0.76, 0.76, 0.68, 0.23, 0.23, 0.23}}, 27 | "SO2-A4": sensorLut{LUT: []float64{1.15, 1.15, 1.15, 1.15, 1.15, 1.82, 3.93, 3.93, 3.93}}, 28 | "SO2-B4": sensorLut{LUT: []float64{0.96, 0.96, 0.96, 0.96, 0.96, 1.34, 1.10, 1.10, 1.10}}, 29 | "O3-A4": sensorLut{LUT: []float64{0.75, 0.75, 0.75, 0.75, 1.28, 1.28, 1.28, 1.28 /*, no value */}}, 30 | "O3-B4": sensorLut{LUT: []float64{0.77, 0.77, 0.77, 0.77, 1.56, 1.56, 1.56, 2.85 /*, no value */}}, 31 | } 32 | 33 | // Create correction functions for the sensors we use 34 | correctSensor1 = correctionFuncFromName("NO-A4") 35 | correctSensor2 = correctionFuncFromName("NO2-A4") 36 | correctSensor3 = correctionFuncFromName("O3-A4") 37 | ) 38 | 39 | // CalculateSensorValues calculates sensor values using measured data 40 | // and calibration data specific to the the device. 41 | func CalculateSensorValues(m *Message, cal *Cal) { 42 | 43 | // Calculate the temperature. 44 | // TODO(borud): have @tlan and @hansj double-check this 45 | m.AFE3TempValue = ((float64(m.AFE3TempRaw) * afe3ScalingFactor) - cal.Vt20Offset + 0.02) * 1000.0 46 | 47 | var sensor1TempCorrectionFactor = correctSensor1(m.AFE3TempValue) 48 | var sensor2TempCorrectionFactor = correctSensor1(m.AFE3TempValue) 49 | var sensor3TempCorrectionFactor = correctSensor1(m.AFE3TempValue) 50 | 51 | // Sensor 1 - NO2 sensor 52 | { 53 | wmV := voltage(m.Sensor1Work, cal.Sensor1WEe, cal.Sensor1WE0) 54 | amV := voltage(m.Sensor1Aux, cal.Sensor1AEe, cal.Sensor1AE0) * sensor1TempCorrectionFactor 55 | m.NO2PPB = (wmV - amV) / cal.Sensor1WESensitivity 56 | } 57 | 58 | // Sensor 2 - O3 + NO2 sensor, calculate O3 by subtracting NO2 sensor value 59 | { 60 | wmV := voltage(m.Sensor2Work, cal.Sensor2WEe, cal.Sensor2WE0) 61 | amV := voltage(m.Sensor2Aux, cal.Sensor2AEe, cal.Sensor2AE0) * sensor2TempCorrectionFactor 62 | m.O3PPB = ((wmV - amV) / cal.Sensor2WESensitivity) - m.NO2PPB 63 | } 64 | 65 | // Sensor 3 - NO sensor 66 | { 67 | wmV := voltage(m.Sensor3Work, cal.Sensor3WEe, cal.Sensor3WE0) 68 | amV := voltage(m.Sensor3Aux, cal.Sensor3AEe, cal.Sensor3AE0) * sensor3TempCorrectionFactor 69 | m.NOPPB = (wmV - amV) / cal.Sensor3WESensitivity 70 | } 71 | } 72 | 73 | func voltage(w uint32, offset int32, zero int32) float64 { 74 | return (float64(w) * afe3ScalingFactor * 1000) - float64(offset+zero) 75 | } 76 | 77 | func correctionFuncFromName(name string) func(float64) float64 { 78 | lut, ok := afe3Luts[name] 79 | if !ok { 80 | panic("Sensor name not found: " + name) 81 | } 82 | 83 | f := piecewiselinear.Function{Y: lut.LUT} 84 | f.X = afe3LutTemperatures[:len(lut.LUT)] 85 | 86 | return func(t float64) float64 { 87 | return f.At(t) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /server/cmd/aq/fetch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/calculate" 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline/persist" 11 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store/sqlitestore" 12 | "github.com/telenordigital/nbiot-go" 13 | ) 14 | 15 | // FetchCommand fetches backlog of data 16 | type FetchCommand struct { 17 | PageSize int `short:"p" long:"page-size" description:"Number of rows to fetch per page" default:"250"` 18 | } 19 | 20 | // For this application we say that time begins on 2020-03-25 21 | var beginningOfTime = int64(1585094400000) 22 | 23 | func init() { 24 | parser.AddCommand( 25 | "fetch", 26 | "Fetch historical data", 27 | "Fetch historical sensor data from Horde server", 28 | &FetchCommand{}) 29 | } 30 | 31 | // Execute ... 32 | func (a *FetchCommand) Execute(args []string) error { 33 | client, err := nbiot.New() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | db, err := sqlitestore.New(options.DBFilename) 39 | if err != nil { 40 | log.Fatalf("Unable to open or create database file '%s': %v", options.DBFilename, err) 41 | } 42 | defer db.Close() 43 | 44 | // Load the calibration data from dir to ensure we have latest 45 | loadCalibrationData(db, options.CalibrationDataDir) 46 | 47 | data, err := db.ListMessages(0, 1) 48 | if err != nil { 49 | log.Fatalf("Unable to list messages: %v", err) 50 | } 51 | 52 | if len(data) == 1 { 53 | // I'm assuming we have to add an entire second of data here 54 | // in order to make up for the API not having millisecond 55 | // resolution? 56 | beginningOfTime = data[0].ReceivedTime + 1 57 | log.Printf("Will fetch back to %s", msToTime(beginningOfTime)) 58 | } 59 | 60 | // Set up pipeline 61 | pipelineRoot := pipeline.New(&options, db) 62 | pipelineCalc := calculate.New(&options, db) 63 | pipelinePersist := persist.New(&options, db) 64 | 65 | pipelineRoot.AddNext(pipelineCalc) 66 | pipelineCalc.AddNext(pipelinePersist) 67 | 68 | var since = beginningOfTime 69 | var until = time.Now().UnixNano() / int64(time.Millisecond) 70 | var count = 0 71 | var countTotal = 0 72 | 73 | // CollectionData coming from horde arrives in descending order 74 | // from Received. So we have to work our way backwards. We do 75 | // this by starting with until being equal to "now" and then set 76 | // the next until value from the last entry we got. 77 | for { 78 | data, err := client.CollectionData(options.HordeCollection, msToTime(since), msToTime(until), a.PageSize) 79 | if err != nil { 80 | log.Fatalf("Error while reading data: %v", err) 81 | } 82 | 83 | if len(data) == 0 { 84 | break 85 | } 86 | 87 | for _, d := range data { 88 | pb, err := model.ProtobufFromData(d.Payload) 89 | if err != nil { 90 | log.Printf("Failed to decode protobuffer len=%d: %v", len(d.Payload), err) 91 | continue 92 | } 93 | 94 | m := model.MessageFromProtobuf(pb) 95 | if m == nil { 96 | log.Printf("Unable to create Message from protobuf") 97 | continue 98 | } 99 | 100 | m.DeviceID = d.Device.ID 101 | m.ReceivedTime = d.Received 102 | m.PacketSize = len(d.Payload) 103 | 104 | pipelineRoot.Publish(m) 105 | count++ 106 | countTotal++ 107 | } 108 | 109 | until = data[len(data)-1].Received - 1 110 | if count >= 500 { 111 | log.Printf("Imported %d records...", countTotal) 112 | count = 0 113 | } 114 | } 115 | 116 | log.Printf("Fetched a total of %d messages", countTotal) 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /Firmware/src/opc_n3.h: -------------------------------------------------------------------------------- 1 | /* 2 | ** Copyright 2020 Telenor Digital AS 3 | ** 4 | ** Licensed under the Apache License, Version 2.0 (the "License"); 5 | ** you may not use this file except in compliance with the License. 6 | ** You may obtain a copy of the License at 7 | ** 8 | ** http://www.apache.org/licenses/LICENSE-2.0 9 | ** 10 | ** Unless required by applicable law or agreed to in writing, software 11 | ** distributed under the License is distributed on an "AS IS" BASIS, 12 | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ** See the License for the specific language governing permissions and 14 | ** limitations under the License. 15 | */ 16 | 17 | #pragma once 18 | 19 | #include 20 | 21 | #define OPC_HISTOGRAM_SIZE 86 22 | 23 | // Data offsets in the histogram 24 | #define OPC_BINS 24 25 | #define OPC_BIN_0_INDEX 0 // 16 bit unsigned integers 26 | #define OPC_SAMPLING_PERIOD_INDEX 52 /* Sampling period is a 16 bit unsigned \ 27 | integer and \ 28 | is a measure of the histogram's \ 29 | actual sampling period in seconds x 100 */ 30 | #define OPC_SAMPLE_FLOWRATE_INDEX 54 /* Sampling period is a 16 bit unsigned integer and \ 31 | is a measure of the sample flow rate in ml/s x 100 \ 32 | */ 33 | #define OPC_TEMPERATURE_INDEX 56 // 16 bit unsigned integer 34 | #define OPC_HUMIDITY_INDEX 58 // 16 bit unsigned integer 35 | #define OPC_PM_A_INDEX 60 // float. Units are ug/m3 36 | #define OPC_PM_B_INDEX 64 // float. Units are ug/m3 37 | #define OPC_PM_C_INDEX 68 // float. Units are ug/m3 38 | #define OPC_FAN_REV_COUNT_INDEX 80 // 16 bit unsigned integer 39 | #define OPC_LASER_STATUS_INDEX 82 // 16 bit unsigned integer 40 | #define OPC_HISTOGRAM_CHECKSUM_INDEX 84 // 16 bit unsigned integer 41 | 42 | typedef struct 43 | { 44 | uint16_t bin[OPC_BINS]; 45 | uint16_t period; 46 | uint16_t flowrate; 47 | uint16_t temperature; 48 | uint16_t humidity; 49 | float pm_a; 50 | float pm_b; 51 | float pm_c; 52 | uint16_t fan_rev_count; 53 | uint16_t laser_status; 54 | bool valid; 55 | } OPC_SAMPLE; 56 | 57 | // OPC commands (not the complete set) 58 | #define OPC_N3_WRITE_PERIPHERAL_POWER_STATUS 0x03 59 | #define OPC_N3_READ_INFORMATION_STRING 0x3F 60 | #define OPC_N3_READ_SERIAL_NUMBER_STRING 0x10 61 | #define OPC_N3_READ_HISTOGRAM_DATA_AND_RESET_HISTOGRAM 0x30 62 | #define OPC_N3_READ_PM_DATA_AND_RESET_HISTOGRAM 0x32 63 | #define OPC_N3_READ_DAC_AND_POWER_STATUS 0x13 64 | #define OPC_RESET 0x06 65 | 66 | // OPC status codes 67 | #define OPC_BUSY 0x31 68 | #define OPC_N3_DATA_READY 0xF3 69 | 70 | // OPC peripherals / options 71 | #define OPC_OPTION_FAN_ON 0x03 72 | #define OPC_OPTION_FAN_OFF 0x02 73 | #define OPC_OPTION_LASER_ON 0x07 74 | #define OPC_OPTION_LASER_OFF 0x06 75 | #define OPC_OPTION_NONE 0xFF 76 | 77 | // All settling times are in microseconds 78 | #define FAN_SETTLING_TIME_MS 10000 79 | #define LASER_SETTLING_TIME_MS 50 80 | #define INITIATE_TRANSMISSION_SETTLING_TIME_US 10 81 | #define BYTE_READ_SETTLING_TIME_US 10 82 | 83 | // According to Alphasense User Manual OPC-N3 Optical 84 | // Particle Counter Issue 2. Chapter 8 ("repeat interval ms") 85 | #define OPC_SAMPLING_TIME_MS 60000 86 | 87 | #define OPC_N3_CMD_COMMAND_ACK_WAIT 10 88 | #define OPC_N3_SPI_BUFFER_RESET_WAIT 2000 89 | 90 | typedef enum 91 | { 92 | OPC_OK = 0, 93 | OPC_NOT_IMPLEMENTED = -1, 94 | OPC_UNEXPECTED_OPC_RESPONSE = -2, 95 | OPC_CRC_ERROR = -3 96 | } OPC_N3_RESULT; 97 | 98 | void opc_init(); 99 | void opc_n3_get_sample(OPC_SAMPLE *msg); 100 | void opc_n3_sample_data(); -------------------------------------------------------------------------------- /Firmware/README.md: -------------------------------------------------------------------------------- 1 | # Air quality sensor node firmware 2 | 3 | This project contains firmware for an air quality sensor node 4 | 5 | The board is configured for sampling an Alfasense AFE-3 board populated with NO2, NO and O3 sensors, along with temperature, humidity and an OPC-N3 particulate sensor. 6 | 7 | Building: 8 | 9 | ```sh 10 | west update 11 | source ../deps/zephyr/zephyr-env.sh 12 | west build --pristine -b nrf52_pca10040 Firmware 13 | west flash 14 | ``` 15 | NOTE! west v0.7 is required for this to work. 16 | 17 | ## Required tools 18 | 19 | To compile protobuf files you need to install protobuf compiler and python bindingds for protobuf. 20 | 21 | For MacOS this can be installed with `brew install protobuf` and `pip install protobuf`. 22 | For Linux this can be installed with `apt-get install protobuf-compiler python-protobuf`. 23 | 24 | You'll need `make` to build the image. Releases are managed via the `reto` release tool (available at github.com/ExploratoryEngineering/reto) 25 | 26 | ## Building 27 | 28 | You'll need MCUBoot and a key for the application image. If you change the key 29 | you can't deploy the image on existing devices with a different key. A new key 30 | means you'll have to reinstall the bootloader on all your devices. 31 | 32 | ### Initial setup 33 | 34 | Install the "reto" tool: 35 | 36 | `go get -u https://github.com/ExploratoryEngineering/reto` 37 | 38 | Ensure your Zephyr installation is up and running by running `west build`. 39 | 40 | Create a new key and install the bootloader on your device: 41 | 42 | ```shell 43 | make fwkey 44 | make build_mcuboot 45 | make install_mcuboot 46 | ``` 47 | 48 | ### Flashing and debugging 49 | 50 | #### Prerequisites 51 | 52 | 1. Install [JLink](https://www.segger.com/downloads/jlink/) 53 | 2. The ZEPHYR_BASE environment variable has to point to the deps\zephyr 54 | 3. Source deps\zephyr\zephyr-env.sh 55 | 56 | #### Flashing 57 | 58 | 1. The target device has to be powered on 59 | 2. Connect the EE-04 programmer to a computer via a USB cable 60 | 3. Connect the ribbon debug cable between the EE-04 and the target device 61 | 4. `make flash`. 62 | 63 | #### Debug output 64 | 65 | Open a new shell and issue the following command to connect JLink: 66 | 67 | ```shell 68 | JLinkExe -if swd -device nrf52 -speed 4000 -autoconnect 1 69 | ``` 70 | 71 | Open a second shell and issue the following command to see log output: 72 | 73 | ```shell 74 | JLinkRTTClient 75 | ``` 76 | 77 | If you have an existing key put it into the `aq_fota.pem` file and skip this step. This is included in `.gitignore` so you won't check it in by accident. 78 | 79 | 80 | ### Message format 81 | 82 | The message format uses protobuffers. You can find these under 83 | **common/protobuf** in this Github repository. Both the server and 84 | the firmware uses the same protobuffer definition file. 85 | 86 | ## Enabling FOTA 87 | 88 | First commit everything verify the release is read (run `reto status` 89 | to see the current status), the build the release: 90 | 91 | `touch CMakeFiles.txt && make` 92 | 93 | (this ensures a new build with the updated version number) 94 | 95 | Upload firmware image, set version number and assign it to the device in Horde: 96 | 97 | ```shell 98 | curl -s -HX-API-Token:$(cat .apikey) \ 99 | -XPOST -F image=@build/zephyr/zephyr.signed.bin \ 100 | https://api.nbiot.engineering/collections/$(cat .collectionid)/firmware | jq -r ".imageId" > .firmwareid 101 | 102 | curl -XPATCH -d'{"version":"0.0.3"}' -HX-API-Token:$(cat .apikey) \ 103 | https://api.nbiot.engineering/collections/$(cat .collectionid)/firmware/$(cat .firmwareid) 104 | 105 | FIRMWAREID=$(cat .firmwareid) curl -HX-API-Token:$(cat .apikey)\ 106 | -XPATCH -d'{"firmware":{"targetFirmwareId": "${FIRMWAREID}"}}' \ 107 | https://api.nbiot.engineering/collections/$(cat .collectionid)/devices/$(cat .deviceid) 108 | 109 | ``` 110 | 111 | Prepare the files `.apikey` with the API token, `.collectionid` and 112 | `.deviceid` with the collection and device ID from Horde. These can be 113 | found and/or created in [The Horde console](https://nbiot.engineering) 114 | -------------------------------------------------------------------------------- /common/protobuf/aq.proto: -------------------------------------------------------------------------------- 1 | // 2 | // Protocol buffer definitions for 4 gen TKAQ units. 3 | // 4 | syntax = "proto3"; 5 | 6 | package aqpb; 7 | 8 | option go_package = ".;aqpb"; 9 | 10 | // Sample -- represents one packet of data from the air quality unit. 11 | // When extending this keep in mind that dealing with protobuffers on 12 | // constrained platforms dealing with nested structures can be a bit 13 | // of a pain, so we try to keep this message as simple as possible. 14 | // 15 | message Sample { 16 | // ---------- Board fields ---------- 17 | uint64 sysid = 1; // Hardware id 18 | uint64 firmware_version = 2; // Versioning info 19 | int64 uptime = 3; // Uptime of the system - number of milliseconds since reboot. 20 | float board_temp = 4; // Board temperature in celsius 21 | float board_rel_humidity = 5; // Board relative humidity in percent 22 | uint64 status = 6; // Generic status bit field (for future use) 23 | 24 | // ---------- GPS fields ---------- 25 | // If the GPS cannot get a fix these fields will all be zeroed. 26 | float gps_timestamp = 7; // Timestamp from the GPS 27 | float lat = 8; // Latitude in radians 28 | float lon = 9; // Longitude in radians 29 | float alt = 10; // Altitude in meters 30 | 31 | // ---------- AFE3 fields ---------- 32 | // In order to obtain the measurement in mV the sensor readinggs 33 | // below have to be multiplied by 0.000000596046. On the 34 | // calibration datasheet for each sensor there will be an offset 35 | // value for each sensor (given in mV). 36 | // 37 | uint32 sensor_1_work = 20; // OP1 ADC reading - NO2 working electrode 38 | uint32 sensor_1_aux = 21; // OP2 ADC reading - NO2 auxillary electrode 39 | uint32 sensor_2_work = 22; // OP3 ADC reading - O3+NO2 working electrode 40 | uint32 sensor_2_aux = 23; // OP4 ADC reading - O3+NO2 auxillary electrode 41 | uint32 sensor_3_work = 24; // OP5 ADC reading - NO working electrode 42 | uint32 sensor_3_aux = 25; // OP6 ADC reading - NO aux electrode 43 | uint32 afe3_temp_raw = 26; // Pt1000 ADC reading - AFE-3 ambient temperature 44 | 45 | // ---------- OPC-N3 fields ---------- 46 | // Strictly speaking we should have another field that specifies 47 | // what the pm_a, pm_b, and pm_c values are set to. These values 48 | // are not very useful if we don't know what they are. 49 | 50 | uint32 opc_pm_a = 30; // OPC PM A (default PM1) 51 | uint32 opc_pm_b = 31; // OPC PM B (default PM2.5) 52 | uint32 opc_pm_c = 32; // OPC PM C (default PM10) 53 | 54 | uint32 opc_sample_period = 33; // OPC sample period, in ms 55 | uint32 opc_sample_flow_rate = 34; // OPC sample flow rate, in 56 | uint32 opc_temp = 35; // OPC temperature, in 57 | uint32 opc_hum = 36; // OPC temperature, in 58 | uint32 opc_fan_revcount = 37; // OPC fan rev count 59 | uint32 opc_laser_status = 38; // OPC laser status, 60 | uint32 opc_sample_valid = 39; // OPC Sample valid 61 | 62 | // OPC PM bin 0 to 23 63 | uint32 opc_bin_0 = 40; // OPC PM bin 0 64 | uint32 opc_bin_1 = 41; // OPC PM bin 1 65 | uint32 opc_bin_2 = 42; // OPC PM bin 2 66 | uint32 opc_bin_3 = 43; // OPC PM bin 3 67 | uint32 opc_bin_4 = 44; // OPC PM bin 4 68 | uint32 opc_bin_5 = 45; // OPC PM bin 5 69 | uint32 opc_bin_6 = 46; // OPC PM bin 6 70 | uint32 opc_bin_7 = 47; // OPC PM bin 7 71 | uint32 opc_bin_8 = 48; // OPC PM bin 8 72 | uint32 opc_bin_9 = 49; // OPC PM bin 9 73 | uint32 opc_bin_10 = 50; // OPC PM bin 10 74 | uint32 opc_bin_11 = 51; // OPC PM bin 11 75 | uint32 opc_bin_12 = 52; // OPC PM bin 12 76 | uint32 opc_bin_13 = 53; // OPC PM bin 13 77 | uint32 opc_bin_14 = 54; // OPC PM bin 14 78 | uint32 opc_bin_15 = 55; // OPC PM bin 15 79 | uint32 opc_bin_16 = 56; // OPC PM bin 16 80 | uint32 opc_bin_17 = 57; // OPC PM bin 17 81 | uint32 opc_bin_18 = 58; // OPC PM bin 18 82 | uint32 opc_bin_19 = 59; // OPC PM bin 19 83 | uint32 opc_bin_20 = 60; // OPC PM bin 20 84 | uint32 opc_bin_21 = 61; // OPC PM bin 21 85 | uint32 opc_bin_22 = 62; // OPC PM bin 22 86 | uint32 opc_bin_23 = 63; // OPC PM bin 23 87 | 88 | float pm1 = 64; // OPC PM A (default PM1) 89 | float pm25 = 65; // OPC PM B (default PM2.5) 90 | float pm10 = 66; // OPC PM C (default PM10) 91 | } 92 | -------------------------------------------------------------------------------- /server/pkg/pipeline/calculate/calculate.go: -------------------------------------------------------------------------------- 1 | package calculate 2 | 3 | import ( 4 | "log" 5 | "sort" 6 | "time" 7 | 8 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/model" 9 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/opts" 10 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/pipeline" 11 | "github.com/ExploratoryEngineering/air-quality-sensor-node/server/pkg/store" 12 | ) 13 | 14 | // Calculate holds the configuration the calculation processor. 15 | type Calculate struct { 16 | next pipeline.Pipeline 17 | opts *opts.Opts 18 | db store.Store 19 | calibrationCache map[uint64][]model.Cal 20 | cacheRefreshChan chan bool 21 | lastCacheUpdate time.Time 22 | } 23 | 24 | const ( 25 | maxint = ^0 >> 1 // no idea why maxint doesn't already exist 26 | minCacheUpdateDelay = (5 * time.Second) 27 | ) 28 | 29 | // New creates a new instance of Calculate pipeline element 30 | func New(opts *opts.Opts, db store.Store) *Calculate { 31 | c := &Calculate{ 32 | opts: opts, 33 | db: db, 34 | cacheRefreshChan: make(chan bool), 35 | } 36 | 37 | err := c.loadCache() 38 | if err != nil { 39 | log.Fatalf("Unable to pre-populate calibration cache: %v", err) 40 | } 41 | return c 42 | } 43 | 44 | func (p *Calculate) loadCache() error { 45 | cals, err := p.db.ListCals(0, maxint) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | p.populateCache(cals) 51 | 52 | return nil 53 | } 54 | 55 | func (p *Calculate) populateCache(cals []model.Cal) { 56 | m := make(map[uint64][]model.Cal) 57 | 58 | for _, cal := range cals { 59 | m[cal.SysID] = append(m[cal.SysID], cal) 60 | } 61 | 62 | // Since the findCacheEntry algorithm absolutely depends on the 63 | // correct date order and we shouldn't depend on the store layer 64 | // doing things correctly (even though ordering is specified) 65 | // people can screw up and change that. So better safe than 66 | // sorry. 67 | for _, v := range m { 68 | sort.Slice(v, func(i, j int) bool { 69 | return v[i].ValidFrom.After(v[j].ValidFrom) 70 | }) 71 | } 72 | 73 | p.calibrationCache = m 74 | p.lastCacheUpdate = time.Now() 75 | } 76 | 77 | // findCacheEntry assumes that the calibration entries are sorted in 78 | // descending order by date in the cache. 79 | func (p *Calculate) findCacheEntry(sysID uint64, t int64) *model.Cal { 80 | // Somewhat hokey caching logic. Replace this nonsense with a 81 | // proper caching layer that uses the Store interface. 82 | refreshedCache := false 83 | var deviceCalEntries []model.Cal 84 | for { 85 | deviceCalEntries = p.calibrationCache[sysID] 86 | if deviceCalEntries != nil { 87 | // We found cache entry so bail out 88 | break 89 | } 90 | 91 | // We did not find a cached entry. If we have already 92 | // refreshed, we bail and accept the consequences. 93 | if refreshedCache { 94 | log.Printf("Missing calibration data for '%d' (will only report every %.2f seconds)", sysID, minCacheUpdateDelay.Seconds()) 95 | break 96 | } 97 | 98 | // Check when we last updated cache. If it is less than 99 | // minCacheUpdateDelay we skip the update 100 | if time.Now().Before(p.lastCacheUpdate.Add(minCacheUpdateDelay)) { 101 | break 102 | } 103 | 104 | // We load refresh the cache and go around once more 105 | err := p.loadCache() 106 | if err != nil { 107 | log.Printf("Error updating cache, continuing with possibly stale data: %v", err) 108 | } 109 | refreshedCache = true 110 | log.Print("Refreshed calibration data cache") 111 | } 112 | 113 | date := time.Unix(0, t*int64(time.Millisecond)) 114 | 115 | var cal model.Cal 116 | for i := 0; i < len(deviceCalEntries); i++ { 117 | cal = deviceCalEntries[i] 118 | if !date.Before(cal.ValidFrom) { 119 | break 120 | } 121 | } 122 | 123 | return &cal 124 | } 125 | 126 | // Publish ... 127 | func (p *Calculate) Publish(m *model.Message) error { 128 | cal := p.findCacheEntry(m.SysID, m.ReceivedTime) 129 | 130 | // This is a workaround for when we use MIC and we do not get 131 | // access to the underlying DeviceID. We use the deviceID from the 132 | // calibration data to populate the DeviceID field. 133 | if m.DeviceID == "" { 134 | m.DeviceID = cal.DeviceID 135 | } 136 | 137 | model.CalculateSensorValues(m, cal) 138 | 139 | if p.next != nil { 140 | return p.next.Publish(m) 141 | } 142 | return nil 143 | } 144 | 145 | // AddNext ... 146 | func (p *Calculate) AddNext(pe pipeline.Pipeline) { 147 | p.next = pe 148 | } 149 | 150 | // Next ... 151 | func (p *Calculate) Next() pipeline.Pipeline { 152 | return p.next 153 | } 154 | -------------------------------------------------------------------------------- /server/pkg/store/sqlitestore/schema.go: -------------------------------------------------------------------------------- 1 | package sqlitestore 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | const schema = ` 11 | -- PRAGMA foreign_keys = ON; 12 | -- PRAGMA defer_foreign_keys = FALSE; 13 | 14 | CREATE TABLE IF NOT EXISTS messages ( 15 | id INTEGER PRIMARY KEY AUTOINCREMENT, 16 | device_id TEXT NOT NULL, 17 | received_time BIGINT NOT NULL, 18 | packetsize INTEGER NOT NULL, 19 | 20 | sysid INTEGER NOT NULL, 21 | firmware_ver INTEGER NOT NULL, 22 | uptime INTEGER NOT NULL, 23 | boardtemp REAL NOT NULL, 24 | board_rel_hum REAL NOT NULL, 25 | status INTEGER NOT NULL, 26 | 27 | gpstimestamp REAL NOT NULL, 28 | lon REAL NOT NULL, 29 | lat REAL NOT NULL, 30 | alt REAL NOT NULL, 31 | 32 | sensor1work INTEGER NOT NULL, 33 | sensor1aux INTEGER NOT NULL, 34 | sensor2work INTEGER NOT NULL, 35 | sensor2aux INTEGER NOT NULL, 36 | sensor3work INTEGER NOT NULL, 37 | sensor3aux INTEGER NOT NULL, 38 | afe3_temp_raw INTEGER NOT NULL, 39 | 40 | no2_ppb REAL NOT NULL, 41 | o3_ppb REAL NOT NULL, 42 | no_ppb REAL NOT NULL, 43 | afe3_temp_value REAL NOT NULL, 44 | 45 | opcpma INTEGER NOT NULL, 46 | opcpmb INTEGER NOT NULL, 47 | opcpmc INTEGER NOT NULL, 48 | 49 | pm1 REAL NOT NULL, 50 | pm10 REAL NOT NULL, 51 | pm25 REAL NOT NULL, 52 | 53 | opcsampleperiod INTEGER NOT NULL, 54 | opcsampleflowrate INTEGER NOT NULL, 55 | opctemp INTEGER NOT NULL, 56 | opchum INTEGER NOT NULL, 57 | opcfanrevcount INTEGER NOT NULL, 58 | opclaserstatus INTEGER NOT NULL, 59 | 60 | opcbin_0 INTEGER NOT NULL, 61 | opcbin_1 INTEGER NOT NULL, 62 | opcbin_2 INTEGER NOT NULL, 63 | opcbin_3 INTEGER NOT NULL, 64 | opcbin_4 INTEGER NOT NULL, 65 | opcbin_5 INTEGER NOT NULL, 66 | opcbin_6 INTEGER NOT NULL, 67 | opcbin_7 INTEGER NOT NULL, 68 | opcbin_8 INTEGER NOT NULL, 69 | opcbin_9 INTEGER NOT NULL, 70 | opcbin_10 INTEGER NOT NULL, 71 | opcbin_11 INTEGER NOT NULL, 72 | opcbin_12 INTEGER NOT NULL, 73 | opcbin_13 INTEGER NOT NULL, 74 | opcbin_14 INTEGER NOT NULL, 75 | opcbin_15 INTEGER NOT NULL, 76 | opcbin_16 INTEGER NOT NULL, 77 | opcbin_17 INTEGER NOT NULL, 78 | opcbin_18 INTEGER NOT NULL, 79 | opcbin_19 INTEGER NOT NULL, 80 | opcbin_20 INTEGER NOT NULL, 81 | opcbin_21 INTEGER NOT NULL, 82 | opcbin_22 INTEGER NOT NULL, 83 | opcbin_23 INTEGER NOT NULL, 84 | opcsamplevalid INTEGER NOT NULL 85 | ); 86 | 87 | CREATE TABLE IF NOT EXISTS cal ( 88 | id INTEGER PRIMARY KEY AUTOINCREMENT, 89 | device_id TEXT NOT NULL, 90 | sysid INTEGER NOT NULL, 91 | collection_id TEXT NOT NULL, 92 | valid_from DATETIME NOT NULL, 93 | 94 | afe_serial TEXT NOT NULL, 95 | 96 | circuit_type TEXT NOT NULL, 97 | afe_type TEXT NOT NULL, 98 | sensor1_serial TEXT NOT NULL, 99 | sensor2_serial TEXT NOT NULL, 100 | sensor3_serial TEXT NOT NULL, 101 | 102 | afe_cal_date DATETIME NOT NULL, 103 | 104 | vt20_offset REAL NOT NULL, 105 | 106 | sensor1_we_e REAL NOT NULL, 107 | sensor1_we_0 REAL NOT NULL, 108 | sensor1_ae_e REAL NOT NULL, 109 | sensor1_ae_0 REAL NOT NULL, 110 | sensor1_pcb_gain REAL NOT NULL, 111 | sensor1_we_sensitivity REAL NOT NULL, 112 | 113 | sensor2_we_e REAL NOT NULL, 114 | sensor2_we_0 REAL NOT NULL, 115 | sensor2_ae_e REAL NOT NULL, 116 | sensor2_ae_0 REAL NOT NULL, 117 | sensor2_pcb_gain REAL NOT NULL, 118 | sensor2_we_sensitivity REAL NOT NULL, 119 | 120 | sensor3_we_e REAL NOT NULL, 121 | sensor3_we_0 REAL NOT NULL, 122 | sensor3_ae_e REAL NOT NULL, 123 | sensor3_ae_0 REAL NOT NULL, 124 | sensor3_pcb_gain REAL NOT NULL, 125 | sensor3_we_sensitivity REAL NOT NULL, 126 | 127 | FOREIGN KEY(device_id) REFERENCES devices(id), 128 | 129 | UNIQUE(device_id, collection_id, afe_serial, valid_from) 130 | ); 131 | ` 132 | 133 | func createSchema(db *sqlx.DB, fileName string) { 134 | for n, statement := range strings.Split(schema, ";") { 135 | if _, err := db.Exec(statement); err != nil { 136 | panic(fmt.Sprintf("Statement %d failed: \"%s\" : %s", n+1, statement, err)) 137 | } 138 | } 139 | } 140 | --------------------------------------------------------------------------------