From a2ff29ce847ceaa82fa1399cde3648416d0b2bf0 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Sun, 12 Dec 2021 12:43:43 -0800 Subject: [PATCH] Add weather service --- go.mod | 1 + go.sum | 6 ++ infinitime.go | 41 +++++++++- weather/weather.go | 193 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 weather/weather.go diff --git a/go.mod b/go.mod index 1961eb8..c591e2f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module go.arsenm.dev/infinitime go 1.16 require ( + github.com/fxamacker/cbor/v2 v2.3.0 github.com/godbus/dbus/v5 v5.0.3 github.com/muka/go-bluetooth v0.0.0-20211122080231-b99792bbe62a ) diff --git a/go.sum b/go.sum index 517922c..216009e 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,10 @@ 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/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= +github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= +github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik= +github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -24,6 +28,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/suapapa/go_eddystone v1.3.1/go.mod h1:bXC11TfJOS+3g3q/Uzd7FKd5g62STQEfeEIhcKe4Qy8= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/infinitime.go b/infinitime.go index 0988c4d..409ab79 100644 --- a/infinitime.go +++ b/infinitime.go @@ -4,9 +4,11 @@ import ( "bytes" "encoding/binary" "errors" + "reflect" "strings" "time" + "github.com/fxamacker/cbor/v2" bt "github.com/muka/go-bluetooth/api" "github.com/muka/go-bluetooth/bluez/profile/adapter" "github.com/muka/go-bluetooth/bluez/profile/device" @@ -27,6 +29,7 @@ const ( HeartRateChar = "00002a37-0000-1000-8000-00805f9b34fb" FSTransferChar = "adaf0200-4669-6c65-5472-616e73666572" FSVersionChar = "adaf0100-4669-6c65-5472-616e73666572" + WeatherDataChar = "00040001-78fc-48fe-8e23-433b3a1942d0" ) type Device struct { @@ -42,6 +45,7 @@ type Device struct { heartRateChar *gatt.GattCharacteristic1 fsVersionChar *gatt.GattCharacteristic1 fsTransferChar *gatt.GattCharacteristic1 + weatherDataChar *gatt.GattCharacteristic1 notifEventCh chan uint8 notifEventDone bool onReconnect func() @@ -50,10 +54,11 @@ type Device struct { } var ( - ErrNoDevices = errors.New("no InfiniTime devices found") - ErrNotFound = errors.New("could not find any advertising InfiniTime devices") - ErrNotConnected = errors.New("not connected") - ErrCharNotAvail = errors.New("required characteristic is not available") + ErrNoDevices = errors.New("no InfiniTime devices found") + ErrNotFound = errors.New("could not find any advertising InfiniTime devices") + ErrNotConnected = errors.New("not connected") + ErrCharNotAvail = errors.New("required characteristic is not available") + ErrNoTimelineHeader = errors.New("events must contain the timeline header") ) type Options struct { @@ -333,6 +338,8 @@ func (i *Device) resolveChars() error { i.fsTransferChar = char case FSVersionChar: i.fsVersionChar = char + case WeatherDataChar: + i.weatherDataChar = char } } return nil @@ -685,6 +692,32 @@ func (i *Device) FS() (*blefs.FS, error) { return blefs.New(i.fsTransferChar) } +// AddWeatherEvent adds one of the event structs from +// the weather package to the timeline. Input must be +// a struct containing TimelineHeader. +func (i *Device) AddWeatherEvent(event interface{}) error { + if err := i.checkStatus(i.weatherDataChar); err != nil { + return err + } + // Get type of input + inputType := reflect.TypeOf(event) + // Check if input contains TimelineHeader + _, hdrExists := inputType.FieldByName("TimelineHeader") + // If header does not exist or input is not struct + if !hdrExists || inputType.Kind() != reflect.Struct { + return ErrNoTimelineHeader + } + + // Encode event as CBOR + data, err := cbor.Marshal(event) + if err != nil { + return err + } + + // Write data to weather data characteristic + return i.weatherDataChar.WriteValue(data, nil) +} + func (i *Device) checkStatus(char *gatt.GattCharacteristic1) error { if !i.device.Properties.Connected { return ErrNotConnected diff --git a/weather/weather.go b/weather/weather.go new file mode 100644 index 0000000..7a8765d --- /dev/null +++ b/weather/weather.go @@ -0,0 +1,193 @@ +package weather + +import ( + "time" +) + +// Type of weather event +type EventType uint8 + +// These event types correspond to structs +// that can be used to add an event to the +// weather timeline +const ( + EventTypeObscuration EventType = iota + EventTypePrecipitation + EventTypeWind + EventTypeTemperature + EventTypeAirQuality + EventTypeSpecial + EventTypePressure + EventTypeLocation + EventTypeClouds + EventTypeHumidity +) + +// Special event type +type SpecialType uint8 + +// See https://git.io/JM7Oe for the meaning of each type +const ( + SpecialTypeSquall SpecialType = iota + SpecialTypeTsunami + SpecialTypeTornado + SpecialTypeFire + SpecialTypeThunder +) + +// Precipitation type +type PrecipitationType uint8 + +// See https://git.io/JM7YM for the meaning of each type +const ( + PrecipitationTypeNone PrecipitationType = iota + PrecipitationTypeRain + PrecipitationTypeDrizzle + PrecipitationTypeFreezingRain + PrecipitationTypeSleet + PrecipitationTypeHail + PrecipitationTypeSmallHail + PrecipitationTypeSnow + PrecipitationTypeSnowGrains + PrecipitationTypeIceCrystals + PrecipitationTypeAsh +) + +// Visibility obscuration type +type ObscurationType uint8 + +// See https://git.io/JM7Yd for the meaning of each type +const ( + ObscurationTypeNone ObscurationType = iota + ObscurationTypeFog + ObscurationTypeHaze + ObscurationTypeSmoke + ObscurationTypeAsh + ObscurationTypeDust + ObscurationTypeSand + ObscurationTypeMist + ObscurationTypePrecipitation +) + +// TimelineHeader contains the header for a timeline envent +type TimelineHeader struct { + // UNIX timestamp with timezone offset + Timestamp uint64 + // Seconds until the event expires + Expires uint32 + // Type of weather event + EventType EventType +} + +// NewHeader creates and populates a new timeline header +// and returns it +func NewHeader(evtType EventType, expires time.Duration) TimelineHeader { + return TimelineHeader{ + Timestamp: uint64(time.Now().Unix()), + Expires: uint32(expires.Seconds()), + EventType: evtType, + } +} + +// CloudsEvent corresponds to EventTypeClouds +type CloudsEvent struct { + TimelineHeader + // Cloud coverage percentage + Amount uint8 +} + +// ObscurationEvent corresponds to EventTypeObscuration +type ObscurationEvent struct { + TimelineHeader + // Type of obscuration + Type ObscurationType + // Visibility in meters. 65535 is unspecified. + Amount uint16 +} + +// PrecipitationEvent corresponds to EventTypePrecipitation +type PrecipitationEvent struct { + TimelineHeader + // Type of precipitation + Type PrecipitationType + // Amount of rain in millimeters. 255 is unspecified. + Amount uint8 +} + +// WindEvent corresponds to EventTypeWind +type WindEvent struct { + TimelineHeader + // Minimum speed in meters per second + SpeedMin uint8 + // Maximum speed in meters per second + SpeedMax uint8 + // Unitless direction, about 1 unit per 0.71 degrees. + DirectionMin uint8 + // Unitless direction, about 1 unit per 0.71 degrees + DirectionMax uint8 +} + +// TemperatureEvent corresponds to EventTypeTemperature +type TemperatureEvent struct { + TimelineHeader + // Temperature in celcius multiplied by 100. + // -32768 is "no data" + Temperature int16 + // Dew point in celcius multiplied by 100. + // -32768 is "no data" + DewPoint int16 +} + +// LocationEvent corresponds to EventTypeLocation +type LocationEvent struct { + TimelineHeader + // Location name + Location string + // Altitude from sea level in meters + Altitude int16 + // EPSG:3857 latitude (Google Maps, Openstreetmaps) + Latitude int32 + // EPSG:3857 longitude (Google Maps, Openstreetmaps) + Longitude int32 +} + +// HumidityEvent corresponds to EventTypeHumidity +type HumidityEvent struct { + TimelineHeader + // Relative humidity percentage + Humidity uint8 +} + +// PressureEvent corresponds to EventTypePressure +type PressureEvent struct { + TimelineHeader + // Air pressure in hectopascals (hPa) + Pressure int16 +} + +// SpecialEvent corresponds to EventTypeSpecial +type SpecialEvent struct { + TimelineHeader + // Type of special event + Type SpecialType +} + +// AirQualityEvent corresponds to EventTypeAirQuality +type AirQualityEvent struct { + TimelineHeader + // Name of the polluting item + // + // Do not localize the name. That should be handled by the watch. + // + // For particulate matter, use "PM0.1"`, "PM5", or "PM10". + // + // For chemicals, use the molecular formula ("NO2", "CO2", or "O3"). + // + // For pollen, use the genus of the plant. + Polluter string + // Amount of pollution in SI units + // + // https://ec.europa.eu/environment/air/quality/standards.htm + // http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf + Amount uint32 +}