commit a5f244a207e079f21d612e14f1952ef7e2d979b9
Author: Chris Bracken <chris@bracken.jp>
Date: Thu, 11 Nov 2021 08:47:34 -0800
Initial import
Diffstat:
7 files changed, 180 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+tempestas
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2021, Chris Bracken <chris@bracken.jp>
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/airrohr/airrohr.go b/airrohr/airrohr.go
@@ -0,0 +1,39 @@
+package airrohr
+
+import (
+ "encoding/json"
+ "io"
+)
+
+// Value is a reading from a sensor. Values have a type, which specifies the
+// type of reading and the value of the reading itself.
+//
+// Known value types are:
+// SDS_P1: 10 μm particulate matter
+// SDS_P2: 2.5 μm particulate matter
+// BME280_temperature: temperature in degrees Celsius
+// BME280_pression: pressure in Pascals
+// BME280_humidity: percent humidity
+// samples: the count of loops the main() function ran
+// min_micro: the minimum time for a loop through main() in μs
+// max_micro: the maximum time for a loop through main() in μs
+// signal: WiFi signal strength in dBm
+type Value struct {
+ Type string `json:"value_type"`
+ Value float64 `json:"value,string"`
+}
+
+// Sensor report from an ESP8266 NodeMCU running AirRohr firmware.
+type Report struct {
+ SensorId string `json:"esp8266id"`
+ SoftwareVersion string `json:"software_version"`
+ Values []Value `json:"sensordatavalues"`
+}
+
+// Parse reads a JSON-encoded value and stores it in the value pointed to by v.
+func Parse(r io.Reader, v *Report) error {
+ decoder := json.NewDecoder(r)
+ decoder.DisallowUnknownFields()
+ err := decoder.Decode(&v)
+ return err
+}
diff --git a/go.mod b/go.mod
@@ -0,0 +1,8 @@
+module git.bracken.jp/tempestas
+
+go 1.17
+
+require (
+ github.com/lib/pq v1.10.3
+ goji.io v2.0.2+incompatible
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,4 @@
+github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
+github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c=
+goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk=
diff --git a/main.go b/main.go
@@ -0,0 +1,89 @@
+package main
+
+import (
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "time"
+
+ "git.bracken.jp/tempestas/airrohr"
+ _ "github.com/lib/pq"
+ "goji.io"
+ "goji.io/pat"
+)
+
+const (
+ DB_USER = "tempestas"
+ DB_PASSWORD = "<SET_PASSWORD_HERE>" // TODO: read from file/env
+ DB_NAME = "tempestas"
+ INSERT_REPORT = "INSERT INTO " +
+ "sensor_data(sensor_id, sw_version, reading_time, reading_type, reading_value) " +
+ "VALUES($1, $2, $3, $4, $5)"
+)
+
+var (
+ db *sql.DB
+)
+
+func setupDB() *sql.DB {
+ dbinfo := fmt.Sprintf("user=%s password=%s dbname=%s sslmode=disable", DB_USER, DB_PASSWORD, DB_NAME)
+ db, err := sql.Open("postgres", dbinfo)
+ if err != nil {
+ panic("Error connecting to database")
+ }
+ return db
+}
+
+func storeReport(report *airrohr.Report, t time.Time) {
+ for _, v := range report.Values {
+ _, err := db.Exec(INSERT_REPORT, report.SensorId, report.SoftwareVersion, t, v.Type, v.Value)
+ if err != nil {
+ fmt.Println("Error: failed to write sensor data")
+ fmt.Println(err)
+ }
+ }
+}
+
+func PostAirrohr(w http.ResponseWriter, r *http.Request) {
+ contentType := r.Header.Get("Content-Type")
+ if contentType != "application/json" {
+ errorResponse(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
+ return
+ }
+ var report airrohr.Report
+ err := airrohr.Parse(r.Body, &report)
+ if err != nil {
+ s, err := io.ReadAll(r.Body)
+ fmt.Println("Error: unknown JSON format from sensor: " + err.Error() + ". Message: " + string(s))
+ errorResponse(w, "Bad Request. "+err.Error(), http.StatusBadRequest)
+ return
+ }
+ t := time.Now().UTC()
+ fmt.Println(t.String() + " Report received from sensor " + report.SensorId)
+ storeReport(&report, t)
+ errorResponse(w, "Success", http.StatusOK)
+}
+
+func errorResponse(w http.ResponseWriter, message string, httpStatusCode int) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(httpStatusCode)
+ resp := make(map[string]string)
+ resp["message"] = message
+ jsonResp, _ := json.Marshal(resp)
+ w.Write(jsonResp)
+}
+
+func main() {
+ fmt.Println("Initializing database")
+ db = setupDB()
+ defer db.Close()
+
+ fmt.Println("Waiting for requests")
+ mux := goji.NewMux()
+ mux.HandleFunc(pat.Post("/sensor/airrohr/"), PostAirrohr)
+ log.Fatal(http.ListenAndServe(":8080", mux))
+
+}
diff --git a/setup_db.sql b/setup_db.sql
@@ -0,0 +1,13 @@
+CREATE ROLE tempestas WITH LOGIN PASSWORD '<password>';
+
+CREATE DATABASE tempestas WITH OWNER tempestas TEMPLATE template0 ENCODING UTF8 LC_COLLATE 'en_CA.UTF-8' LC_CTYPE 'en_CA.UTF-8';
+
+CREATE TABLE sensor_data (
+ sensor_id VARCHAR(63) NOT NULL,
+ sw_version VARCHAR(63) NOT NULL,
+ reading_time TIMESTAMP NOT NULL,
+ reading_type VARCHAR(63) NOT NULL,
+ reading_value NUMERIC(20,6) NOT NULL
+);
+
+GRANT SELECT, INSERT ON sensor_data TO tempestas;