From 019cdc1d01ab6ea3d35e63a8a924b8b76ecb4ab2 Mon Sep 17 00:00:00 2001 From: Victor Antonovich Date: Wed, 30 Oct 2019 16:53:44 +0400 Subject: [PATCH] Unify codebase for both ESP and RPi stations --- Makefile | 17 ++- README.md | 6 +- contrib/scripts/install.sh | 8 +- contrib/scripts/update.sh | 4 +- go.mod | 8 +- go.sum | 14 ++ main.go | 35 ++++- station.go | 288 ++++++++++++++++++++++++++++++++++--- util.go | 13 ++ 9 files changed, 352 insertions(+), 41 deletions(-) diff --git a/Makefile b/Makefile index 45c1a64..1907b90 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ .PHONY: all -BIN := openair-station-esp -PKG := github.com/openairtech/station-esp +BIN := openair-station +PKG := github.com/openairtech/station ARCH := amd64 arm PUB_SERVER := openair.city -PUB_DIR := /var/www/get.openair.city/station-esp +PUB_DIR := /var/www/get.openair.city/station BINDIR = bin @@ -13,7 +13,7 @@ VERSION_VAR := main.Version TIMESTAMP_VAR := main.Timestamp VERSION ?= $(shell git describe --always --dirty --tags) -TIMESTAMP := $(shell date -u '+%Y-%m-%d_%I:%M:%S%p') +TIMESTAMP := $(shell date '+%Y-%m-%d_%T%Z') GOBUILD_LDFLAGS := -ldflags "-s -w -X $(VERSION_VAR)=$(VERSION) -X $(TIMESTAMP_VAR)=$(TIMESTAMP)" @@ -24,10 +24,13 @@ all: build build: go build -x $(GOBUILD_LDFLAGS) -v -o $(BINDIR)/$(BIN) -build-static: $(ARCH) +build-static: $(addprefix build-static-, $(ARCH)) -$(ARCH): - env CGO_ENABLED=0 GOOS=linux GOARCH=$@ go build -a -installsuffix "static" $(GOBUILD_LDFLAGS) -o $(BINDIR)/$(BIN).$@ +build-static-amd64: + env CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -a -installsuffix "static" $(GOBUILD_LDFLAGS) -o $(BINDIR)/$(BIN).amd64 + +build-static-arm: + env CGO_ENABLED=1 CC=arm-linux-gnueabi-gcc GOOS=linux GOARCH=arm go build -a -installsuffix "static" $(GOBUILD_LDFLAGS) -o $(BINDIR)/$(BIN).arm shasum: cd $(BINDIR) && for file in $(ARCH) ; do sha256sum ./$(BIN).$${file} > ./$(BIN).$${file}.sha256.txt; done diff --git a/README.md b/README.md index c14b31c..95ca873 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# OpenAir-Station-ESP +# OpenAir-Station -Data relay station for ESP8266-based monitor for OpenAir realtime air quality map project. +Data relay station for OpenAir air quality map project. ## License -OpenAir-Station-ESP is released under the Apache 2.0 license. See [LICENSE.txt](https://github.com/openairtech/station-esp/blob/master/LICENSE.txt) +OpenAir-Station is released under the Apache 2.0 license. See [LICENSE.txt](https://github.com/openairtech/station/blob/master/LICENSE.txt) diff --git a/contrib/scripts/install.sh b/contrib/scripts/install.sh index 6e7e20d..ade56ea 100644 --- a/contrib/scripts/install.sh +++ b/contrib/scripts/install.sh @@ -4,9 +4,9 @@ set -e -BIN="openair-station-esp" +BIN="openair-station" -BASE_URL="https://get.openair.city/station-esp" +BASE_URL="https://get.openair.city/station" BIN_DIR="/usr/bin" @@ -196,11 +196,11 @@ run ${sudo} chmod +x "${BIN_DIR}/${UPD_BIN}" || fatal "can't set executable bit progress "Setting up systemd..." run ${sudo} sh -c "cat > /etc/systemd/system/${BIN}.service" < ../openair-api require ( + github.com/NotifAi/serial v0.2.3 github.com/cenkalti/backoff v2.1.1+incompatible // indirect + github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375 + github.com/d2r2/go-i2c v0.0.0-20181113114621-14f8dd4e89ce + github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e github.com/grandcat/zeroconf v0.0.0-20190424104450-85eadb44205c + github.com/karalabe/xgo v0.0.0-20190301120235-2d6d1848fb02 // indirect github.com/miekg/dns v1.1.8 // indirect github.com/openairtech/api v0.0.0 + github.com/openairtech/sds011 v0.0.0-20191029135153-f4ccb629bd55 github.com/pkg/errors v0.8.1 // indirect github.com/sirupsen/logrus v1.4.1 github.com/stretchr/testify v1.2.2 diff --git a/go.sum b/go.sum index 0e1bbae..c66efd0 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,25 @@ +github.com/NotifAi/serial v0.2.3 h1:kfD6mn4e04/m/braL9S5LKtDJMRmuqvFsfyjQUtnrP0= +github.com/NotifAi/serial v0.2.3/go.mod h1:xT0Wsw9XDw01/Zx7JM7JVchvDxzetEvQ6PKyqyIp/xg= github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375 h1:vdUOwcZdV+bBfGUUh5oPPWSzw9p+lBnNSuGgQwGpCH4= +github.com/d2r2/go-bsbmp v0.0.0-20190515110334-3b4b3aea8375/go.mod h1:3iz1WHlYJU9b4NJei+Q8G7DN3K05arcCMlOQ+qNCDjo= +github.com/d2r2/go-i2c v0.0.0-20181113114621-14f8dd4e89ce h1:Dog7PLNz1fPaXqHPOHonpERqsF57Oh4X76pM80T1GDY= +github.com/d2r2/go-i2c v0.0.0-20181113114621-14f8dd4e89ce/go.mod h1:AwxDPnsgIpy47jbGXZHA9Rv7pDkOJvQbezPuK1Y+nNk= +github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e h1:ZG3JBA6rPRl0xxQ+nNSfO7tor8w+CNCTs05DNJQYbLM= +github.com/d2r2/go-logger v0.0.0-20181221090742-9998a510495e/go.mod h1:oA+9PUt8F1aKJ6o4YU1T120i7sgo1T6/1LWEEBy0BSs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/grandcat/zeroconf v0.0.0-20190424104450-85eadb44205c h1:svzQzfVE9t7Y1CGULS5PsMWs4/H4Au/ZTJzU/0CKgqc= github.com/grandcat/zeroconf v0.0.0-20190424104450-85eadb44205c/go.mod h1:YjKB0WsLXlMkO9p+wGTCoPIDGRJH0mz7E526PxkQVxI= +github.com/karalabe/xgo v0.0.0-20190301120235-2d6d1848fb02 h1:nI4Q7WQDcng8eRmi8bRP1SXlJIYLkgdgR7ZhJuzPbhs= +github.com/karalabe/xgo v0.0.0-20190301120235-2d6d1848fb02/go.mod h1:iYGcTYIPUvEWhFo6aKUuLchs+AV4ssYdyuBbQJZGcBk= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI= github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/openairtech/sds011 v0.0.0-20191029135153-f4ccb629bd55 h1:A36hCbMr5fqBhsJx5i4AxijEytodI82DL0C4WtmbLek= +github.com/openairtech/sds011 v0.0.0-20191029135153-f4ccb629bd55/go.mod h1:lwkmgZyUp1ReNSy2vSBImedx/2PBVdOd6y2brXXkE8c= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -26,4 +38,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index d549706..6d1a590 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,11 @@ import ( log "github.com/sirupsen/logrus" ) +const ( + StationModeEsp = "esp" + StationModeRpi = "rpi" +) + var ( Version = "unknown" Timestamp = "unknown" @@ -23,14 +28,19 @@ func main() { debugFlag := flag.Bool("d", false, "enable debug logging") + mode := flag.String("m", StationModeEsp, "station mode (esp/rpi)") + espHost := flag.String("h", "OpenAir.local", "ESP station address") espPort := flag.Int("p", 80, "ESP station port") + rpiI2cBusId := flag.Int("i", 1, "RPi station I2C bus ID") + rpiSerialPort := flag.String("s", "/dev/ttyAMA0", "RPi station serial port name") + apiServerUrl := flag.String("a", "https://api.openair.city/v1/feeder", "feeder endpoint address") updatePeriod := flag.Duration("t", 1*time.Minute, "data update period") - settleTime := flag.Duration("S", 5*time.Minute, "data settle time after ESP station reboot") + settleTime := flag.Duration("S", 5*time.Minute, "data settle time after station restart") resolverTimeout := flag.Duration("r", 15*time.Second, "name resolver timeout") @@ -50,10 +60,6 @@ func main() { log.SetLevel(log.DebugLevel) } - version := fmt.Sprintf("esp-%s_%s-%s_%s", Version, Timestamp, runtime.GOARCH, runtime.GOOS) - - log.Printf("starting station, version: %s", version) - InitResolvers(*resolverTimeout) InitHttp(*httpTimeout) @@ -83,7 +89,24 @@ func main() { } }() - RunStation(ctx, version, *espHost, *espPort, *apiServerUrl, *updatePeriod, *settleTime, *disablePmCorrectionFlag) + var station Station + + if *mode == StationModeEsp { + station = NewEspStation(*espHost, *espPort) + } else if *mode == StationModeRpi { + var err error + if station, err = NewRpiStation(*rpiI2cBusId, 0x76, *rpiSerialPort, 3); err != nil { + log.Fatalf("can't initialize RPi station: %v", err) + } + } else { + log.Fatalf("unknown station mode: %s", *mode) + } + + version := fmt.Sprintf("%s-%s_%s-%s_%s", *mode, Version, Timestamp, runtime.GOARCH, runtime.GOOS) + + log.Printf("starting station, version: %s", version) + + RunStation(ctx, station, version, *apiServerUrl, *updatePeriod, *settleTime, *disablePmCorrectionFlag) log.Printf("exiting...") } diff --git a/station.go b/station.go index 120416c..e9a2b18 100644 --- a/station.go +++ b/station.go @@ -16,45 +16,298 @@ package main import ( "context" + "errors" "fmt" "math" "strings" + "sync" "time" log "github.com/sirupsen/logrus" + "github.com/d2r2/go-bsbmp" + "github.com/d2r2/go-i2c" + bmelogger "github.com/d2r2/go-logger" + + "github.com/NotifAi/serial" + + "github.com/openairtech/sds011/go/sds011" + "github.com/openairtech/api" ) -func RunStation(ctx context.Context, version string, espHost string, espPort int, apiServerUrl string, +// System epoch time (2019-01-01 GMT) as an Unix time +const systemEpoch = 1546300800 + +type StationData struct { + TokenId string + Uptime time.Duration + LastMeasurement *api.Measurement +} + +type Station interface { + Start() error + Stop() + GetData() (*StationData, error) +} + +type EspStation struct { + host string + port int +} + +func NewEspStation(host string, port int) *EspStation { + return &EspStation{ + host: host, + port: port, + } +} + +func (es *EspStation) Start() error { + log.Print("started ESP station") + return nil +} + +func (es *EspStation) Stop() { + log.Print("stopped ESP station") +} + +func (es *EspStation) GetData() (*StationData, error) { + url := fmt.Sprintf("http://%s:%d/json", es.host, es.port) + + log.Debugf("getting sensor data from ESP station %s", url) + + var data EspData + if err := GetData(url, &data); err != nil { + log.Errorf("sensor data request failed: %v", err) + return nil, err + } + + log.Debugf("received sensor data: %+v", data) + + m := data.Measurement(api.UnixTime(time.Now())) + + return &StationData{ + TokenId: stationTokenId(data.WiFi.MacAddress()), + Uptime: time.Duration(data.System.Uptime) * time.Minute, + LastMeasurement: m, + }, nil +} + +type RpiStation struct { + i2cBusId int + bmeSensorAddress int + sdsSensorPort string + sdsSensorInterval int + + startTime time.Time + macAddress string + tokenId string + + i2cBus *i2c.I2C + serialPort serial.Port + + bmeSensor *bsbmp.BMP + sdsSensor *sds011.Sensor + + pmLock sync.RWMutex + pm25 float32 + pm10 float32 +} + +func NewRpiStation(i2cBusId int, bmeSensorAddress int, sdsSensorPort string, sdsSensorInterval int) (*RpiStation, error) { + macAddress := WirelessInterfaceMacAddr() + if macAddress == "" { + return nil, errors.New("can't determine station MAC address") + } + return &RpiStation{ + startTime: time.Now(), + macAddress: macAddress, + tokenId: stationTokenId(macAddress), + i2cBusId: i2cBusId, + bmeSensorAddress: bmeSensorAddress, + sdsSensorPort: sdsSensorPort, + sdsSensorInterval: sdsSensorInterval, + }, nil +} + +func (rs *RpiStation) Start() error { + log.Print("starting RPi station...") + + // Init BME280 sensor I2C bus + if err := rs.initI2cBus(); err != nil { + return fmt.Errorf("I2C bus init error: %v", err) + } + + // Init BME280 sensor + if err := rs.initBmeSensor(); err != nil { + return fmt.Errorf("BME280 sensor init error: %v", err) + } + + // Open SDS011 sensor serial port + if err := rs.initSerialPort(); err != nil { + return fmt.Errorf("serial port init error: %v", err) + } + + // Init SDS011 sensor + if err := rs.initSdsSensor(); err != nil { + return fmt.Errorf("SDS011 sensor init error: %v", err) + } + + // Start SDS011 sensor data reading + go rs.readSdsSensor() + + return nil +} + +func (rs *RpiStation) initSerialPort() error { + var err error + rs.serialPort, err = serial.OpenPort(serial.Config{ + Name: rs.sdsSensorPort, + Baud: 9600, + }) + if err != nil { + return err + } + return rs.flushSerialPort() +} + +func (rs *RpiStation) flushSerialPort() error { + return rs.serialPort.Flush() +} + +func (rs *RpiStation) initI2cBus() (err error) { + rs.i2cBus, err = i2c.NewI2C(uint8(rs.bmeSensorAddress), rs.i2cBusId) + return +} + +func (rs *RpiStation) initBmeSensor() error { + _ = bmelogger.ChangePackageLogLevel("i2c", bmelogger.ErrorLevel) + _ = bmelogger.ChangePackageLogLevel("bsbmp", bmelogger.ErrorLevel) + + // Check BME280 sensor presence + var err error + if rs.bmeSensor, err = bsbmp.NewBMP(bsbmp.BME280, rs.i2cBus); err != nil { + return fmt.Errorf("can't find BME280 sensor: %v", err) + } + + // Check BME280 sensor have valid state + if err = rs.bmeSensor.IsValidCoefficients(); err != nil { + return fmt.Errorf("invalid BME280 sensor state: %v", err) + } + + return nil +} + +func (rs *RpiStation) initSdsSensor() error { + rs.sdsSensor = sds011.NewSensor(rs.serialPort) + return rs.sdsSensor.SetCycle(uint8(rs.sdsSensorInterval)) +} + +func (rs *RpiStation) readSdsSensor() { + for { + point, err := rs.sdsSensor.Get() + if err != nil { + log.Errorf("can't read SDS011 sensor: %v", err) + time.Sleep(3 * time.Second) + _ = rs.flushSerialPort() + continue + } + rs.pmLock.Lock() + rs.pm25 = float32(point.PM25) + rs.pm10 = float32(point.PM10) + rs.pmLock.Unlock() + log.Debugf("read SDS011 sensor values, PM2.5: %v, PM10: %v", point.PM25, point.PM10) + } +} + +func (rs *RpiStation) Stop() { + log.Print("stopping RPi station...") + _ = rs.i2cBus.Close() + rs.sdsSensor.Close() +} + +func (rs *RpiStation) GetData() (*StationData, error) { + timestamp := api.UnixTime(time.Now()) + + // Read temperature in Celsius degree + temperature, err := rs.bmeSensor.ReadTemperatureC(bsbmp.ACCURACY_STANDARD) + if err != nil { + return nil, err + } + + // Read relative humidity + _, humidity, err := rs.bmeSensor.ReadHumidityRH(bsbmp.ACCURACY_STANDARD) + if err != nil { + return nil, err + } + + // Read pressure in Pa + pressure, err := rs.bmeSensor.ReadPressurePa(bsbmp.ACCURACY_STANDARD) + if err != nil { + return nil, err + } + // Convert pressure to hPa + pressure /= 100 + + rs.pmLock.RLock() + pm25 := rs.pm25 + pm10 := rs.pm10 + rs.pmLock.RUnlock() + + m := &api.Measurement{ + Timestamp: ×tamp, + Temperature: &temperature, + Humidity: &humidity, + Pressure: &pressure, + Pm25: &pm25, + Pm10: &pm10, + Aqi: nil, + } + + return &StationData{ + TokenId: rs.tokenId, + Uptime: time.Since(rs.startTime), + LastMeasurement: m, + }, nil +} + +func RunStation(ctx context.Context, station Station, version string, apiServerUrl string, updatePeriod time.Duration, settleTime time.Duration, disablePmCorrectionFlag bool) { p := time.Duration(0) + if err := station.Start(); err != nil { + log.Errorf("can't start station: %v", err) + return + } + + defer station.Stop() + for { select { case <-time.After(p): p = updatePeriod - url := fmt.Sprintf("http://%s:%d/json", espHost, espPort) + data, err := station.GetData() + if err != nil { + log.Errorf("station data request failed: %v", err) + continue + } - log.Debugf("getting sensor data from station %s", url) + log.Debugf("station data: %+v", data) - var data EspData - if err := GetData(url, &data); err != nil { - log.Errorf("sensor data request failed: %v", err) + if time.Now().Before(time.Unix(systemEpoch, 0)) { + log.Info("skipping station data posting since station system time probably is not in sync") continue } - log.Debugf("received sensor data: %+v", data) - - uptime := time.Duration(data.System.Uptime) * time.Minute - if uptime < settleTime { - log.Debugf("ignoring sensor data since station uptime (%+v) is "+ - "shorter than data settle time (%+v)", uptime, settleTime) + if data.Uptime < settleTime { + log.Infof("skipping station data posting since station uptime (%+v) is "+ + "shorter than data settle time (%+v)", data.Uptime, settleTime) continue } - m := data.Measurement(api.UnixTime(time.Now())) + m := data.LastMeasurement if !disablePmCorrectionFlag { correctPm(m) @@ -65,7 +318,7 @@ func RunStation(ctx context.Context, version string, espHost string, espPort int Float32RefToString(m.Pm25), Float32RefToString(m.Pm10)) f := api.FeederData{ - TokenId: stationTokenId(&data), + TokenId: data.TokenId, Version: version, Measurements: []api.Measurement{*m}, } @@ -74,7 +327,7 @@ func RunStation(ctx context.Context, version string, espHost string, espPort int var r api.Result - err := PostData(apiServerUrl, f, &r) + err = PostData(apiServerUrl, f, &r) if err != nil { log.Errorf("data posting failed: %v", err) continue @@ -84,14 +337,13 @@ func RunStation(ctx context.Context, version string, espHost string, espPort int } case <-ctx.Done(): - log.Printf("stopping") return } } } -func stationTokenId(stationData *EspData) string { - return Sha1(strings.ToUpper(stationData.WiFi.MacAddress())) +func stationTokenId(stationMacAddress string) string { + return Sha1(strings.ToUpper(stationMacAddress)) } func correctPm(m *api.Measurement) { diff --git a/util.go b/util.go index 9d9c234..d766546 100644 --- a/util.go +++ b/util.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "math" + "net" "strings" ) @@ -39,6 +40,18 @@ func ParseAddr(addr string) (host, port string) { return e[0], e[1] } +// Get wireless interface (with name starting with 'wl') MAC address +// or empty string if no wireless interface found +func WirelessInterfaceMacAddr() string { + interfaces, _ := net.Interfaces() + for _, iface := range interfaces { + if strings.HasPrefix(iface.Name, "wl") { + return iface.HardwareAddr.String() + } + } + return "" +} + // Compute SHA1 checksum for given string func Sha1(s string) string { h := sha1.New()