Skip to content

Commit

Permalink
Added all files, fixed info in README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
danielfaust committed Apr 28, 2019
1 parent 1038b52 commit a7e7e71
Show file tree
Hide file tree
Showing 9 changed files with 1,556 additions and 23 deletions.
46 changes: 23 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ A Python 2.7 script for the Sanitas SBF70 / Silvercrest SBF75 / Beurer BF700 / B
**scale.py**

```python
DEFAULT_SCALE_MAC_ADDRESS = users.SCALE # the users.py file contains the mac address
DEFAULT_MEASURE_ALIAS = users.ALIAS # DOWNLOAD DATA FROM THE SCALE AND PERFORM A LIVE MEASURING FOR THIS ALIAS
DEFAULT_MEASURE_ALIAS = None # DO NOT PERFORM A LIVE MEASURING, ONLY DOWNLOAD DATA FROM THE SCALE

# very safe and informative defaults
DO_CHECK_UNKNOWN = True
DO_SAVE_UNKNOWN = True
Expand All @@ -26,11 +22,7 @@ LOG_PEXPECT = False # may be helpful with debugging



The script *scale.py* can be executed directly via `python scale.py`. Before you do this, you need to adjust the default Bluetooth LE address of the scale. You can find this `mac address` with the bundled executable `bt-scale` or any smartphone ble-scanner like <https://play.google.com/store/apps/details?id=com.macdom.ble.blescanner>



Initially it is highly recommended to leave `DEFAULT_MEASURE_ALIAS` set to `None`. This means that the script will not start a live measurement, but only fetch data from the scale, or adjust user properties.
The script *scale.py* can be executed directly via `python scale.py`. Before you do this, you need to adjust the default Bluetooth LE address of the scale in the file *users.py*. You can find this `mac address` with the bundled executable `bt-scale` or any smartphone ble-scanner like <https://play.google.com/store/apps/details?id=com.macdom.ble.blescanner>. Scroll way down for info on `bt-scale`.



Expand Down Expand Up @@ -73,7 +65,7 @@ A successful readout would result in the following information:



If it were a live measurement, then `measurement-weight` with the stable weight and `measurement-weights` with an array of all the weights leading to the stable weight would also be found in this `JSON` object, as well as a `measurement` object containing the data of the live measurement. Maybe those live weight readings could be live-streamed via MQTT to a smartwatch or something for live monitoring.
If it were a live measurement (as compared to a simple readout of the scale), then `measurement-weight` with the stable weight and `measurement-weights` with an array of all the weights leading to the stable weight would also be found in this `JSON` object, as well as a `measurement` object containing the data of the live measurement.



Expand All @@ -83,15 +75,15 @@ All the `time` fields contain the *localized* ISO format (the default locale of

<u>Live measurements</u>

You should generally avoid using live measurement, because it is far more efficient to just stand on the scale and let the scale store the measurement in its internal storage, and the next time the script runs it will download all the measurements. The scale needs to be manually woken up so that the script can connect to it, I think that this is a problem with `gatttool`, which this script makes use of. If you step on the scale and do a measurement, without this script, then the scale stays awake for about 15 seconds, which is a good time to start this script.
You should generally avoid using live measurement, because it is far more efficient to just stand on the scale and let the scale store the measurement in its internal storage, and the next time the script runs (possibly through a `cronjob`) it will download all the measurements. The scale needs to be manually woken up so that the script can connect to it, I think that this is a problem with `gatttool`, which this script makes use of. If you step on the scale and do a measurement, without this script, then the scale stays awake for about 15 seconds, which is a good time to start this script. The helper executable `bt-scale` is also capable of waking up the scale.



Live measurements do no harm, but can be problematic. If you want to perform a live measurement, then you need to define the `alias` on which this measurement will be made. This alias then **must** be in the *users.py* file in the USER_MAPPING and the corresponding `uid` in the USERS list. There are safeguards built into the script if an alias cannot be resolved to a USERS list entry. It then simply does not start a live measurement, but only behave as if it were `None`



The main problem with live measurements is that you need to know when to stand on the scale. If you stand on the scale while any other commands are being executed (like get the scale status, download stored measurements), then the scale aborts those commands and starts issuing weight measuring notifications. In that case, the command would need to get replayed, but this script doesn't handle this (apparently smartphone apps also don't handle this). So you either need to keep an eye on the script, which tells you when you can step on the scale, or watch the bottom left corner of the scale for your (up-to-)three-letter username to appear on the scale. It's hard to read on the scale. So it's best to leave live measurements deactivated and trigger the script right after the bubbles on the scale display have moved to the right, just when the summary shows up. During this entire time, while the summary shows, the script can connect without problems to the scale and download the fresh measurement.
*The main problem with live measurements is that you need to know when to stand on the scale.* If you stand on the scale while any other commands are being executed (like get the scale status, download stored measurements), then the scale aborts those commands and starts issuing weight measuring notifications. In that case, the command would need to get replayed, but this script doesn't handle this. So you either need to keep an eye on the script, which tells you when you can step on the scale, or watch the bottom left corner of the scale for your (up-to-)three-letter user name to appear on the scale. It's hard to read that on the unlit scale. So it's best to leave live measurements deactivated and trigger the script right after the bubbles on the scale display have moved to the right, just when the summary shows up (or with a `cronjob` at night). During this entire time, while the summary shows, the script can connect without problems to the scale and download the fresh measurement.



Expand Down Expand Up @@ -140,28 +132,36 @@ You could calculate all the other parameters with this information, if you know

**users.py**

This file could well be named *config.py*.

```python
SCALE = 'XX:XX:XX:XX:XX:XX' # mac address of the scale
ALIAS = 'somebody' # default alias to use
SCALE = 'XX:XX:XX:XX:XX:XX' # mac address of the scale

#ALIAS = "somebody" # by default, request a live measurement for "somebody"
ALIAS = None # by default, do not request a live measurement

USER_MAPPING = {
"somebody": "0000000000001234", # map alias somebody to uid 0000000000001234
#"somebody": "0000000000001234",
}

USERS = [ # this data is also stored on the scale
{
"name": "XY", # 1 to 3 UPPERCASE LETTERS!
"birthday": "1999-01-23",
"height": 123, # cm or foot, depending on scale setting
"gender": "male", # or "female", depending on the body
"activity": 3, # see list in scale.py
"uid": "0000000000001234" # MUST BE 16 hexadecimal characters!
},
#{
# "name": "XY", # 1 to 3 UPPERCASE LETTERS!
# "birthday": "1999-01-23",
# "height": 123, # cm or foot, depending on scale setting
# "gender": "male", # or "female", depending on the body
# "activity": 3, # see list in scale.py
# "uid": "0000000000001234" # MUST BE 16 hexadecimal characters!
#},
]
```



Initially it is highly recommended to leave `ALIAS` set to `None`. This means that the script will not start a live measurement, but only fetch data from the scale, or adjust user properties.



<u>User Management</u>

1. The `uid` of the user is what defines the user. Any other parameter that gets changed in the file will get uploaded to the scale. If you change the name or the birthday, you may run into problems when you're using a smartphone app in parallel, some of them rely on this information to define a user. This means that you can change the `height` or `activity` even after the user has been added to the scale, in that case the data on the scale will get updated. Are more/less active? Change the `activity` value in the file. You have grown? Change the `height` value in the file. *You will always get prompted before any change is made to the scale*. You can then either `ctrl-c` out of the script, or just type `enter` to abort the script, or type and submit `YES` to upload the changes to the scale. Afterwards the script will exit and will have to be re-run for any other action.
Expand Down
Binary file added bt-scale
Binary file not shown.
187 changes: 187 additions & 0 deletions bt-scale.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// +build

/*
* This file is part of BT-Scale (https://github.com/danielfaust/bt-scale).
* Copyright (c) 2019 Daniel Faust.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package main

import (
"os"
"fmt"
"log"
"flag"
"time"
"io/ioutil"
"encoding/hex"
"github.com/paypal/gatt"
)

var done = make(chan struct{})
var mac string
var uid string
var final_message string
var output = os.Stdout
var program_abortion_timeout = 30;
var prevent_program_abortion = false

func onPeriphConnected(p gatt.Peripheral, err error) {

prevent_program_abortion = true

fmt.Fprintf(output, "connected\n")

if err := p.SetMTU(500); err != nil {
fmt.Fprintf(output, "Failed to set MTU, err: %s\n", err)
}

ss, err := p.DiscoverServices(nil)
if err != nil {
fmt.Fprintf(output, "failed to discover services, err: %s\n", err)
return
}

for _, s := range ss {

if (s.UUID().String() != "ffe0") {
continue
}

cs, err := p.DiscoverCharacteristics(nil, s)
if err != nil {
fmt.Fprintf(output, "failed to discover characteristics, err: %s\n", err)
continue
}

for _, c := range cs {
if ((c.UUID().String() == "ffe1") && (c.Properties() & gatt.CharWrite) != 0 && (c.Properties() & gatt.CharNotify) != 0) {
_, err := p.DiscoverDescriptors(nil, c)
if err != nil {
fmt.Fprintf(output, "failed to discover descriptors, err: %s\n", err)
continue
}
f := func(c *gatt.Characteristic, b []byte, err error) {
if (b[0] == 0xe6 && b[1] == 0x00 && b[2] == 0x20) {
fmt.Fprintf(output, "ack init request | % X\n", b)
measuer_user := "E740" + uid
fmt.Fprintf(output, "sending measurement request %s\n", measuer_user)
measure, err := hex.DecodeString(measuer_user)
if err != nil {
panic(err)
}
p.WriteCharacteristic(c, measure, false);
} else if (b[0] == 0xe7 && b[1] == 0xf0 && b[2] == 0x40) {
fmt.Fprintf(output, "ack measurement request | % X\n", b)
if (b[3] == 0x00) {
final_message = "measurement trigger succeeded"
} else {
final_message = "measurement trigger failed, unknown user id"
}
p.Device().CancelConnection(p)
} else {
fmt.Fprintf(output, "notified: % X | %q\n", b, b)
}
}
if err := p.SetNotifyValue(c, f); err != nil {
fmt.Fprintf(output, "failed to subscribe characteristic, err: %s\n", err)
continue
}
fmt.Fprintf(output, "sending init request E601\n")
init := []byte{0xe6, 0x01}
p.WriteCharacteristic(c, init, false);
}
}
}
}

func onPeriphDisconnected(p gatt.Peripheral, err error) {
fmt.Fprintf(output, "done, disconnecting...\n")
fmt.Fprintf(os.Stdout, "disconnected, %s\n", final_message)
close(done)
}

func onPeriphDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
fmt.Fprintf(output, "found %s | %s\n", p.ID(), p.Name())
if (mac != "" && uid != "") {
if (p.ID() == mac) {
p.Device().StopScanning()
fmt.Fprintf(output, "connecting...\n")
p.Device().Connect(p)
}
}
}

func onStateChanged(d gatt.Device, s gatt.State) {
switch s {
case gatt.StatePoweredOn:
fmt.Fprintf(output, "scanning...\n")
d.Scan([]gatt.UUID{}, true)
if (mac != "" && uid != "") {
time.Sleep(time.Duration(program_abortion_timeout) * time.Second)
}
if (prevent_program_abortion == false) {
fmt.Fprintf(os.Stdout, "disconnected, program execution timeout\n")
close(done)
}
return
default:
d.StopScanning()
}
}

func main() {

final_message = "measurement trigger failed, unknown reason"

flag.StringVar(&mac, "mac", "", "mac address")
flag.StringVar(&uid, "uid", "", "user id")

flag.IntVar(&program_abortion_timeout, "timeout", program_abortion_timeout, "program timeout in seconds")

var output_to_stderr bool
flag.BoolVar(&output_to_stderr, "stderr", false, "print everything but the result to stderr instead of stdout?")

flag.Parse()

if (output_to_stderr) {
output = os.Stderr
}

log.SetFlags(0)
log.SetOutput(ioutil.Discard)

if (mac == "" || uid == "") {
fmt.Fprintf(output, "incomplete parameters, will only scan and print out found mac adresses and their respective names\n")
}

DefaultClientOptions := []gatt.Option{
gatt.LnxMaxConnections(1),
gatt.LnxDeviceID(-1, false),
}

d, err := gatt.NewDevice(DefaultClientOptions...)
if err != nil {
fmt.Fprintf(output, "no permissions to access device? failed to open device, err: %s\n", err)
return
}
d.Handle(
gatt.PeripheralDiscovered(onPeriphDiscovered),
gatt.PeripheralConnected(onPeriphConnected),
gatt.PeripheralDisconnected(onPeriphDisconnected),
)
d.Init(onStateChanged)
<-done
}
83 changes: 83 additions & 0 deletions cronjob.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-

# 好

'''
* This file is part of BT-Scale (https://github.com/danielfaust/bt-scale).
* Copyright (c) 2019 Daniel Faust.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
'''

import os
import sys
import json
import time
import datetime
import subprocess

#===============================================
sys.dont_write_bytecode = True
#===============================================

print ''
print '----------------------------------------------'
print 'cronjob.py started running at', datetime.datetime.now().isoformat()
sys.stdout.flush()

# ensure that the current working directory is the one of this script.
# this is probably bad practice but it safeguards a lot of things.
os.chdir(os.path.dirname(os.path.abspath(__file__)))

import users
mac = users.SCALE
uid = None
if len(users.USERS) > 0:
uid = users.USERS[0]['uid'] # any user can be used in order to wake up the scale

if mac == 'XX:XX:XX:XX:XX:XX':
print 'there is no scale configured in users.py, exiting.'
sys.exit(0)

if uid is None:
print 'there is no user configured in users.py, exiting.'
sys.exit(0)

failure_counter = 0

while True:
try:
print '----------------------------------------------'
print ''
sys.stdout.flush()
result = subprocess.check_output(["sudo", "./bt-scale", "-mac=" + mac, "-uid=" + uid, "-stderr=true"], stderr=sys.stdout, universal_newlines=True).strip()
print result
if result == 'disconnected, measurement trigger succeeded':
try:
print ''
print '----------------------------------------------'
print 'will start running scale.py in 5 seconds'
time.sleep(5)
import scale
response = scale.start_measurement_script()
json.dumps(response, indent=2)
break
except:
failure_counter += 1
print 'connection or readout failed, will retry in 5 seconds.', failure_counter, 'attempts were made.'
if failure_counter == 5: # this is probably a real problem, just terminate the script.
sys.exit(0)
time.sleep(5)
except:
print 'this failure is too complex to handle, this needs manual intervention.'
sys.exit(0)
Loading

0 comments on commit a7e7e71

Please sign in to comment.