bluetooth scale by nbit informatik
This commit is contained in:
parent
053bbacb01
commit
f1c6115c03
|
|
@ -20,8 +20,6 @@ mailfrom: info@nbit.ch
|
|||
mailto: joerg.lehmann@nbit.ch
|
||||
mailuser: nbitinf@nbit.ch
|
||||
mailpwd: ukihefak27
|
||||
balance_number: "444"
|
||||
balance_command: "STATUS"
|
||||
forward_sms_from_this_number: "444"
|
||||
balance_ussd: "*121#"
|
||||
master_sms_number: "+41765006123"
|
||||
manipulation_duration_minutes: 60
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import random
|
|||
import string
|
||||
import glob
|
||||
import re
|
||||
import json
|
||||
import datetime
|
||||
|
||||
# Root Path
|
||||
APP_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
|
@ -58,6 +60,9 @@ class Scale(object):
|
|||
def __init__(self, scale_config):
|
||||
self.last_values = []
|
||||
self.scale_config = scale_config
|
||||
self.accu = INVALID_VALUE
|
||||
self.hum = INVALID_VALUE
|
||||
self.temp = INVALID_VALUE
|
||||
|
||||
def __del__(self):
|
||||
pass
|
||||
|
|
@ -76,6 +81,26 @@ class Scale(object):
|
|||
with open(datafilename, 'a') as file:
|
||||
file.write('%s,%d\n' % (timestamp,weigh_in_gram))
|
||||
|
||||
if self.accu != INVALID_VALUE and not(swarm_alarm):
|
||||
prefix = 'accu'
|
||||
datafilename = "%s/data/%s-%s-%s%s%s.log" % (APP_ROOT,prefix,self.scale_config['scale_uuid'],year,month,day)
|
||||
with open(datafilename, 'a') as file:
|
||||
file.write('%s,%.1f\n' % (timestamp,self.accu / 100.0))
|
||||
|
||||
if self.hum != INVALID_VALUE and not(swarm_alarm):
|
||||
prefix = 'humidity'
|
||||
datafilename = "%s/data/%s-%s-%s%s%s.log" % (APP_ROOT,prefix,self.scale_config['scale_uuid'],year,month,day)
|
||||
with open(datafilename, 'a') as file:
|
||||
file.write('%s,%d\n' % (timestamp,self.hum / 10))
|
||||
|
||||
if self.temp != INVALID_VALUE and not(swarm_alarm):
|
||||
prefix = 'temp'
|
||||
datafilename = "%s/data/%s-%s-%s%s%s.log" % (APP_ROOT,prefix,self.scale_config['scale_uuid'],year,month,day)
|
||||
with open(datafilename, 'a') as file:
|
||||
file.write('%s,%d\n' % (timestamp,self.temp / 10))
|
||||
|
||||
|
||||
|
||||
def AppendReading(self,weigh_in_gram):
|
||||
self.last_values.append(weigh_in_gram)
|
||||
if len(self.last_values) > config_data['number_of_samples']:
|
||||
|
|
@ -129,6 +154,41 @@ class ScaleUSB_PCE(Scale):
|
|||
if res != INVALID_VALUE:
|
||||
self.AppendReading(res)
|
||||
|
||||
class ScaleBT(Scale):
|
||||
def __init__(self,scale_config):
|
||||
Scale.__init__(self, scale_config)
|
||||
|
||||
def Read(self):
|
||||
res = INVALID_VALUE
|
||||
|
||||
filename = "/home/beieli/bt-readings/%s.json" % (self.scale_config['mac'])
|
||||
try:
|
||||
data = json.load(open(filename))
|
||||
except:
|
||||
return
|
||||
|
||||
# Wenn die Daten aelter als 15 Minuten sind loeschen wir das File
|
||||
delta_in_seconds = (datetime.datetime.now() - datetime.datetime.strptime(data['datetime'],'%d.%m.%Y %H:%M')).total_seconds()
|
||||
print(delta_in_seconds)
|
||||
if ( delta_in_seconds > 15*60):
|
||||
print('DEBUG [%s,%s]' % (datetime.datetime.now(),data['datetime']))
|
||||
print('Readings are older than 15 minutes, removing them [%s]' % (self.scale_config['mac']))
|
||||
os.remove(filename)
|
||||
else:
|
||||
res = ((data['w1'] - self.scale_config['offset1']) / self.scale_config['ratio1']) + ((data['w2'] - self.scale_config['offset2']) / self.scale_config['ratio2'])
|
||||
if res < 0:
|
||||
print('DEBUG: Read Bluetooth Scale; Value less than 0, set it to 0: %d' % (res))
|
||||
res = 0
|
||||
|
||||
self.accu = data['accu']
|
||||
self.hum = data['hum']
|
||||
self.temp = data['temp']
|
||||
|
||||
print('DEBUG: Read Bluetooth Scale: %d' % (res))
|
||||
|
||||
if res != INVALID_VALUE:
|
||||
self.AppendReading(res)
|
||||
|
||||
|
||||
class ScaleDummy(Scale):
|
||||
def __init__(self,scale_config):
|
||||
|
|
@ -153,6 +213,8 @@ class ScaleDummy(Scale):
|
|||
if (last_value + delta < 0):
|
||||
delta = random.randint(0,4)
|
||||
|
||||
|
||||
print('DEBUG: Read Dummy Scale: %d' % (last_value + delta))
|
||||
self.AppendReading(last_value + delta)
|
||||
|
||||
|
||||
|
|
@ -177,6 +239,9 @@ def main():
|
|||
timeout=20)
|
||||
scale=ScaleUSB_PCE(ser, scale_config)
|
||||
scales.append(scale)
|
||||
elif scale_config['interface_type'] == 'btscale':
|
||||
scale=ScaleBT(scale_config)
|
||||
scales.append(scale)
|
||||
elif scale_config['interface_type'] == 'dummy':
|
||||
scale=ScaleDummy(scale_config)
|
||||
scales.append(scale)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# vim: expandtab sw=4 ts=4 sts=4:
|
||||
#
|
||||
# Bluetooth Scale Monitor
|
||||
#
|
||||
# Author: Joerg Lehmann, nbit Informatik GmbH
|
||||
#
|
||||
"""Bluetooth Scale Monitor"""
|
||||
|
||||
import json
|
||||
import datetime
|
||||
from bluepy.btle import Scanner, DefaultDelegate
|
||||
|
||||
LONG = 2147483648L
|
||||
INT = 32768
|
||||
|
||||
class ScanDelegate(DefaultDelegate):
|
||||
def __init__(self):
|
||||
DefaultDelegate.__init__(self)
|
||||
|
||||
def ProcessReading(self, mac, value):
|
||||
data = {}
|
||||
data['datetime'] = datetime.datetime.now().strftime("%d.%m.%Y %H:%M")
|
||||
data['w1'] = long(value[0:8],16) - LONG
|
||||
data['w2'] = long(value[8:16],16) - LONG
|
||||
data['temp'] = int(value[16:20],16) - INT
|
||||
data['hum'] = int(value[20:24],16)
|
||||
data['accu'] = int(value[24:28],16)
|
||||
|
||||
filename = "/home/beieli/bt-readings/%s.json" % (mac)
|
||||
with open(filename,'w') as outfile:
|
||||
json.dump(data, outfile, indent=4, sort_keys=True)
|
||||
|
||||
def handleDiscovery(self, dev, isNewDev, isNewData):
|
||||
if isNewData:
|
||||
for (adtype, desc, value) in dev.getScanData():
|
||||
if desc == 'Manufacturer' and value.startswith('1234' ):
|
||||
self.ProcessReading(dev.addr, value[4:])
|
||||
|
||||
scanner = Scanner().withDelegate(ScanDelegate())
|
||||
while True:
|
||||
scanner.scan(5)
|
||||
|
|
@ -20,6 +20,7 @@ import glob
|
|||
import shutil
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
from os.path import basename
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
|
@ -273,8 +274,11 @@ def is_muted():
|
|||
|
||||
return res
|
||||
|
||||
def balance():
|
||||
send_sms([config_data['balance_number']],config_data['balance_command'])
|
||||
def balance(phonenumber):
|
||||
sm.Terminate()
|
||||
sms_message = subprocess.check_output(["/usr/bin/sudo","/home/beieli/root-bin/get_balance.sh", config_data['balance_ussd']])
|
||||
sm.Init()
|
||||
send_sms([phonenumber],sms_message)
|
||||
|
||||
def reboot():
|
||||
os.system('/usr/bin/sudo /sbin/init 6')
|
||||
|
|
@ -293,57 +297,51 @@ def command_not_understood(phonenumber, message):
|
|||
|
||||
|
||||
def process_received_sms(phonenumber,message):
|
||||
# Falls es von forward_sms_from_this_number kommt, machen wir ein Foward an
|
||||
# die Master Nummer
|
||||
if phonenumber == config_data['forward_sms_from_this_number']:
|
||||
send_sms([config_data['master_sms_number']],message)
|
||||
# message in Grossbuchstaben (damit Gross-/Kleinschreibung keine Rolle spielt)
|
||||
message_uc = message.upper()
|
||||
|
||||
else:
|
||||
# message in Grossbuchstaben (damit Gross-/Kleinschreibung keine Rolle spielt)
|
||||
message_uc = message.upper()
|
||||
|
||||
# Bestimmung, ob es eine gueltige Telefonnummer ist
|
||||
valid_number = False
|
||||
for s in config_data['scales']:
|
||||
if phonenumber in s['sms_alert_phonenumbers']:
|
||||
valid_number = True
|
||||
# Bestimmung, ob es eine gueltige Telefonnummer ist
|
||||
valid_number = False
|
||||
for s in config_data['scales']:
|
||||
if phonenumber in s['sms_alert_phonenumbers']:
|
||||
valid_number = True
|
||||
|
||||
if not valid_number:
|
||||
print("Da versucht ein unberechtigter, etwas abzufragen... (Nummer: %s)" % (phonenumber))
|
||||
return
|
||||
if not valid_number:
|
||||
print("Da versucht ein unberechtigter, etwas abzufragen... (Nummer: %s)" % (phonenumber))
|
||||
return
|
||||
|
||||
if 'HELP INFO' in message_uc:
|
||||
send_help(phonenumber,'info')
|
||||
elif 'HELP MANIPULATION' in message_uc:
|
||||
send_help(phonenumber,'manipulation')
|
||||
elif 'HELP HELP' in message_uc:
|
||||
send_help(phonenumber,'help')
|
||||
elif 'HELP BALANCE' in message_uc:
|
||||
send_help(phonenumber,'balance')
|
||||
elif 'HELP REBOOT' in message_uc:
|
||||
send_help(phonenumber,'reboot')
|
||||
elif 'HELP SHUTDOWN' in message_uc:
|
||||
send_help(phonenumber,'shutdown')
|
||||
elif 'HELP HOTSPOT' in message_uc:
|
||||
send_help(phonenumber,'hotspot')
|
||||
elif 'HELP' in message_uc:
|
||||
send_help(phonenumber,'')
|
||||
elif 'INFO' in message_uc:
|
||||
send_info(phonenumber, message_uc)
|
||||
elif 'MANIPULATION' in message_uc:
|
||||
start_manipulation()
|
||||
elif 'BALANCE' in message_uc:
|
||||
balance()
|
||||
elif 'REBOOT' in message_uc:
|
||||
reboot()
|
||||
elif 'SHUTDOWN' in message_uc:
|
||||
shutdown()
|
||||
elif 'HOTSPOT OFF' in message_uc:
|
||||
hotspot_off()
|
||||
elif 'HOTSPOT ON' in message_uc:
|
||||
hotspot_on()
|
||||
else:
|
||||
command_not_understood(phonenumber, message)
|
||||
if 'HELP INFO' in message_uc:
|
||||
send_help(phonenumber,'info')
|
||||
elif 'HELP MANIPULATION' in message_uc:
|
||||
send_help(phonenumber,'manipulation')
|
||||
elif 'HELP HELP' in message_uc:
|
||||
send_help(phonenumber,'help')
|
||||
elif 'HELP BALANCE' in message_uc:
|
||||
send_help(phonenumber,'balance')
|
||||
elif 'HELP REBOOT' in message_uc:
|
||||
send_help(phonenumber,'reboot')
|
||||
elif 'HELP SHUTDOWN' in message_uc:
|
||||
send_help(phonenumber,'shutdown')
|
||||
elif 'HELP HOTSPOT' in message_uc:
|
||||
send_help(phonenumber,'hotspot')
|
||||
elif 'HELP' in message_uc:
|
||||
send_help(phonenumber,'')
|
||||
elif 'INFO' in message_uc:
|
||||
send_info(phonenumber, message_uc)
|
||||
elif 'MANIPULATION' in message_uc:
|
||||
start_manipulation()
|
||||
elif 'BALANCE' in message_uc:
|
||||
balance(phonenumber)
|
||||
elif 'REBOOT' in message_uc:
|
||||
reboot()
|
||||
elif 'SHUTDOWN' in message_uc:
|
||||
shutdown()
|
||||
elif 'HOTSPOT OFF' in message_uc:
|
||||
hotspot_off()
|
||||
elif 'HOTSPOT ON' in message_uc:
|
||||
hotspot_on()
|
||||
else:
|
||||
command_not_understood(phonenumber, message)
|
||||
|
||||
|
||||
def main():
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
@reboot root /home/beieli/root-bin/gsm_poweron.py
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
[gammu]
|
||||
# Please configure this!
|
||||
port = /dev/ttyAMA0
|
||||
device = /dev/ttyUSB0
|
||||
name = Phone on USB serial port HUAWEI HUAWEI_Mobile
|
||||
connection = at
|
||||
|
||||
[gammu1]
|
||||
device = /dev/ttyUSB1
|
||||
name = Phone on USB serial port HUAWEI HUAWEI_Mobile
|
||||
connection = at
|
||||
|
||||
[gammu2]
|
||||
device = /dev/ttyUSB2
|
||||
name = Phone on USB serial port HUAWEI HUAWEI_Mobile
|
||||
connection = at
|
||||
# Debugging
|
||||
#logformat = textall
|
||||
logformat = errorsdate
|
||||
#pin = 8296
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
#imis/internet is the apn for idea connection
|
||||
connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T gprs.swisscom.ch"
|
||||
#connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T gprs.swisscom.ch"
|
||||
connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T internet"
|
||||
|
||||
# For Raspberry Pi3 use /dev/ttyS0 as the communication port:
|
||||
/dev/ttyAMA0
|
||||
/dev/ttyUSB0
|
||||
|
||||
# Baudrate
|
||||
115200
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
beieli ALL=(ALL) NOPASSWD: /sbin/init,/home/beieli/root-bin/connect_to_internet,/home/beieli/root-bin/disconnect_from_internet,/home/beieli/root-bin/hotspot_on,/home/beieli/root-bin/hotspot_off,/home/beieli/root-bin/sync_time_with_internet
|
||||
beieli ALL=(ALL) NOPASSWD: /sbin/init,/home/beieli/root-bin/connect_to_internet,/home/beieli/root-bin/disconnect_from_internet,/home/beieli/root-bin/hotspot_on,/home/beieli/root-bin/hotspot_off,/home/beieli/root-bin/sync_time_with_internet,/usr/bin/hcitool,/home/beieli/root-bin/get_balance.sh
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
[Unit]
|
||||
Description=btmon service
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
Group=root
|
||||
WorkingDirectory=/home/beieli/bin
|
||||
ExecStart=/home/beieli/bin/btmon.py
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
# Erster Parameter ist der USSD Code, also z.B. *121#
|
||||
/home/beieli/root-bin/ussd.py $1 | grep -v '^python-gammu' |sed 's/@//'
|
||||
|
|
@ -87,6 +87,33 @@ def CreateDatafile(scale_uuid,infotime):
|
|||
with open(ifile, 'r') as ifile:
|
||||
for line in ifile:
|
||||
file.write(line)
|
||||
files = glob.glob("%s/data/temp-%s-%s.log" % (APP_ROOT,scale_uuid,my_pattern))
|
||||
filename = "%s/temp-%s-%s.csv" % (mycsvdir,scale_uuid,infotime)
|
||||
with open(filename, 'w') as file:
|
||||
for ifile in sorted(files):
|
||||
if infotime in ifile:
|
||||
with_data = True
|
||||
with open(ifile, 'r') as ifile:
|
||||
for line in ifile:
|
||||
file.write(line)
|
||||
files = glob.glob("%s/data/humidity-%s-%s.log" % (APP_ROOT,scale_uuid,my_pattern))
|
||||
filename = "%s/humidity-%s-%s.csv" % (mycsvdir,scale_uuid,infotime)
|
||||
with open(filename, 'w') as file:
|
||||
for ifile in sorted(files):
|
||||
if infotime in ifile:
|
||||
with_data = True
|
||||
with open(ifile, 'r') as ifile:
|
||||
for line in ifile:
|
||||
file.write(line)
|
||||
files = glob.glob("%s/data/accu-%s-%s.log" % (APP_ROOT,scale_uuid,my_pattern))
|
||||
filename = "%s/accu-%s-%s.csv" % (mycsvdir,scale_uuid,infotime)
|
||||
with open(filename, 'w') as file:
|
||||
for ifile in sorted(files):
|
||||
if infotime in ifile:
|
||||
with_data = True
|
||||
with open(ifile, 'r') as ifile:
|
||||
for line in ifile:
|
||||
file.write(line)
|
||||
|
||||
def GetInfoText():
|
||||
with open('%s/bin/beielimon-config.yaml' % (APP_ROOT), 'r') as f:
|
||||
|
|
@ -176,15 +203,18 @@ def server_static(filepath):
|
|||
def scale_data(scale,infotime):
|
||||
scale_uuid = ''
|
||||
scale_alias = ''
|
||||
interface_type = ''
|
||||
for s in config_data['scales']:
|
||||
if s['alias'].replace(' ','_') == scale:
|
||||
scale_uuid = s['scale_uuid']
|
||||
scale_alias = s['alias']
|
||||
interface_type = s['interface_type']
|
||||
|
||||
CreateDatafile(scale_uuid, infotime)
|
||||
data = {
|
||||
'scale_uuid': scale_uuid,
|
||||
'scale_alias': scale_alias,
|
||||
'interface_type': interface_type,
|
||||
'infotime': infotime
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
# vim: expandtab sw=4 ts=4 sts=4:
|
||||
#
|
||||
# Copyright © 2003 - 2017 Michal Čihař <michal@cihar.com>
|
||||
#
|
||||
# This file is part of python-gammu <https://wammu.eu/python-gammu/>
|
||||
#
|
||||
# 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; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# 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, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
#
|
||||
'''
|
||||
Service numbers dialogue example.
|
||||
'''
|
||||
|
||||
from __future__ import print_function
|
||||
import gammu
|
||||
import sys
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf-8')
|
||||
import time
|
||||
|
||||
REPLY = False
|
||||
|
||||
gsm = (u"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>"
|
||||
u"?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà")
|
||||
|
||||
def gsm7bitdecode(f):
|
||||
f = ''.join(["{0:08b}".format(int(f[i:i+2], 16)) for i in range(0, len(f), 2)][::-1])
|
||||
return ''.join([gsm[int(f[::-1][i:i+7][::-1], 2)] for i in range(0, len(f), 7)])
|
||||
|
||||
def gsm7bitencode(src):
|
||||
result, count, last = [], 0, 0
|
||||
for c in src:
|
||||
this = ord(c) << (8 - count)
|
||||
if count:
|
||||
result.append('%02X' % ((last >> 8) | (this & 0xFF)))
|
||||
count = (count + 1) % 8
|
||||
last = this
|
||||
result.append('%02x' % (last >> 8))
|
||||
return ''.join(result)
|
||||
|
||||
def callback(state_machine, callback_type, data):
|
||||
'''
|
||||
Callback on USSD data.
|
||||
'''
|
||||
global REPLY
|
||||
if callback_type != 'USSD':
|
||||
print('Unexpected event type: {0}'.format(callback_type))
|
||||
sys.exit(1)
|
||||
|
||||
REPLY = True
|
||||
|
||||
#print('Network reply:')
|
||||
#print('Status: {0}'.format(data['Status']))
|
||||
#print(data['Text'])
|
||||
print(gsm7bitdecode(data['Text']))
|
||||
|
||||
if data['Status'] == 'ActionNeeded':
|
||||
do_service(state_machine)
|
||||
|
||||
|
||||
def init():
|
||||
'''
|
||||
Intializes gammu and callbacks.
|
||||
'''
|
||||
state_machine = gammu.StateMachine()
|
||||
state_machine.ReadConfig()
|
||||
state_machine.Init()
|
||||
state_machine.SetIncomingCallback(callback)
|
||||
try:
|
||||
state_machine.SetIncomingUSSD()
|
||||
except gammu.ERR_NOTSUPPORTED:
|
||||
print('Incoming USSD notification is not supported.')
|
||||
sys.exit(1)
|
||||
return state_machine
|
||||
|
||||
|
||||
def do_service(state_machine):
|
||||
'''
|
||||
Main code to talk with worker.
|
||||
'''
|
||||
global REPLY
|
||||
|
||||
if len(sys.argv) >= 2:
|
||||
code = sys.argv[1]
|
||||
del sys.argv[1]
|
||||
else:
|
||||
prompt = 'Enter code (empty string to end): '
|
||||
try:
|
||||
code = raw_input(prompt)
|
||||
except NameError:
|
||||
code = input(prompt)
|
||||
code=gsm7bitencode(code)
|
||||
#print(code)
|
||||
if code != '':
|
||||
#print('Talking to network...')
|
||||
REPLY = False
|
||||
state_machine.DialService(code)
|
||||
loops = 0
|
||||
while not REPLY and loops < 10:
|
||||
#print("Loop")
|
||||
state_machine.ReadDevice()
|
||||
time.sleep(1)
|
||||
loops += 1
|
||||
|
||||
|
||||
def main():
|
||||
state_machine = init()
|
||||
do_service(state_machine)
|
||||
state_machine.Terminate()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
@ -9,44 +9,46 @@
|
|||
<link rel="icon" href="/static/images/favicon.ico">
|
||||
<title>BeieliPi - Messkurve für {{ scale_alias }}</title>
|
||||
|
||||
<link href="/static/dist/toolkit.min.css" rel="stylesheet">
|
||||
<link href="/static/css/customize.css" rel="stylesheet">
|
||||
|
||||
<script type="text/javascript" src="/static/js/dygraph.js"></script>
|
||||
<script type="text/javascript" src="/static/js/smooth-plotter.js"></script>
|
||||
<link rel="stylesheet" src="/static/css/dygraph.css" />
|
||||
<style type="text/css">
|
||||
.dygraph-axis-label-x {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
#mylabel {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
#mylabel {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#graphdiv {
|
||||
position: absolute;
|
||||
left: 1%;
|
||||
right: 1%;
|
||||
top: 1%;
|
||||
bottom: 1%;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mylabel"></div>
|
||||
<div id="graphdiv"></div>
|
||||
<div class="container">
|
||||
<div class="text-center"><h1>{{ scale_alias }}</h1></div>
|
||||
<div id="graphdiv"></div>
|
||||
% if interface_type == 'btscale':
|
||||
<div id="graphdivtemp"></div>
|
||||
<div id="graphdivhum"></div>
|
||||
<div id="graphdivaccu"></div>
|
||||
% end
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
g = new Dygraph(
|
||||
document.getElementById("graphdiv"),
|
||||
"/data/{{ scale_uuid }}-{{ infotime }}.csv",
|
||||
{
|
||||
title: '<b>{{ scale_alias }}</b>',
|
||||
title: '<b>Gewicht</b><div id="mylabel"></div>',
|
||||
titleHeight: 36,
|
||||
showRangeSelector: true,
|
||||
rangeSelectorHeight: 40,
|
||||
|
|
@ -54,6 +56,7 @@
|
|||
pointSize: 2,
|
||||
labels: [ 'Timestamp', 'Gewicht in Gramm' ],
|
||||
labelsDiv: document.getElementById("mylabel"),
|
||||
plotter: smoothPlotter,
|
||||
axes: {
|
||||
x: {
|
||||
pixelsPerLabel: 90,
|
||||
|
|
@ -68,6 +71,91 @@
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
gt = new Dygraph(
|
||||
document.getElementById("graphdivtemp"),
|
||||
"/data/temp-{{ scale_uuid }}-{{ infotime }}.csv",
|
||||
{
|
||||
title: '<b>Temparatur</b><div id="mylabel"></div>',
|
||||
titleHeight: 36,
|
||||
showRangeSelector: true,
|
||||
rangeSelectorHeight: 40,
|
||||
drawPoints: true,
|
||||
pointSize: 2,
|
||||
labels: [ 'Timestamp', 'Temparatur in Grad Celsius' ],
|
||||
labelsDiv: document.getElementById("mylabel"),
|
||||
plotter: smoothPlotter,
|
||||
axes: {
|
||||
x: {
|
||||
pixelsPerLabel: 90,
|
||||
axisLabelWidth: 90,
|
||||
axisLabelFormatter: function(d, gran) {
|
||||
return new Date(d).toLocaleString('de-CH').slice(0, -3);
|
||||
},
|
||||
valueFormatter: function(ms) {
|
||||
return new Date(ms).toLocaleString('de-CH').slice(0, -3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
gh = new Dygraph(
|
||||
document.getElementById("graphdivhum"),
|
||||
"/data/humidity-{{ scale_uuid }}-{{ infotime }}.csv",
|
||||
{
|
||||
title: '<b>Luftfeuchtigkeit</b><div id="mylabel"></div>',
|
||||
titleHeight: 36,
|
||||
showRangeSelector: true,
|
||||
rangeSelectorHeight: 40,
|
||||
drawPoints: true,
|
||||
pointSize: 2,
|
||||
labels: [ 'Timestamp', 'Luftfeuchtigkeit in Prozent' ],
|
||||
labelsDiv: document.getElementById("mylabel"),
|
||||
plotter: smoothPlotter,
|
||||
axes: {
|
||||
x: {
|
||||
pixelsPerLabel: 90,
|
||||
axisLabelWidth: 90,
|
||||
axisLabelFormatter: function(d, gran) {
|
||||
return new Date(d).toLocaleString('de-CH').slice(0, -3);
|
||||
},
|
||||
valueFormatter: function(ms) {
|
||||
return new Date(ms).toLocaleString('de-CH').slice(0, -3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ga = new Dygraph(
|
||||
document.getElementById("graphdivaccu"),
|
||||
"/data/accu-{{ scale_uuid }}-{{ infotime }}.csv",
|
||||
{
|
||||
title: '<b>Batteriespannung</b><div id="mylabel"></div>',
|
||||
titleHeight: 36,
|
||||
showRangeSelector: true,
|
||||
rangeSelectorHeight: 40,
|
||||
drawPoints: true,
|
||||
pointSize: 2,
|
||||
labels: [ 'Timestamp', 'Batteriespannung in Volt' ],
|
||||
labelsDiv: document.getElementById("mylabel"),
|
||||
plotter: smoothPlotter,
|
||||
axes: {
|
||||
x: {
|
||||
pixelsPerLabel: 90,
|
||||
axisLabelWidth: 90,
|
||||
axisLabelFormatter: function(d, gran) {
|
||||
return new Date(d).toLocaleString('de-CH').slice(0, -3);
|
||||
},
|
||||
valueFormatter: function(ms) {
|
||||
return new Date(ms).toLocaleString('de-CH').slice(0, -3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ hr {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
#graphdiv {
|
||||
#graphdiv, #graphdivtemp, #graphdivhum, #graphdivaccu {
|
||||
width: 100% !important;
|
||||
min-height: 400px;
|
||||
margin: 20px 20px 20px 20px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -169,6 +169,10 @@ process.off = noop;
|
|||
process.removeListener = noop;
|
||||
process.removeAllListeners = noop;
|
||||
process.emit = noop;
|
||||
process.prependListener = noop;
|
||||
process.prependOnceListener = noop;
|
||||
|
||||
process.listeners = function (name) { return [] }
|
||||
|
||||
process.binding = function (name) {
|
||||
throw new Error('process.binding is not supported');
|
||||
|
|
@ -2837,7 +2841,8 @@ DygraphInteraction.defaultModel = {
|
|||
// Give plugins a chance to grab this event.
|
||||
var e = {
|
||||
canvasx: context.dragEndX,
|
||||
canvasy: context.dragEndY
|
||||
canvasy: context.dragEndY,
|
||||
cancelable: true
|
||||
};
|
||||
if (g.cascadeEvents_('dblclick', e)) {
|
||||
return;
|
||||
|
|
@ -3495,6 +3500,12 @@ if (typeof process !== 'undefined') {
|
|||
"type": "integer",
|
||||
"description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
|
||||
},
|
||||
"pixelRatio": {
|
||||
"default": "(devicePixelRatio / context.backingStoreRatio)",
|
||||
"labels": ["Overall display"],
|
||||
"type": "float",
|
||||
"description": "Overrides the pixel ratio scaling factor for the canvas's 2d context. Ordinarily, this is set to the devicePixelRatio / (context.backingStoreRatio || 1), so on mobile devices, where the devicePixelRatio can be somewhere around 3, performance can be improved by overriding this value to something less precise, like 1, at the expense of resolution."
|
||||
},
|
||||
"interactionModel": {
|
||||
"default": "...",
|
||||
"labels": ["Interactive Elements"],
|
||||
|
|
@ -3764,7 +3775,7 @@ if (typeof process !== 'undefined') {
|
|||
"default": "null",
|
||||
"labels": ["Axis display", "Interactive Elements"],
|
||||
"type": "float",
|
||||
"description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
|
||||
"description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% passed the edges of the displayed values. null means no bounds."
|
||||
},
|
||||
"title": {
|
||||
"labels": ["Chart labels"],
|
||||
|
|
@ -4694,29 +4705,36 @@ var dateTicker = function dateTicker(a, b, pixels, opts, dygraph, vals) {
|
|||
exports.dateTicker = dateTicker;
|
||||
// Time granularity enumeration
|
||||
var Granularity = {
|
||||
SECONDLY: 0,
|
||||
TWO_SECONDLY: 1,
|
||||
FIVE_SECONDLY: 2,
|
||||
TEN_SECONDLY: 3,
|
||||
THIRTY_SECONDLY: 4,
|
||||
MINUTELY: 5,
|
||||
TWO_MINUTELY: 6,
|
||||
FIVE_MINUTELY: 7,
|
||||
TEN_MINUTELY: 8,
|
||||
THIRTY_MINUTELY: 9,
|
||||
HOURLY: 10,
|
||||
TWO_HOURLY: 11,
|
||||
SIX_HOURLY: 12,
|
||||
DAILY: 13,
|
||||
TWO_DAILY: 14,
|
||||
WEEKLY: 15,
|
||||
MONTHLY: 16,
|
||||
QUARTERLY: 17,
|
||||
BIANNUAL: 18,
|
||||
ANNUAL: 19,
|
||||
DECADAL: 20,
|
||||
CENTENNIAL: 21,
|
||||
NUM_GRANULARITIES: 22
|
||||
MILLISECONDLY: 0,
|
||||
TWO_MILLISECONDLY: 1,
|
||||
FIVE_MILLISECONDLY: 2,
|
||||
TEN_MILLISECONDLY: 3,
|
||||
FIFTY_MILLISECONDLY: 4,
|
||||
HUNDRED_MILLISECONDLY: 5,
|
||||
FIVE_HUNDRED_MILLISECONDLY: 6,
|
||||
SECONDLY: 7,
|
||||
TWO_SECONDLY: 8,
|
||||
FIVE_SECONDLY: 9,
|
||||
TEN_SECONDLY: 10,
|
||||
THIRTY_SECONDLY: 11,
|
||||
MINUTELY: 12,
|
||||
TWO_MINUTELY: 13,
|
||||
FIVE_MINUTELY: 14,
|
||||
TEN_MINUTELY: 15,
|
||||
THIRTY_MINUTELY: 16,
|
||||
HOURLY: 17,
|
||||
TWO_HOURLY: 18,
|
||||
SIX_HOURLY: 19,
|
||||
DAILY: 20,
|
||||
TWO_DAILY: 21,
|
||||
WEEKLY: 22,
|
||||
MONTHLY: 23,
|
||||
QUARTERLY: 24,
|
||||
BIANNUAL: 25,
|
||||
ANNUAL: 26,
|
||||
DECADAL: 27,
|
||||
CENTENNIAL: 28,
|
||||
NUM_GRANULARITIES: 29
|
||||
};
|
||||
|
||||
exports.Granularity = Granularity;
|
||||
|
|
@ -4747,6 +4765,13 @@ var DateField = {
|
|||
* @type {Array.<{datefield:number, step:number, spacing:number}>}
|
||||
*/
|
||||
var TICK_PLACEMENT = [];
|
||||
TICK_PLACEMENT[Granularity.MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 1, spacing: 1 };
|
||||
TICK_PLACEMENT[Granularity.TWO_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 2, spacing: 2 };
|
||||
TICK_PLACEMENT[Granularity.FIVE_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 5, spacing: 5 };
|
||||
TICK_PLACEMENT[Granularity.TEN_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 10, spacing: 10 };
|
||||
TICK_PLACEMENT[Granularity.FIFTY_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 50, spacing: 50 };
|
||||
TICK_PLACEMENT[Granularity.HUNDRED_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 100, spacing: 100 };
|
||||
TICK_PLACEMENT[Granularity.FIVE_HUNDRED_MILLISECONDLY] = { datefield: DateField.DATEFIELD_MS, step: 500, spacing: 500 };
|
||||
TICK_PLACEMENT[Granularity.SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 1, spacing: 1000 * 1 };
|
||||
TICK_PLACEMENT[Granularity.TWO_SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 2, spacing: 1000 * 2 };
|
||||
TICK_PLACEMENT[Granularity.FIVE_SECONDLY] = { datefield: DateField.DATEFIELD_SS, step: 5, spacing: 1000 * 5 };
|
||||
|
|
@ -5011,7 +5036,7 @@ var logRangeFraction = function logRangeFraction(r0, r1, pct) {
|
|||
// Original calcuation:
|
||||
// pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0])));
|
||||
//
|
||||
// Multiply both sides by the right-side demoninator.
|
||||
// Multiply both sides by the right-side denominator.
|
||||
// pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0])
|
||||
//
|
||||
// add log(xRange[0]) to both sides
|
||||
|
|
@ -5279,7 +5304,7 @@ function isValidPoint(p, opt_allowNaNY) {
|
|||
;
|
||||
|
||||
/**
|
||||
* Number formatting function which mimicks the behavior of %g in printf, i.e.
|
||||
* Number formatting function which mimics the behavior of %g in printf, i.e.
|
||||
* either exponential or fixed format (without trailing 0s) is used depending on
|
||||
* the length of the generated string. The advantage of this format is that
|
||||
* there is a predictable upper bound on the resulting string length,
|
||||
|
|
@ -5433,7 +5458,7 @@ function hmsString_(hh, mm, ss, ms) {
|
|||
/**
|
||||
* Convert a JS date (millis since epoch) to a formatted string.
|
||||
* @param {number} time The JavaScript time value (ms since epoch)
|
||||
* @param {boolean} utc Wether output UTC or local time
|
||||
* @param {boolean} utc Whether output UTC or local time
|
||||
* @return {string} A date of one of these forms:
|
||||
* "YYYY/MM/DD", "YYYY/MM/DD HH:MM" or "YYYY/MM/DD HH:MM:SS"
|
||||
* @private
|
||||
|
|
@ -6341,6 +6366,12 @@ function dateAxisLabelFormatter(date, granularity, opts) {
|
|||
if (frac === 0 || granularity >= DygraphTickers.Granularity.DAILY) {
|
||||
// e.g. '21 Jan' (%d%b)
|
||||
return zeropad(day) + ' ' + SHORT_MONTH_NAMES_[month];
|
||||
} else if (granularity < DygraphTickers.Granularity.SECONDLY) {
|
||||
// e.g. 40.310 (meaning 40 seconds and 310 milliseconds)
|
||||
var str = "" + millis;
|
||||
return zeropad(secs) + "." + ('000' + str).substring(str.length);
|
||||
} else if (granularity > DygraphTickers.Granularity.MINUTELY) {
|
||||
return hmsString_(hours, mins, secs, 0);
|
||||
} else {
|
||||
return hmsString_(hours, mins, secs, millis);
|
||||
}
|
||||
|
|
@ -6420,7 +6451,7 @@ function dateValueFormatter(d, opts) {
|
|||
* @param {Object} attrs Various other attributes, e.g. errorBars determines
|
||||
* whether the input data contains error ranges. For a complete list of
|
||||
* options, see http://dygraphs.com/options.html.
|
||||
*/var Dygraph=function Dygraph(div,data,opts){this.__init__(div,data,opts);};Dygraph.NAME = "Dygraph";Dygraph.VERSION = "2.0.0"; // Various default values
|
||||
*/var Dygraph=function Dygraph(div,data,opts){this.__init__(div,data,opts);};Dygraph.NAME = "Dygraph";Dygraph.VERSION = "2.1.0"; // Various default values
|
||||
Dygraph.DEFAULT_ROLL_PERIOD = 1;Dygraph.DEFAULT_WIDTH = 480;Dygraph.DEFAULT_HEIGHT = 320; // For max 60 Hz. animation:
|
||||
Dygraph.ANIMATION_STEPS = 12;Dygraph.ANIMATION_DURATION = 200; /**
|
||||
* Standard plotters. These may be used by clients.
|
||||
|
|
@ -6691,9 +6722,9 @@ var target=e.target || e.fromElement;var relatedTarget=e.relatedTarget || e.toEl
|
|||
// This happens when the graph is resized.
|
||||
if(!this.resizeHandler_){this.resizeHandler_ = function(e){dygraph.resize();}; // Update when the window is resized.
|
||||
// TODO(danvk): drop frames depending on complexity of the chart.
|
||||
this.addAndTrackEvent(window,'resize',this.resizeHandler_);}};Dygraph.prototype.resizeElements_ = function(){this.graphDiv.style.width = this.width_ + "px";this.graphDiv.style.height = this.height_ + "px";var canvasScale=utils.getContextPixelRatio(this.canvas_ctx_);this.canvas_.width = this.width_ * canvasScale;this.canvas_.height = this.height_ * canvasScale;this.canvas_.style.width = this.width_ + "px"; // for IE
|
||||
this.addAndTrackEvent(window,'resize',this.resizeHandler_);}};Dygraph.prototype.resizeElements_ = function(){this.graphDiv.style.width = this.width_ + "px";this.graphDiv.style.height = this.height_ + "px";var pixelRatioOption=this.getNumericOption('pixelRatio');var canvasScale=pixelRatioOption || utils.getContextPixelRatio(this.canvas_ctx_);this.canvas_.width = this.width_ * canvasScale;this.canvas_.height = this.height_ * canvasScale;this.canvas_.style.width = this.width_ + "px"; // for IE
|
||||
this.canvas_.style.height = this.height_ + "px"; // for IE
|
||||
if(canvasScale !== 1){this.canvas_ctx_.scale(canvasScale,canvasScale);}var hiddenScale=utils.getContextPixelRatio(this.hidden_ctx_);this.hidden_.width = this.width_ * hiddenScale;this.hidden_.height = this.height_ * hiddenScale;this.hidden_.style.width = this.width_ + "px"; // for IE
|
||||
if(canvasScale !== 1){this.canvas_ctx_.scale(canvasScale,canvasScale);}var hiddenScale=pixelRatioOption || utils.getContextPixelRatio(this.hidden_ctx_);this.hidden_.width = this.width_ * hiddenScale;this.hidden_.height = this.height_ * hiddenScale;this.hidden_.style.width = this.width_ + "px"; // for IE
|
||||
this.hidden_.style.height = this.height_ + "px"; // for IE
|
||||
if(hiddenScale !== 1){this.hidden_ctx_.scale(hiddenScale,hiddenScale);}}; /**
|
||||
* Detach DOM elements in the dygraph and null out all data references.
|
||||
|
|
@ -6849,6 +6880,7 @@ var oldValueRanges=this.yAxisRanges();var newValueRanges=[];for(var i=0;i < this
|
|||
*/Dygraph.prototype.resetZoom = function(){var _this4=this;var dirtyX=this.isZoomed('x');var dirtyY=this.isZoomed('y');var dirty=dirtyX || dirtyY; // Clear any selection, since it's likely to be drawn in the wrong place.
|
||||
this.clearSelection();if(!dirty)return; // Calculate extremes to avoid lack of padding on reset.
|
||||
var _xAxisExtremes=this.xAxisExtremes();var _xAxisExtremes2=_slicedToArray(_xAxisExtremes,2);var minDate=_xAxisExtremes2[0];var maxDate=_xAxisExtremes2[1];var animatedZooms=this.getBooleanOption('animatedZooms');var zoomCallback=this.getFunctionOption('zoomCallback'); // TODO(danvk): merge this block w/ the code below.
|
||||
// TODO(danvk): factor out a generic, public zoomTo method.
|
||||
if(!animatedZooms){this.dateWindow_ = null;this.axes_.forEach(function(axis){if(axis.valueRange)delete axis.valueRange;});this.drawGraph_();if(zoomCallback){zoomCallback.call(this,minDate,maxDate,this.yAxisRanges());}return;}var oldWindow=null,newWindow=null,oldValueRanges=null,newValueRanges=null;if(dirtyX){oldWindow = this.xAxisRange();newWindow = [minDate,maxDate];}if(dirtyY){oldValueRanges = this.yAxisRanges();newValueRanges = this.yAxisExtremes();}this.doAnimatedZoom(oldWindow,newWindow,oldValueRanges,newValueRanges,function(){_this4.dateWindow_ = null;_this4.axes_.forEach(function(axis){if(axis.valueRange)delete axis.valueRange;});if(zoomCallback){zoomCallback.call(_this4,minDate,maxDate,_this4.yAxisRanges());}});}; /**
|
||||
* Combined animation logic for all zoom functions.
|
||||
* either the x parameters or y parameters may be null.
|
||||
|
|
@ -7265,7 +7297,7 @@ if('rollPeriod' in attrs){this.rollPeriod_ = attrs.rollPeriod;}if('dateWindow' i
|
|||
// highlightCircleSize
|
||||
// Check if this set options will require new points.
|
||||
var requiresNewPoints=utils.isPixelChangingOptionList(this.attr_("labels"),attrs);utils.updateDeep(this.user_attrs_,attrs);this.attributes_.reparseSeries();if(file){ // This event indicates that the data is about to change, but hasn't yet.
|
||||
// TODO(danvk): support cancelation of the update via this event.
|
||||
// TODO(danvk): support cancellation of the update via this event.
|
||||
this.cascadeEvents_('dataWillUpdate',{});this.file_ = file;if(!block_redraw)this.start_();}else {if(!block_redraw){if(requiresNewPoints){this.predraw_();}else {this.renderGraph_(false);}}}}; /**
|
||||
* Make a copy of input attributes, removing file as a convenience.
|
||||
* @private
|
||||
|
|
@ -8796,8 +8828,8 @@ rangeSelector.prototype.updateVisibility_ = function () {
|
|||
* Resizes the range selector.
|
||||
*/
|
||||
rangeSelector.prototype.resize_ = function () {
|
||||
function setElementRect(canvas, context, rect) {
|
||||
var canvasScale = utils.getContextPixelRatio(context);
|
||||
function setElementRect(canvas, context, rect, pixelRatioOption) {
|
||||
var canvasScale = pixelRatioOption || utils.getContextPixelRatio(context);
|
||||
|
||||
canvas.style.top = rect.y + 'px';
|
||||
canvas.style.left = rect.x + 'px';
|
||||
|
|
@ -8824,8 +8856,9 @@ rangeSelector.prototype.resize_ = function () {
|
|||
h: this.getOption_('rangeSelectorHeight')
|
||||
};
|
||||
|
||||
setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_);
|
||||
setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_);
|
||||
var pixelRatioOption = this.dygraph_.getNumericOption('pixelRatio');
|
||||
setElementRect(this.bgcanvas_, this.bgcanvas_ctx_, this.canvasRect_, pixelRatioOption);
|
||||
setElementRect(this.fgcanvas_, this.fgcanvas_ctx_, this.canvasRect_, pixelRatioOption);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue