Mercurial > code > home > repos > homeauto
diff service/arduinoNode/devices.py @ 1023:3e6fac8569cd
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
darcs-hash:bc0f86023166e9b1d37c99506f6bc389309db327
author | drewp <drewp@bigasterisk.com> |
---|---|
date | Sun, 03 Jan 2016 02:29:14 -0800 |
parents | b24885725f59 |
children | 0aa54404df19 |
line wrap: on
line diff
--- 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__):