comparison service/arduinoNode/devices.py @ 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 6c6897a139da
children 0aa54404df19
comparison
equal deleted inserted replaced
217:163cfa384372 218:f8ffb9d8d982
1 from __future__ import division 1 from __future__ import division
2 import itertools 2 import itertools, logging, struct, os
3 from rdflib import Namespace, RDF, URIRef, Literal 3 from rdflib import Namespace, RDF, URIRef, Literal
4 import time 4 import time
5 5
6 ROOM = Namespace('http://projects.bigasterisk.com/room/') 6 ROOM = Namespace('http://projects.bigasterisk.com/room/')
7 XSD = Namespace('http://www.w3.org/2001/XMLSchema#') 7 XSD = Namespace('http://www.w3.org/2001/XMLSchema#')
8 log = logging.getLogger()
8 9
9 def readLine(read): 10 def readLine(read):
10 buf = '' 11 buf = ''
11 for c in iter(lambda: read(1), '\n'): 12 for c in iter(lambda: read(1), '\n'):
12 buf += c 13 buf += c
29 ?dev a ?thisType . 30 ?dev a ?thisType .
30 } ORDER BY ?dev""", 31 } ORDER BY ?dev""",
31 initBindings=dict(board=board, 32 initBindings=dict(board=board,
32 thisType=cls.deviceType), 33 thisType=cls.deviceType),
33 initNs={'': ROOM}): 34 initNs={'': ROOM}):
35 log.info('found %s, a %s', row.dev, cls.deviceType)
34 yield cls(graph, row.dev, int(row.pinNumber)) 36 yield cls(graph, row.dev, int(row.pinNumber))
35 37
36 # subclasses may add args to this 38 # subclasses may add args to this
37 def __init__(self, graph, uri, pinNumber): 39 def __init__(self, graph, uri, pinNumber):
38 self.graph, self.uri = graph, uri 40 self.graph, self.uri = graph, uri
39 self.pinNumber = pinNumber 41 self.pinNumber = pinNumber
42 self.hostStateInit()
43
44 def hostStateInit(self):
45 """
46 If you don't want to use __init__, you can use this to set up
47 whatever storage you might need for hostStatements
48 """
40 49
41 def description(self): 50 def description(self):
42 return { 51 return {
43 'uri': self.uri, 52 'uri': self.uri,
44 'className': self.__class__.__name__, 53 'className': self.__class__.__name__,
55 input (e.g. IR receiver). 64 input (e.g. IR receiver).
56 Returns rdf statements. 65 Returns rdf statements.
57 """ 66 """
58 raise NotImplementedError('readFromPoll in %s' % self.__class__) 67 raise NotImplementedError('readFromPoll in %s' % self.__class__)
59 68
69 def hostStatements(self):
70 """
71 Like readFromPoll but these statements come from the host-side
72 python code, not the connected device. Include output state
73 (e.g. light brightness) if its master version is in this
74 object. This method is called on /graph requests so it should
75 be fast.
76 """
77 return []
78
60 def watchPrefixes(self): 79 def watchPrefixes(self):
61 """ 80 """
62 subj,pred pairs of the statements that might be returned from 81 subj,pred pairs of the statements that might be returned from
63 readFromPoll, so the dashboard knows what it should 82 readFromPoll, so the dashboard knows what it should
64 watch. This should be eliminated, as the dashboard should just 83 watch. This should be eliminated, as the dashboard should just
145 164
146 def generatePollCode(self): 165 def generatePollCode(self):
147 return "Serial.write('k');" 166 return "Serial.write('k');"
148 167
149 def readFromPoll(self, read): 168 def readFromPoll(self, read):
150 if read(1) != 'k': 169 byte = read(1)
151 raise ValueError('invalid ping response') 170 if byte != 'k':
171 raise ValueError('invalid ping response: chr(%s)' % ord(byte))
152 return [(self.uri, ROOM['ping'], ROOM['ok'])] 172 return [(self.uri, ROOM['ping'], ROOM['ok'])]
153 173
154 def watchPrefixes(self): 174 def watchPrefixes(self):
155 return [(self.uri, ROOM['ping'])] 175 return [(self.uri, ROOM['ping'])]
156 176
194 (self.uri, ROOM['sees']), 214 (self.uri, ROOM['sees']),
195 (self.uri, ROOM['seesRecently']), 215 (self.uri, ROOM['seesRecently']),
196 ] 216 ]
197 217
198 @register 218 @register
219 class PushbuttonInput(DeviceType):
220 """add a switch to ground; we'll turn on pullup"""
221 deviceType = ROOM['Pushbutton']
222 def generateSetupCode(self):
223 return 'pinMode(%(pin)d, INPUT); digitalWrite(%(pin)d, HIGH);' % {
224 'pin': self.pinNumber,
225 }
226
227 def generatePollCode(self):
228 # note: pulldown means unpressed reads as a 1
229 return "Serial.write(digitalRead(%(pin)d) ? '0' : '1');" % {
230 'pin': self.pinNumber
231 }
232
233 def readFromPoll(self, read):
234 b = read(1)
235 if b not in '01':
236 raise ValueError('unexpected response %r' % b)
237 motion = b == '1'
238
239 #and exactly once for the transition
240 return [
241 (self.uri, ROOM['buttonState'],
242 ROOM['pressed'] if motion else ROOM['notPressed']),
243 ]
244
245 def watchPrefixes(self):
246 return [
247 (self.uri, ROOM['buttonState']),
248 ]
249
250 @register
199 class OneWire(DeviceType): 251 class OneWire(DeviceType):
200 """ 252 """
201 A OW bus with temperature sensors (and maybe other devices, which 253 A OW bus with temperature sensors (and maybe other devices, which
202 are also to be handled under this object) 254 are also to be handled under this object). We return graph
255 statements for all devices we find, even if we don't scan them, so
256 you can more easily add them to your config. Onewire search
257 happens only at device startup (not even program startup, yet).
258
259 self.uri is a resource representing the bus.
260
261 DS18S20 pin 1: ground, pin 2: data and pull-up with 4.7k.
203 """ 262 """
204 deviceType = ROOM['OneWire'] 263 deviceType = ROOM['OneWire']
205 264 def hostStateInit(self):
265 # eliminate this as part of removing watchPrefixes
266 self._knownTempSubjects = set()
206 def generateIncludes(self): 267 def generateIncludes(self):
207 return ['OneWire.h', 'DallasTemperature.h'] 268 return ['OneWire.h', 'DallasTemperature.h']
208 269
209 def generateArduinoLibs(self): 270 def generateArduinoLibs(self):
210 return ['OneWire', 'DallasTemperature'] 271 return ['OneWire', 'DallasTemperature']
212 def generateGlobalCode(self): 273 def generateGlobalCode(self):
213 # not yet isolated to support multiple OW buses 274 # not yet isolated to support multiple OW buses
214 return ''' 275 return '''
215 OneWire oneWire(%(pinNumber)s); 276 OneWire oneWire(%(pinNumber)s);
216 DallasTemperature sensors(&oneWire); 277 DallasTemperature sensors(&oneWire);
217 DeviceAddress tempSensorAddress; 278 #define MAX_DEVICES 8
218 #define NUM_TEMPERATURE_RETRIES 2 279 DeviceAddress tempSensorAddress[MAX_DEVICES];
219 280
220 void initSensors() { 281 void initSensors() {
221 sensors.begin(); 282 sensors.begin();
283 sensors.setResolution(12);
222 sensors.setWaitForConversion(false); 284 sensors.setWaitForConversion(false);
223 sensors.getAddress(tempSensorAddress, 0); 285 for (uint8_t i=0; i < sensors.getDeviceCount(); ++i) {
224 sensors.setResolution(tempSensorAddress, 9); // down from 12 to avoid flicker 286 sensors.getAddress(tempSensorAddress[i], i);
287 }
225 } 288 }
226 ''' % dict(pinNumber=self.pinNumber) 289 ''' % dict(pinNumber=self.pinNumber)
227 290
228 def generateSetupCode(self): 291 def generateSetupCode(self):
229 return 'initSensors();' 292 return 'initSensors();'
230 293
231 def generatePollCode(self): 294 def generatePollCode(self):
232 return r''' 295 return r'''
233 for (int i=0; i<NUM_TEMPERATURE_RETRIES; i++) {
234 sensors.requestTemperatures(); 296 sensors.requestTemperatures();
235 // not waiting for conversion at all is fine- the temps will update soon 297
236 //unsigned long until = millis() + 750; while(millis() < until) {idle();} 298 // If we need frequent idle calls or fast polling again, this needs
237 float newTemp = sensors.getTempF(tempSensorAddress); 299 // to be changed, but it makes temp sensing work. I had a note that I
238 idle(); 300 // could just wait until the next cycle to get my reading, but that's
239 if (i < NUM_TEMPERATURE_RETRIES-1 && 301 // not working today, maybe because of a changed poll rate.
240 (newTemp < -100 || newTemp > 180)) { 302 sensors.setWaitForConversion(true); // ~100ms
241 // too many errors that were fixed by restarting arduino. 303
242 // trying repeating this much init 304 Serial.write((uint8_t)sensors.getDeviceCount());
243 initSensors(); 305 for (uint8_t i=0; i < sensors.getDeviceCount(); ++i) {
244 continue; 306 float newTemp = sensors.getTempF(tempSensorAddress[i]);
307
308 Serial.write(tempSensorAddress[i], 8);
309 Serial.write((uint8_t*)(&newTemp), 4);
245 } 310 }
246 Serial.print(newTemp);
247 idle();
248 Serial.print('\n');
249 idle();
250 Serial.print((char)i);
251 idle();
252 break;
253 }
254 ''' 311 '''
255 312
256 def readFromPoll(self, read): 313 def readFromPoll(self, read):
257 newTemp = readLine(read) 314 t1 = time.time()
258 retries = ord(read(1)) 315 count = ord(read(1))
259 # uri will change; there could (likely) be multiple connected sensors 316 stmts = []
260 return [ 317 for i in range(count):
261 (self.uri, ROOM['temperatureF'], 318 addr = struct.unpack('>Q', read(8))[0]
262 Literal(newTemp, datatype=XSD['decimal'])), 319 tempF = struct.unpack('<f', read(4))[0]
263 (self.uri, ROOM['temperatureRetries'], Literal(retries)), 320 sensorUri = URIRef(os.path.join(self.uri, 'dev-%s' % hex(addr)[2:]))
264 ] 321 stmts.extend([
322 (self.uri, ROOM['connectedTo'], sensorUri),
323 (sensorUri, ROOM['temperatureF'], Literal(tempF))])
324 self._knownTempSubjects.add(sensorUri)
325
326 log.debug("read temp in %.1fms" % ((time.time() - t1) * 1000))
327 return stmts
265 328
266 def watchPrefixes(self): 329 def watchPrefixes(self):
267 # these uris will become dynamic! see note on watchPrefixes 330 # these uris will become dynamic! see note on watchPrefixes
268 # about eliminating it. 331 # about eliminating it.
269 return [(self.uri, ROOM['temperatureF']), 332 return [(uri, ROOM['temperatureF']) for uri in self._knownTempSubjects]
270 (self.uri, ROOM['temperatureRetries']),
271 ]
272 333
273 def byteFromFloat(f): 334 def byteFromFloat(f):
274 return chr(int(min(255, max(0, f * 255)))) 335 return chr(int(min(255, max(0, f * 255))))
275 336
276 @register 337 @register
277 class LedOutput(DeviceType): 338 class LedOutput(DeviceType):
278 deviceType = ROOM['LedOutput'] 339 deviceType = ROOM['LedOutput']
340 def hostStateInit(self):
341 self.value = 0
342
279 def generateSetupCode(self): 343 def generateSetupCode(self):
280 return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % { 344 return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % {
281 'pin': self.pinNumber, 345 'pin': self.pinNumber,
282 } 346 }
283 347
285 return [(self.uri, ROOM['brightness'], None)] 349 return [(self.uri, ROOM['brightness'], None)]
286 350
287 def sendOutput(self, statements, write, read): 351 def sendOutput(self, statements, write, read):
288 assert len(statements) == 1 352 assert len(statements) == 1
289 assert statements[0][:2] == (self.uri, ROOM['brightness']) 353 assert statements[0][:2] == (self.uri, ROOM['brightness'])
290 value = float(statements[0][2]) 354 self.value = float(statements[0][2])
291 if (self.uri, RDF.type, ROOM['ActiveLowOutput']) in self.graph: 355 if (self.uri, RDF.type, ROOM['ActiveLowOutput']) in self.graph:
292 value = 1 - value 356 self.value = 1 - self.value
293 write(byteFromFloat(value)) 357 write(byteFromFloat(self.value))
358
359 def hostStatements(self):
360 return [(self.uri, ROOM['brightness'], Literal(self.value))]
294 361
295 def generateActionCode(self): 362 def generateActionCode(self):
296 return r''' 363 return r'''
297 while(Serial.available() < 1) NULL; 364 while(Serial.available() < 1) NULL;
298 analogWrite(%(pin)d, Serial.read()); 365 analogWrite(%(pin)d, Serial.read());
309 }] 376 }]
310 377
311 @register 378 @register
312 class DigitalOutput(DeviceType): 379 class DigitalOutput(DeviceType):
313 deviceType = ROOM['DigitalOutput'] 380 deviceType = ROOM['DigitalOutput']
381 def hostStateInit(self):
382 self.value = 0
383
314 def generateSetupCode(self): 384 def generateSetupCode(self):
315 return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % { 385 return 'pinMode(%(pin)d, OUTPUT); digitalWrite(%(pin)d, LOW);' % {
316 'pin': self.pinNumber, 386 'pin': self.pinNumber,
317 } 387 }
318 388
320 return [(self.uri, ROOM['level'], None)] 390 return [(self.uri, ROOM['level'], None)]
321 391
322 def sendOutput(self, statements, write, read): 392 def sendOutput(self, statements, write, read):
323 assert len(statements) == 1 393 assert len(statements) == 1
324 assert statements[0][:2] == (self.uri, ROOM['level']) 394 assert statements[0][:2] == (self.uri, ROOM['level'])
325 value = {"high": 1, "low": 0}[str(statements[0][2])] 395 self.value = {"high": 1, "low": 0}[str(statements[0][2])]
326 write(chr(value)) 396 write(chr(self.value))
397
398 def hostStatements(self):
399 return [(self.uri, ROOM['level'],
400 Literal('high' if self.value else 'low'))]
327 401
328 def generateActionCode(self): 402 def generateActionCode(self):
329 return r''' 403 return r'''
330 while(Serial.available() < 1) NULL; 404 while(Serial.available() < 1) NULL;
331 digitalWrite(%(pin)d, Serial.read()); 405 digitalWrite(%(pin)d, Serial.read());
336 'element': 'output-switch', 410 'element': 'output-switch',
337 'subj': self.uri, 411 'subj': self.uri,
338 'pred': ROOM['level'], 412 'pred': ROOM['level'],
339 }] 413 }]
340 414
415
341 @register 416 @register
342 class PwmBoard(DeviceType): 417 class PwmBoard(DeviceType):
343 deviceType = ROOM['PwmBoard'] 418 deviceType = ROOM['PwmBoard']
344 @classmethod 419 @classmethod
345 def findInstances(cls, graph, board): 420 def findInstances(cls, graph, board):
360 }""", initBindings=dict(dev=row.dev), initNs={'': ROOM}): 435 }""", initBindings=dict(dev=row.dev), initNs={'': ROOM}):
361 outs[out.area] = out.chan.toPython() 436 outs[out.area] = out.chan.toPython()
362 yield cls(graph, row.dev, outs=outs) 437 yield cls(graph, row.dev, outs=outs)
363 438
364 def __init__(self, graph, dev, outs): 439 def __init__(self, graph, dev, outs):
365 super(PwmBoard, self).__init__(graph, dev, pinNumber=None)
366 self.codeVals = {'pwm': 'pwm%s' % (hash(str(dev)) % 99999)} 440 self.codeVals = {'pwm': 'pwm%s' % (hash(str(dev)) % 99999)}
367 self.outs = outs 441 self.outs = outs
368 442 super(PwmBoard, self).__init__(graph, dev, pinNumber=None)
443
444 def hostStateInit(self):
445 self.values = {uri: 0 for uri in self.outs.keys()} # uri: brightness
446
447 def hostStatements(self):
448 return [(uri, ROOM['brightness'], Literal(b))
449 for uri, b in self.values.items()]
450
369 def generateIncludes(self): 451 def generateIncludes(self):
370 return ['Wire.h', 'Adafruit_PWMServoDriver.h'] 452 return ['Wire.h', 'Adafruit_PWMServoDriver.h']
371 453
372 def generateArduinoLibs(self): 454 def generateArduinoLibs(self):
373 return ['Wire', 'Adafruit-PWM-Servo-Driver-Library'] 455 return ['Wire', 'Adafruit-PWM-Servo-Driver-Library']
398 def sendOutput(self, statements, write, read): 480 def sendOutput(self, statements, write, read):
399 assert len(statements) == 1 481 assert len(statements) == 1
400 assert statements[0][1] == ROOM['brightness']; 482 assert statements[0][1] == ROOM['brightness'];
401 chan = self.outs[statements[0][0]] 483 chan = self.outs[statements[0][0]]
402 value = float(statements[0][2]) 484 value = float(statements[0][2])
485 self.values[statements[0][0]] = value
403 v12 = int(min(4095, max(0, value * 4095))) 486 v12 = int(min(4095, max(0, value * 4095)))
404 write(chr(chan) + chr(v12 >> 8) + chr(v12 & 0xff)) 487 write(chr(chan) + chr(v12 >> 8) + chr(v12 & 0xff))
405 488
406 def outputWidgets(self): 489 def outputWidgets(self):
407 return [{ 490 return [{
436 yield cls(graph, dev, connections=connections) 519 yield cls(graph, dev, connections=connections)
437 520
438 def __init__(self, graph, dev, connections): 521 def __init__(self, graph, dev, connections):
439 super(ST7576Lcd, self).__init__(graph, dev, pinNumber=None) 522 super(ST7576Lcd, self).__init__(graph, dev, pinNumber=None)
440 self.connections = connections 523 self.connections = connections
524 self.text = ''
441 525
442 def generateIncludes(self): 526 def generateIncludes(self):
443 return ['ST7565.h'] 527 return ['ST7565.h']
444 528
445 def generateArduinoLibs(self): 529 def generateArduinoLibs(self):
470 return [(self.uri, ROOM['text'], None)] 554 return [(self.uri, ROOM['text'], None)]
471 555
472 def sendOutput(self, statements, write, read): 556 def sendOutput(self, statements, write, read):
473 assert len(statements) == 1 557 assert len(statements) == 1
474 assert statements[0][:2] == (self.uri, ROOM['text']) 558 assert statements[0][:2] == (self.uri, ROOM['text'])
475 value = str(statements[0][2]) 559 self.text = str(statements[0][2])
476 assert len(value) < 254, repr(value) 560 assert len(self.text) < 254, repr(self.text)
477 write(chr(len(value)) + value) 561 write(chr(len(self.text)) + self.text)
478 562
563 def hostStatements(self):
564 return [(self.uri, ROOM['text'], Literal(self.text))]
565
479 def outputWidgets(self): 566 def outputWidgets(self):
480 return [{ 567 return [{
481 'element': 'output-fixed-text', 568 'element': 'output-fixed-text',
482 'cols': 21, 569 'cols': 21,
483 'rows': 8, 570 'rows': 8,
499 glcd.clear(); 586 glcd.clear();
500 glcd.drawstring(0,0, newtxt); 587 glcd.drawstring(0,0, newtxt);
501 glcd.display(); 588 glcd.display();
502 ''' 589 '''
503 590
591 @register
592 class RgbPixels(DeviceType):
593 """chain of FastLED-controllable rgb pixels"""
594 deviceType = ROOM['RgbPixels']
595
596 def __init__(self, graph, uri, pinNumber):
597 super(RgbPixels, self).__init__(graph, uri, pinNumber)
598 px = graph.value(self.uri, ROOM['pixels'])
599 self.pixelUris = list(graph.items(px))
600 self.values = dict((uri, Literal('#000000')) for uri in self.pixelUris)
601 self.replace = {'ledArray': 'leds_%s' % self.pinNumber,
602 'ledCount': len(self.pixelUris),
603 'pin': self.pinNumber,
604 'ledType': 'WS2812',
605 }
606
607 def generateIncludes(self):
608 """filenames of .h files to #include"""
609 return ['FastLED.h']
610
611 def generateArduinoLibs(self):
612 """names of libraries for the ARDUINO_LIBS line in the makefile"""
613 return ['FastLED-3.1.0']
614
615 def myId(self):
616 return 'rgb_%s' % self.pinNumber
617
618 def generateGlobalCode(self):
619 return 'CRGB {ledArray}[{ledCount}];'.format(**self.replace)
620
621 def generateSetupCode(self):
622 return 'FastLED.addLeds<{ledType}, {pin}>({ledArray}, {ledCount});'.format(**self.replace)
623
624 def _rgbFromHex(self, h):
625 rrggbb = h.lstrip('#')
626 return [int(x, 16) for x in [rrggbb[0:2], rrggbb[2:4], rrggbb[4:6]]]
627
628 def sendOutput(self, statements, write, read):
629 px, pred, color = statements[0]
630 if pred != ROOM['color']:
631 raise ValueError(pred)
632 rgb = self._rgbFromHex(color)
633 if px not in self.values:
634 raise ValueError(px)
635 self.values[px] = Literal(color)
636 write(chr(self.pixelUris.index(px)) +
637 chr(rgb[1]) + # my WS2812 need these flipped
638 chr(rgb[0]) +
639 chr(rgb[2]))
640
641 def hostStatements(self):
642 return [(uri, ROOM['color'], hexCol)
643 for uri, hexCol in self.values.items()]
644
645 def outputPatterns(self):
646 return [(px, ROOM['color'], None) for px in self.pixelUris]
647
648 def generateActionCode(self):
649
650 return '''
651
652 while(Serial.available() < 1) NULL;
653 byte id = Serial.read();
654
655 while(Serial.available() < 1) NULL;
656 byte r = Serial.read();
657
658 while(Serial.available() < 1) NULL;
659 byte g = Serial.read();
660
661 while(Serial.available() < 1) NULL;
662 byte b = Serial.read();
663
664 {ledArray}[id] = CRGB(r, g, b); FastLED.show();
665
666 '''.format(**self.replace)
667
668 def outputWidgets(self):
669 return [{
670 'element': 'output-rgb',
671 'subj': px,
672 'pred': ROOM['color'],
673 } for px in self.pixelUris]
674
504 def makeDevices(graph, board): 675 def makeDevices(graph, board):
505 out = [] 676 out = []
506 for dt in sorted(_knownTypes, key=lambda cls: cls.__name__): 677 for dt in sorted(_knownTypes, key=lambda cls: cls.__name__):
507 out.extend(dt.findInstances(graph, board)) 678 out.extend(dt.findInstances(graph, board))
508 return out 679 return out