mirror of
https://github.com/cmur2/miflorad.git
synced 2024-12-22 12:54:25 +01:00
munin-miflora: refactor into two implementations using go-ble and gatt libraries, have common package
This commit is contained in:
parent
51bb5a93c5
commit
e75c04785a
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
|
.envrc
|
||||||
cmd/munin-miflora/munin-miflora
|
cmd/munin-miflora/munin-miflora
|
||||||
miflorad
|
cmd/munin-miflora-gatt/munin-miflora-gatt
|
||||||
|
12
Makefile
12
Makefile
@ -1,21 +1,29 @@
|
|||||||
.POSIX:
|
.POSIX:
|
||||||
|
|
||||||
|
MIFLORA_ADDR?=00:00:00:00:00:00
|
||||||
|
|
||||||
.PHONY: all
|
.PHONY: all
|
||||||
all: clean build test
|
all: clean build test
|
||||||
|
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run: clean build test
|
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
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -f cmd/munin-miflora/munin-miflora
|
rm -f cmd/munin-miflora/munin-miflora
|
||||||
|
rm -f cmd/munin-miflora/munin-miflora-gatt
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: cmd/munin-miflora/munin-miflora
|
build: cmd/munin-miflora/munin-miflora cmd/munin-miflora/munin-miflora-gatt
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: build
|
test: build
|
||||||
|
|
||||||
|
.PHONY: cmd/munin-miflora/munin-miflora
|
||||||
cmd/munin-miflora/munin-miflora:
|
cmd/munin-miflora/munin-miflora:
|
||||||
cd cmd/munin-miflora && go build && cd ../..
|
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 ../..
|
||||||
|
181
cmd/munin-miflora-gatt/main.go
Normal file
181
cmd/munin-miflora-gatt/main.go
Normal file
@ -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()
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@ -8,74 +9,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miflorad/common"
|
impl "miflorad/common/ble"
|
||||||
|
|
||||||
"github.com/currantlabs/gatt"
|
"github.com/go-ble/ble"
|
||||||
"github.com/currantlabs/gatt/examples/option"
|
"github.com/go-ble/ble/examples/lib/dev"
|
||||||
)
|
)
|
||||||
|
|
||||||
const discoveryTimeout = 4 * time.Second
|
const discoveryTimeout = 4 * time.Second
|
||||||
const connectionTimeout = 4 * time.Second
|
const connectionTimeout = 4 * time.Second
|
||||||
|
|
||||||
type DiscoveryResult struct {
|
func readData(client ble.Client, profile *ble.Profile) {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := flag.Args()[0]
|
prefix := flag.Args()[0]
|
||||||
|
|
||||||
regexNonAlphaNumeric, err4 := regexp.Compile("[^a-z0-9]+")
|
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]), "")
|
id := regexNonAlphaNumeric.ReplaceAllString(strings.ToLower(flag.Args()[1]), "")
|
||||||
|
|
||||||
metaData, err := common.MifloraRequestVersionBattery(p)
|
metaData, err := impl.RequestVersionBattery(client, profile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to request version battery, err: %s\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to request version battery, err: %s\n", err)
|
||||||
return
|
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.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.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
|
// for the newer models a magic number must be written before we can read the current data
|
||||||
if metaData.FirmwareVersion >= "2.6.6" {
|
if metaData.FirmwareVersion >= "2.6.6" {
|
||||||
err2 := common.MifloraRequestModeChange(p)
|
err2 := impl.RequestModeChange(client, profile)
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to request mode change, err: %s\n", err2)
|
fmt.Fprintf(os.Stderr, "Failed to request mode change, err: %s\n", err2)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sensorData, err3 := common.MifloraRequstSensorData(p)
|
sensorData, err3 := impl.RequestSensorData(client, profile)
|
||||||
if err3 != nil {
|
if err3 != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to request sensor data, err: %s\n", err3)
|
fmt.Fprintf(os.Stderr, "Failed to request sensor data, err: %s\n", err3)
|
||||||
return
|
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())
|
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() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if len(flag.Args()) != 2 {
|
if len(flag.Args()) != 2 {
|
||||||
@ -127,49 +65,52 @@ func main() {
|
|||||||
os.Exit(1)
|
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 {
|
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)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register discovery handler
|
// Source: https://github.com/go-ble/ble/blob/master/examples/basic/explorer/main.go#L53
|
||||||
device.Handle(gatt.PeripheralDiscovered(onPeriphDiscovered))
|
// 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
|
profile, err := client.DiscoverProfile(true)
|
||||||
|
if err != nil {
|
||||||
select {
|
fmt.Fprintf(os.Stderr, "Failed to discover profile, err: %s\n", err)
|
||||||
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)
|
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
|
fmt.Fprintln(os.Stderr, "Connection done")
|
||||||
device.Handle(
|
|
||||||
gatt.PeripheralConnected(onPeriphConnected),
|
|
||||||
gatt.PeripheralDisconnected(onPeriphDisconnected),
|
|
||||||
)
|
|
||||||
|
|
||||||
device.Connect(discoveryResult.p)
|
client.CancelConnection()
|
||||||
|
|
||||||
select {
|
<-done
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
84
common/ble/impl.go
Normal file
84
common/ble/impl.go
Normal file
@ -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
|
||||||
|
}
|
125
common/common.go
125
common/common.go
@ -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
|
|
||||||
}
|
|
33
common/datatypes.go
Normal file
33
common/datatypes.go
Normal file
@ -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
|
||||||
|
}
|
86
common/gatt/impl.go
Normal file
86
common/gatt/impl.go
Normal file
@ -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
|
||||||
|
}
|
32
common/proto.go
Normal file
32
common/proto.go
Normal file
@ -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]),
|
||||||
|
}
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ require (
|
|||||||
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect
|
github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect
|
||||||
github.com/currantlabs/gatt v0.0.0-20161006170101-f949eac78f4e
|
github.com/currantlabs/gatt v0.0.0-20161006170101-f949eac78f4e
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
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/godbus/dbus v0.0.0-20181031085051-66d97ae // indirect
|
||||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
github.com/mattn/go-isatty v0.0.4 // indirect
|
||||||
|
2
go.sum
2
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/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 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
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 h1:NTs1uIj/Ru/QlpKwd9C9dnv/5zblvCXH7Dbn2oi3p98=
|
||||||
github.com/godbus/dbus v0.0.0-20181031085051-66d97ae/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
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=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
|
Loading…
Reference in New Issue
Block a user