diff --git a/.gitignore b/.gitignore index 6026acb..4f7c156 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.envrc cmd/munin-miflora/munin-miflora -miflorad +cmd/munin-miflora-gatt/munin-miflora-gatt diff --git a/Makefile b/Makefile index 3567402..a4fc3ff 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,29 @@ .POSIX: +MIFLORA_ADDR?=00:00:00:00:00:00 + .PHONY: all all: clean build test .PHONY: run run: clean build test - sudo cmd/munin-miflora/munin-miflora xyz 00:00:00:00:00:00 + sudo cmd/munin-miflora/munin-miflora xyz $(MIFLORA_ADDR) .PHONY: clean clean: rm -f cmd/munin-miflora/munin-miflora + rm -f cmd/munin-miflora/munin-miflora-gatt .PHONY: build -build: cmd/munin-miflora/munin-miflora +build: cmd/munin-miflora/munin-miflora cmd/munin-miflora/munin-miflora-gatt .PHONY: test test: build +.PHONY: cmd/munin-miflora/munin-miflora cmd/munin-miflora/munin-miflora: cd cmd/munin-miflora && go build && cd ../.. + +.PHONY: cmd/munin-miflora/munin-miflora-gatt +cmd/munin-miflora/munin-miflora-gatt: + cd cmd/munin-miflora-gatt && go build && cd ../.. diff --git a/cmd/munin-miflora-gatt/main.go b/cmd/munin-miflora-gatt/main.go new file mode 100644 index 0000000..9e39f88 --- /dev/null +++ b/cmd/munin-miflora-gatt/main.go @@ -0,0 +1,181 @@ +package main + +import ( + "flag" + "fmt" + "os" + "regexp" + "strings" + "time" + + "miflorad/common" + impl "miflorad/common/gatt" + + "github.com/currantlabs/gatt" + "github.com/currantlabs/gatt/examples/option" +) + +const ( + discoveryTimeout = 4 * time.Second + connectionTimeout = 4 * time.Second +) + +type DiscoveryResult struct { + p gatt.Peripheral + a *gatt.Advertisement + rssi int +} + +var discoveryDone = make(chan DiscoveryResult) +var connectionDone = make(chan struct{}) + +func onStateChanged(device gatt.Device, state gatt.State) { + fmt.Fprintln(os.Stderr, "State:", state) + switch state { + case gatt.StatePoweredOn: + fmt.Fprintln(os.Stderr, "Scanning...") + device.Scan([]gatt.UUID{}, false) + return + default: + device.StopScanning() + } +} + +func onPeriphDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) { + id := strings.ToUpper(flag.Args()[1]) + if strings.ToUpper(p.ID()) != id { + return + } + + // Stop scanning once we've got the peripheral we're looking for. + p.Device().StopScanning() + + discoveryDone <- DiscoveryResult{p, a, rssi} +} + +func onPeriphConnected(p gatt.Peripheral, err error) { + fmt.Fprintln(os.Stderr, "Connected") + + // Note: can hang due when device has terminated the connection on it's own already + // defer p.Device().CancelConnection(p) + + if err := p.SetMTU(500); err != nil { + fmt.Fprintf(os.Stderr, "Failed to set MTU, err: %s\n", err) + } + + // Discover services and characteristics + { + _, err := p.DiscoverServices([]gatt.UUID{gatt.MustParseUUID(common.MifloraServiceUUID)}) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to discover services, err: %s\n", err) + return + } + } + for _, service := range p.Services() { + _, err := p.DiscoverCharacteristics(nil, service) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to discover characteristics, err: %s\n", err) + return + } + } + + prefix := flag.Args()[0] + + regexNonAlphaNumeric, err4 := regexp.Compile("[^a-z0-9]+") + if err4 != nil { + fmt.Fprintf(os.Stderr, "Failed to compile regex, err: %s\n", err4) + } + id := regexNonAlphaNumeric.ReplaceAllString(strings.ToLower(flag.Args()[1]), "") + + metaData, err := impl.MifloraRequestVersionBattery(p) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to request version battery, err: %s\n", err) + return + } + + fmt.Fprintf(os.Stderr, "Firmware version: %s\n", metaData.FirmwareVersion) + + fmt.Fprintf(os.Stdout, "%s.miflora.%s.battery_level %d %d\n", prefix, id, metaData.BatteryLevel, time.Now().Unix()) + fmt.Fprintf(os.Stdout, "%s.miflora.%s.firmware_version %d %d\n", prefix, id, metaData.NumericFirmwareVersion(), time.Now().Unix()) + + // for the newer models a magic number must be written before we can read the current data + if metaData.FirmwareVersion >= "2.6.6" { + err2 := impl.MifloraRequestModeChange(p) + if err2 != nil { + fmt.Fprintf(os.Stderr, "Failed to request mode change, err: %s\n", err2) + return + } + } + + sensorData, err3 := impl.MifloraRequstSensorData(p) + if err3 != nil { + fmt.Fprintf(os.Stderr, "Failed to request sensor data, err: %s\n", err3) + return + } + fmt.Fprintf(os.Stdout, "%s.miflora.%s.temperature %.1f %d\n", prefix, id, sensorData.Temperature, time.Now().Unix()) + fmt.Fprintf(os.Stdout, "%s.miflora.%s.brightness %d %d\n", prefix, id, sensorData.Brightness, time.Now().Unix()) + fmt.Fprintf(os.Stdout, "%s.miflora.%s.moisture %d %d\n", prefix, id, sensorData.Moisture, time.Now().Unix()) + fmt.Fprintf(os.Stdout, "%s.miflora.%s.conductivity %d %d\n", prefix, id, sensorData.Conductivity, time.Now().Unix()) + + // TODO: report that we are done without closing connection, since it could hang + close(connectionDone) +} + +func onPeriphDisconnected(p gatt.Peripheral, err error) { + fmt.Fprintln(os.Stderr, "Disconnected") + close(connectionDone) +} + +func main() { + flag.Parse() + if len(flag.Args()) != 2 { + fmt.Fprintf(os.Stderr, "Usage: %s [options] prefix peripheral-id\n", os.Args[0]) + os.Exit(1) + } + + device, err := gatt.NewDevice(option.DefaultClientOptions...) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to open device, err: %s\n", err) + os.Exit(1) + } + + // Register discovery handler + device.Handle(gatt.PeripheralDiscovered(onPeriphDiscovered)) + + device.Init(onStateChanged) + + var discoveryResult DiscoveryResult + + select { + case discoveryResult = <-discoveryDone: + fmt.Fprintln(os.Stderr, "Discovery done") + case <-time.After(discoveryTimeout): + fmt.Fprintln(os.Stderr, "Discovery timed out") + device.StopScanning() + device.Stop() + os.Exit(1) + } + + fmt.Fprintf(os.Stderr, "Discovered peripheral ID:%s, NAME:(%s), RSSI:%d\n", discoveryResult.p.ID(), discoveryResult.p.Name(), discoveryResult.rssi) + + // Register connection handlers + device.Handle( + gatt.PeripheralConnected(onPeriphConnected), + // gatt.PeripheralDisconnected(onPeriphDisconnected), + ) + + device.Connect(discoveryResult.p) + + select { + case <-connectionDone: + fmt.Fprintln(os.Stderr, "Connection done") + case <-time.After(connectionTimeout): + fmt.Fprintln(os.Stderr, "Connection timed out") + // TODO: can hang due when device has terminated the connection on it's own already + // device.CancelConnection(discoveryResult.p) + os.Exit(1) + } + + // Note: calls CancelConnection() and thus suffers the same problem, kernel will cleanup after our process finishes + // device.Stop() +} diff --git a/cmd/munin-miflora/main.go b/cmd/munin-miflora/main.go index 4680e1d..4634b42 100644 --- a/cmd/munin-miflora/main.go +++ b/cmd/munin-miflora/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "flag" "fmt" "os" @@ -8,74 +9,16 @@ import ( "strings" "time" - "miflorad/common" + impl "miflorad/common/ble" - "github.com/currantlabs/gatt" - "github.com/currantlabs/gatt/examples/option" + "github.com/go-ble/ble" + "github.com/go-ble/ble/examples/lib/dev" ) const discoveryTimeout = 4 * time.Second const connectionTimeout = 4 * time.Second -type DiscoveryResult struct { - p gatt.Peripheral - a *gatt.Advertisement - rssi int -} - -var discoveryDone = make(chan DiscoveryResult) -var connectionDone = make(chan struct{}) - -func onStateChanged(device gatt.Device, state gatt.State) { - fmt.Fprintln(os.Stderr, "State:", state) - switch state { - case gatt.StatePoweredOn: - fmt.Fprintln(os.Stderr, "Scanning...") - device.Scan([]gatt.UUID{}, false) - return - default: - device.StopScanning() - } -} - -func onPeriphDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) { - id := strings.ToUpper(flag.Args()[1]) - if strings.ToUpper(p.ID()) != id { - return - } - - // Stop scanning once we've got the peripheral we're looking for. - p.Device().StopScanning() - - discoveryDone <- DiscoveryResult{p, a, rssi} -} - -func onPeriphConnected(p gatt.Peripheral, err error) { - fmt.Fprintln(os.Stderr, "Connected") - - // Note: can hang due when device has terminated the connection on it's own already - // defer p.Device().CancelConnection(p) - - if err := p.SetMTU(500); err != nil { - fmt.Fprintf(os.Stderr, "Failed to set MTU, err: %s\n", err) - } - - // Discover services and characteristics - { - _, err := p.DiscoverServices([]gatt.UUID{common.MifloraServiceUUID}) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to discover services, err: %s\n", err) - return - } - } - for _, service := range p.Services() { - _, err := p.DiscoverCharacteristics(nil, service) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to discover characteristics, err: %s\n", err) - return - } - } - +func readData(client ble.Client, profile *ble.Profile) { prefix := flag.Args()[0] regexNonAlphaNumeric, err4 := regexp.Compile("[^a-z0-9]+") @@ -84,7 +27,7 @@ func onPeriphConnected(p gatt.Peripheral, err error) { } id := regexNonAlphaNumeric.ReplaceAllString(strings.ToLower(flag.Args()[1]), "") - metaData, err := common.MifloraRequestVersionBattery(p) + metaData, err := impl.RequestVersionBattery(client, profile) if err != nil { fmt.Fprintf(os.Stderr, "Failed to request version battery, err: %s\n", err) return @@ -93,18 +36,18 @@ func onPeriphConnected(p gatt.Peripheral, err error) { fmt.Fprintf(os.Stderr, "Firmware version: %s\n", metaData.FirmwareVersion) fmt.Fprintf(os.Stdout, "%s.miflora.%s.battery_level %d %d\n", prefix, id, metaData.BatteryLevel, time.Now().Unix()) - fmt.Fprintf(os.Stdout, "%s.miflora.%s.firmware_version %s %d\n", prefix, id, metaData.NumericFirmwareVersion(), time.Now().Unix()) + fmt.Fprintf(os.Stdout, "%s.miflora.%s.firmware_version %d %d\n", prefix, id, metaData.NumericFirmwareVersion(), time.Now().Unix()) // for the newer models a magic number must be written before we can read the current data if metaData.FirmwareVersion >= "2.6.6" { - err2 := common.MifloraRequestModeChange(p) + err2 := impl.RequestModeChange(client, profile) if err2 != nil { fmt.Fprintf(os.Stderr, "Failed to request mode change, err: %s\n", err2) return } } - sensorData, err3 := common.MifloraRequstSensorData(p) + sensorData, err3 := impl.RequestSensorData(client, profile) if err3 != nil { fmt.Fprintf(os.Stderr, "Failed to request sensor data, err: %s\n", err3) return @@ -115,11 +58,6 @@ func onPeriphConnected(p gatt.Peripheral, err error) { fmt.Fprintf(os.Stdout, "%s.miflora.%s.conductivity %d %d\n", prefix, id, sensorData.Conductivity, time.Now().Unix()) } -func onPeriphDisconnected(p gatt.Peripheral, err error) { - fmt.Fprintln(os.Stderr, "Disconnected") - close(connectionDone) -} - func main() { flag.Parse() if len(flag.Args()) != 2 { @@ -127,49 +65,52 @@ func main() { os.Exit(1) } - device, err := gatt.NewDevice(option.DefaultClientOptions...) + { + device, err := dev.NewDevice("default") + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to open device, err: %s\n", err) + os.Exit(1) + } + ble.SetDefaultDevice(device) + } + + filter := func(adv ble.Advertisement) bool { + return strings.ToUpper(adv.Addr().String()) == strings.ToUpper(flag.Args()[1]) + } + + fmt.Fprintln(os.Stderr, "Scanning...") + ctx := ble.WithSigHandler(context.WithTimeout(context.Background(), discoveryTimeout)) + client, err := ble.Connect(ctx, filter) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to open device, err: %s\n", err) + fmt.Fprintf(os.Stderr, "Failed to connect to %s, err: %s\n", flag.Args()[1], err) os.Exit(1) } - // Register discovery handler - device.Handle(gatt.PeripheralDiscovered(onPeriphDiscovered)) + // Source: https://github.com/go-ble/ble/blob/master/examples/basic/explorer/main.go#L53 + // Make sure we had the chance to print out the message. + done := make(chan struct{}) + // Normally, the connection is disconnected by us after our exploration. + // However, it can be asynchronously disconnected by the remote peripheral. + // So we wait(detect) the disconnection in the go routine. + go func() { + <-client.Disconnected() + fmt.Fprintln(os.Stderr, "Disconnected") + close(done) + }() - device.Init(onStateChanged) + fmt.Fprintln(os.Stderr, "Connected") - var discoveryResult DiscoveryResult - - select { - case discoveryResult = <-discoveryDone: - fmt.Fprintln(os.Stderr, "Discovery done") - case <-time.After(discoveryTimeout): - fmt.Fprintln(os.Stderr, "Discovery timed out") - device.StopScanning() - device.Stop() + profile, err := client.DiscoverProfile(true) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to discover profile, err: %s\n", err) os.Exit(1) } - fmt.Fprintf(os.Stderr, "Discovered peripheral ID:%s, NAME:(%s), RSSI:%d\n", discoveryResult.p.ID(), discoveryResult.p.Name(), discoveryResult.rssi) + readData(client, profile) - // Register connection handlers - device.Handle( - gatt.PeripheralConnected(onPeriphConnected), - gatt.PeripheralDisconnected(onPeriphDisconnected), - ) + fmt.Fprintln(os.Stderr, "Connection done") - device.Connect(discoveryResult.p) + client.CancelConnection() - select { - case <-connectionDone: - fmt.Fprintln(os.Stderr, "Connection done") - case <-time.After(connectionTimeout): - fmt.Fprintln(os.Stderr, "Connection timed out") - // TODO: can hang due when device has terminated the connection on it's own already - // device.CancelConnection(discoveryResult.p) - os.Exit(1) - } - - // Note: calls CancelConnetcion() and thus suffers the same problem, kernel will cleanup after our process finishes - // device.Stop() + <-done } diff --git a/common/ble/impl.go b/common/ble/impl.go new file mode 100644 index 0000000..bc9d4f1 --- /dev/null +++ b/common/ble/impl.go @@ -0,0 +1,84 @@ +package ble + +import ( + "errors" + + "miflorad/common" + + "github.com/go-ble/ble" +) + +func FindServiceByUUID(services []*ble.Service, u ble.UUID) *ble.Service { + for _, service := range services { + if service.UUID.Equal(u) { + return service + } + } + return nil +} + +func FindCharacteristicByUUID(characteristics []*ble.Characteristic, u ble.UUID) *ble.Characteristic { + for _, characteristic := range characteristics { + if characteristic.UUID.Equal(u) { + return characteristic + } + } + return nil +} + +func RequestVersionBattery(client ble.Client, profile *ble.Profile) (common.VersionBatteryResponse, error) { + mifloraService := FindServiceByUUID(profile.Services, ble.MustParse(common.MifloraServiceUUID)) + if mifloraService == nil { + return common.VersionBatteryResponse{}, errors.New("Failed to get the miflora service") + } + + mifloraVersionBatteryChar := FindCharacteristicByUUID(mifloraService.Characteristics, ble.MustParse(common.MifloraCharVersionBatteryUUID)) + if mifloraVersionBatteryChar == nil { + return common.VersionBatteryResponse{}, errors.New("Failed to get the version battery characteristic") + } + + bytes, err := client.ReadCharacteristic(mifloraVersionBatteryChar) + if err != nil { + return common.VersionBatteryResponse{}, err + } + + return common.ParseVersionBattery(bytes), nil +} + +func RequestModeChange(client ble.Client, profile *ble.Profile) error { + mifloraService := FindServiceByUUID(profile.Services, ble.MustParse(common.MifloraServiceUUID)) + if mifloraService == nil { + return errors.New("Failed to get the miflora service") + } + + mifloraModeChangeChar := FindCharacteristicByUUID(mifloraService.Characteristics, ble.MustParse(common.MifloraCharModeChangeUUID)) + if mifloraModeChangeChar == nil { + return errors.New("Failed to discover the mode change characteristic") + } + + err := client.WriteCharacteristic(mifloraModeChangeChar, common.MifloraGetModeChangeData(), false) + if err != nil { + return err + } + + return nil +} + +func RequestSensorData(client ble.Client, profile *ble.Profile) (common.SensorDataResponse, error) { + mifloraService := FindServiceByUUID(profile.Services, ble.MustParse(common.MifloraServiceUUID)) + if mifloraService == nil { + return common.SensorDataResponse{}, errors.New("Failed to get the miflora service") + } + + mifloraSensorDataChar := FindCharacteristicByUUID(mifloraService.Characteristics, ble.MustParse(common.MifloraCharReadSensorDataUUID)) + if mifloraSensorDataChar == nil { + return common.SensorDataResponse{}, errors.New("Failed to discover the sensor data characteristic") + } + + bytes, err := client.ReadCharacteristic(mifloraSensorDataChar) + if err != nil { + return common.SensorDataResponse{}, err + } + + return common.ParseSensorData(bytes), nil +} diff --git a/common/common.go b/common/common.go deleted file mode 100644 index 7dcd0aa..0000000 --- a/common/common.go +++ /dev/null @@ -1,125 +0,0 @@ -package common - -import ( - "encoding/binary" - "errors" - "math" - "strconv" - "strings" - - "github.com/currantlabs/gatt" -) - -type VersionBatteryResponse struct { - FirmwareVersion string // as "x.y.z" - BatteryLevel uint8 // in percent 0-100 -} - -type SensorDataResponse struct { - Temperature float64 // in degree C - Brightness uint32 // in lux - Moisture uint8 // in percent 0-100 - Conductivity uint16 // in µS/cm -} - -func MifloraGetModeChangeData() []byte { - return []byte{0xa0, 0x1f} -} - -var MifloraServiceUUID = gatt.MustParseUUID("00001204-0000-1000-8000-00805f9b34fb") -var MifloraCharModeChangeUUID = gatt.MustParseUUID("00001a00-0000-1000-8000-00805f9b34fb") -var MifloraCharReadSensorDataUUID = gatt.MustParseUUID("00001a01-0000-1000-8000-00805f9b34fb") -var MifloraCharVersionBatteryUUID = gatt.MustParseUUID("00001a02-0000-1000-8000-00805f9b34fb") - -func FindServiceByUUID(services []*gatt.Service, u gatt.UUID) *gatt.Service { - for _, service := range services { - if service.UUID().Equal(u) { - return service - } - } - return nil -} - -func FindCharacteristicByUUID(characteristics []*gatt.Characteristic, u gatt.UUID) *gatt.Characteristic { - for _, characteristic := range characteristics { - if characteristic.UUID().Equal(u) { - return characteristic - } - } - return nil -} - -func MifloraRequestVersionBattery(p gatt.Peripheral) (VersionBatteryResponse, error) { - mifloraService := FindServiceByUUID(p.Services(), MifloraServiceUUID) - if mifloraService == nil { - return VersionBatteryResponse{}, errors.New("Failed to get the miflora service") - } - - mifloraVersionBatteryChar := FindCharacteristicByUUID(mifloraService.Characteristics(), MifloraCharVersionBatteryUUID) - if mifloraVersionBatteryChar == nil { - return VersionBatteryResponse{}, errors.New("Failed to get the version battery characteristic") - } - - bytes, err := p.ReadCharacteristic(mifloraVersionBatteryChar) - if err != nil { - return VersionBatteryResponse{}, err - } - - return VersionBatteryResponse{string(bytes[2:]), uint8(bytes[0])}, nil -} - -func MifloraRequestModeChange(p gatt.Peripheral) error { - mifloraService := FindServiceByUUID(p.Services(), MifloraServiceUUID) - if mifloraService == nil { - return errors.New("Failed to get the miflora service") - } - - mifloraModeChangeChar := FindCharacteristicByUUID(mifloraService.Characteristics(), MifloraCharModeChangeUUID) - if mifloraModeChangeChar == nil { - return errors.New("Failed to discover the mode change characteristic") - } - - err := p.WriteCharacteristic(mifloraModeChangeChar, MifloraGetModeChangeData(), false) - if err != nil { - return err - } - - return nil -} - -func MifloraRequstSensorData(p gatt.Peripheral) (SensorDataResponse, error) { - mifloraService := FindServiceByUUID(p.Services(), MifloraServiceUUID) - if mifloraService == nil { - return SensorDataResponse{}, errors.New("Failed to get the miflora service") - } - - mifloraSensorDataChar := FindCharacteristicByUUID(mifloraService.Characteristics(), MifloraCharReadSensorDataUUID) - if mifloraSensorDataChar == nil { - return SensorDataResponse{}, errors.New("Failed to discover the sensor data characteristic") - } - - bytes, err := p.ReadCharacteristic(mifloraSensorDataChar) - if err != nil { - return SensorDataResponse{}, err - } - - return SensorDataResponse{ - Temperature: float64(binary.LittleEndian.Uint16(bytes[0:2])) / 10.0, - Brightness: binary.LittleEndian.Uint32(bytes[3:7]), - Moisture: uint8(bytes[7]), - Conductivity: binary.LittleEndian.Uint16(bytes[8:10]), - }, nil -} - -func (res VersionBatteryResponse) NumericFirmwareVersion() int { - // turns "2.3.4" into 20304 - version := 0 - parts := strings.Split(res.FirmwareVersion, ".") - for i, part := range parts { - partNumber, err := strconv.Atoi(part) - if err != nil { - version += int(math.Pow10((len(parts)-(i+1))*2)) * partNumber - } - } - return version -} diff --git a/common/datatypes.go b/common/datatypes.go new file mode 100644 index 0000000..69f36b2 --- /dev/null +++ b/common/datatypes.go @@ -0,0 +1,33 @@ +package common + +import ( + "math" + "strconv" + "strings" +) + +type VersionBatteryResponse struct { + FirmwareVersion string // as "x.y.z" + BatteryLevel uint8 // in percent 0-100 +} + +type SensorDataResponse struct { + Temperature float64 // in degree C + Brightness uint32 // in lux + Moisture uint8 // in percent 0-100 + Conductivity uint16 // in µS/cm +} + +func (res VersionBatteryResponse) NumericFirmwareVersion() int { + // turns "2.3.4" into 20304 + version := 0 + parts := strings.Split(res.FirmwareVersion, ".") + for i, part := range parts { + partNumber, err := strconv.Atoi(part) + if err != nil { + continue + } + version += int(math.Pow10((len(parts)-(i+1))*2)) * partNumber + } + return version +} diff --git a/common/gatt/impl.go b/common/gatt/impl.go new file mode 100644 index 0000000..50a1d72 --- /dev/null +++ b/common/gatt/impl.go @@ -0,0 +1,86 @@ +package gatt + +import ( + "errors" + + "miflorad/common" + + "github.com/currantlabs/gatt" +) + +var MifloraCharVersionBatteryUUID = gatt.MustParseUUID("00001a02-0000-1000-8000-00805f9b34fb") + +func FindServiceByUUID(services []*gatt.Service, u gatt.UUID) *gatt.Service { + for _, service := range services { + if service.UUID().Equal(u) { + return service + } + } + return nil +} + +func FindCharacteristicByUUID(characteristics []*gatt.Characteristic, u gatt.UUID) *gatt.Characteristic { + for _, characteristic := range characteristics { + if characteristic.UUID().Equal(u) { + return characteristic + } + } + return nil +} + +func MifloraRequestVersionBattery(p gatt.Peripheral) (common.VersionBatteryResponse, error) { + mifloraService := FindServiceByUUID(p.Services(), gatt.MustParseUUID(common.MifloraServiceUUID)) + if mifloraService == nil { + return common.VersionBatteryResponse{}, errors.New("Failed to get the miflora service") + } + + mifloraVersionBatteryChar := FindCharacteristicByUUID(mifloraService.Characteristics(), MifloraCharVersionBatteryUUID) + if mifloraVersionBatteryChar == nil { + return common.VersionBatteryResponse{}, errors.New("Failed to get the version battery characteristic") + } + + bytes, err := p.ReadCharacteristic(mifloraVersionBatteryChar) + if err != nil { + return common.VersionBatteryResponse{}, err + } + + return common.ParseVersionBattery(bytes), nil +} + +func MifloraRequestModeChange(p gatt.Peripheral) error { + mifloraService := FindServiceByUUID(p.Services(), gatt.MustParseUUID(common.MifloraServiceUUID)) + if mifloraService == nil { + return errors.New("Failed to get the miflora service") + } + + mifloraModeChangeChar := FindCharacteristicByUUID(mifloraService.Characteristics(), gatt.MustParseUUID(common.MifloraCharModeChangeUUID)) + if mifloraModeChangeChar == nil { + return errors.New("Failed to discover the mode change characteristic") + } + + err := p.WriteCharacteristic(mifloraModeChangeChar, common.MifloraGetModeChangeData(), false) + if err != nil { + return err + } + + return nil +} + +func MifloraRequstSensorData(p gatt.Peripheral) (common.SensorDataResponse, error) { + mifloraService := FindServiceByUUID(p.Services(), gatt.MustParseUUID(common.MifloraServiceUUID)) + if mifloraService == nil { + return common.SensorDataResponse{}, errors.New("Failed to get the miflora service") + } + + mifloraSensorDataChar := FindCharacteristicByUUID(mifloraService.Characteristics(), gatt.MustParseUUID(common.MifloraCharReadSensorDataUUID)) + if mifloraSensorDataChar == nil { + return common.SensorDataResponse{}, errors.New("Failed to discover the sensor data characteristic") + } + + bytes, err := p.ReadCharacteristic(mifloraSensorDataChar) + if err != nil { + return common.SensorDataResponse{}, err + } + + return common.ParseSensorData(bytes), nil +} diff --git a/common/proto.go b/common/proto.go new file mode 100644 index 0000000..937570c --- /dev/null +++ b/common/proto.go @@ -0,0 +1,32 @@ +package common + +import ( + "encoding/binary" +) + +const ( + MifloraServiceUUID = "00001204-0000-1000-8000-00805f9b34fb" + MifloraCharModeChangeUUID = "00001a00-0000-1000-8000-00805f9b34fb" + MifloraCharReadSensorDataUUID = "00001a01-0000-1000-8000-00805f9b34fb" + MifloraCharVersionBatteryUUID = "00001a02-0000-1000-8000-00805f9b34fb" +) + +func MifloraGetModeChangeData() []byte { + return []byte{0xa0, 0x1f} +} + +func ParseVersionBattery(bytes []byte) VersionBatteryResponse { + return VersionBatteryResponse{ + string(bytes[2:]), + uint8(bytes[0]), + } +} + +func ParseSensorData(bytes []byte) SensorDataResponse { + return SensorDataResponse{ + Temperature: float64(binary.LittleEndian.Uint16(bytes[0:2])) / 10.0, + Brightness: binary.LittleEndian.Uint32(bytes[3:7]), + Moisture: uint8(bytes[7]), + Conductivity: binary.LittleEndian.Uint16(bytes[8:10]), + } +} diff --git a/go.mod b/go.mod index 29632da..3ab5ae5 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ require ( github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect github.com/currantlabs/gatt v0.0.0-20161006170101-f949eac78f4e github.com/fatih/structs v1.1.0 // indirect + github.com/go-ble/ble v0.0.0-20181002102605-e78417b510a3 github.com/godbus/dbus v0.0.0-20181031085051-66d97ae // indirect github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect diff --git a/go.sum b/go.sum index 47f162a..dabb90b 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ 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/go-ble/ble v0.0.0-20181002102605-e78417b510a3 h1:rsLGztXl2QJvj4x/PAWzC1Zx6tnTDKlosaXAZfaXM8M= +github.com/go-ble/ble v0.0.0-20181002102605-e78417b510a3/go.mod h1:UMPB54/KFpdTdfH7Yovhk3J6kzgzE88e3QZi8cbayis= github.com/godbus/dbus v0.0.0-20181031085051-66d97ae h1:NTs1uIj/Ru/QlpKwd9C9dnv/5zblvCXH7Dbn2oi3p98= github.com/godbus/dbus v0.0.0-20181031085051-66d97ae/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=