Mercurial > code > home > repos > homeauto
changeset 218:f8ffb9d8d982
multi-boards on one service, new devices, devices return their current
Ignore-this: e214852bca67519e79f9ddb3644576e1
values in the graph, jsonld support, multiple temp sensors on OW bus
author | drewp@bigasterisk.com |
---|---|
date | Sun, 03 Jan 2016 02:29:14 -0800 |
parents | 163cfa384372 |
children | 366b061ba71d |
files | service/arduinoNode/arduinoNode.py service/arduinoNode/devices.py service/arduinoNode/static/index.html service/arduinoNode/static/output-widgets.html |
diffstat | 4 files changed, 302 insertions(+), 63 deletions(-) [+] |
line wrap: on
line diff
--- a/service/arduinoNode/arduinoNode.py Sun Jan 03 02:28:44 2016 -0800 +++ b/service/arduinoNode/arduinoNode.py Sun Jan 03 02:29:14 2016 -0800 @@ -133,7 +133,6 @@ if stmt[0] == s and stmt[1] in graphitePredicates: self._carbon.send(graphiteName, stmt[2].toPython(), now) - def currentGraph(self): g = Graph() @@ -142,6 +141,9 @@ for si in self._statementsFromInputs.values(): for s in si: g.add(s) + for dev in self._devs: + for stmt in dev.hostStatements(): + g.add(stmt) return g def outputStatements(self, stmts): @@ -165,9 +167,9 @@ "matching output bytes" % dev.__class__) log.info("success") if unused: - log.warn("No devices cared about these statements:") + log.info("Board %s doesn't care about these statements:", self.uri) for s in unused: - log.warn(repr(s)) + log.info("%r", s) def generateArduinoCode(self): generated = { @@ -343,7 +345,12 @@ if self.get_argument('config', 'no') == 'yes': for stmt in self.settings.config.graph: g.add(stmt) - + + if self.request.headers.get('accept') == 'application/ld+json': + self.set_header('Content-type', 'application/ld+json') + self.write(g.asJsonLd()) + return + self.set_header('Content-type', 'application/x-trig') self.write(g.asTrig()) @@ -357,7 +364,7 @@ def get(self): board = [b for b in self.settings.boards if b.uri == URIRef(self.get_argument('board'))][0] - self.set_header('Content-type', 'text/plain') + self.set_header('Content-Type', 'text/plain') code, cksum = board.generateArduinoCode() self.write(code) @@ -402,7 +409,7 @@ def main(): arg = docopt(""" - Usage: reasoning.py [options] + Usage: arduinoNode.py [options] -v Verbose """) @@ -428,7 +435,8 @@ b = Board(dev, config.graph, board, onChange) boards.append(b) - boards[0].deployToArduino() + for b in boards: + b.deployToArduino() log.info('open boards') for b in boards:
--- a/service/arduinoNode/devices.py Sun Jan 03 02:28:44 2016 -0800 +++ b/service/arduinoNode/devices.py Sun Jan 03 02:29:14 2016 -0800 @@ -1,10 +1,11 @@ from __future__ import division -import itertools +import itertools, logging, struct, os from rdflib import Namespace, RDF, URIRef, Literal import time ROOM = Namespace('http://projects.bigasterisk.com/room/') XSD = Namespace('http://www.w3.org/2001/XMLSchema#') +log = logging.getLogger() def readLine(read): buf = '' @@ -31,12 +32,20 @@ initBindings=dict(board=board, thisType=cls.deviceType), initNs={'': ROOM}): + log.info('found %s, a %s', row.dev, cls.deviceType) yield cls(graph, row.dev, int(row.pinNumber)) # subclasses may add args to this def __init__(self, graph, uri, pinNumber): self.graph, self.uri = graph, uri self.pinNumber = pinNumber + self.hostStateInit() + + def hostStateInit(self): + """ + If you don't want to use __init__, you can use this to set up + whatever storage you might need for hostStatements + """ def description(self): return { @@ -57,6 +66,16 @@ """ raise NotImplementedError('readFromPoll in %s' % self.__class__) + def hostStatements(self): + """ + Like readFromPoll but these statements come from the host-side + python code, not the connected device. Include output state + (e.g. light brightness) if its master version is in this + object. This method is called on /graph requests so it should + be fast. + """ + return [] + def watchPrefixes(self): """ subj,pred pairs of the statements that might be returned from @@ -147,8 +166,9 @@ return "Serial.write('k');" def readFromPoll(self, read): - if read(1) != 'k': - raise ValueError('invalid ping response') + byte = read(1) + if byte != 'k': + raise ValueError('invalid ping response: chr(%s)' % ord(byte)) return [(self.uri, ROOM['ping'], ROOM['ok'])] def watchPrefixes(self): @@ -196,13 +216,54 @@ ] @register +class PushbuttonInput(DeviceType): + """add a switch to ground; we'll turn on pullup""" + deviceType = ROOM['Pushbutton'] + def generateSetupCode(self): + return 'pinMode(%(pin)d, INPUT); digitalWrite(%(pin)d, HIGH);' % { + 'pin': self.pinNumber, + } + + def generatePollCode(self): + # note: pulldown means unpressed reads as a 1 + return "Serial.write(digitalRead(%(pin)d) ? '0' : '1');" % { + 'pin': self.pinNumber + } + + def readFromPoll(self, read): + b = read(1) + if b not in '01': + raise ValueError('unexpected response %r' % b) + motion = b == '1' + + #and exactly once for the transition + return [ + (self.uri, ROOM['buttonState'], + ROOM['pressed'] if motion else ROOM['notPressed']), + ] + + def watchPrefixes(self): + return [ + (self.uri, ROOM['buttonState']), + ] + +@register class OneWire(DeviceType): """ A OW bus with temperature sensors (and maybe other devices, which - are also to be handled under this object) + are also to be handled under this object). We return graph + statements for all devices we find, even if we don't scan them, so + you can more easily add them to your config. Onewire search + happens only at device startup (not even program startup, yet). + + self.uri is a resource representing the bus. + + DS18S20 pin 1: ground, pin 2: data and pull-up with 4.7k. """ deviceType = ROOM['OneWire'] - + def hostStateInit(self): + # eliminate this as part of removing watchPrefixes + self._knownTempSubjects = set() def generateIncludes(self): return ['OneWire.h', 'DallasTemperature.h'] @@ -214,14 +275,16 @@ return ''' OneWire oneWire(%(pinNumber)s); DallasTemperature sensors(&oneWire); -DeviceAddress tempSensorAddress; -#define NUM_TEMPERATURE_RETRIES 2 +#define MAX_DEVICES 8 +DeviceAddress tempSensorAddress[MAX_DEVICES]; -void initSensors() { +void initSensors() { sensors.begin(); + sensors.setResolution(12); sensors.setWaitForConversion(false); - sensors.getAddress(tempSensorAddress, 0); - sensors.setResolution(tempSensorAddress, 9); // down from 12 to avoid flicker + for (uint8_t i=0; i < sensors.getDeviceCount(); ++i) { + sensors.getAddress(tempSensorAddress[i], i); + } } ''' % dict(pinNumber=self.pinNumber) @@ -230,45 +293,43 @@ def generatePollCode(self): return r''' -for (int i=0; i<NUM_TEMPERATURE_RETRIES; i++) { sensors.requestTemperatures(); - // not waiting for conversion at all is fine- the temps will update soon - //unsigned long until = millis() + 750; while(millis() < until) {idle();} - float newTemp = sensors.getTempF(tempSensorAddress); - idle(); - if (i < NUM_TEMPERATURE_RETRIES-1 && - (newTemp < -100 || newTemp > 180)) { - // too many errors that were fixed by restarting arduino. - // trying repeating this much init - initSensors(); - continue; + + // If we need frequent idle calls or fast polling again, this needs + // to be changed, but it makes temp sensing work. I had a note that I + // could just wait until the next cycle to get my reading, but that's + // not working today, maybe because of a changed poll rate. + sensors.setWaitForConversion(true); // ~100ms + + Serial.write((uint8_t)sensors.getDeviceCount()); + for (uint8_t i=0; i < sensors.getDeviceCount(); ++i) { + float newTemp = sensors.getTempF(tempSensorAddress[i]); + + Serial.write(tempSensorAddress[i], 8); + Serial.write((uint8_t*)(&newTemp), 4); } - Serial.print(newTemp); -idle(); - Serial.print('\n'); -idle(); - Serial.print((char)i); -idle(); - break; -} ''' def readFromPoll(self, read): - newTemp = readLine(read) - retries = ord(read(1)) - # uri will change; there could (likely) be multiple connected sensors - return [ - (self.uri, ROOM['temperatureF'], - Literal(newTemp, datatype=XSD['decimal'])), - (self.uri, ROOM['temperatureRetries'], Literal(retries)), - ] + t1 = time.time() + count = ord(read(1)) + stmts = [] + for i in range(count): + addr = struct.unpack('>Q', read(8))[0] + tempF = struct.unpack('<f', read(4))[0] + sensorUri = URIRef(os.path.join(self.uri, 'dev-%s' % hex(addr)[2:])) + stmts.extend([ + (self.uri, ROOM['connectedTo'], sensorUri), + (sensorUri, ROOM['temperatureF'], Literal(tempF))]) + self._knownTempSubjects.add(sensorUri) + + log.debug("read temp in %.1fms" % ((time.time() - t1) * 1000)) + return stmts def watchPrefixes(self): # these uris will become dynamic! see note on watchPrefixes # about eliminating it. - return [(self.uri, ROOM['temperatureF']), - (self.uri, ROOM['temperatureRetries']), - ] + return [(uri, ROOM['temperatureF']) for uri in self._knownTempSubjects] def byteFromFloat(f): return chr(int(min(255, max(0, f * 255)))) @@ -276,6 +337,9 @@ @register class LedOutput(DeviceType): deviceType = ROOM['LedOutput'] + def hostStateInit(self): + self.value = 0 + def generateSetupCode(self): return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % { 'pin': self.pinNumber, @@ -287,10 +351,13 @@ def sendOutput(self, statements, write, read): assert len(statements) == 1 assert statements[0][:2] == (self.uri, ROOM['brightness']) - value = float(statements[0][2]) + self.value = float(statements[0][2]) if (self.uri, RDF.type, ROOM['ActiveLowOutput']) in self.graph: - value = 1 - value - write(byteFromFloat(value)) + self.value = 1 - self.value + write(byteFromFloat(self.value)) + + def hostStatements(self): + return [(self.uri, ROOM['brightness'], Literal(self.value))] def generateActionCode(self): return r''' @@ -311,6 +378,9 @@ @register class DigitalOutput(DeviceType): deviceType = ROOM['DigitalOutput'] + def hostStateInit(self): + self.value = 0 + def generateSetupCode(self): return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % { 'pin': self.pinNumber, @@ -322,8 +392,12 @@ def sendOutput(self, statements, write, read): assert len(statements) == 1 assert statements[0][:2] == (self.uri, ROOM['level']) - value = {"high": 1, "low": 0}[str(statements[0][2])] - write(chr(value)) + self.value = {"high": 1, "low": 0}[str(statements[0][2])] + write(chr(self.value)) + + def hostStatements(self): + return [(self.uri, ROOM['level'], + Literal('high' if self.value else 'low'))] def generateActionCode(self): return r''' @@ -338,6 +412,7 @@ 'pred': ROOM['level'], }] + @register class PwmBoard(DeviceType): deviceType = ROOM['PwmBoard'] @@ -362,10 +437,17 @@ yield cls(graph, row.dev, outs=outs) def __init__(self, graph, dev, outs): - super(PwmBoard, self).__init__(graph, dev, pinNumber=None) self.codeVals = {'pwm': 'pwm%s' % (hash(str(dev)) % 99999)} self.outs = outs + super(PwmBoard, self).__init__(graph, dev, pinNumber=None) + def hostStateInit(self): + self.values = {uri: 0 for uri in self.outs.keys()} # uri: brightness + + def hostStatements(self): + return [(uri, ROOM['brightness'], Literal(b)) + for uri, b in self.values.items()] + def generateIncludes(self): return ['Wire.h', 'Adafruit_PWMServoDriver.h'] @@ -400,6 +482,7 @@ assert statements[0][1] == ROOM['brightness']; chan = self.outs[statements[0][0]] value = float(statements[0][2]) + self.values[statements[0][0]] = value v12 = int(min(4095, max(0, value * 4095))) write(chr(chan) + chr(v12 >> 8) + chr(v12 & 0xff)) @@ -438,6 +521,7 @@ def __init__(self, graph, dev, connections): super(ST7576Lcd, self).__init__(graph, dev, pinNumber=None) self.connections = connections + self.text = '' def generateIncludes(self): return ['ST7565.h'] @@ -472,10 +556,13 @@ def sendOutput(self, statements, write, read): assert len(statements) == 1 assert statements[0][:2] == (self.uri, ROOM['text']) - value = str(statements[0][2]) - assert len(value) < 254, repr(value) - write(chr(len(value)) + value) + self.text = str(statements[0][2]) + assert len(self.text) < 254, repr(self.text) + write(chr(len(self.text)) + self.text) + def hostStatements(self): + return [(self.uri, ROOM['text'], Literal(self.text))] + def outputWidgets(self): return [{ 'element': 'output-fixed-text', @@ -501,6 +588,90 @@ glcd.display(); ''' +@register +class RgbPixels(DeviceType): + """chain of FastLED-controllable rgb pixels""" + deviceType = ROOM['RgbPixels'] + + def __init__(self, graph, uri, pinNumber): + super(RgbPixels, self).__init__(graph, uri, pinNumber) + px = graph.value(self.uri, ROOM['pixels']) + self.pixelUris = list(graph.items(px)) + self.values = dict((uri, Literal('#000000')) for uri in self.pixelUris) + self.replace = {'ledArray': 'leds_%s' % self.pinNumber, + 'ledCount': len(self.pixelUris), + 'pin': self.pinNumber, + 'ledType': 'WS2812', + } + + def generateIncludes(self): + """filenames of .h files to #include""" + return ['FastLED.h'] + + def generateArduinoLibs(self): + """names of libraries for the ARDUINO_LIBS line in the makefile""" + return ['FastLED-3.1.0'] + + def myId(self): + return 'rgb_%s' % self.pinNumber + + def generateGlobalCode(self): + return 'CRGB {ledArray}[{ledCount}];'.format(**self.replace) + + def generateSetupCode(self): + return 'FastLED.addLeds<{ledType}, {pin}>({ledArray}, {ledCount});'.format(**self.replace) + + def _rgbFromHex(self, h): + rrggbb = h.lstrip('#') + return [int(x, 16) for x in [rrggbb[0:2], rrggbb[2:4], rrggbb[4:6]]] + + def sendOutput(self, statements, write, read): + px, pred, color = statements[0] + if pred != ROOM['color']: + raise ValueError(pred) + rgb = self._rgbFromHex(color) + if px not in self.values: + raise ValueError(px) + self.values[px] = Literal(color) + write(chr(self.pixelUris.index(px)) + + chr(rgb[1]) + # my WS2812 need these flipped + chr(rgb[0]) + + chr(rgb[2])) + + def hostStatements(self): + return [(uri, ROOM['color'], hexCol) + for uri, hexCol in self.values.items()] + + def outputPatterns(self): + return [(px, ROOM['color'], None) for px in self.pixelUris] + + def generateActionCode(self): + + return ''' + + while(Serial.available() < 1) NULL; + byte id = Serial.read(); + + while(Serial.available() < 1) NULL; + byte r = Serial.read(); + + while(Serial.available() < 1) NULL; + byte g = Serial.read(); + + while(Serial.available() < 1) NULL; + byte b = Serial.read(); + + {ledArray}[id] = CRGB(r, g, b); FastLED.show(); + + '''.format(**self.replace) + + def outputWidgets(self): + return [{ + 'element': 'output-rgb', + 'subj': px, + 'pred': ROOM['color'], + } for px in self.pixelUris] + def makeDevices(graph, board): out = [] for dt in sorted(_knownTypes, key=lambda cls: cls.__name__):
--- a/service/arduinoNode/static/index.html Sun Jan 03 02:28:44 2016 -0800 +++ b/service/arduinoNode/static/index.html Sun Jan 03 02:29:14 2016 -0800 @@ -29,7 +29,8 @@ dev: 'http://projects.bigasterisk.com/device/', room: 'http://projects.bigasterisk.com/room/', rdfs: 'http://www.w3.org/2000/01/rdf-schema#', - sensor: 'http://bigasterisk.com/homeauto/sensor/' + sensor: 'http://bigasterisk.com/homeauto/sensor/', + b0: 'http://bigasterisk.com/homeauto/board0/', }; </script>
--- a/service/arduinoNode/static/output-widgets.html Sun Jan 03 02:28:44 2016 -0800 +++ b/service/arduinoNode/static/output-widgets.html Sun Jan 03 02:29:14 2016 -0800 @@ -5,6 +5,7 @@ <dom-module id="output-sender"> <template> + <iron-ajax id="graphGet" url="../graph" method="GET" headers='{"Accept": "application/ld+json"}'></iron-ajax> <iron-ajax id="output" url="../output" method="PUT"></iron-ajax> Set <a href$="{{subj}}">{{compactUri(subj)}}</a>'s <span>{{compactUri(pred)}}</span> to @@ -16,7 +17,57 @@ properties: { subj: { notify: true }, pred: { notify: true }, - value: { observer: 'valueChanged' } + value: { notify: true, observer: 'valueChanged' } + }, + ready: function() { + this.waitOnChangeMs = 100; + this.smallestRequestPeriodMs = 200; + this.synced = false; + + this.newRequestNeedsSending = false; + this.lastSendMs = 0; + this.$.output.addEventListener('response', this.onResponse.bind(this)); + + this.loadInitialValue(); + }, + loadInitialValue: function() { + this.$.graphGet.addEventListener('response', function(ev) { + ev.target.removeEventListener(ev.type, arguments.callee); + + ev.detail.response.forEach(function(row) { + var subj = row['@id']; + if (subj == this.subj) { + Object.keys(row).forEach(function(pred) { + if (pred == this.pred) { + row[pred].forEach(function(obj) { + this.value = obj['@value']; + this.synced = true; + }.bind(this)); + } + }.bind(this)); + } + }.bind(this)); + + }.bind(this)); + this.$.graphGet.generateRequest(); + }, + onResponse: function() { + if (!this.newRequestNeedsSending) { + return; + } + if (this.$.output.activeRequests.length > 0) { + return; // 'response' event will call us back + } + + var now = Date.now(), dt = now - this.lastSendMs; + if (dt < this.smallestRequestPeriodMs) { + setTimeout(this.onResponse.bind(this), + this.smallestRequestPeriodMs - dt); + return; + } + this.newRequestNeedsSending = false; + this.lastSendMs = now; + this.$.output.generateRequest(); }, valueChanged: function () { if (!this.subj || !this.pred) { @@ -25,7 +76,8 @@ //this.$.output.headers = {'content-type': ...} this.$.output.params = {s: this.subj, p: this.pred}; this.$.output.body = this.value; - this.$.output.generateRequest(); + this.newRequestNeedsSending = true; + setTimeout(this.onResponse.bind(this), this.waitOnChangeMs); } }); </script> @@ -33,11 +85,18 @@ <dom-module id="output-rgb"> <template> - <output-sender subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender> - <color-picker id="pick" width="200" height="100" color="{{value}}"></color-picker> - color pick <span>{{value}}</span> - <button on-click="black">Black</button> + <div style="display: flex"> + <div> + <output-sender subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender> + <div>color pick <span>{{value}}</span> + <button on-click="black">Black</button> <button on-click="white">White</button> + </div> + </div> + <div> + <color-picker id="pick" width="200" height="100" color="{{value}}"></color-picker> + </div> + </div> </template> <script> Polymer({