Skip to content
Browse files

- added sensor files to git repo

Signed-off-by: Luke Closs <gravatar@5thplane.com>
  • Loading branch information...
1 parent 471caaf commit 3ffa07d18414598c046024750b496786a45b4fe9 @safetydank safetydank committed with lukec Aug 6, 2009
Showing with 511 additions and 0 deletions.
  1. +79 −0 sensor/mockdoor.pde
  2. +208 −0 sensor/serialserver.py
  3. +224 −0 sensor/www/sensor.py
View
79 sensor/mockdoor.pde
@@ -0,0 +1,79 @@
+/* Mock Door Tweet
+ gang programmed by VHS (c) copyright 2009.
+ goldfish vanjuggler tristan danv
+ see LICENSE file for full details.
+*/
+
+#define doorPin (4)
+#define bathroomDoorPin (7)
+#define tempPin (0)
+#define INPUT_BUFFER(S) (strcmp((S), buffer) == 0)
+
+// Incoming serial msg buffer
+#define BUFSIZE 256
+char buffer[BUFSIZE];
+int buffer_idx = 0;
+
+void setup() {
+ Serial.begin( 9600 );
+}
+
+/// Check serial port for incoming messages and update global buffer
+int checkSerialCommand() {
+ int pending = 0;
+
+ if (buffer_idx == 0) {
+ memset(buffer, 0, BUFSIZE);
+ }
+
+ // Read an incoming command
+ // if bytes are available, addthemto the buffer
+ int incoming = Serial.available();
+ while (incoming-- > 0) {
+ char foo = Serial.read();
+
+ if (foo == '\n') {
+ pending = 1;
+ buffer[buffer_idx++] = 0;
+ buffer_idx = 0;
+ Serial.print("#RECV: ");
+ Serial.println(buffer);
+ }
+ else {
+ buffer[buffer_idx++] = foo;
+ buffer[buffer_idx] = 0;
+ }
+ }
+
+ return pending;
+}
+
+int cycles = 0;
+bool doorOpen = false;
+
+void loop() {
+ if (checkSerialCommand()) {
+ if (INPUT_BUFFER("temperature")) {
+ Serial.println("temperature 24.44C");
+ }
+ else if (INPUT_BUFFER("buzz")) {
+ Serial.println("buzz played");
+ }
+ else {
+ Serial.print(buffer);
+ Serial.println(" !unknown command");
+ }
+ }
+
+ if (++cycles % 100 == 0) {
+ if (doorOpen) {
+ Serial.println("door open");
+ }
+ else {
+ Serial.println("door closed");
+ }
+ doorOpen = !doorOpen;
+ }
+
+ delay(100);
+}
View
208 sensor/serialserver.py
@@ -0,0 +1,208 @@
+#!/usr/bin/env python
+
+"""
+serialserver multiplexes the Arduino serial port to concurrent incoming TCP
+socket connections. this allows multiple clients to use the connected arduino
+at once.
+
+socket server listens on port 9994 and keeps open all incoming TCP connections
+
+messages sent to the open socket are passed to the serial port and responses
+ are written to the client socket
+
+the serial port is also polled periodically and any other messages are broadcast
+ to all open connections
+
+future improvements:
+ - convert print's to logging.*
+ - subclass SocketServer.TCPServer constructor to eliminate global SERIAL_DAEMON
+ - switch from Lock protected lists to python's synchronized Queue class
+ - better granularity of existing locks
+"""
+
+from __future__ import with_statement
+
+import serial
+import SocketServer, threading, socket, re, os, logging
+import traceback
+
+SERVER_HOST_PORT = 'localhost', 9994
+
+SERIAL_PORT = 'COM12' if os.name is 'nt' else '/dev/ttyUSB0'
+
+# socket read timeout in seconds
+TIMEOUT = 0.01
+# serial port timeout in seconds
+SERIAL_TIMEOUT = 0.05
+
+global SERIAL_DAEMON
+
+class TCPHandler(SocketServer.BaseRequestHandler):
+ def setup(self):
+ global SERIAL_DAEMON
+ assert(SERIAL_DAEMON)
+
+ self.seriald = SERIAL_DAEMON
+
+ (host, port) = self.client_address
+ self.client_id = port
+ self.seriald.register_client(self.client_id)
+
+ self.request.settimeout(TIMEOUT)
+
+ def handle(self):
+ print 'Opened TCP client connection %s' % str(self.client_address)
+
+ while True:
+ try:
+ self.data = self.request.recv(1024)
+
+ if self.data:
+ message = self.data.strip()
+ if not message: continue
+
+ print 'received message', message, 'length %d' % len(message)
+ # send to serial port
+ self.seriald.outgoing((self.client_id, message))
+ else:
+ # stream closed
+ self.request.close()
+ print 'Closed TCP client connection #%d' % self.client_id
+ break
+ except socket.timeout:
+ # check if there are any incoming messages for this client
+ messages = self.seriald.incoming(self.client_id, pop=True)
+ for (id, msg) in messages:
+ print 'sending message %s to %d' % (msg.strip(), self.client_id)
+ self.request.send(msg)
+
+ def finish(self):
+ self.seriald.unregister_client(self.client_id)
+
+class TCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+ pass
+
+class SerialDaemon(object):
+ def __init__(self, ser):
+ assert(ser)
+
+ self.ser = ser # serial port
+ self.incoming_serial = [] # message queue from the serial port
+ self.outgoing_serial = [] # message queue writing to serial port
+ self.clients = set() # track a global list of clients for broadcast messages
+
+ self.client_lock = threading.Lock() # clients mutex
+ self.message_lock = threading.Lock() # message queues mutex
+
+ def register_client(self, client_id):
+ with self.client_lock:
+ self.clients.add(client_id)
+
+ def unregister_client(self, client_id):
+ print 'unregistering client ', client_id
+ with self.client_lock:
+ self.clients.remove(client_id)
+
+ # remove any messages related to this client
+ self.incoming(client_id, pop=True)
+ with self.message_lock:
+ self.outgoing_serial = filter(lambda (id, msg): id != client_id,
+ self.outgoing_serial)
+
+ def update(self):
+ """Called from main loop"""
+
+ valid_message = lambda msg: msg and not msg.startswith('#')
+
+ def broadcast(message):
+ for client in self.clients:
+ self.incoming_serial.append((client, msg))
+
+ # read and broadcast any new global messages
+ msg = self.ser.readline()
+ if valid_message(msg):
+ with self.message_lock: broadcast(msg)
+
+ # send client requests and accumulate responses
+ with self.message_lock:
+ while len(self.outgoing_serial):
+ (id, msg) = self.outgoing_serial.pop(0)
+ print 'writing "%s" to serial port' % msg
+ self.ser.write(msg + '\n')
+
+ # wait till we get a response to our query
+ if valid_message(msg):
+ attempts = 100
+ command = msg.split()[0]
+
+ while attempts:
+ response = self.ser.readline()
+ if valid_message(response):
+ print 'response "%s"\ncommand "%s"' % (response.strip(), command)
+ if response and command == response.split()[0]:
+ print 'matched response %s' % response.strip()
+ self.incoming_serial.append((id, response))
+ break
+ else:
+ broadcast(msg)
+
+ attempts -= 1
+ else:
+ # send out a timeout message
+ self.incoming_serial.append((id, '!timeout\r\n'))
+
+ def outgoing(self, (client_id, message)):
+ """Send a message to the serial port"""
+
+ with self.message_lock:
+ self.outgoing_serial.append((client_id, message))
+
+ def incoming(self, client_id, pop=True):
+ """
+ Get incoming messages from the serial port
+
+ client_id -- matches only incoming messages associated with the given
+ client id
+ pop -- remove matching messages from the incoming queue
+ """
+
+ messages = list()
+ popped_queue = list()
+
+ with self.message_lock:
+ for msg in self.incoming_serial:
+ if msg[0] == client_id:
+ messages.append(msg)
+ else:
+ popped_queue.append(msg)
+ if pop:
+ self.incoming_serial = popped_queue
+
+ return messages
+
+if __name__ == '__main__':
+ ser = serial.Serial(SERIAL_PORT, 9600, timeout=SERIAL_TIMEOUT)
+
+ try:
+ global SERIAL_DAEMON
+ SERIAL_DAEMON = SerialDaemon(ser)
+
+ # start up server thread
+ server = TCPServer(SERVER_HOST_PORT, TCPHandler)
+ server_thread = threading.Thread(target=server.serve_forever)
+ server_thread.setDaemon(True)
+ server_thread.start()
+
+ print 'Serial server initialized'
+ print ' -- listening on serial port %s and %s\n' % (
+ SERIAL_PORT, '%s:%d' % (SERVER_HOST_PORT))
+ while True:
+ # poll serial port
+ SERIAL_DAEMON.update()
+
+ except Exception, e:
+ print 'serialserver exception:', e
+ traceback.print_exc()
+ finally:
+ ser.close()
+
View
224 sensor/www/sensor.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python
+
+import web, serial
+import os, re, socket, datetime, sha, httplib
+
+import yaml
+import xml.dom.minidom
+
+import decree # http://bitbucket.org/dantakk/decree
+
+DOC = """\
+sensor.hackspace.ca - a <a href="http://vancouver.hackspace.ca">VHS</a> project
+
+<a href="/door/state">/door/state</a>
+ text/plain response: open|closed (entrance door)
+
+<a href="/door/photo">/door/photo</a>
+ image/* response
+
+<a href="/bathroom/door/state">/bathroom/door/state</a>
+ text/plain response: open|closed (bathroom door)
+
+<a href="/temperature/celsius">/temperature/celsius</a>
+ text/plain response: (\d+(\.\d*)?) (space temperature in celsius)
+
+<a href="/temperature/fahrenheit">/temperature/fahrenheit</a>
+ text/plain response: (\d+\.(\d*)?) (space temperature in fahrenheit)
+
+<a href="/buzz">/buzz</a>
+ text/plain response: (raw buzzer response)
+
+<a href="/feed/eeml">/feed/eeml</a>
+ text/xml response: an <a href="http://www.eeml.org">EEML</a> XML feed for use with <a href="http://www.pachube.com">pachube</a>
+"""
+
+SERIAL_HOST_PORT = ('localhost', 9994)
+
+# route urls to handler classes
+urls = (
+ r'/door/(state|photo)/?', 'Door',
+ r'/bathroom/door/(state)/?', 'BathroomDoor',
+ r'/temperature/(celsius|fahrenheit)/?', 'Temperature',
+ r'/buzz/?', 'Buzz',
+ r'/feed/(eeml)/?', 'Feed',
+ r'/feed/(pusheeml)/?', 'Feed',
+ r'.*', 'Static',
+)
+
+app = web.application(urls, globals())
+
+def serial_query(query):
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect(SERIAL_HOST_PORT)
+ s.send(query + '\n')
+ response = s.recv(1024)
+ s.close()
+ return response
+
+def get_celsius():
+ response = serial_query('temperature')
+ match = re.search(r'((\d+)(\.\d+)?)C?', response)
+ if match:
+ return match.groups()[0]
+ else:
+ return None
+
+def take_door_photo():
+ # based on lukec's code in VHS.pm
+ config = yaml.load(file('/etc/vhs.yaml'))
+ short_hash = sha.sha(str(datetime.datetime.now())).hexdigest()[0:6]
+ pic_base = config.get('picture_base')
+ if pic_base:
+ filename = os.path.join(pic_base, '%s.jpeg' % short_hash)
+ os.system('streamer -c /dev/video0 -b 16 -o %s' % filename)
+ short_file = os.path.splitext(filename)[0] + '.jpg'
+ os.rename(filename, short_file)
+ pic_uri_base = config.get('picture_uri_base')
+ if pic_uri_base and os.path.exists(short_file):
+ return '%s/%s' % (pic_uri_base, os.path.basename(short_file))
+
+ return None
+
+class Door(object):
+ def __init__(self, *args, **kw):
+ super(Door, self).__init__(*args, **kw)
+ self.doorname = self.response_cmd = 'door'
+
+ @property
+ def door_state(self):
+ response = serial_query('%s state' % self.doorname).strip()
+ if response == '%s closed' % self.response_cmd:
+ return 'closed'
+ elif response == '%s open' % self.response_cmd:
+ return 'opened'
+ else:
+ return '!unknown response <%s>' % response
+
+ def GET(self, query):
+ if query == 'state':
+ return self.door_state
+ elif query == 'photo':
+ door_photo_uri = take_door_photo()
+ # if door_photo_uri:
+ # raise web.seeother(door_photo_uri)
+ # return '!door photo not taken - check config'
+ return str(door_photo_uri)
+
+class BathroomDoor(Door):
+ def __init__(self, *args, **kw):
+ super(BathroomDoor, self).__init__(*args, **kw)
+ self.doorname = 'bathroom door'
+ self.response_cmd = 'bathroom'
+
+class Temperature:
+ @property
+ def celsius(self):
+ return get_celsius() or '#no match in response "%s"' % response
+
+ @property
+ def fahrenheit(self):
+ celsius = self.celsius
+
+ if celsius.startswith('#'):
+ return celsius
+
+ fahr = 1.8 * float(celsius) + 32
+ return '%.2f' % fahr
+
+ def GET(self, scale):
+ if scale == 'celsius':
+ return self.celsius
+ elif scale == 'fahrenheit':
+ return self.fahrenheit
+
+class Static:
+ def GET(self):
+ web.header('Content-Type', 'text/html')
+ return """\
+<html>
+ <head>
+ <title>sensor.hackspace.ca</title>
+ </head>
+ <body>
+ <pre>%s</pre>
+ </body>
+</html>
+""" % DOC
+
+class Buzz:
+ def GET(self):
+ return serial_query('buzz');
+
+class Feed:
+ @property
+ def eeml(self):
+ tag = decree.DecreeTagger()
+ xmlns = { 'xmlns': 'http://www.eeml.org/xsd/005',
+ 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation': 'http://www.eeml.org/xad/005 http://www.eeml.org/xsd/005/005.xsd',
+ }
+
+ try:
+ celsius = float(get_celsius())
+ except ValueError:
+ celsius = None
+
+ # TODO include properly formatted date
+ feed = tag.eeml[xmlns] (
+ tag.environment (
+ tag.title('Vancouver Hackerspace (VHS)'),
+ # tag.feed('http://www..com/feeds/1.xml'),
+ tag.description('VHS @ 45 West Hastings St'),
+ tag.website('http://vancouver.hackspace.ca'),
+ tag.email('info@hackspace.ca'),
+ tag.location[{ 'exposure': 'indoor',
+ 'domain': 'physical',
+ 'disposition': 'fixed'}] (
+ tag.name('VHS first floor'),
+ tag.lat('49.282319'),
+ tag.lon('-123.106152'),
+ ),
+ tag.data[{'id':'0'}] (
+ tag.tag('temperature'),
+ tag.value(celsius),
+ tag.unit[{'symbol': 'C', 'type': 'derivedSI'}]('Celsius')
+ ) if celsius else None,
+ )
+ )
+ builder = decree.XmlDomBuilder(xml.dom.minidom.getDOMImplementation())
+ doc = builder.create_xml_dom(feed)
+ # for debugging return doc.toprettyxml()
+ return doc.toxml()
+
+ def pachube_update(self):
+ config = yaml.load(file('/etc/vhs.yaml'))
+ pachube_apikey = config.get('pachube_apikey')
+ if pachube_apikey:
+ conn = httplib.HTTPConnection('www.pachube.com:80')
+ conn.request('PUT', 'http://www.pachube.com/api/2417.xml', self.eeml, {'X-PachubeApiKey': pachube_apikey})
+ rsp = conn.getresponse()
+ if rsp.status != 200:
+ raise Exception(rsp.reason)
+ response = rsp.read()
+ conn.close()
+
+ return response
+
+ return 'pachube_apikey not defined in /etc/vhs.yaml'
+
+ def GET(self, kind):
+ if kind == 'eeml':
+ web.header('Content-Type', 'text/xml')
+ return self.eeml
+ elif kind == 'pusheeml':
+ web.header('Content-Type', 'text/plain')
+ try:
+ return self.pachube_update()
+ except Exception, e:
+ return 'Error: %s' % e
+
+
+if __name__ == '__main__':
+ app.run()
+

0 comments on commit 3ffa07d

Please sign in to comment.
Something went wrong with that request. Please try again.