From 0c8c9ad976879f7c90f9915a60845ccb0cdb337d Mon Sep 17 00:00:00 2001 From: manuel Date: Wed, 25 Dec 2013 13:25:16 +0100 Subject: initial commit --- python/config | 136 +++++ python/native/bridge.c | 720 +++++++++++++++++++++++++++ python/native/cpuinfo.c | 65 +++ python/native/cpuinfo.h | 23 + python/native/gpio.c | 329 ++++++++++++ python/native/gpio.h | 73 +++ python/passwd | 1 + python/setup.py | 37 ++ python/webiopi-passwd.py | 57 +++ python/webiopi.init.sh | 155 ++++++ python/webiopi.sh | 2 + python/webiopi/__init__.py | 34 ++ python/webiopi/__main__.py | 79 +++ python/webiopi/clients/__init__.py | 209 ++++++++ python/webiopi/decorators/__init__.py | 0 python/webiopi/decorators/rest.py | 19 + python/webiopi/devices/__init__.py | 13 + python/webiopi/devices/analog/__init__.py | 267 ++++++++++ python/webiopi/devices/analog/ads1x1x.py | 82 +++ python/webiopi/devices/analog/mcp3x0x.py | 75 +++ python/webiopi/devices/analog/mcp4725.py | 38 ++ python/webiopi/devices/analog/mcp492X.py | 53 ++ python/webiopi/devices/analog/pca9685.py | 63 +++ python/webiopi/devices/bus.py | 117 +++++ python/webiopi/devices/digital/__init__.py | 144 ++++++ python/webiopi/devices/digital/ds2408.py | 84 ++++ python/webiopi/devices/digital/gpio.py | 189 +++++++ python/webiopi/devices/digital/mcp23XXX.py | 153 ++++++ python/webiopi/devices/digital/pcf8574.py | 70 +++ python/webiopi/devices/i2c.py | 75 +++ python/webiopi/devices/instance.py | 6 + python/webiopi/devices/manager.py | 77 +++ python/webiopi/devices/onewire.py | 74 +++ python/webiopi/devices/sensor/__init__.py | 177 +++++++ python/webiopi/devices/sensor/bmp085.py | 100 ++++ python/webiopi/devices/sensor/onewiretemp.py | 58 +++ python/webiopi/devices/sensor/tmpXXX.py | 60 +++ python/webiopi/devices/sensor/tslXXXX.py | 247 +++++++++ python/webiopi/devices/sensor/vcnl4000.py | 211 ++++++++ python/webiopi/devices/serial.py | 86 ++++ python/webiopi/devices/shield/__init__.py | 16 + python/webiopi/devices/shield/piface.py | 66 +++ python/webiopi/devices/spi.py | 145 ++++++ python/webiopi/protocols/__init__.py | 14 + python/webiopi/protocols/coap.py | 537 ++++++++++++++++++++ python/webiopi/protocols/http.py | 249 +++++++++ python/webiopi/protocols/rest.py | 254 ++++++++++ python/webiopi/server/__init__.py | 139 ++++++ python/webiopi/utils/__init__.py | 16 + python/webiopi/utils/config.py | 35 ++ python/webiopi/utils/crypto.py | 17 + python/webiopi/utils/loader.py | 26 + python/webiopi/utils/logger.py | 45 ++ python/webiopi/utils/thread.py | 50 ++ python/webiopi/utils/types.py | 30 ++ python/webiopi/utils/version.py | 29 ++ 56 files changed, 6126 insertions(+) create mode 100644 python/config create mode 100644 python/native/bridge.c create mode 100644 python/native/cpuinfo.c create mode 100644 python/native/cpuinfo.h create mode 100644 python/native/gpio.c create mode 100644 python/native/gpio.h create mode 100644 python/passwd create mode 100644 python/setup.py create mode 100755 python/webiopi-passwd.py create mode 100755 python/webiopi.init.sh create mode 100755 python/webiopi.sh create mode 100644 python/webiopi/__init__.py create mode 100644 python/webiopi/__main__.py create mode 100644 python/webiopi/clients/__init__.py create mode 100644 python/webiopi/decorators/__init__.py create mode 100644 python/webiopi/decorators/rest.py create mode 100644 python/webiopi/devices/__init__.py create mode 100644 python/webiopi/devices/analog/__init__.py create mode 100644 python/webiopi/devices/analog/ads1x1x.py create mode 100644 python/webiopi/devices/analog/mcp3x0x.py create mode 100644 python/webiopi/devices/analog/mcp4725.py create mode 100644 python/webiopi/devices/analog/mcp492X.py create mode 100644 python/webiopi/devices/analog/pca9685.py create mode 100644 python/webiopi/devices/bus.py create mode 100644 python/webiopi/devices/digital/__init__.py create mode 100644 python/webiopi/devices/digital/ds2408.py create mode 100644 python/webiopi/devices/digital/gpio.py create mode 100644 python/webiopi/devices/digital/mcp23XXX.py create mode 100644 python/webiopi/devices/digital/pcf8574.py create mode 100644 python/webiopi/devices/i2c.py create mode 100644 python/webiopi/devices/instance.py create mode 100644 python/webiopi/devices/manager.py create mode 100644 python/webiopi/devices/onewire.py create mode 100644 python/webiopi/devices/sensor/__init__.py create mode 100644 python/webiopi/devices/sensor/bmp085.py create mode 100644 python/webiopi/devices/sensor/onewiretemp.py create mode 100644 python/webiopi/devices/sensor/tmpXXX.py create mode 100644 python/webiopi/devices/sensor/tslXXXX.py create mode 100644 python/webiopi/devices/sensor/vcnl4000.py create mode 100644 python/webiopi/devices/serial.py create mode 100644 python/webiopi/devices/shield/__init__.py create mode 100644 python/webiopi/devices/shield/piface.py create mode 100644 python/webiopi/devices/spi.py create mode 100644 python/webiopi/protocols/__init__.py create mode 100644 python/webiopi/protocols/coap.py create mode 100644 python/webiopi/protocols/http.py create mode 100644 python/webiopi/protocols/rest.py create mode 100644 python/webiopi/server/__init__.py create mode 100644 python/webiopi/utils/__init__.py create mode 100644 python/webiopi/utils/config.py create mode 100644 python/webiopi/utils/crypto.py create mode 100644 python/webiopi/utils/loader.py create mode 100644 python/webiopi/utils/logger.py create mode 100644 python/webiopi/utils/thread.py create mode 100644 python/webiopi/utils/types.py create mode 100644 python/webiopi/utils/version.py (limited to 'python') diff --git a/python/config b/python/config new file mode 100644 index 0000000..77b99ec --- /dev/null +++ b/python/config @@ -0,0 +1,136 @@ +[GPIO] +# Initialize following GPIOs with given function and optional value +# This is used during WebIOPi start process +#21 = IN +#23 = OUT 0 +#24 = OUT 0 +#25 = OUT 1 + +#------------------------------------------------------------------------# + +[~GPIO] +# Reset following GPIOs with given function and optional value +# This is used at the end of WebIOPi stop process +#21 = IN +#23 = IN +#24 = IN +#25 = OUT 0 + +#------------------------------------------------------------------------# + +[SCRIPTS] +# Load custom scripts syntax : +# name = sourcefile +# each sourcefile may have setup, loop and destroy functions and macros +#myscript = /home/pi/webiopi/examples/scripts/macros/script.py + +#------------------------------------------------------------------------# + +[HTTP] +# HTTP Server configuration +enabled = true +port = 8000 + +# File containing sha256(base64("user:password")) +# Use webiopi-passwd command to generate it +passwd-file = /etc/webiopi/passwd + +# Use doc-root to change default HTML and resource files location +#doc-root = /home/pi/webiopi/examples/scripts/macros + +# Use welcome-file to change the default "Welcome" file +#welcome-file = index.html + +#------------------------------------------------------------------------# + +[COAP] +# CoAP Server configuration +enabled = true +port = 5683 +# Enable CoAP multicast +multicast = true + +#------------------------------------------------------------------------# + +[DEVICES] +# Device configuration syntax: +# name = device [args...] +# name : used in the URL mapping +# device : device name +# args : (optional) see device driver doc +# If enabled, devices configured here are mapped on REST API /device/name +# Devices are also accessible in custom scripts using deviceInstance(name) +# See device driver doc for methods and URI scheme available + +# Raspberry native UART on GPIO, uncomment to enable +# Don't forget to remove console on ttyAMA0 in /boot/cmdline.txt +# And also disable getty on ttyAMA0 in /etc/inittab +#serial0 = Serial device:ttyAMA0 baudrate:9600 + +# USB serial adapters +#usb0 = Serial device:ttyUSB0 baudrate:9600 +#usb1 = Serial device:ttyACM0 baudrate:9600 + +#temp0 = TMP102 +#temp1 = TMP102 slave:0x49 +#temp2 = DS18B20 +#temp3 = DS18B20 slave:28-0000049bc218 + +#bmp = BMP085 + +#gpio0 = PCF8574 +#gpio1 = PCF8574 slave:0x21 + +#light0 = TSL2561T +#light1 = TSL2561T slave:0b0101001 + +#gpio0 = MCP23017 +#gpio1 = MCP23017 slave:0x21 +#gpio2 = MCP23017 slave:0x22 + +#pwm0 = PCA9685 +#pwm1 = PCA9685 slave:0x41 + +#adc = MCP3008 +#dac = MCP4922 chip:1 + +#------------------------------------------------------------------------# + +[REST] +# By default, REST API allows to GET/POST on all GPIOs +# Use gpio-export to limit GPIO available through REST API +#gpio-export = 21, 23, 24, 25 + +# Uncomment to forbid changing GPIO values +#gpio-post-value = false + +# Uncomment to forbid changing GPIO functions +#gpio-post-function = false + +# Uncomment to disable automatic device mapping +#device-mapping = false + +#------------------------------------------------------------------------# + +[ROUTES] +# Custom REST API route syntax : +# source = destination +# source : URL to route +# destination : Resulting URL +# Adding routes allows to simplify access with Human comprehensive URLs + +# In the next example with have the bedroom light connected to GPIO 25 +# and a temperature sensor named temp2, defined in [DEVICES] section +# - GET /bedroom/light => GET /GPIO/25/value, returns the light state +# - POST /bedroom/light/0 => POST /GPIO/25/value/0, turn off the light +# - POST /bedroom/light/1 => POST /GPIO/25/value/1, turn on the light +# - GET /bedroom/temperature => GET /devices/temp2/temperature/c, returns the temperature in celsius +#/bedroom/light = /GPIO/25/value +#/bedroom/temperature = /devices/temp2/temperature/c + +#/livingroom/light = /devices/expander0/0 +#/livingroom/brightness = /devices/adc/0/float +#/livingroom/temperature = /devices/temp0/temperature/c + +#/weather/temperature = /devices/bmp/temperature/c +#/weather/pressure = /devices/bmp/pressure/hpa diff --git a/python/native/bridge.c b/python/native/bridge.c new file mode 100644 index 0000000..8b2b8da --- /dev/null +++ b/python/native/bridge.c @@ -0,0 +1,720 @@ +/* +Copyright (c) 2012 Ben Croston / 2012-2013 Eric PTAK + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "Python.h" +#include "gpio.h" +#include "cpuinfo.h" + +static PyObject *_SetupException; +static PyObject *_InvalidDirectionException; +static PyObject *_InvalidChannelException; +static PyObject *_InvalidPullException; + +static PyObject *_gpioCount; + + +static PyObject *_low; +static PyObject *_high; + +static PyObject *_in; +static PyObject *_out; +static PyObject *_alt0; +static PyObject *_alt1; +static PyObject *_alt2; +static PyObject *_alt3; +static PyObject *_alt4; +static PyObject *_alt5; +static PyObject *_pwm; + +static PyObject *_pud_off; +static PyObject *_pud_up; +static PyObject *_pud_down; + +static PyObject *_board_revision; + +static char* FUNCTIONS[] = {"IN", "OUT", "ALT5", "ALT4", "ALT0", "ALT1", "ALT2", "ALT3", "PWM"}; +static char* PWM_MODES[] = {"none", "ratio", "angle"}; + +static int module_state = -1; + +// setup function run on import of the RPi.GPIO module +static int module_setup(void) +{ + if (module_state == SETUP_OK) { + return SETUP_OK; + } + + module_state = setup(); + if (module_state == SETUP_DEVMEM_FAIL) + { + PyErr_SetString(_SetupException, "No access to /dev/mem. Try running as root!"); + } else if (module_state == SETUP_MALLOC_FAIL) { + PyErr_NoMemory(); + } else if (module_state == SETUP_MMAP_FAIL) { + PyErr_SetString(_SetupException, "Mmap failed on module import"); + } + + return module_state; +} + +// python function getFunction(channel) +static PyObject *py_get_function(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, f; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + f = get_function(channel); + return Py_BuildValue("i", f); +} + +// python function getFunctionString(channel) +static PyObject *py_get_function_string(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, f; + char *str; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + f = get_function(channel); + str = FUNCTIONS[f]; + return Py_BuildValue("s", str); +} + +// python function setFunction(channel, direction, pull_up_down=PUD_OFF) +static PyObject *py_set_function(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, function; + int pud = PUD_OFF; + static char *kwlist[] = {"channel", "function", "pull_up_down", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|i", kwlist, &channel, &function, &pud)) + return NULL; + + if (function != IN && function != OUT && function != PWM) + { + PyErr_SetString(_InvalidDirectionException, "Invalid function"); + return NULL; + } + + if (function == OUT || function == PWM) + pud = PUD_OFF; + + if (pud != PUD_OFF && pud != PUD_DOWN && pud != PUD_UP) + { + PyErr_SetString(_InvalidPullException, "Invalid value for pull_up_down - should be either PUD_OFF, PUD_UP or PUD_DOWN"); + return NULL; + } + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + set_function(channel, function, pud); + + Py_INCREF(Py_None); + return Py_None; +} + +// python function value = input(channel) +static PyObject *py_input(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + if (input(channel)) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + +// python function output(channel, value) +static PyObject *py_output(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, value; + static char *kwlist[] = {"channel", "value", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii", kwlist, &channel, &value)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + if (get_function(channel) != OUT) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT"); + return NULL; + } + + output(channel, value); + + Py_INCREF(Py_None); + return Py_None; +} + +// python function outputSequence(channel, period, sequence) +static PyObject *py_output_sequence(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, period; + char* sequence; + static char *kwlist[] = {"channel", "period", "sequence", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "iis", kwlist, &channel, &period, &sequence)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + if (get_function(channel) != OUT) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT"); + return NULL; + } + + outputSequence(channel, period, sequence); + + Py_INCREF(Py_None); + return Py_None; +} + + +static PyObject *py_pulseMilli(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, function, up, down; + static char *kwlist[] = {"channel", "up", "down", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "iii", kwlist, &channel, &up, &down)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + function = get_function(channel); + if ((function != OUT) && (function != PWM)) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT or PWM"); + return NULL; + } + + pulseMilli(channel, up, down); + + Py_INCREF(Py_None); + return Py_None; +} + + +static PyObject *py_pulseMilliRatio(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, function, width; + float ratio; + static char *kwlist[] = {"channel", "width", "ratio", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "iif", kwlist, &channel, &width, &ratio)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + function = get_function(channel); + if ((function != OUT) && (function != PWM)) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT or PWM"); + return NULL; + } + + pulseMilliRatio(channel, width, ratio); + + Py_INCREF(Py_None); + return Py_None; +} + + +static PyObject *py_pulseMicro(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, function, up, down; + static char *kwlist[] = {"channel", "up", "down", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "iii", kwlist, &channel, &up, &down)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + function = get_function(channel); + if ((function != OUT) && (function != PWM)) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT or PWM"); + return NULL; + } + + pulseMicro(channel, up, down); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *py_pulseMicroRatio(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, function, width; + float ratio; + static char *kwlist[] = {"channel", "width", "ratio", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "iif", kwlist, &channel, &width, &ratio)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + function = get_function(channel); + if ((function != OUT) && (function != PWM)) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT or PWM"); + return NULL; + } + + pulseMicroRatio(channel, width, ratio); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *py_pulseAngle(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, function; + float angle; + static char *kwlist[] = {"channel", "angle", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "if", kwlist, &channel, &angle)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + function = get_function(channel); + if ((function != OUT) && (function != PWM)) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT or PWM"); + return NULL; + } + + pulseAngle(channel, angle); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *py_pulseRatio(PyObject *self, PyObject *args, PyObject *kwargs) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel, function; + float ratio; + static char *kwlist[] = {"channel", "ratio", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "if", kwlist, &channel, &ratio)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + function = get_function(channel); + if ((function != OUT) && (function != PWM)) + { + PyErr_SetString(_InvalidDirectionException, "The GPIO channel is not an OUTPUT or PWM"); + return NULL; + } + + pulseRatio(channel, ratio); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyObject *py_pulse(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + pulseRatio(channel, 0.5); + return Py_None; +} + +static PyObject *py_getPulse(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel; + char str[256]; + struct pulse *p; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + p = getPulse(channel); + + sprintf(str, "%s:%.2f", PWM_MODES[p->type], p->value); +#if PY_MAJOR_VERSION > 2 + return PyUnicode_FromString(str); +#else + return PyString_FromString(str); +#endif +} + +static PyObject *py_enablePWM(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + enablePWM(channel); + return Py_None; +} + +static PyObject *py_disablePWM(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + disablePWM(channel); + return Py_None; +} + + + +static PyObject *py_isPWMEnabled(PyObject *self, PyObject *args) +{ + if (module_setup() != SETUP_OK) { + return NULL; + } + + int channel; + + if (!PyArg_ParseTuple(args, "i", &channel)) + return NULL; + + if (channel < 0 || channel >= GPIO_COUNT) + { + PyErr_SetString(_InvalidChannelException, "The GPIO channel is invalid"); + return NULL; + } + + if (isPWMEnabled(channel)) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + +PyMethodDef python_methods[] = { + {"getFunction", py_get_function, METH_VARARGS, "Return the current GPIO setup (IN, OUT, ALT0)"}, + {"getSetup", py_get_function, METH_VARARGS, "Return the current GPIO setup (IN, OUT, ALT0)"}, + + {"getFunctionString", py_get_function_string, METH_VARARGS, "Return the current GPIO setup (IN, OUT, ALT0) as string"}, + {"getSetupString", py_get_function_string, METH_VARARGS, "Return the current GPIO setup (IN, OUT, ALT0) as string"}, + + {"setFunction", (PyCFunction)py_set_function, METH_VARARGS | METH_KEYWORDS, "Setup the GPIO channel, direction and (optional) pull/up down control\nchannel - BCM GPIO number\ndirection - IN or OUT\n[pull_up_down] - PUD_OFF (default), PUD_UP or PUD_DOWN"}, + {"setup", (PyCFunction)py_set_function, METH_VARARGS | METH_KEYWORDS, "Setup the GPIO channel, direction and (optional) pull/up down control\nchannel - BCM GPIO number\ndirection - IN or OUT\n[pull_up_down] - PUD_OFF (default), PUD_UP or PUD_DOWN"}, + + {"input", py_input, METH_VARARGS, "Input from a GPIO channel - Deprecated, use digitalRead instead"}, + {"digitalRead", py_input, METH_VARARGS, "Read a GPIO channel"}, + + {"output", (PyCFunction)py_output, METH_VARARGS | METH_KEYWORDS, "Output to a GPIO channel - Deprecated, use digitalWrite instead"}, + {"digitalWrite", (PyCFunction)py_output, METH_VARARGS | METH_KEYWORDS, "Write to a GPIO channel"}, + + {"outputSequence", (PyCFunction)py_output_sequence, METH_VARARGS | METH_KEYWORDS, "Output a sequence to a GPIO channel"}, + + {"getPulse", py_getPulse, METH_VARARGS, "Read current PWM output"}, + {"pwmRead", py_getPulse, METH_VARARGS, "Read current PWM output"}, + + {"pulseMilli", (PyCFunction)py_pulseMilli, METH_VARARGS | METH_KEYWORDS, "Output a PWM to a GPIO channel using milliseconds for both HIGH and LOW state widths"}, + {"pulseMilliRatio", (PyCFunction)py_pulseMilliRatio, METH_VARARGS | METH_KEYWORDS, "Output a PWM to a GPIO channel using millisecond for the total width and a ratio (duty cycle) for the HIGH state width"}, + {"pulseMicro", (PyCFunction)py_pulseMicro, METH_VARARGS | METH_KEYWORDS, "Output a PWM pulse to a GPIO channel using microseconds for both HIGH and LOW state widths"}, + {"pulseMicroRatio", (PyCFunction)py_pulseMicroRatio, METH_VARARGS | METH_KEYWORDS, "Output a PWM to a GPIO channel using microseconds for the total width and a ratio (duty cycle) for the HIGH state width"}, + + {"pulseAngle", (PyCFunction)py_pulseAngle, METH_VARARGS | METH_KEYWORDS, "Output a PWM to a GPIO channel using an angle - Deprecated, use pwmWriteAngle instead"}, + {"pwmWriteAngle", (PyCFunction)py_pulseAngle, METH_VARARGS | METH_KEYWORDS, "Output a PWM to a GPIO channel using an angle"}, + + {"pulseRatio", (PyCFunction)py_pulseRatio, METH_VARARGS | METH_KEYWORDS, "Output a PWM to a GPIO channel using a ratio (duty cycle) with the default 50Hz signal - Deprecated, use pwmWrite instead"}, + {"pwmWrite", (PyCFunction)py_pulseRatio, METH_VARARGS | METH_KEYWORDS, "Output a PWM to a GPIO channel using a ratio (duty cycle) with the default 50Hz signal"}, + + {"pulse", py_pulse, METH_VARARGS, "Output a PWM to a GPIO channel using a 50% ratio (duty cycle) with the default 50Hz signal"}, + + {"enablePWM", py_enablePWM, METH_VARARGS, "Enable software PWM loop for a GPIO channel"}, + {"disablePWM", py_disablePWM, METH_VARARGS, "Disable software PWM loop of a GPIO channel"}, + {"isPWMEnabled", py_isPWMEnabled, METH_VARARGS, "Returns software PWM state"}, + + {NULL, NULL, 0, NULL} +}; + +#if PY_MAJOR_VERSION > 2 +static struct PyModuleDef python_module = { + PyModuleDef_HEAD_INIT, + "_webiopi.GPIO", /* name of module */ + NULL, /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, + or -1 if the module keeps state in global variables. */ + python_methods +}; +#endif + +#if PY_MAJOR_VERSION > 2 +PyMODINIT_FUNC PyInit_GPIO(void) +#else +PyMODINIT_FUNC initGPIO(void) +#endif +{ + PyObject *module = NULL; + int revision = -1; + +#if PY_MAJOR_VERSION > 2 + if ((module = PyModule_Create(&python_module)) == NULL) + goto exit; +#else + if ((module = Py_InitModule("_webiopi.GPIO", python_methods)) == NULL) + goto exit; +#endif + + _SetupException = PyErr_NewException("_webiopi.GPIO.SetupException", NULL, NULL); + PyModule_AddObject(module, "SetupException", _SetupException); + + _InvalidDirectionException = PyErr_NewException("_webiopi.GPIO.InvalidDirectionException", NULL, NULL); + PyModule_AddObject(module, "InvalidDirectionException", _InvalidDirectionException); + + _InvalidChannelException = PyErr_NewException("_webiopi.GPIO.InvalidChannelException", NULL, NULL); + PyModule_AddObject(module, "InvalidChannelException", _InvalidChannelException); + + _InvalidPullException = PyErr_NewException("_webiopi.GPIO.InvalidPullException", NULL, NULL); + PyModule_AddObject(module, "InvalidPullException", _InvalidPullException); + + _gpioCount = Py_BuildValue("i", GPIO_COUNT); + PyModule_AddObject(module, "GPIO_COUNT", _gpioCount); + + _low = Py_BuildValue("i", LOW); + PyModule_AddObject(module, "LOW", _low); + + _high = Py_BuildValue("i", HIGH); + PyModule_AddObject(module, "HIGH", _high); + + _in = Py_BuildValue("i", IN); + PyModule_AddObject(module, "IN", _in); + + _out = Py_BuildValue("i", OUT); + PyModule_AddObject(module, "OUT", _out); + + _alt0 = Py_BuildValue("i", ALT0); + PyModule_AddObject(module, "ALT0", _alt0); + + _alt1 = Py_BuildValue("i", ALT1); + PyModule_AddObject(module, "ALT1", _alt1); + + _alt2 = Py_BuildValue("i", ALT2); + PyModule_AddObject(module, "ALT2", _alt2); + + _alt3 = Py_BuildValue("i", ALT3); + PyModule_AddObject(module, "ALT3", _alt3); + + _alt4 = Py_BuildValue("i", ALT4); + PyModule_AddObject(module, "ALT4", _alt4); + + _alt5 = Py_BuildValue("i", ALT5); + PyModule_AddObject(module, "ALT5", _alt5); + + _pwm = Py_BuildValue("i", PWM); + PyModule_AddObject(module, "PWM", _pwm); + + _pud_off = Py_BuildValue("i", PUD_OFF); + PyModule_AddObject(module, "PUD_OFF", _pud_off); + + _pud_up = Py_BuildValue("i", PUD_UP); + PyModule_AddObject(module, "PUD_UP", _pud_up); + + _pud_down = Py_BuildValue("i", PUD_DOWN); + PyModule_AddObject(module, "PUD_DOWN", _pud_down); + + // detect board revision and set up accordingly + revision = get_rpi_revision(); + if (revision == -1) + { + PyErr_SetString(_SetupException, "This module can only be run on a Raspberry Pi!"); +#if PY_MAJOR_VERSION > 2 + return NULL; +#else + return; +#endif + } + + _board_revision = Py_BuildValue("i", revision); + PyModule_AddObject(module, "BOARD_REVISION", _board_revision); + + if (Py_AtExit(cleanup) != 0) + { + cleanup(); +#if PY_MAJOR_VERSION > 2 + return NULL; +#else + return; +#endif + } + +exit: +#if PY_MAJOR_VERSION > 2 + return module; +#else + return; +#endif +} diff --git a/python/native/cpuinfo.c b/python/native/cpuinfo.c new file mode 100644 index 0000000..a69d97e --- /dev/null +++ b/python/native/cpuinfo.c @@ -0,0 +1,65 @@ +/* +Copyright (c) 2012 Ben Croston + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include "cpuinfo.h" + +char *get_cpuinfo_revision(char *revision) +{ + FILE *fp; + char buffer[1024]; + char hardware[1024]; + int rpi_found = 0; + + if ((fp = fopen("/proc/cpuinfo", "r")) == NULL) + return 0; + + while(!feof(fp)) { + fgets(buffer, sizeof(buffer) , fp); + sscanf(buffer, "Hardware : %s", hardware); + if (strcmp(hardware, "BCM2708") == 0) + rpi_found = 1; + sscanf(buffer, "Revision : %s", revision); + } + fclose(fp); + + if (!rpi_found) + revision = NULL; + return revision; +} + +int get_rpi_revision(void) +{ + char revision[1024] = {'\0'}; + + if (get_cpuinfo_revision(revision) == NULL) + return -1; + + if ((strcmp(revision, "0002") == 0) || + (strcmp(revision, "1000002") == 0 ) || + (strcmp(revision, "0003") == 0) || + (strcmp(revision, "1000003") == 0 )) + return 1; + else // assume rev 2 (0004 0005 0006 1000004 1000005 1000006) + return 2; +} diff --git a/python/native/cpuinfo.h b/python/native/cpuinfo.h new file mode 100644 index 0000000..e84ea7d --- /dev/null +++ b/python/native/cpuinfo.h @@ -0,0 +1,23 @@ +/* +Copyright (c) 2012 Ben Croston + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +int get_rpi_revision(void); diff --git a/python/native/gpio.c b/python/native/gpio.c new file mode 100644 index 0000000..3950b16 --- /dev/null +++ b/python/native/gpio.c @@ -0,0 +1,329 @@ +/* +Copyright (c) 2012 Ben Croston / 2012-2013 Eric PTAK + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include "gpio.h" + +#define BCM2708_PERI_BASE 0x20000000 +#define GPIO_BASE (BCM2708_PERI_BASE + 0x200000) +#define FSEL_OFFSET 0 // 0x0000 +#define SET_OFFSET 7 // 0x001c / 4 +#define CLR_OFFSET 10 // 0x0028 / 4 +#define PINLEVEL_OFFSET 13 // 0x0034 / 4 +#define EVENT_DETECT_OFFSET 16 // 0x0040 / 4 +#define RISING_ED_OFFSET 19 // 0x004c / 4 +#define FALLING_ED_OFFSET 22 // 0x0058 / 4 +#define HIGH_DETECT_OFFSET 25 // 0x0064 / 4 +#define LOW_DETECT_OFFSET 28 // 0x0070 / 4 +#define PULLUPDN_OFFSET 37 // 0x0094 / 4 +#define PULLUPDNCLK_OFFSET 38 // 0x0098 / 4 + +#define PAGE_SIZE (4*1024) +#define BLOCK_SIZE (4*1024) + +static volatile uint32_t *gpio_map; + +struct tspair { + struct timespec up; + struct timespec down; +}; + +static struct pulse gpio_pulses[GPIO_COUNT]; +static struct tspair gpio_tspairs[GPIO_COUNT]; +static pthread_t *gpio_threads[GPIO_COUNT]; + +void short_wait(void) +{ + int i; + + for (i=0; i<150; i++) // wait 150 cycles + { + asm volatile("nop"); + } +} + +int setup(void) +{ + int mem_fd; + uint8_t *gpio_mem; + + if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) + { + return SETUP_DEVMEM_FAIL; + } + + if ((gpio_mem = malloc(BLOCK_SIZE + (PAGE_SIZE-1))) == NULL) + return SETUP_MALLOC_FAIL; + + if ((uint32_t)gpio_mem % PAGE_SIZE) + gpio_mem += PAGE_SIZE - ((uint32_t)gpio_mem % PAGE_SIZE); + + gpio_map = (uint32_t *)mmap( (caddr_t)gpio_mem, BLOCK_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_FIXED, mem_fd, GPIO_BASE); + + if ((uint32_t)gpio_map < 0) + return SETUP_MMAP_FAIL; + + return SETUP_OK; +} + +void set_pullupdn(int gpio, int pud) +{ + int clk_offset = PULLUPDNCLK_OFFSET + (gpio/32); + int shift = (gpio%32); + + if (pud == PUD_DOWN) + *(gpio_map+PULLUPDN_OFFSET) = (*(gpio_map+PULLUPDN_OFFSET) & ~3) | PUD_DOWN; + else if (pud == PUD_UP) + *(gpio_map+PULLUPDN_OFFSET) = (*(gpio_map+PULLUPDN_OFFSET) & ~3) | PUD_UP; + else // pud == PUD_OFF + *(gpio_map+PULLUPDN_OFFSET) &= ~3; + + short_wait(); + *(gpio_map+clk_offset) = 1 << shift; + short_wait(); + *(gpio_map+PULLUPDN_OFFSET) &= ~3; + *(gpio_map+clk_offset) = 0; +} + +//updated Eric PTAK - trouch.com +void set_function(int gpio, int function, int pud) +{ + if (function == PWM) { + function = OUT; + enablePWM(gpio); + } + else { + disablePWM(gpio); + } + + int offset = FSEL_OFFSET + (gpio/10); + int shift = (gpio%10)*3; + + set_pullupdn(gpio, pud); + *(gpio_map+offset) = (*(gpio_map+offset) & ~(7<>= shift; + value &= 7; + if ((value == OUT) && isPWMEnabled(gpio)) { + value = PWM; + } + return value; // 0=input, 1=output, 4=alt0 +} + +//updated Eric PTAK - trouch.com +int input(int gpio) +{ + int offset, value, mask; + + offset = PINLEVEL_OFFSET + (gpio/32); + mask = (1 << gpio%32); + value = *(gpio_map+offset) & mask; + return value; +} + +void output(int gpio, int value) +{ + int offset, shift; + + if (value) // value == HIGH + offset = SET_OFFSET + (gpio/32); + else // value == LOW + offset = CLR_OFFSET + (gpio/32); + + shift = (gpio%32); + + *(gpio_map+offset) = 1 << shift; +} + +//added Eric PTAK - trouch.com +void outputSequence(int gpio, int period, char* sequence) { + int i, value; + struct timespec ts; + ts.tv_sec = period/1000; + ts.tv_nsec = (period%1000) * 1000000; + + for (i=0; sequence[i] != '\0'; i++) { + if (sequence[i] == '1') { + value = 1; + } + else { + value = 0; + } + output(gpio, value); + nanosleep(&ts, NULL); + } +} + +void resetPWM(int gpio) { + gpio_pulses[gpio].type = 0; + gpio_pulses[gpio].value = 0; + + gpio_tspairs[gpio].up.tv_sec = 0; + gpio_tspairs[gpio].up.tv_nsec = 0; + gpio_tspairs[gpio].down.tv_sec = 0; + gpio_tspairs[gpio].down.tv_nsec = 0; +} + +//added Eric PTAK - trouch.com +void pulseTS(int gpio, struct timespec *up, struct timespec *down) { + if ((up->tv_sec > 0) || (up->tv_nsec > 0)) { + output(gpio, 1); + nanosleep(up, NULL); + } + + if ((down->tv_sec > 0) || (down->tv_nsec > 0)) { + output(gpio, 0); + nanosleep(down, NULL); + } +} + +//added Eric PTAK - trouch.com +void pulseOrSaveTS(int gpio, struct timespec *up, struct timespec *down) { + if (gpio_threads[gpio] != NULL) { + memcpy(&gpio_tspairs[gpio].up, up, sizeof(struct timespec)); + memcpy(&gpio_tspairs[gpio].down, down, sizeof(struct timespec)); + } + else { + pulseTS(gpio, up, down); + } +} + +//added Eric PTAK - trouch.com +void pulseMilli(int gpio, int up, int down) { + struct timespec tsUP, tsDOWN; + + tsUP.tv_sec = up/1000; + tsUP.tv_nsec = (up%1000) * 1000000; + + tsDOWN.tv_sec = down/1000; + tsDOWN.tv_nsec = (down%1000) * 1000000; + pulseOrSaveTS(gpio, &tsUP, &tsDOWN); +} + +//added Eric PTAK - trouch.com +void pulseMilliRatio(int gpio, int width, float ratio) { + int up = ratio*width; + int down = width - up; + pulseMilli(gpio, up, down); +} + +//added Eric PTAK - trouch.com +void pulseMicro(int gpio, int up, int down) { + struct timespec tsUP, tsDOWN; + + tsUP.tv_sec = 0; + tsUP.tv_nsec = up * 1000; + + tsDOWN.tv_sec = 0; + tsDOWN.tv_nsec = down * 1000; + pulseOrSaveTS(gpio, &tsUP, &tsDOWN); +} + +//added Eric PTAK - trouch.com +void pulseMicroRatio(int gpio, int width, float ratio) { + int up = ratio*width; + int down = width - up; + pulseMicro(gpio, up, down); +} + +//added Eric PTAK - trouch.com +void pulseAngle(int gpio, float angle) { + gpio_pulses[gpio].type = ANGLE; + gpio_pulses[gpio].value = angle; + int up = 1520 + (angle*400)/45; + int down = 20000-up; + pulseMicro(gpio, up, down); +} + +//added Eric PTAK - trouch.com +void pulseRatio(int gpio, float ratio) { + gpio_pulses[gpio].type = RATIO; + gpio_pulses[gpio].value = ratio; + int up = ratio * 20000; + int down = 20000 - up; + pulseMicro(gpio, up, down); +} + +struct pulse* getPulse(int gpio) { + return &gpio_pulses[gpio]; +} + +//added Eric PTAK - trouch.com +void* pwmLoop(void* data) { + int gpio = (int)data; + + while (1) { + pulseTS(gpio, &gpio_tspairs[gpio].up, &gpio_tspairs[gpio].down); + } +} + +//added Eric PTAK - trouch.com +void enablePWM(int gpio) { + pthread_t *thread = gpio_threads[gpio]; + if (thread != NULL) { + return; + } + + resetPWM(gpio); + + thread = (pthread_t*) malloc(sizeof(pthread_t)); + pthread_create(thread, NULL, pwmLoop, (void*)gpio); + gpio_threads[gpio] = thread; +} + +//added Eric PTAK - trouch.com +void disablePWM(int gpio) { + pthread_t *thread = gpio_threads[gpio]; + if (thread == NULL) { + return; + } + + pthread_cancel(*thread); + gpio_threads[gpio] = NULL; + output(gpio, 0); + resetPWM(gpio); +} + +//added Eric PTAK - trouch.com +int isPWMEnabled(int gpio) { + return gpio_threads[gpio] != NULL; +} + + +void cleanup(void) +{ + // fixme - set all gpios back to input + munmap((caddr_t)gpio_map, BLOCK_SIZE); +} diff --git a/python/native/gpio.h b/python/native/gpio.h new file mode 100644 index 0000000..cb0147c --- /dev/null +++ b/python/native/gpio.h @@ -0,0 +1,73 @@ +/* +Copyright (c) 2012 Ben Croston / 2012-2013 Eric PTAK + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#define SETUP_OK 0 +#define SETUP_DEVMEM_FAIL 1 +#define SETUP_MALLOC_FAIL 2 +#define SETUP_MMAP_FAIL 3 + +#define GPIO_COUNT 54 + +#define IN 0 +#define OUT 1 +#define ALT5 2 +#define ALT4 3 +#define ALT0 4 +#define ALT1 5 +#define ALT2 6 +#define ALT3 7 +#define PWM 8 + +#define LOW 0 +#define HIGH 1 + +#define PUD_OFF 0 +#define PUD_DOWN 1 +#define PUD_UP 2 + +#define RATIO 1 +#define ANGLE 2 + +struct pulse { + int type; + float value; +}; + +int setup(void); +int get_function(int gpio); +void set_function(int gpio, int function, int pud); +int input(int gpio); +void output(int gpio, int value); +void outputSequence(int gpio, int period, char* sequence); +struct pulse* getPulse(int gpio); +void pulseMilli(int gpio, int up, int down); +void pulseMilliRatio(int gpio, int width, float ratio); +void pulseMicro(int gpio, int up, int down); +void pulseMicroRatio(int gpio, int width, float ratio); +void pulseAngle(int gpio, float angle); +void pulseRatio(int gpio, float ratio); +void enablePWM(int gpio); +void disablePWM(int gpio); +int isPWMEnabled(int gpio); + +void cleanup(void); + diff --git a/python/passwd b/python/passwd new file mode 100644 index 0000000..4639dc4 --- /dev/null +++ b/python/passwd @@ -0,0 +1 @@ +a4f849b74f8d12e35fad61c06489b70676affd6ddc599fa3de47210e351b7875 \ No newline at end of file diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..671cb27 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup, Extension + +classifiers = ['Development Status :: 3 - Alpha', + 'Operating System :: POSIX :: Linux', + 'License :: OSI Approved :: MIT License', + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Topic :: Software Development', + 'Topic :: Home Automation', + 'Topic :: System :: Hardware'] + +setup(name = 'WebIOPi', + version = '0.6.2', + author = 'Eric PTAK', + author_email = 'trouch@trouch.com', + description = 'A package to control Raspberry Pi GPIO from the web', + long_description = open('../doc/README').read(), + license = 'MIT', + keywords = 'RaspberryPi GPIO Python REST', + url = 'http://code.google.com/p/webiopi/', + classifiers = classifiers, + packages = ["webiopi", + "webiopi.utils", + "webiopi.clients", + "webiopi.protocols", + "webiopi.server", + "webiopi.decorators", + "webiopi.devices", + "webiopi.devices.digital", + "webiopi.devices.analog", + "webiopi.devices.sensor", + "webiopi.devices.shield" + ], + ext_modules = [Extension('_webiopi.GPIO', ['native/bridge.c', 'native/gpio.c', 'native/cpuinfo.c'])], + ) diff --git a/python/webiopi-passwd.py b/python/webiopi-passwd.py new file mode 100755 index 0000000..7c0fbff --- /dev/null +++ b/python/webiopi-passwd.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +file = None + +print("WebIOPi passwd file generator") +if len(sys.argv) == 2: + file = sys.argv[1] + if file == "--help" or file == "-h": + print("Usage: webiopi-passwd [--help|file]") + print("Compute and display hash used by WebIOPi for Authentication") + print("Login and Password are prompted") + print("\t--help\tDisplay this help") + print("\t-h") + print("\tfile\tSave hash to file") + sys.exit() +else: + file = "/etc/webiopi/passwd" + +f = open(file, "w") +_LOGIN = "Enter Login: " +_PASSWORD = "Enter Password: " +_CONFIRM = "Confirm password: " +_DONTMATCH = "Passwords don't match !" + +import getpass +try: + login = raw_input(_LOGIN) +except NameError: + login = input(_LOGIN) +password = getpass.getpass(_PASSWORD) +password2 = getpass.getpass(_CONFIRM) +while password != password2: + print(_DONTMATCH) + password = getpass.getpass(_PASSWORD) + password2 = getpass.getpass(_CONFIRM) + +from webiopi.utils.crypto import encryptCredentials +auth = encryptCredentials(login, password) +print("\nHash: %s" % auth) +if file: + f.write(auth) + f.close() + print("Saved to %s" % file) diff --git a/python/webiopi.init.sh b/python/webiopi.init.sh new file mode 100755 index 0000000..29457f5 --- /dev/null +++ b/python/webiopi.init.sh @@ -0,0 +1,155 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: webiopi +# Required-Start: $remote_fs $syslog $network +# Required-Stop: $remote_fs $syslog $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: WebIOPi initscript +# Description: WebIOPi initscript +### END INIT INFO + +# Author: trouch +LOG_FILE=/var/log/webiopi +CONFIG_FILE=/etc/webiopi/config + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="WebIOPi" +NAME=webiopi +HOME=/usr/share/webiopi/htdocs +DAEMON=/usr/bin/python +DAEMON_ARGS="-m webiopi -l $LOG_FILE -c $CONFIG_FILE" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --chdir $HOME --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --chdir $HOME --pidfile $PIDFILE --exec $DAEMON --background --make-pidfile -- \ + $DAEMON_ARGS \ + || return 2 + # Add code here, if necessary, that waits for the process to be ready + # to handle requests from services started subsequently which depend + # on this one. As a last resort, sleep for some time. +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/python/webiopi.sh b/python/webiopi.sh new file mode 100755 index 0000000..0cf7bb5 --- /dev/null +++ b/python/webiopi.sh @@ -0,0 +1,2 @@ +#!/bin/sh +python -m webiopi $* diff --git a/python/webiopi/__init__.py b/python/webiopi/__init__.py new file mode 100644 index 0000000..c0f811e --- /dev/null +++ b/python/webiopi/__init__.py @@ -0,0 +1,34 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from time import sleep + +from webiopi.utils.version import BOARD_REVISION, VERSION +from webiopi.utils.logger import setInfo, setDebug, info, debug, warn, error, exception +from webiopi.utils.thread import runLoop +from webiopi.server import Server +from webiopi.devices.instance import deviceInstance +from webiopi.decorators.rest import macro + +from webiopi.devices import bus as _bus + +try: + import _webiopi.GPIO as GPIO +except: + pass + + +setInfo() +_bus.checkAllBus() diff --git a/python/webiopi/__main__.py b/python/webiopi/__main__.py new file mode 100644 index 0000000..dc57bc2 --- /dev/null +++ b/python/webiopi/__main__.py @@ -0,0 +1,79 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from webiopi.server import Server +from webiopi.utils.loader import loadScript +from webiopi.utils.logger import exception, setDebug, info, logToFile +from webiopi.utils.version import VERSION_STRING +from webiopi.utils.thread import runLoop, stop + +def displayHelp(): + print("WebIOPi command-line usage") + print("webiopi [-h] [-c config] [-l log] [-s script] [-d] [port]") + print("") + print("Options:") + print(" -h, --help Display this help") + print(" -c, --config file Load config from file") + print(" -l, --log file Log to file") + print(" -s, --script file Load script from file") + print(" -d, --debug Enable DEBUG") + print("") + print("Arguments:") + print(" port Port to bind the HTTP Server") + exit() + +def main(argv): + port = 8000 + configfile = None + logfile = None + + i = 1 + while i < len(argv): + if argv[i] in ["-c", "-C", "--config-file"]: + configfile = argv[i+1] + i+=1 + elif argv[i] in ["-l", "-L", "--log-file"]: + logfile = argv[i+1] + i+=1 + elif argv[i] in ["-s", "-S", "--script-file"]: + scriptfile = argv[i+1] + scriptname = scriptfile.split("/")[-1].split(".")[0] + loadScript(scriptname, scriptfile) + i+=1 + elif argv[i] in ["-h", "-H", "--help"]: + displayHelp() + elif argv[i] in ["-d", "--debug"]: + setDebug() + else: + try: + port = int(argv[i]) + except ValueError: + displayHelp() + i+=1 + + if logfile: + logToFile(logfile) + + info("Starting %s" % VERSION_STRING) + server = Server(port=port, configfile=configfile) + runLoop() + server.stop() + +if __name__ == "__main__": + try: + main(sys.argv) + except Exception as e: + exception(e) + stop() diff --git a/python/webiopi/clients/__init__.py b/python/webiopi/clients/__init__.py new file mode 100644 index 0000000..d8527af --- /dev/null +++ b/python/webiopi/clients/__init__.py @@ -0,0 +1,209 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.logger import LOGGER +from webiopi.utils.version import PYTHON_MAJOR +from webiopi.utils.crypto import encodeCredentials +from webiopi.protocols.coap import COAPClient, COAPGet, COAPPost, COAPPut, COAPDelete + +if PYTHON_MAJOR >= 3: + import http.client as httplib +else: + import httplib + +class PiMixedClient(): + def __init__(self, host, port=8000, coap=5683): + self.host = host + if coap > 0: + self.coapport = coap + self.coapclient = COAPClient() + else: + self.coapclient = None + if port > 0: + self.httpclient = httplib.HTTPConnection(host, port) + else: + self.httpclient = None + self.forceHttp = False + self.coapfailure = 0 + self.maxfailure = 2 + self.auth= None; + + def setCredentials(self, login, password): + self.auth = "Basic " + encodeCredentials(login, password) + + def sendRequest(self, method, uri): + if self.coapclient != None and not self.forceHttp: + if method == "GET": + response = self.coapclient.sendRequest(COAPGet("coap://%s:%d%s" % (self.host, self.coapport, uri))) + elif method == "POST": + response = self.coapclient.sendRequest(COAPPost("coap://%s:%d%s" % (self.host, self.coapport, uri))) + + if response: + return str(response.payload) + elif self.httpclient != None: + self.coapfailure += 1 + print("No CoAP response, fall-back to HTTP") + if (self.coapfailure > self.maxfailure): + self.forceHttp = True + self.coapfailure = 0 + print("Too many CoAP failure forcing HTTP") + + if self.httpclient != None: + headers = {} + if self.auth != None: + headers["Authorization"] = self.auth + + self.httpclient.request(method, uri, None, headers) + response = self.httpclient.getresponse() + if response.status == 200: + data = response.read() + return data + elif response.status == 401: + raise Exception("Missing credentials") + else: + raise Exception("Unhandled HTTP Response %d %s" % (response.status, response.reason)) + + raise Exception("No data received") + +class PiHttpClient(PiMixedClient): + def __init__(self, host, port=8000): + PiMixedClient.__init__(self, host, port, -1) + +class PiCoapClient(PiMixedClient): + def __init__(self, host, port=5683): + PiMixedClient.__init__(self, host, -1, port) + +class PiMulticastClient(PiMixedClient): + def __init__(self, port=5683): + PiMixedClient.__init__(self, "224.0.1.123", -1, port) + +class RESTAPI(): + def __init__(self, client, path): + self.client = client + self.path = path + + def sendRequest(self, method, path): + return self.client.sendRequest(method, self.path + path) + +class Macro(RESTAPI): + def __init__(self, client, name): + RESTAPI.__init__(self, client, "/macros/" + name + "/") + + def call(self, *args): + values = ",".join(["%s" % i for i in args]) + if values == None: + values = "" + return self.sendRequest("POST", values) + +class Device(RESTAPI): + def __init__(self, client, name, category): + RESTAPI.__init__(self, client, "/devices/" + name + "/" + category) + +class GPIO(Device): + def __init__(self, client, name): + Device.__init__(self, client, name, "digital") + + def getFunction(self, channel): + return self.sendRequest("GET", "/%d/function" % channel) + + def setFunction(self, channel, func): + return self.sendRequest("POST", "/%d/function/%s" % (channel, func)) + + def digitalRead(self, channel): + return int(self.sendRequest("GET", "/%d/value" % channel)) + + def digitalWrite(self, channel, value): + return int(self.sendRequest("POST", "/%d/value/%d" % (channel, value))) + + def portRead(self): + return int(self.sendRequest("GET", "/integer")) + + def portWrite(self, value): + return int(self.sendRequest("POST", "/integer/%d" % value)) + +class NativeGPIO(GPIO): + def __init__(self, client): + RESTAPI.__init__(self, client, "/GPIO") + +class ADC(Device): + def __init__(self, client, name): + Device.__init__(self, client, name, "analog") + + def read(self, channel): + return float(self.sendRequest("GET", "/%d/integer" % channel)) + + def readFloat(self, channel): + return float(self.sendRequest("GET", "/%d/float" % channel)) + + def readVolt(self, channel): + return float(self.sendRequest("GET", "/%d/volt" % channel)) + +class DAC(ADC): + def __init__(self, client, name): + Device.__init__(self, client, name, "analog") + + def write(self, channel, value): + return float(self.sendRequest("POST", "/%d/integer/%d" % (channel, value))) + + def writeFloat(self, channel, value): + return float(self.sendRequest("POST", "/%d/float/%f" % (channel, value))) + + def writeVolt(self, channel, value): + return float(self.sendRequest("POST", "/%d/volt/%f" % (channel, value))) + +class PWM(DAC): + def __init__(self, client, name): + Device.__init__(self, client, name, "pwm") + + def readAngle(self, channel, value): + return float(self.sendRequest("GET", "/%d/angle" % (channel))) + + def writeAngle(self, channel, value): + return float(self.sendRequest("POST", "/%d/angle/%f" % (channel, value))) + +class Sensor(Device): + def __init__(self, client, name): + Device.__init__(self, client, name, "sensor") + +class Temperature(Sensor): + def getKelvin(self): + return float(self.sendRequest("GET", "/temperature/k")) + + def getCelsius(self): + return float(self.sendRequest("GET", "/temperature/c")) + + def getFahrenheit(self): + return float(self.sendRequest("GET", "/temperature/f")) + +class Pressure(Sensor): + def getPascal(self): + return float(self.sendRequest("GET", "/pressure/pa")) + + def getHectoPascal(self): + return float(self.sendRequest("GET", "/pressure/hpa")) + +class Luminosity(Sensor): + def getLux(self): + return float(self.sendRequest("GET", "/luminosity/lux")) + +class Distance(Sensor): + def getMillimeter(self): + return float(self.sendRequest("GET", "/distance/mm")) + + def getCentimeter(self): + return float(self.sendRequest("GET", "/distance/cm")) + + def getInch(self): + return float(self.sendRequest("GET", "/distance/in")) + diff --git a/python/webiopi/decorators/__init__.py b/python/webiopi/decorators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/webiopi/decorators/rest.py b/python/webiopi/decorators/rest.py new file mode 100644 index 0000000..7fffacc --- /dev/null +++ b/python/webiopi/decorators/rest.py @@ -0,0 +1,19 @@ +def request(method="GET", path="", data=None): + def wrapper(func): + func.routed = True + func.method = method + func.path = path + func.data = data + return func + return wrapper + +def response(fmt="%s", contentType="text/plain"): + def wrapper(func): + func.format = fmt + func.contentType = contentType + return func + return wrapper + +def macro(func): + func.macro = True + return func diff --git a/python/webiopi/devices/__init__.py b/python/webiopi/devices/__init__.py new file mode 100644 index 0000000..ecd45ec --- /dev/null +++ b/python/webiopi/devices/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/python/webiopi/devices/analog/__init__.py b/python/webiopi/devices/analog/__init__.py new file mode 100644 index 0000000..4c94b9c --- /dev/null +++ b/python/webiopi/devices/analog/__init__.py @@ -0,0 +1,267 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.decorators.rest import request, response +from webiopi.utils.types import M_JSON + +class ADC(): + def __init__(self, channelCount, resolution, vref): + self._analogCount = channelCount + self._analogResolution = resolution + self._analogMax = 2**resolution - 1 + self._analogRef = vref + + def __family__(self): + return "ADC" + + def checkAnalogChannel(self, channel): + if not 0 <= channel < self._analogCount: + raise ValueError("Channel %d out of range [%d..%d]" % (channel, 0, self._analogCount-1)) + + def checkAnalogValue(self, value): + if not 0 <= value <= self._analogMax: + raise ValueError("Value %d out of range [%d..%d]" % (value, 0, self._analogMax)) + + @request("GET", "analog/count") + @response("%d") + def analogCount(self): + return self._analogCount + + @request("GET", "analog/resolution") + @response("%d") + def analogResolution(self): + return self._analogResolution + + @request("GET", "analog/max") + @response("%d") + def analogMaximum(self): + return int(self._analogMax) + + @request("GET", "analog/vref") + @response("%.2f") + def analogReference(self): + return self._analogRef + + def __analogRead__(self, channel, diff): + raise NotImplementedError + + @request("GET", "analog/%(channel)d/integer") + @response("%d") + def analogRead(self, channel, diff=False): + self.checkAnalogChannel(channel) + return self.__analogRead__(channel, diff) + + @request("GET", "analog/%(channel)d/float") + @response("%.2f") + def analogReadFloat(self, channel, diff=False): + return self.analogRead(channel, diff) / float(self._analogMax) + + @request("GET", "analog/%(channel)d/volt") + @response("%.2f") + def analogReadVolt(self, channel, diff=False): + if self._analogRef == 0: + raise NotImplementedError + return self.analogReadFloat(channel, diff) * self._analogRef + + @request("GET", "analog/*/integer") + @response(contentType=M_JSON) + def analogReadAll(self): + values = {} + for i in range(self._analogCount): + values[i] = self.analogRead(i) + return values + + @request("GET", "analog/*/float") + @response(contentType=M_JSON) + def analogReadAllFloat(self): + values = {} + for i in range(self._analogCount): + values[i] = float("%.2f" % self.analogReadFloat(i)) + return values + + @request("GET", "analog/*/volt") + @response(contentType=M_JSON) + def analogReadAllVolt(self): + values = {} + for i in range(self._analogCount): + values[i] = float("%.2f" % self.analogReadVolt(i)) + return values + +class DAC(ADC): + def __init__(self, channelCount, resolution, vref): + ADC.__init__(self, channelCount, resolution, vref) + + def __family__(self): + return "DAC" + + def __analogWrite__(self, channel, value): + raise NotImplementedError + + @request("POST", "analog/%(channel)d/integer/%(value)d") + @response("%d") + def analogWrite(self, channel, value): + self.checkAnalogChannel(channel) + self.checkAnalogValue(value) + self.__analogWrite__(channel, value) + return self.analogRead(channel) + + @request("POST", "analog/%(channel)d/float/%(value)f") + @response("%.2f") + def analogWriteFloat(self, channel, value): + self.analogWrite(channel, int(value * self._analogMax)) + return self.analogReadFloat(channel) + + @request("POST", "analog/%(channel)d/volt/%(value)f") + @response("%.2f") + def analogWriteVolt(self, channel, value): + self.analogWriteFloat(channel, value /self._analogRef) + return self.analogReadVolt(channel) + + +class PWM(): + def __init__(self, channelCount, resolution, frequency): + self._pwmCount = channelCount + self._pwmResolution = resolution + self._pwmMax = 2**resolution - 1 + self.frequency = frequency + self.period = 1.0/frequency + + # Futaba servos standard + self.servo_neutral = 0.00152 + self.servo_travel_time = 0.0004 + self.servo_travel_angle = 45.0 + + self.reverse = [False for i in range(channelCount)] + + def __family__(self): + return "PWM" + + def checkPWMChannel(self, channel): + if not 0 <= channel < self._pwmCount: + raise ValueError("Channel %d out of range [%d..%d]" % (channel, 0, self._pwmCount-1)) + + def checkPWMValue(self, value): + if not 0 <= value <= self._pwmMax: + raise ValueError("Value %d out of range [%d..%d]" % (value, 0, self._pwmMax)) + + def __pwmRead__(self, channel): + raise NotImplementedError + + def __pwmWrite__(self, channel, value): + raise NotImplementedError + + @request("GET", "pwm/count") + @response("%d") + def pwmCount(self): + return self._pwmCount + + @request("GET", "pwm/resolution") + @response("%d") + def pwmResolution(self): + return self._pwmResolution + + @request("GET", "pwm/max") + @response("%d") + def pwmMaximum(self): + return int(self._pwmMax) + + @request("GET", "pwm/%(channel)d/integer") + @response("%d") + def pwmRead(self, channel): + self.checkPWMChannel(channel) + return self.__pwmRead__(channel) + + @request("GET", "pwm/%(channel)d/float") + @response("%.2f") + def pwmReadFloat(self, channel): + return self.pwmRead(channel) / float(self._pwmMax) + + @request("POST", "pwm/%(channel)d/integer/%(value)d") + @response("%d") + def pwmWrite(self, channel, value): + self.checkPWMChannel(channel) + self.checkPWMValue(value) + self.__pwmWrite__(channel, value) + return self.pwmRead(channel) + + @request("POST", "pwm/%(channel)d/float/%(value)f") + @response("%.2f") + def pwmWriteFloat(self, channel, value): + self.pwmWrite(channel, int(value * self._pwmMax)) + return self.pwmReadFloat(channel) + + def getReverse(self, channel): + self.checkChannel(channel) + return self.reverse[channel] + + def setReverse(self, channel, value): + self.checkChannel(channel) + self.reverse[channel] = value + return value + + def RatioToAngle(self, value): + f = value + f *= self.period + f -= self.servo_neutral + f *= self.servo_travel_angle + f /= self.servo_travel_time + return f + + def AngleToRatio(self, value): + f = value + f *= self.servo_travel_time + f /= self.servo_travel_angle + f += self.servo_neutral + f /= self.period + return f + + @request("GET", "pwm/%(channel)d/angle") + @response("%.2f") + def pwmReadAngle(self, channel): + f = self.pwmReadFloat(channel) + f = self.RatioToAngle(f) + if self.reverse[channel]: + f = -f + else: + f = f + return f + + @request("POST", "pwm/%(channel)d/angle/%(value)f") + @response("%.2f") + def pwmWriteAngle(self, channel, value): + if self.reverse[channel]: + f = -value + else: + f = value + f = self.AngleToRatio(f) + self.pwmWriteFloat(channel, f) + return self.pwmReadAngle(channel) + + @request("GET", "pwm/*") + @response(contentType=M_JSON) + def pwmWildcard(self): + values = {} + for i in range(self._pwmCount): + val = self.pwmReadFloat(i) + values[i] = {} + values[i]["float"] = float("%.2f" % val) + values[i]["angle"] = float("%.2f" % self.RatioToAngle(val)) + return values + +DRIVERS = {} +DRIVERS["ads1x1x"] = ["ADS1014", "ADS1015", "ADS1114", "ADS1115"] +DRIVERS["mcp3x0x"] = ["MCP3004", "MCP3008", "MCP3204", "MCP3208"] +DRIVERS["mcp4725"] = ["MCP4725"] +DRIVERS["mcp492X"] = ["MCP4921", "MCP4922"] +DRIVERS["pca9685"] = ["PCA9685"] diff --git a/python/webiopi/devices/analog/ads1x1x.py b/python/webiopi/devices/analog/ads1x1x.py new file mode 100644 index 0000000..969fd8f --- /dev/null +++ b/python/webiopi/devices/analog/ads1x1x.py @@ -0,0 +1,82 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from time import sleep +from webiopi.utils.types import toint, signInteger +from webiopi.devices.i2c import I2C +from webiopi.devices.analog import ADC + + +class ADS1X1X(ADC, I2C): + VALUE = 0x00 + CONFIG = 0x01 + LO_THRESH = 0x02 + HI_THRESH = 0x03 + + CONFIG_STATUS_MASK = 0x80 + CONFIG_CHANNEL_MASK = 0x70 + CONFIG_GAIN_MASK = 0x0E + CONFIG_MODE_MASK = 0x01 + + def __init__(self, slave, channelCount, resolution, name): + I2C.__init__(self, toint(slave)) + ADC.__init__(self, channelCount, resolution, 4.096) + self._analogMax = 2**(resolution-1) + self.name = name + + config = self.readRegisters(self.CONFIG, 2) + + mode = 0 # continuous + config[0] &= ~self.CONFIG_MODE_MASK + config[0] |= mode + + gain = 0x1 # FS = +/- 4.096V + config[0] &= ~self.CONFIG_GAIN_MASK + config[0] |= gain << 1 + + self.writeRegisters(self.CONFIG, config) + + def __str__(self): + return "%s(slave=0x%02X)" % (self.name, self.slave) + + def __analogRead__(self, channel, diff=False): + config = self.readRegisters(self.CONFIG, 2) + config[0] &= ~self.CONFIG_CHANNEL_MASK + if diff: + config[0] |= channel << 4 + else: + config[0] |= (channel + 4) << 4 + self.writeRegisters(self.CONFIG, config) + sleep(0.001) + d = self.readRegisters(self.VALUE, 2) + value = (d[0] << 8 | d[1]) >> (16-self._analogResolution) + return signInteger(value, self._analogResolution) + + +class ADS1014(ADS1X1X): + def __init__(self, slave=0x48): + ADS1X1X.__init__(self, slave, 1, 12, "ADS1014") + +class ADS1015(ADS1X1X): + def __init__(self, slave=0x48): + ADS1X1X.__init__(self, slave, 4, 12, "ADS1015") + +class ADS1114(ADS1X1X): + def __init__(self, slave=0x48): + ADS1X1X.__init__(self, slave, 1, 16, "ADS1114") + +class ADS1115(ADS1X1X): + def __init__(self, slave=0x48): + ADS1X1X.__init__(self, slave, 4, 16, "ADS1115") + diff --git a/python/webiopi/devices/analog/mcp3x0x.py b/python/webiopi/devices/analog/mcp3x0x.py new file mode 100644 index 0000000..6a23bf0 --- /dev/null +++ b/python/webiopi/devices/analog/mcp3x0x.py @@ -0,0 +1,75 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import toint +from webiopi.devices.spi import SPI +from webiopi.devices.analog import ADC + +class MCP3X0X(SPI, ADC): + def __init__(self, chip, channelCount, resolution, name): + SPI.__init__(self, toint(chip), 0, 8, 10000) + ADC.__init__(self, channelCount, resolution, 3.3) + self.name = name + self.MSB_MASK = 2**(resolution-8) - 1 + + def __str__(self): + return "%s(chip=%d)" % (self.name, self.chip) + + def __analogRead__(self, channel, diff): + data = self.__command__(channel, diff) + r = self.xfer(data) + return ((r[1] & self.MSB_MASK) << 8) | r[2] + +class MCP300X(MCP3X0X): + def __init__(self, chip, channelCount, name): + MCP3X0X.__init__(self, chip, channelCount, 10, name) + + def __command__(self, channel, diff): + d = [0x00, 0x00, 0x00] + d[0] |= 1 + d[1] |= (not diff) << 7 + d[1] |= ((channel >> 2) & 0x01) << 6 + d[1] |= ((channel >> 1) & 0x01) << 5 + d[1] |= ((channel >> 0) & 0x01) << 4 + return d + +class MCP3004(MCP300X): + def __init__(self, chip=0): + MCP300X.__init__(self, chip, 4, "MCP3004") + +class MCP3008(MCP300X): + def __init__(self, chip=0): + MCP300X.__init__(self, chip, 8, "MCP3008") + +class MCP320X(MCP3X0X): + def __init__(self, chip, channelCount, name): + MCP3X0X.__init__(self, chip, channelCount, 12, name) + + def __command__(self, channel, diff): + d = [0x00, 0x00, 0x00] + d[0] |= 1 << 2 + d[0] |= (not diff) << 1 + d[0] |= (channel >> 2) & 0x01 + d[1] |= ((channel >> 1) & 0x01) << 7 + d[1] |= ((channel >> 0) & 0x01) << 6 + return d + +class MCP3204(MCP320X): + def __init__(self, chip=0): + MCP320X.__init__(self, chip, 4, "MCP3204") + +class MCP3208(MCP320X): + def __init__(self, chip=0): + MCP320X.__init__(self, chip, 8, "MCP3208") + diff --git a/python/webiopi/devices/analog/mcp4725.py b/python/webiopi/devices/analog/mcp4725.py new file mode 100644 index 0000000..a46337d --- /dev/null +++ b/python/webiopi/devices/analog/mcp4725.py @@ -0,0 +1,38 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import toint +from webiopi.devices.i2c import I2C +from webiopi.devices.analog import DAC + + +class MCP4725(DAC, I2C): + def __init__(self, slave=0x60): + I2C.__init__(self, toint(slave)) + DAC.__init__(self, 1, 12, 3.3) + + def __str__(self): + return "MCP4725(slave=0x%02X)" % self.slave + + def __analogRead__(self, channel, diff=False): + d = self.readBytes(3) + value = (d[1] << 8 | d[2]) >> 4 + return value + + + def __analogWrite__(self, channel, value): + d = bytearray(2) + d[0] = (value >> 8) & 0x0F + d[1] = value & 0xFF + self.writeBytes(d) \ No newline at end of file diff --git a/python/webiopi/devices/analog/mcp492X.py b/python/webiopi/devices/analog/mcp492X.py new file mode 100644 index 0000000..4489149 --- /dev/null +++ b/python/webiopi/devices/analog/mcp492X.py @@ -0,0 +1,53 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import toint +from webiopi.devices.spi import SPI +from webiopi.devices.analog import DAC + +class MCP492X(SPI, DAC): + def __init__(self, chip, channelCount): + SPI.__init__(self, toint(chip), 0, 8, 10000000) + DAC.__init__(self, channelCount, 12, 3.3) + self.buffered=False + self.gain=False + self.shutdown=False + self.values = [0 for i in range(channelCount)] + + def __str__(self): + return "MCP492%d(chip=%d)" % (self._analogCount, self.chip) + + def __analogRead__(self, channel, diff=False): + return self.values[channel] + + def __analogWrite__(self, channel, value): + d = bytearray(2) + d[0] = 0 + d[0] |= (channel & 0x01) << 7 + d[0] |= (self.buffered & 0x01) << 6 + d[0] |= (not self.gain & 0x01) << 5 + d[0] |= (not self.shutdown & 0x01) << 4 + d[0] |= (value >> 8) & 0x0F + d[1] = value & 0xFF + self.writeBytes(d) + self.values[channel] = value + +class MCP4921(MCP492X): + def __init__(self, chip=0): + MCP492X.__init__(self, chip, 1) + +class MCP4922(MCP492X): + def __init__(self, chip=0): + MCP492X.__init__(self, chip, 2) + diff --git a/python/webiopi/devices/analog/pca9685.py b/python/webiopi/devices/analog/pca9685.py new file mode 100644 index 0000000..3b91121 --- /dev/null +++ b/python/webiopi/devices/analog/pca9685.py @@ -0,0 +1,63 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from webiopi.utils.types import toint +from webiopi.devices.i2c import I2C +from webiopi.devices.analog import PWM + +class PCA9685(PWM, I2C): + MODE1 = 0x00 + PWM_BASE = 0x06 + PRESCALE = 0xFE + + M1_SLEEP = 1<<4 + M1_AI = 1<<5 + M1_RESTART = 1<<7 + + def __init__(self, slave=0x40, frequency=50): + I2C.__init__(self, toint(slave)) + PWM.__init__(self, 16, 12, toint(frequency)) + self.VREF = 0 + + self.prescale = int(25000000.0/((2**12)*self.frequency)) + self.mode1 = self.M1_RESTART | self.M1_AI + + self.writeRegister(self.MODE1, self.M1_SLEEP) + self.writeRegister(self.PRESCALE, self.prescale) + time.sleep(0.01) + + self.writeRegister(self.MODE1, self.mode1) + + def __str__(self): + return "PCA9685(slave=0x%02X)" % self.slave + + def getChannelAddress(self, channel): + return int(channel * 4 + self.PWM_BASE) + + def __pwmRead__(self, channel): + addr = self.getChannelAddress(channel) + d = self.readRegisters(addr, 4) + start = d[1] << 8 | d[0] + end = d[3] << 8 | d[2] + return end-start + + def __pwmWrite__(self, channel, value): + addr = self.getChannelAddress(channel) + d = bytearray(4) + d[0] = 0 + d[1] = 0 + d[2] = (value & 0x0FF) + d[3] = (value & 0xF00) >> 8 + self.writeRegisters(addr, d) diff --git a/python/webiopi/devices/bus.py b/python/webiopi/devices/bus.py new file mode 100644 index 0000000..cd271e0 --- /dev/null +++ b/python/webiopi/devices/bus.py @@ -0,0 +1,117 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import subprocess + +from webiopi.utils.logger import debug, info + +BUSLIST = { + "I2C": {"enabled": False, "gpio": {0:"SDA", 1:"SCL", 2:"SDA", 3:"SCL"}, "modules": ["i2c-bcm2708", "i2c-dev"]}, + "SPI": {"enabled": False, "gpio": {7:"CE1", 8:"CE0", 9:"MISO", 10:"MOSI", 11:"SCLK"}, "modules": ["spi-bcm2708", "spidev"]}, + "UART": {"enabled": False, "gpio": {14:"TX", 15:"RX"}}, + "ONEWIRE": {"enabled": False, "gpio": {4:"DATA"}, "modules": ["w1-gpio"], "wait": 2} +} + +def loadModule(module): + debug("Loading module : %s" % module) + subprocess.call(["modprobe", module]) + +def unloadModule(module): + subprocess.call(["modprobe", "-r", module]) + +def loadModules(bus): + if BUSLIST[bus]["enabled"] == False and not modulesLoaded(bus): + info("Loading %s modules" % bus) + for module in BUSLIST[bus]["modules"]: + loadModule(module) + if "wait" in BUSLIST[bus]: + info("Sleeping %ds to let %s modules load" % (BUSLIST[bus]["wait"], bus)) + time.sleep(BUSLIST[bus]["wait"]) + + BUSLIST[bus]["enabled"] = True + +def unloadModules(bus): + info("Unloading %s modules" % bus) + for module in BUSLIST[bus]["modules"]: + unloadModule(module) + BUSLIST[bus]["enabled"] = False + +def __modulesLoaded__(modules, lines): + if len(modules) == 0: + return True + for line in lines: + if modules[0].replace("-", "_") == line.split(" ")[0]: + return __modulesLoaded__(modules[1:], lines) + return False + +def modulesLoaded(bus): + if not bus in BUSLIST or not "modules" in BUSLIST[bus]: + return True + + try: + with open("/proc/modules") as f: + c = f.read() + f.close() + lines = c.split("\n") + return __modulesLoaded__(BUSLIST[bus]["modules"], lines) + except: + return False + +def checkAllBus(): + for bus in BUSLIST: + if modulesLoaded(bus): + BUSLIST[bus]["enabled"] = True + +class Bus(): + def __init__(self, busName, device, flag=os.O_RDWR): + loadModules(busName) + self.busName = busName + self.device = device + self.flag = flag + self.fd = 0 + self.open() + + def open(self): + self.fd = os.open(self.device, self.flag) + if self.fd < 0: + raise Exception("Cannot open %s" % self.device) + + def close(self): + if self.fd > 0: + os.close(self.fd) + + def read(self, size=1): + if self.fd > 0: + return os.read(self.fd, size) + raise Exception("Device %s not open" % self.device) + + def readBytes(self, size=1): + return bytearray(self.read(size)) + + def readByte(self): + return self.readBytes()[0] + + def write(self, string): + if self.fd > 0: + return os.write(self.fd, string) + raise Exception("Device %s not open" % self.device) + + def writeBytes(self, data): + return self.write(bytearray(data)) + + def writeByte(self, value): + self.writeBytes([value]) + diff --git a/python/webiopi/devices/digital/__init__.py b/python/webiopi/devices/digital/__init__.py new file mode 100644 index 0000000..2c992b3 --- /dev/null +++ b/python/webiopi/devices/digital/__init__.py @@ -0,0 +1,144 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.decorators.rest import request, response +from webiopi.utils.types import M_JSON + +class GPIOPort(): + IN = 0 + OUT = 1 + + LOW = False + HIGH = True + + def __init__(self, channelCount): + self.digitalChannelCount = channelCount + + def checkDigitalChannel(self, channel): + if not 0 <= channel < self.digitalChannelCount: + raise ValueError("Channel %d out of range [%d..%d]" % (channel, 0, self.digitalChannelCount-1)) + + def checkDigitalValue(self, value): + if not (value == 0 or value == 1): + raise ValueError("Value %d not in {0, 1}") + + + @request("GET", "count") + @response("%d") + def digitalCount(self): + return self.digitalChannelCount + + def __family__(self): + return "GPIOPort" + + def __getFunction__(self, channel): + raise NotImplementedError + + def __setFunction__(self, channel, func): + raise NotImplementedError + + def __digitalRead__(self, chanel): + raise NotImplementedError + + def __portRead__(self): + raise NotImplementedError + + def __digitalWrite__(self, chanel, value): + raise NotImplementedError + + def __portWrite__(self, value): + raise NotImplementedError + + def getFunction(self, channel): + self.checkDigitalChannel(channel) + return self.__getFunction__(channel) + + @request("GET", "%(channel)d/function") + def getFunctionString(self, channel): + func = self.getFunction(channel) + if func == self.IN: + return "IN" + elif func == self.OUT: + return "OUT" +# elif func == GPIO.PWM: +# return "PWM" + else: + return "UNKNOWN" + + def setFunction(self, channel, value): + self.checkDigitalChannel(channel) + self.__setFunction__(channel, value) + return self.getFunction(channel) + + @request("POST", "%(channel)d/function/%(value)s") + def setFunctionString(self, channel, value): + value = value.lower() + if value == "in": + self.setFunction(channel, self.IN) + elif value == "out": + self.setFunction(channel, self.OUT) +# elif value == "pwm": +# self.setFunction(channel, GPIO.PWM) + else: + raise ValueError("Bad Function") + return self.getFunctionString(channel) + + @request("GET", "%(channel)d/value") + @response("%d") + def digitalRead(self, channel): + self.checkDigitalChannel(channel) + return self.__digitalRead__(channel) + + @request("GET", "*") + @response(contentType=M_JSON) + def wildcard(self, compact=False): + if compact: + f = "f" + v = "v" + else: + f = "function" + v = "value" + + values = {} + for i in range(self.digitalChannelCount): + if compact: + func = self.getFunction(i) + else: + func = self.getFunctionString(i) + values[i] = {f: func, v: int(self.digitalRead(i))} + return values + + @request("GET", "*/integer") + @response("%d") + def portRead(self): + return self.__portRead__() + + @request("POST", "%(channel)d/value/%(value)d") + @response("%d") + def digitalWrite(self, channel, value): + self.checkDigitalChannel(channel) + self.checkDigitalValue(value) + self.__digitalWrite__(channel, value) + return self.digitalRead(channel) + + @request("POST", "*/integer/%(value)d") + @response("%d") + def portWrite(self, value): + self.__portWrite__(value) + return self.portRead() + +DRIVERS = {} +DRIVERS["mcp23XXX"] = ["MCP23008", "MCP23009", "MCP23017", "MCP23018", "MCP23S08", "MCP23S09", "MCP23S17", "MCP23S18"] +DRIVERS["pcf8574" ] = ["PCF8574", "PCF8574A"] +DRIVERS["ds2408" ] = ["DS2408"] diff --git a/python/webiopi/devices/digital/ds2408.py b/python/webiopi/devices/digital/ds2408.py new file mode 100644 index 0000000..4784593 --- /dev/null +++ b/python/webiopi/devices/digital/ds2408.py @@ -0,0 +1,84 @@ +# Copyright 2013 Stuart Marsden +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.devices.onewire import OneWire +from webiopi.devices.digital import GPIOPort + +class DS2408(OneWire, GPIOPort): + FUNCTIONS = [GPIOPort.IN for i in range(8)] + + def __init__(self, slave=None): + OneWire.__init__(self, slave, 0x29, "2408") + GPIOPort.__init__(self, 8) + self.portWrite(0x00) + + def __str__(self): + return "DS2408(slave=%s)" % self.slave + + def __getFunction__(self, channel): + return self.FUNCTIONS[channel] + + def __setFunction__(self, channel, value): + if not value in [self.IN, self.OUT]: + raise ValueError("Requested function not supported") + self.FUNCTIONS[channel] = value + if value == self.IN: + self.__output__(channel, 0) + + def __digitalRead__(self, channel): + mask = 1 << channel + d = self.readState() + if d != None: + return (d & mask) == mask + + + def __digitalWrite__(self, channel, value): + mask = 1 << channel + b = self.readByte() + if value: + b |= mask + else: + b &= ~mask + self.writeByte(b) + + def __portWrite__(self, value): + self.writeByte(value) + + def __portRead__(self): + return self.readByte() + + def readState(self): + try: + with open("/sys/bus/w1/devices/%s/state" % self.slave, "rb") as f: + data = f.read(1) + return ord(data) + except IOError: + return -1 + + def readByte(self): + try: + with open("/sys/bus/w1/devices/%s/output" % self.slave, "rb") as f: + data = f.read(1) + return bytearray(data)[0] + except IOError: + return -1 + + def writeByte(self, value): + try: + with open("/sys/bus/w1/devices/%s/output" % self.slave, "wb") as f: + f.write(bytearray([value])) + except IOError: + pass + + diff --git a/python/webiopi/devices/digital/gpio.py b/python/webiopi/devices/digital/gpio.py new file mode 100644 index 0000000..5da6cc5 --- /dev/null +++ b/python/webiopi/devices/digital/gpio.py @@ -0,0 +1,189 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import M_JSON +from webiopi.utils.logger import debug +from webiopi.devices.digital import GPIOPort +from webiopi.decorators.rest import request, response +try: + import _webiopi.GPIO as GPIO +except: + pass + +EXPORT = [] + +class NativeGPIO(GPIOPort): + def __init__(self): + GPIOPort.__init__(self, 54) + self.export = range(54) + self.post_value = True + self.post_function = True + self.gpio_setup = [] + self.gpio_reset = [] + + def __str__(self): + return "GPIO" + + def addGPIO(self, lst, gpio, params): + gpio = int(gpio) + params = params.split(" ") + func = params[0].lower() + if func == "in": + func = GPIO.IN + elif func == "out": + func = GPIO.OUT + else: + raise Exception("Unknown function") + + value = -1 + if len(params) > 1: + value = int(params[1]) + lst.append({"gpio": gpio, "func": func, "value": value}) + + def addGPIOSetup(self, gpio, params): + self.addGPIO(self.gpio_setup, gpio, params) + + def addGPIOReset(self, gpio, params): + self.addGPIO(self.gpio_reset, gpio, params) + + def addSetups(self, gpios): + for (gpio, params) in gpios: + self.addGPIOSetup(gpio, params) + + def addResets(self, gpios): + for (gpio, params) in gpios: + self.addGPIOReset(gpio, params) + + def setup(self): + for g in self.gpio_setup: + gpio = g["gpio"] + debug("Setup GPIO %d" % gpio) + GPIO.setFunction(gpio, g["func"]) + if g["value"] >= 0 and GPIO.getFunction(gpio) == GPIO.OUT: + GPIO.digitalWrite(gpio, g["value"]) + + def close(self): + for g in self.gpio_reset: + gpio = g["gpio"] + debug("Reset GPIO %d" % gpio) + GPIO.setFunction(gpio, g["func"]) + if g["value"] >= 0 and GPIO.getFunction(gpio) == GPIO.OUT: + GPIO.digitalWrite(gpio, g["value"]) + + def checkDigitalChannelExported(self, channel): + if not channel in self.export: + raise GPIO.InvalidChannelException("Channel %d is not allowed" % channel) + + def checkPostingFunctionAllowed(self): + if not self.post_function: + raise ValueError("POSTing function to native GPIO not allowed") + + def checkPostingValueAllowed(self): + if not self.post_value: + raise ValueError("POSTing value to native GPIO not allowed") + + def __digitalRead__(self, channel): + self.checkDigitalChannelExported(channel) + return GPIO.digitalRead(channel) + + def __digitalWrite__(self, channel, value): + self.checkDigitalChannelExported(channel) + self.checkPostingValueAllowed() + GPIO.digitalWrite(channel, value) + + def __getFunction__(self, channel): + self.checkDigitalChannelExported(channel) + return GPIO.getFunction(channel) + + def __setFunction__(self, channel, value): + self.checkDigitalChannelExported(channel) + self.checkPostingFunctionAllowed() + GPIO.setFunction(channel, value) + + def __portRead__(self): + value = 0 + for i in self.export: + value |= GPIO.digitalRead(i) << i + return value + + def __portWrite__(self, value): + if len(self.export) < 54: + for i in self.export: + if GPIO.getFunction(i) == GPIO.OUT: + GPIO.digitalWrite(i, (value >> i) & 1) + else: + raise Exception("Please limit exported GPIO to write integers") + + @request("GET", "*") + @response(contentType=M_JSON) + def wildcard(self, compact=False): + if compact: + f = "f" + v = "v" + else: + f = "function" + v = "value" + + values = {} + print(self.export) + for i in self.export: + if compact: + func = GPIO.getFunction(i) + else: + func = GPIO.getFunctionString(i) + values[i] = {f: func, v: int(GPIO.digitalRead(i))} + return values + + + @request("GET", "%(channel)d/pulse", "%s") + def getPulse(self, channel): + self.checkDigitalChannelExported(channel) + self.checkDigitalChannel(channel) + return GPIO.getPulse(channel) + + @request("POST", "%(channel)d/sequence/%(args)s") + @response("%d") + def outputSequence(self, channel, args): + self.checkDigitalChannelExported(channel) + self.checkPostingValueAllowed() + self.checkDigitalChannel(channel) + (period, sequence) = args.split(",") + period = int(period) + GPIO.outputSequence(channel, period, sequence) + return int(sequence[-1]) + + @request("POST", "%(channel)d/pulse/") + def pulse(self, channel): + self.checkDigitalChannelExported(channel) + self.checkPostingValueAllowed() + self.checkDigitalChannel(channel) + GPIO.pulse(channel) + return "OK" + + @request("POST", "%(channel)d/pulseRatio/%(value)f") + def pulseRatio(self, channel, value): + self.checkDigitalChannelExported(channel) + self.checkPostingValueAllowed() + self.checkDigitalChannel(channel) + GPIO.pulseRatio(channel, value) + return GPIO.getPulse(channel) + + @request("POST", "%(channel)d/pulseAngle/%(value)f") + def pulseAngle(self, channel, value): + self.checkDigitalChannelExported(channel) + self.checkPostingValueAllowed() + self.checkDigitalChannel(channel) + GPIO.pulseAngle(channel, value) + return GPIO.getPulse(channel) + diff --git a/python/webiopi/devices/digital/mcp23XXX.py b/python/webiopi/devices/digital/mcp23XXX.py new file mode 100644 index 0000000..99edd61 --- /dev/null +++ b/python/webiopi/devices/digital/mcp23XXX.py @@ -0,0 +1,153 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import toint +from webiopi.devices.i2c import I2C +from webiopi.devices.spi import SPI +from webiopi.devices.digital import GPIOPort + +class MCP23XXX(GPIOPort): + IODIR = 0x00 + IPOL = 0x01 + GPINTEN = 0x02 + DEFVAL = 0x03 + INTCON = 0x04 + IOCON = 0x05 + GPPU = 0x06 + INTF = 0x07 + INTCAP = 0x08 + GPIO = 0x09 + OLAT = 0x0A + + def __init__(self, channelCount): + GPIOPort.__init__(self, channelCount) + self.banks = int(channelCount / 8) + + def getAddress(self, register, channel=0): + return register * self.banks + int(channel / 8) + + def getChannel(self, register, channel): + self.checkDigitalChannel(channel) + addr = self.getAddress(register, channel) + mask = 1 << (channel % 8) + return (addr, mask) + + def __digitalRead__(self, channel): + (addr, mask) = self.getChannel(self.GPIO, channel) + d = self.readRegister(addr) + return (d & mask) == mask + + def __digitalWrite__(self, channel, value): + (addr, mask) = self.getChannel(self.GPIO, channel) + d = self.readRegister(addr) + if value: + d |= mask + else: + d &= ~mask + self.writeRegister(addr, d) + + def __getFunction__(self, channel): + (addr, mask) = self.getChannel(self.IODIR, channel) + d = self.readRegister(addr) + return self.IN if (d & mask) == mask else self.OUT + + def __setFunction__(self, channel, value): + if not value in [self.IN, self.OUT]: + raise ValueError("Requested function not supported") + + (addr, mask) = self.getChannel(self.IODIR, channel) + d = self.readRegister(addr) + if value == self.IN: + d |= mask + else: + d &= ~mask + self.writeRegister(addr, d) + + def __portRead__(self): + value = 0 + for i in range(self.banks): + value |= self.readRegister(self.banks*self.GPIO+i) << 8*i + return value + + def __portWrite__(self, value): + for i in range(self.banks): + self.writeRegister(self.banks*self.GPIO+i, (value >> 8*i) & 0xFF) + +class MCP230XX(MCP23XXX, I2C): + def __init__(self, slave, channelCount, name): + I2C.__init__(self, toint(slave)) + MCP23XXX.__init__(self, channelCount) + self.name = name + + def __str__(self): + return "%s(slave=0x%02X)" % (self.name, self.slave) + +class MCP23008(MCP230XX): + def __init__(self, slave=0x20): + MCP230XX.__init__(self, slave, 8, "MCP23008") + +class MCP23009(MCP230XX): + def __init__(self, slave=0x20): + MCP230XX.__init__(self, slave, 8, "MCP23009") + +class MCP23017(MCP230XX): + def __init__(self, slave=0x20): + MCP230XX.__init__(self, slave, 16, "MCP23017") + +class MCP23018(MCP230XX): + def __init__(self, slave=0x20): + MCP230XX.__init__(self, slave, 16, "MCP23018") + +class MCP23SXX(MCP23XXX, SPI): + SLAVE = 0x20 + + WRITE = 0x00 + READ = 0x01 + + def __init__(self, chip, slave, channelCount, name): + SPI.__init__(self, toint(chip), 0, 8, 10000000) + MCP23XXX.__init__(self, channelCount) + self.slave = self.SLAVE + iocon_value = 0x08 # Hardware Address Enable + iocon_addr = self.getAddress(self.IOCON) + self.writeRegister(iocon_addr, iocon_value) + self.slave = toint(slave) + self.name = name + + def __str__(self): + return "%s(chip=%d, slave=0x%02X)" % (self.name, self.chip, self.slave) + + def readRegister(self, addr): + d = self.xfer([(self.slave << 1) | self.READ, addr, 0x00]) + return d[2] + + def writeRegister(self, addr, value): + self.writeBytes([(self.slave << 1) | self.WRITE, addr, value]) + +class MCP23S08(MCP23SXX): + def __init__(self, chip=0, slave=0x20): + MCP23SXX.__init__(self, chip, slave, 8, "MCP23S08") + +class MCP23S09(MCP23SXX): + def __init__(self, chip=0, slave=0x20): + MCP23SXX.__init__(self, chip, slave, 8, "MCP23S09") + +class MCP23S17(MCP23SXX): + def __init__(self, chip=0, slave=0x20): + MCP23SXX.__init__(self, chip, slave, 16, "MCP23S17") + +class MCP23S18(MCP23SXX): + def __init__(self, chip=0, slave=0x20): + MCP23SXX.__init__(self, chip, slave, 16, "MCP23S18") + diff --git a/python/webiopi/devices/digital/pcf8574.py b/python/webiopi/devices/digital/pcf8574.py new file mode 100644 index 0000000..146e8f4 --- /dev/null +++ b/python/webiopi/devices/digital/pcf8574.py @@ -0,0 +1,70 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import toint +from webiopi.devices.i2c import I2C +from webiopi.devices.digital import GPIOPort + +class PCF8574(I2C, GPIOPort): + FUNCTIONS = [GPIOPort.IN for i in range(8)] + + def __init__(self, slave=0x20): + slave = toint(slave) + if slave in range(0x20, 0x28): + self.name = "PCF8574" + elif slave in range(0x38, 0x40): + self.name = "PCF8574A" + else: + raise ValueError("Bad slave address for PCF8574(A) : 0x%02X not in range [0x20..0x27, 0x38..0x3F]" % slave) + + I2C.__init__(self, slave) + GPIOPort.__init__(self, 8) + self.portWrite(0xFF) + self.portRead() + + def __str__(self): + return "%s(slave=0x%02X)" % (self.name, self.slave) + + def __getFunction__(self, channel): + return self.FUNCTIONS[channel] + + def __setFunction__(self, channel, value): + if not value in [self.IN, self.OUT]: + raise ValueError("Requested function not supported") + self.FUNCTIONS[channel] = value + + def __digitalRead__(self, channel): + mask = 1 << channel + d = self.readByte() + return (d & mask) == mask + + def __portRead__(self): + return self.readByte() + + def __digitalWrite__(self, channel, value): + mask = 1 << channel + b = self.readByte() + if value: + b |= mask + else: + b &= ~mask + self.writeByte(b) + + def __portWrite__(self, value): + self.writeByte(value) + +class PCF8574A(PCF8574): + def __init__(self, slave=0x38): + PCF8574.__init__(self, slave) + diff --git a/python/webiopi/devices/i2c.py b/python/webiopi/devices/i2c.py new file mode 100644 index 0000000..1b3196a --- /dev/null +++ b/python/webiopi/devices/i2c.py @@ -0,0 +1,75 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fcntl + +from webiopi.utils.version import BOARD_REVISION +from webiopi.devices.bus import Bus + +# /dev/i2c-X ioctl commands. The ioctl's parameter is always an +# unsigned long, except for: +# - I2C_FUNCS, takes pointer to an unsigned long +# - I2C_RDWR, takes pointer to struct i2c_rdwr_ioctl_data +# - I2C_SMBUS, takes pointer to struct i2c_smbus_ioctl_data + +I2C_RETRIES = 0x0701 # number of times a device address should + # be polled when not acknowledging +I2C_TIMEOUT = 0x0702 # set timeout in units of 10 ms + +# NOTE: Slave address is 7 or 10 bits, but 10-bit addresses +# are NOT supported! (due to code brokenness) + +I2C_SLAVE = 0x0703 # Use this slave address +I2C_SLAVE_FORCE = 0x0706 # Use this slave address, even if it + # is already in use by a driver! +I2C_TENBIT = 0x0704 # 0 for 7 bit addrs, != 0 for 10 bit + +I2C_FUNCS = 0x0705 # Get the adapter functionality mask + +I2C_RDWR = 0x0707 # Combined R/W transfer (one STOP only) + +I2C_PEC = 0x0708 # != 0 to use PEC with SMBus +I2C_SMBUS = 0x0720 # SMBus transfer */ + + +class I2C(Bus): + def __init__(self, slave): + self.channel = 0 + if BOARD_REVISION > 1: + self.channel = 1 + + Bus.__init__(self, "I2C", "/dev/i2c-%d" % self.channel) + self.slave = slave + if fcntl.ioctl(self.fd, I2C_SLAVE, self.slave): + raise Exception("Error binding I2C slave 0x%02X" % self.slave) + + def __str__(self): + return "I2C(slave=0x%02X)" % self.slave + + def readRegister(self, addr): + self.writeByte(addr) + return self.readByte() + + def readRegisters(self, addr, count): + self.writeByte(addr) + return self.readBytes(count) + + def writeRegister(self, addr, byte): + self.writeBytes([addr, byte]) + + def writeRegisters(self, addr, buff): + d = bytearray(len(buff)+1) + d[0] = addr + d[1:] = buff + self.writeBytes(d) diff --git a/python/webiopi/devices/instance.py b/python/webiopi/devices/instance.py new file mode 100644 index 0000000..633a8c3 --- /dev/null +++ b/python/webiopi/devices/instance.py @@ -0,0 +1,6 @@ +DEVICES = {} +def deviceInstance(name): + if name in DEVICES: + return DEVICES[name]["device"] + else: + return None diff --git a/python/webiopi/devices/manager.py b/python/webiopi/devices/manager.py new file mode 100644 index 0000000..0f1e521 --- /dev/null +++ b/python/webiopi/devices/manager.py @@ -0,0 +1,77 @@ +import imp +from webiopi.utils import logger +from webiopi.utils import types +from webiopi.devices.instance import DEVICES + +from webiopi.devices import serial, digital, analog, sensor, shield + +PACKAGES = [serial, digital, analog, sensor, shield] +def findDeviceClass(name): + for package in PACKAGES: + if hasattr(package, name): + return getattr(package, name) + if hasattr(package, "DRIVERS"): + for driver in package.DRIVERS: + if name in package.DRIVERS[driver]: + (fp, pathname, stuff) = imp.find_module(package.__name__.replace(".", "/") + "/" + driver) + module = imp.load_module(driver, fp, pathname, stuff) + return getattr(module, name) + return None + +def addDevice(name, device, args): + devClass = findDeviceClass(device) + if devClass == None: + raise Exception("Device driver not found for %s" % device) + if len(args) > 0: + dev = devClass(**args) + else: + dev = devClass() + addDeviceInstance(name, dev, args) + +def addDeviceInstance(name, dev, args): + funcs = {"GET": {}, "POST": {}} + for att in dir(dev): + func = getattr(dev, att) + if callable(func) and hasattr(func, "routed"): + if name == "GPIO": + logger.debug("Mapping %s.%s to REST %s /GPIO/%s" % (dev, att, func.method, func.path)) + else: + logger.debug("Mapping %s.%s to REST %s /devices/%s/%s" % (dev, att, func.method, name, func.path)) + funcs[func.method][func.path] = func + + DEVICES[name] = {'device': dev, 'functions': funcs} + if name == "GPIO": + logger.info("GPIO - Native mapped to REST API /GPIO") + else: + logger.info("%s - %s mapped to REST API /devices/%s" % (dev.__family__(), dev, name)) + +def closeDevices(): + devices = [k for k in DEVICES.keys()] + for name in devices: + device = DEVICES[name]["device"] + logger.debug("Closing device %s - %s" % (name, device)) + del DEVICES[name] + device.close() + +def getDevicesJSON(compact=False): + devname = "name" + devtype = "type" + + devices = [] + for devName in DEVICES: + if devName == "GPIO": + continue + instance = DEVICES[devName]["device"] + if hasattr(instance, "__family__"): + family = instance.__family__() + if isinstance(family, str): + devices.append({devname: devName, devtype:family}) + else: + for fam in family: + devices.append({devname: devName, devtype:fam}) + + else: + devices.append({devname: devName, type:instance.__str__()}) + + return types.jsonDumps(sorted(devices, key=lambda dev: dev[devname])) + diff --git a/python/webiopi/devices/onewire.py b/python/webiopi/devices/onewire.py new file mode 100644 index 0000000..b012c90 --- /dev/null +++ b/python/webiopi/devices/onewire.py @@ -0,0 +1,74 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from webiopi.devices.bus import Bus, loadModule + +EXTRAS = { + "TEMP": {"loaded": False, "module": "w1-therm"}, + "2408": {"loaded": False, "module": "w1_ds2408"} + +} + +def loadExtraModule(name): + if EXTRAS[name]["loaded"] == False: + loadModule(EXTRAS[name]["module"]) + EXTRAS[name]["loaded"] = True + +class OneWire(Bus): + def __init__(self, slave=None, family=0, extra=None): + Bus.__init__(self, "ONEWIRE", "/sys/bus/w1/devices/w1_bus_master1/w1_master_slaves", os.O_RDONLY) + if self.fd > 0: + os.close(self.fd) + self.fd = 0 + + self.family = family + if slave != None: + addr = slave.split("-") + if len(addr) == 1: + self.slave = "%02x-%s" % (family, slave) + elif len(addr) == 2: + prefix = int(addr[0], 16) + if family > 0 and family != prefix: + raise Exception("1-Wire slave address %s does not match family %02x" % (slave, family)) + self.slave = slave + else: + devices = self.deviceList() + if len(devices) == 0: + raise Exception("No device match family %02x" % family) + self.slave = devices[0] + + loadExtraModule(extra) + + def __str__(self): + return "1-Wire(slave=%s)" % self.slave + + def deviceList(self): + devices = [] + with open(self.device) as f: + lines = f.read().split("\n") + if self.family > 0: + prefix = "%02x-" % self.family + for line in lines: + if line.startswith(prefix): + devices.append(line) + else: + devices = lines + return devices; + + def read(self): + with open("/sys/bus/w1/devices/%s/w1_slave" % self.slave) as f: + data = f.read() + return data + diff --git a/python/webiopi/devices/sensor/__init__.py b/python/webiopi/devices/sensor/__init__.py new file mode 100644 index 0000000..598a82f --- /dev/null +++ b/python/webiopi/devices/sensor/__init__.py @@ -0,0 +1,177 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import toint +from webiopi.utils.types import M_JSON +from webiopi.devices.instance import deviceInstance +from webiopi.decorators.rest import request, response + +class Pressure(): + def __init__(self, altitude=0, external=None): + self.altitude = toint(altitude) + if isinstance(external, str): + self.external = deviceInstance(external) + else: + self.external = external + + if self.external != None and not isinstance(self.external, Temperature): + raise Exception("external must be a Temperature sensor") + + def __family__(self): + return "Pressure" + + def __getPascal__(self): + raise NotImplementedError + + def __getPascalAtSea__(self): + raise NotImplementedError + + @request("GET", "sensor/pressure/pa") + @response("%d") + def getPascal(self): + return self.__getPascal__() + + @request("GET", "sensor/pressure/hpa") + @response("%.2f") + def getHectoPascal(self): + return float(self.__getPascal__()) / 100.0 + + @request("GET", "sensor/pressure/sea/pa") + @response("%d") + def getPascalAtSea(self): + pressure = self.__getPascal__() + if self.external != None: + k = self.external.getKelvin() + if k != 0: + return float(pressure) / (1.0 / (1.0 + 0.0065 / k * self.altitude)**5.255) + return float(pressure) / (1.0 - self.altitude / 44330.0)**5.255 + + @request("GET", "sensor/pressure/sea/hpa") + @response("%.2f") + def getHectoPascalAtSea(self): + return self.getPascalAtSea() / 100.0 + +class Temperature(): + def __family__(self): + return "Temperature" + + def __getKelvin__(self): + raise NotImplementedError + + def __getCelsius__(self): + raise NotImplementedError + + def __getFahrenheit__(self): + raise NotImplementedError + + def Kelvin2Celsius(self, value=None): + if value == None: + value = self.getKelvin() + return value - 273.15 + + def Kelvin2Fahrenheit(self, value=None): + if value == None: + value = self.getKelvin() + return value * 1.8 - 459.67 + + def Celsius2Kelvin(self, value=None): + if value == None: + value = self.getCelsius() + return value + 273.15 + + def Celsius2Fahrenheit(self, value=None): + if value == None: + value = self.getCelsius() + return value * 1.8 + 32 + + def Fahrenheit2Kelvin(self, value=None): + if value == None: + value = self.getFahrenheit() + return (value - 459.67) / 1.8 + + def Fahrenheit2Celsius(self, value=None): + if value == None: + value = self.getFahrenheit() + return (value - 32) / 1.8 + + @request("GET", "sensor/temperature/k") + @response("%.02f") + def getKelvin(self): + return self.__getKelvin__() + + @request("GET", "sensor/temperature/c") + @response("%.02f") + def getCelsius(self): + return self.__getCelsius__() + + @request("GET", "sensor/temperature/f") + @response("%.02f") + def getFahrenheit(self): + return self.__getFahrenheit__() + +class Luminosity(): + def __family__(self): + return "Luminosity" + + def __getLux__(self): + raise NotImplementedError + + @request("GET", "sensor/luminosity/lx") + @response("%.02f") + def getLux(self): + return self.__getLux__() + +class Distance(): + def __family__(self): + return "Distance" + + def __getMillimeter__(self): + raise NotImplementedError + + @request("GET", "sensor/distance/mm") + @response("%.02f") + def getMillimeter(self): + return self.__getMillimeter__() + + @request("GET", "sensor/distance/cm") + @response("%.02f") + def getCentimeter(self): + return self.getMillimeter() / 10 + + @request("GET", "sensor/distance/m") + @response("%.02f") + def getMeter(self): + return self.getMillimeter() / 1000 + + @request("GET", "sensor/distance/in") + @response("%.02f") + def getInch(self): + return self.getMillimeter() / 0.254 + + @request("GET", "sensor/distance/ft") + @response("%.02f") + def getFoot(self): + return self.getInch() / 12 + + @request("GET", "sensor/distance/yd") + @response("%.02f") + def getYard(self): + return self.getInch() / 36 + +DRIVERS = {} +DRIVERS["bmp085"] = ["BMP085"] +DRIVERS["onewiretemp"] = ["DS1822", "DS1825", "DS18B20", "DS18S20", "DS28EA00"] +DRIVERS["tmpXXX"] = ["TMP75", "TMP102", "TMP275"] +DRIVERS["tslXXXX"] = ["TSL2561", "TSL2561CS", "TSL2561T", "TSL4531", "TSL45311", "TSL45313", "TSL45315", "TSL45317"] +DRIVERS["vcnl4000"] = ["VCNL4000"] diff --git a/python/webiopi/devices/sensor/bmp085.py b/python/webiopi/devices/sensor/bmp085.py new file mode 100644 index 0000000..e21ab9a --- /dev/null +++ b/python/webiopi/devices/sensor/bmp085.py @@ -0,0 +1,100 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +from webiopi.utils.types import signInteger +from webiopi.devices.i2c import I2C +from webiopi.devices.sensor import Temperature, Pressure + +class BMP085(I2C, Temperature, Pressure): + def __init__(self, altitude=0, external=None): + I2C.__init__(self, 0x77) + Pressure.__init__(self, altitude, external) + + self.ac1 = self.readSignedInteger(0xAA) + self.ac2 = self.readSignedInteger(0xAC) + self.ac3 = self.readSignedInteger(0xAE) + self.ac4 = self.readUnsignedInteger(0xB0) + self.ac5 = self.readUnsignedInteger(0xB2) + self.ac6 = self.readUnsignedInteger(0xB4) + self.b1 = self.readSignedInteger(0xB6) + self.b2 = self.readSignedInteger(0xB8) + self.mb = self.readSignedInteger(0xBA) + self.mc = self.readSignedInteger(0xBC) + self.md = self.readSignedInteger(0xBE) + + def __str__(self): + return "BMP085" + + def __family__(self): + return [Temperature.__family__(self), Pressure.__family__(self)] + + def readUnsignedInteger(self, address): + d = self.readRegisters(address, 2) + return d[0] << 8 | d[1] + + def readSignedInteger(self, address): + d = self.readUnsignedInteger(address) + return signInteger(d, 16) + + def readUT(self): + self.writeRegister(0xF4, 0x2E) + time.sleep(0.01) + return self.readUnsignedInteger(0xF6) + + def readUP(self): + self.writeRegister(0xF4, 0x34) + time.sleep(0.01) + return self.readUnsignedInteger(0xF6) + + def getB5(self): + ut = self.readUT() + x1 = ((ut - self.ac6) * self.ac5) / 2**15 + x2 = (self.mc * 2**11) / (x1 + self.md) + return x1 + x2 + + def __getKelvin__(self): + return self.Celsius2Kelvin() + + def __getCelsius__(self): + t = (self.getB5() + 8) / 2**4 + return float(t) / 10.0 + + def __getFahrenheit__(self): + return self.Celsius2Fahrenheit() + + def __getPascal__(self): + b5 = self.getB5() + up = self.readUP() + b6 = b5 - 4000 + x1 = (self.b2 * (b6 * b6 / 2**12)) / 2**11 + x2 = self.ac2 * b6 / 2**11 + x3 = x1 + x2 + b3 = (self.ac1*4 + x3 + 2) / 4 + + x1 = self.ac3 * b6 / 2**13 + x2 = (self.b1 * (b6 * b6 / 2**12)) / 2**16 + x3 = (x1 + x2 + 2) / 2**2 + b4 = self.ac4 * (x3 + 32768) / 2**15 + b7 = (up-b3) * 50000 + if b7 < 0x80000000: + p = (b7 * 2) / b4 + else: + p = (b7 / b4) * 2 + + x1 = (p / 2**8) * (p / 2**8) + x1 = (x1 * 3038) / 2**16 + x2 = (-7357*p) / 2**16 + p = p + (x1 + x2 + 3791) / 2**4 + return int(p) diff --git a/python/webiopi/devices/sensor/onewiretemp.py b/python/webiopi/devices/sensor/onewiretemp.py new file mode 100644 index 0000000..703c32d --- /dev/null +++ b/python/webiopi/devices/sensor/onewiretemp.py @@ -0,0 +1,58 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.devices.onewire import OneWire +from webiopi.devices.sensor import Temperature + +class OneWireTemp(OneWire, Temperature): + def __init__(self, slave=None, family=0, name="1-Wire"): + OneWire.__init__(self, slave, family, "TEMP") + self.name = name + + def __str__(self): + return "%s(slave=%s)" % (self.name, self.slave) + + def __getKelvin__(self): + return self.Celsius2Kelvin() + + def __getCelsius__(self): + data = self.read() + lines = data.split("\n") + if lines[0].endswith("YES"): + i = lines[1].find("=") + temp = lines[1][i+1:] + return int(temp) / 1000.0 + + def __getFahrenheit__(self): + return self.Celsius2Fahrenheit() + +class DS18S20(OneWireTemp): + def __init__(self, slave=None): + OneWireTemp.__init__(self, slave, 0x10, "DS18S20") + +class DS1822(OneWireTemp): + def __init__(self, slave=None): + OneWireTemp.__init__(self, slave, 0x22, "DS1822") + +class DS18B20(OneWireTemp): + def __init__(self, slave=None): + OneWireTemp.__init__(self, slave, 0x28, "DS18B20") + +class DS1825(OneWireTemp): + def __init__(self, slave=None): + OneWireTemp.__init__(self, slave, 0x3B, "DS1825") + +class DS28EA00(OneWireTemp): + def __init__(self, slave=None): + OneWireTemp.__init__(self, slave, 0x42, "DS28EA00") diff --git a/python/webiopi/devices/sensor/tmpXXX.py b/python/webiopi/devices/sensor/tmpXXX.py new file mode 100644 index 0000000..ba41153 --- /dev/null +++ b/python/webiopi/devices/sensor/tmpXXX.py @@ -0,0 +1,60 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import toint, signInteger +from webiopi.devices.i2c import I2C +from webiopi.devices.sensor import Temperature + +class TMP102(I2C, Temperature): + def __init__(self, slave=0x48): + I2C.__init__(self, toint(slave)) + + def __str__(self): + return "TMP102(slave=0x%02X)" % self.slave + + def __getKelvin__(self): + return self.Celsius2Kelvin() + + def __getCelsius__(self): + d = self.readBytes(2) + count = ((d[0] << 4) | (d[1] >> 4)) & 0xFFF + return signInteger(count, 12)*0.0625 + + def __getFahrenheit__(self): + return self.Celsius2Fahrenheit() + +class TMP75(TMP102): + def __init__(self, slave=0x48, resolution=12): + TMP102.__init__(self, slave) + resolution = toint(resolution) + if not resolution in range(9,13): + raise ValueError("%dbits resolution out of range [%d..%d]bits" % (resolution, 9, 12)) + self.resolution = resolution + + config = self.readRegister(0x01) + config &= ~0x60 + config |= (self.resolution - 9) << 5 + self.writeRegister(0x01, config) + self.readRegisters(0x00, 2) + + def __str__(self): + return "TMP75(slave=0x%02X, resolution=%d-bits)" % (self.slave, self.resolution) + +class TMP275(TMP75): + def __init__(self, slave=0x48, resolution=12): + TMP75.__init__(self, slave, resolution) + + def __str__(self): + return "TMP275(slave=0x%02X, resolution=%d-bits)" % (self.slave, self.resolution) + diff --git a/python/webiopi/devices/sensor/tslXXXX.py b/python/webiopi/devices/sensor/tslXXXX.py new file mode 100644 index 0000000..1189d6f --- /dev/null +++ b/python/webiopi/devices/sensor/tslXXXX.py @@ -0,0 +1,247 @@ +# Copyright 2013 Andreas Riegg +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# Changelog +# +# 1.0 2013/02/28 Initial release +# + +from webiopi.utils.types import toint +from webiopi.devices.i2c import I2C +from webiopi.devices.sensor import Luminosity + +class TSL_LIGHT_X(I2C, Luminosity): + VAL_COMMAND = 0x80 + REG_CONTROL = 0x00 | VAL_COMMAND + REG_CONFIG = 0x01 | VAL_COMMAND + + VAL_PWON = 0x03 + VAL_PWOFF = 0x00 + VAL_INVALID = -1 + + def __init__(self, slave, time, name="TSL_LIGHT_X"): + I2C.__init__(self, toint(slave)) + self.name = name + self.wake() # devices are powered down after power reset, wake them + self.setTime(toint(time)) + + def __str__(self): + return "%s(slave=0x%02X)" % (self.name, self.slave) + + def wake(self): + self.__wake__() + + def __wake__(self): + self.writeRegister(self.REG_CONTROL, self.VAL_PWON) + + def sleep(self): + self.__sleep__() + + def __sleep__(self): + self.writeRegister(self.REG_CONTROL, self.VAL_PWOFF) + + def setTime(self, time): + self.__setTime__(time) + + def getTime(self): + return self.__getTime__() + +class TSL2561X(TSL_LIGHT_X): + VAL_TIME_402_MS = 0x02 + VAL_TIME_101_MS = 0x01 + VAL_TIME_14_MS = 0x00 + + REG_CHANNEL_0_LOW = 0x0C | TSL_LIGHT_X.VAL_COMMAND + REG_CHANNEL_1_LOW = 0x0E | TSL_LIGHT_X.VAL_COMMAND + + MASK_GAIN = 0x10 + MASK_TIME = 0x03 + + def __init__(self, slave, time, gain, name="TSL2561X"): + TSL_LIGHT_X.__init__(self, slave, time, name) + self.setGain(toint(gain)) + + def __getLux__(self): + ch0_bytes = self.readRegisters(self.REG_CHANNEL_0_LOW, 2) + ch1_bytes = self.readRegisters(self.REG_CHANNEL_1_LOW, 2) + ch0_word = ch0_bytes[1] << 8 | ch0_bytes[0] + ch1_word = ch1_bytes[1] << 8 | ch1_bytes[0] + if ch0_word == 0 | ch1_word == 0: + return self.VAL_INVALID # Driver security, avoid crash in lux calculation + else: + scaling = self.time_multiplier * self.gain_multiplier + return self.__calculateLux__(scaling * ch0_word, scaling * ch1_word) + + def setGain(self, gain): + if gain == 1: + bit_gain = 0 + self.gain_multiplier = 16 + elif gain == 16: + bit_gain = 1 + self.gain_multiplier = 1 + else: + raise ValueError("Gain %d out of range [%d,%d]" % (gain, 1, 16)) + new_byte_gain = (bit_gain << 4) & self.MASK_GAIN + + current_byte_config = self.readRegister(self.REG_CONFIG) + new_byte_config = (current_byte_config & ~self.MASK_GAIN) | new_byte_gain + self.writeRegister(self.REG_CONFIG, new_byte_config) + + def getGain(self): + current_byte_config = self.readRegister(self.REG_CONFIG) + if (current_byte_config & self.MASK_GAIN): + return 16 + else: + return 1 + + def __setTime__(self, time): + if not time in [14, 101, 402]: + raise ValueError("Time %d out of range [%d,%d,%d]" % (time, 14, 101, 402)) + if time == 402: + bits_time = self.VAL_TIME_402_MS + self.time_multiplier = 1 + elif time == 101: + bits_time = self.VAL_TIME_101_MS + self.time_multiplier = 322 / 81 + elif time == 14: + bits_time = self.VAL_TIME_14_MS + self.time_multiplier = 322 / 11 + new_byte_time = bits_time & self.MASK_TIME + + current_byte_config = self.readRegister(self.REG_CONFIG) + new_byte_config = (current_byte_config & ~self.MASK_TIME) | new_byte_time + self.writeRegister(self.REG_CONFIG, new_byte_config) + + def __getTime__(self): + current_byte_config = self.readRegister(self.REG_CONFIG) + bits_time = (current_byte_config & self.MASK_TIME) + if bits_time == self.VAL_TIME_402_MS: + t = 402 + elif bits_time == self.VAL_TIME_101_MS: + t = 101 + elif bits_time == self.VAL_TIME_14_MS: + t = 14 + else: + t = TSL_LIGHT_X.VAL_INVALID # indicates undefined + return t + +class TSL2561CS(TSL2561X): + # Package CS (Chipscale) chip version + def __init__(self, slave=0x39, time=402, gain=1): + TSL2561X.__init__(self, slave, time, gain, "TSL2561CS") + + def __calculateLux__(self, channel0_value, channel1value): + channelRatio = channel1value / channel0_value + if 0 < channelRatio <= 0.52: + lux = 0.0315 * channel0_value - 0.0593 * channel0_value *(channelRatio**1.4) + elif 0.52 < channelRatio <= 0.65: + lux = 0.0229 * channel0_value - 0.0291 * channel1value + elif 0.65 < channelRatio <= 0.80: + lux = 0.0157 * channel0_value - 0.0180 * channel1value + elif 0.80 < channelRatio <= 1.30: + lux = 0.00338 * channel0_value - 0.00260 * channel1value + else: # if channelRatio > 1.30 + lux = 0 + return lux + +class TSL2561T(TSL2561X): + # Package T (TMB-6) chip version + def __init__(self, slave=0x39, time=402, gain=1): + TSL2561X.__init__(self, slave, time, gain, "TSL2561T") + + def __calculateLux__(self, channel0_value, channel1_value): + channel_ratio = channel1_value / channel0_value + if 0 < channel_ratio <= 0.50: + lux = 0.0304 * channel0_value - 0.062 * channel0_value * (channel_ratio**1.4) + elif 0.50 < channel_ratio <= 0.61: + lux = 0.0224 * channel0_value - 0.031 * channel1_value + elif 0.61 < channel_ratio <= 0.80: + lux = 0.0128 * channel0_value - 0.0153 * channel1_value + elif 0.80 < channel_ratio <= 1.30: + lux = 0.00146 * channel0_value - 0.00112 * channel1_value + else: # if channel_ratio > 1.30 + lux = 0 + return lux + +class TSL2561(TSL2561T): + # Default version for unknown packages, uses T Package class lux calculation + def __init__(self, slave=0x39, time=402, gain=1): + TSL2561X.__init__(self, slave, time, gain, "TSL2561") + + +class TSL4531(TSL_LIGHT_X): + # Default version for unknown subtypes, uses 0x29 as slave address + VAL_TIME_400_MS = 0x00 + VAL_TIME_200_MS = 0x01 + VAL_TIME_100_MS = 0x02 + + REG_DATA_LOW = 0x04 | TSL_LIGHT_X.VAL_COMMAND + + MASK_TCNTRL = 0x03 + + def __init__(self, slave=0x29, time=400, name="TSL4531"): + TSL_LIGHT_X.__init__(self, slave, time, name) + + def __setTime__(self, time): + if not time in [100, 200, 400]: + raise ValueError("Time %d out of range [%d,%d,%d]" % (time, 100, 200, 400)) + if time == 400: + bits_time = self.VAL_TIME_400_MS + self.time_multiplier = 1 + elif time == 200: + bits_time = self.VAL_TIME_200_MS + self.time_multiplier = 2 + elif time == 100: + bits_time = self.VAL_TIME_100_MS + self.time_multiplier = 4 + new_byte_time = bits_time & self.MASK_TCNTRL + + current_byte_config = self.readRegister(self.REG_CONFIG) + new_byte_config = (current_byte_config & ~self.MASK_TCNTRL) | new_byte_time + self.writeRegister(self.REG_CONFIG, new_byte_config) + + def __getTime__(self): + current_byte_config = self.readRegister(self.REG_CONFIG) + bits_time = (current_byte_config & self.MASK_TCNTRL) + if bits_time == self.VAL_TIME_400_MS: + t = 400 + elif bits_time == self.VAL_TIME_200_MS: + t = 200 + elif bits_time == self.VAL_TIME_100_MS: + t = 100 + else: + t = TSL_LIGHT_X.VAL_INVALID # indicates undefined + return t + + def __getLux__(self): + data_bytes = self.readRegisters(self.REG_DATA_LOW, 2) + return self.time_multiplier * (data_bytes[1] << 8 | data_bytes[0]) + +class TSL45311(TSL4531): + def __init__(self, slave=0x39, time=400): + TSL4531.__init__(self, slave, time, "TSL45311") + +class TSL45313(TSL4531): + def __init__(self, slave=0x39, time=400): + TSL4531.__init__(self, slave, time, "TSL45313") + +class TSL45315(TSL4531): + def __init__(self, slave=0x29, time=400): + TSL4531.__init__(self, slave, time, "TSL45315") + +class TSL45317(TSL4531): + def __init__(self, slave=0x29, time=400): + TSL4531.__init__(self, slave, time, "TSL45317") + diff --git a/python/webiopi/devices/sensor/vcnl4000.py b/python/webiopi/devices/sensor/vcnl4000.py new file mode 100644 index 0000000..fb8b40b --- /dev/null +++ b/python/webiopi/devices/sensor/vcnl4000.py @@ -0,0 +1,211 @@ +# Copyright 2013 Andreas Riegg +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# Changelog +# +# 1.0 2013/02/24 Initial release. Luminosity is final. Proximity is good beta +# and a working coarse estimation for distance value. +# + +import time +from webiopi.devices.i2c import I2C +from webiopi.devices.sensor import Luminosity, Distance +from webiopi.utils.types import toint +from webiopi.utils.logger import debug + +class VCNL4000(I2C, Luminosity, Distance): + REG_COMMAND = 0x80 + REG_IR_LED_CURRENT = 0x83 + REG_AMB_PARAMETERS = 0x84 + REG_AMB_RESULT_HIGH = 0x85 + REG_PROX_RESULT_HIGH = 0x87 + REG_PROX_FREQUENCY = 0x89 + REG_PROX_ADJUST = 0x8A + + VAL_MOD_TIMING_DEF = 129 # default from data sheet + + VAL_PR_FREQ_3M125HZ = 0 + VAL_PR_FREQ_1M5625HZ = 1 + VAL_PR_FREQ_781K25HZ = 2 + VAL_PR_FREQ_390K625HZ = 3 + + VAL_START_AMB = 1 << 4 + VAL_START_PROX = 1 << 3 + + VAL_INVALID = -1 + VAL_NO_PROXIMITY = -1 + + MASK_PROX_FREQUENCY = 0b00111111 + MASK_IR_LED_CURRENT = 0b00111111 + MASK_PROX_READY = 0b00100000 + MASK_AMB_READY = 0b01000000 + + def __init__(self, slave=0b0010011, current=20, frequency=781, prox_threshold=15, prox_cycles=10, cal_cycles= 5): + I2C.__init__(self, toint(slave)) + self.setCurrent(toint(current)) + self.setFrequency(toint(frequency)) + self.prox_threshold = toint(prox_threshold) + self.prox_cycles = toint(prox_cycles) + self.cal_cycles = toint(cal_cycles) + self.__setProximityTiming__() + self.__setAmbientMeasuringMode__() + time.sleep(0.001) + self.calibrate() # may have to be repeated from time to time or before every proximity measurement + + def __str__(self): + return "VCNL4000(slave=0x%02X)" % self.slave + + def __family__(self): + return [Luminosity.__family__(self), Distance.__family__(self)] + + def __setProximityTiming__(self): + self.writeRegister(self.REG_PROX_ADJUST, self.VAL_MOD_TIMING_DEF) + + def __setAmbientMeasuringMode__(self): + ambient_parameter_bytes = 1 << 7 | 1 << 3 | 5 + # Parameter is set to + # -continuous conversion mode (bit 7) + # -auto offset compensation (bit 3) + # -averaging 32 samples (5) + self.writeRegister(self.REG_AMB_PARAMETERS, ambient_parameter_bytes) + + def calibrate(self): + self.offset = self.__measureOffset__() + debug ("VCNL4000: offset = %d" % (self.offset)) + return self.offset + + + def setCurrent(self, current): + self.current = current + self.__setCurrent__() + + + def getCurrent(self): + return self.__getCurrent__() + + def setFrequency(self, frequency): + self.frequency = frequency + self.__setFrequency__() + + def getFrequency(self): + return self.__getFrequency__() + + def __setFrequency__(self): + if not self.frequency in [391, 781, 1563, 3125]: + raise ValueError("Frequency %d out of range [%d,%d,%d,,%d]" % (self.frequency, 391, 781, 1563, 3125)) + if self.frequency == 391: + bits_frequency = self.VAL_PR_FREQ_390K625HZ + elif self.frequency == 781: + bits_frequency = self.VAL_PR_FREQ_781K25HZ + elif self.frequency == 1563: + bits_frequency = self.VAL_PR_FREQ_1M5625HZ + elif self.frequency == 3125: + bits_frequency = self.VAL_PR_FREQ_3M125HZ + self.writeRegister(self.REG_PROX_FREQUENCY, bits_frequency) + debug ("VCNL4000: new freq = %d" % (self.readRegister(self.REG_PROX_FREQUENCY))) + + def __getFrequency__(self): + bits_frequency = self.readRegister(self.REG_PROX_FREQUENCY) & self.MASK_PROX_FREQUENCY + if bits_frequency == self.VAL_PR_FREQ_390K625HZ: + f = 391 + elif bits_frequency == self.VAL_PR_FREQ_781K25HZ: + f = 781 + elif bits_frequency == self.VAL_PR_FREQ_1M5625HZ: + f = 1563 + elif bits_frequency == self.VAL_PR_FREQ_3M125HZ: + f = 3125 + else: + f = self.VAL_INVALID # indicates undefined + return f + + def __setCurrent__(self): + if not self.current in range(0,201): + raise ValueError("%d mA LED current out of range [%d..%d] mA" % (self.current, 0, 201)) + self.writeRegister(self.REG_IR_LED_CURRENT, int(self.current / 10)) + debug ("VCNL4000: new curr = %d" % (self.readRegister(self.REG_IR_LED_CURRENT))) + + def __getCurrent__(self): + bits_current = self.readRegister(self.REG_IR_LED_CURRENT) & self.MASK_IR_LED_CURRENT + return bits_current * 10 + + def __getLux__(self): + self.writeRegister(self.REG_COMMAND, self.VAL_START_AMB) + while not (self.readRegister(self.REG_COMMAND) & self.MASK_AMB_READY): + time.sleep(0.001) + light_bytes = self.readRegisters(self.REG_AMB_RESULT_HIGH, 2) + light_word = light_bytes[0] << 8 | light_bytes[1] + return self.__calculateLux__(light_word) + + def __calculateLux__(self, light_word): + return (light_word + 3) * 0.25 # From VISHAY application note + + def __getMillimeter__(self): + success = 0 + fail = 0 + prox = 0 + match_cycles = self.prox_cycles + while (fail < match_cycles) & (success < match_cycles): + real_counts = self.__readProximityCounts__() - self.offset + if real_counts > self.prox_threshold: + success += 1 + prox += real_counts + else: + fail += 1 + if fail == match_cycles: + return self.VAL_NO_PROXIMITY + else: + return self.__calculateMillimeter__(prox // match_cycles) + + def __calculateMillimeter__(self, raw_proximity_counts): + # According to chip spec the proximity counts are strong non-linear with distance and cannot be calculated + # with a direct formula. From experience found on web this chip is generally not suited for really exact + # distance calculations. This is a rough distance estimation lookup table for now. Maybe someone can + # provide a more exact approximation in the future. + + debug ("VCNL4000: prox real raw counts = %d" % (raw_proximity_counts)) + if raw_proximity_counts >= 10000: + estimated_distance = 0 + elif raw_proximity_counts >= 3000: + estimated_distance = 5 + elif raw_proximity_counts >= 900: + estimated_distance = 10 + elif raw_proximity_counts >= 300: + estimated_distance = 20 + elif raw_proximity_counts >= 150: + estimated_distance = 30 + elif raw_proximity_counts >= 75: + estimated_distance = 40 + elif raw_proximity_counts >= 50: + estimated_distance = 50 + elif raw_proximity_counts >= 25: + estimated_distance = 70 + else: + estimated_distance = 100 + return estimated_distance + + def __measureOffset__(self): + offset = 0 + for unused in range(self.cal_cycles): + offset += self.__readProximityCounts__() + return offset // self.cal_cycles + + def __readProximityCounts__(self): + self.writeRegister(self.REG_COMMAND, self.VAL_START_PROX) + while not (self.readRegister(self.REG_COMMAND) & self.MASK_PROX_READY): + time.sleep(0.001) + proximity_bytes = self.readRegisters(self.REG_PROX_RESULT_HIGH, 2) + debug ("VCNL4000: prox raw value = %d" % (proximity_bytes[0] << 8 | proximity_bytes[1])) + return (proximity_bytes[0] << 8 | proximity_bytes[1]) + \ No newline at end of file diff --git a/python/webiopi/devices/serial.py b/python/webiopi/devices/serial.py new file mode 100644 index 0000000..800f96f --- /dev/null +++ b/python/webiopi/devices/serial.py @@ -0,0 +1,86 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import fcntl +import struct +import termios + +from webiopi.devices.bus import Bus +from webiopi.decorators.rest import request + +TIOCINQ = hasattr(termios, 'FIONREAD') and termios.FIONREAD or 0x541B +TIOCM_zero_str = struct.pack('I', 0) + +class Serial(Bus): + def __init__(self, device="/dev/ttyAMA0", baudrate=9600): + if not device.startswith("/dev/"): + device = "/dev/%s" % device + + if isinstance(baudrate, str): + baudrate = int(baudrate) + + aname = "B%d" % baudrate + if not hasattr(termios, aname): + raise Exception("Unsupported baudrate") + self.baudrate = baudrate + + Bus.__init__(self, "UART", device, os.O_RDWR | os.O_NOCTTY) + fcntl.fcntl(self.fd, fcntl.F_SETFL, os.O_NDELAY) + + #backup = termios.tcgetattr(self.fd) + options = termios.tcgetattr(self.fd) + # iflag + options[0] = 0 + + # oflag + options[1] = 0 + + # cflag + options[2] |= (termios.CLOCAL | termios.CREAD) + options[2] &= ~termios.PARENB + options[2] &= ~termios.CSTOPB + options[2] &= ~termios.CSIZE + options[2] |= termios.CS8 + + # lflag + options[3] = 0 + + speed = getattr(termios, aname) + # input speed + options[4] = speed + # output speed + options[5] = speed + + termios.tcsetattr(self.fd, termios.TCSADRAIN, options) + + def __str__(self): + return "Serial(%s, %dbps)" % (self.device, self.baudrate) + + def __family__(self): + return "Serial" + + def available(self): + s = fcntl.ioctl(self.fd, TIOCINQ, TIOCM_zero_str) + return struct.unpack('I',s)[0] + + @request("GET", "") + def read(self): + if self.available() > 0: + return Bus.read(self, self.available()).decode() + return "" + + @request("POST", "", "data") + def write(self, data): + Bus.write(self, data) diff --git a/python/webiopi/devices/shield/__init__.py b/python/webiopi/devices/shield/__init__.py new file mode 100644 index 0000000..ef77597 --- /dev/null +++ b/python/webiopi/devices/shield/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +DRIVERS = {} +DRIVERS["piface"] = ["PiFaceDigital"] \ No newline at end of file diff --git a/python/webiopi/devices/shield/piface.py b/python/webiopi/devices/shield/piface.py new file mode 100644 index 0000000..ffe6215 --- /dev/null +++ b/python/webiopi/devices/shield/piface.py @@ -0,0 +1,66 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.types import M_JSON +from webiopi.devices.digital.mcp23XXX import MCP23S17 +from webiopi.decorators.rest import request, response + + +class PiFaceDigital(): + def __init__(self, board=0): + mcp = MCP23S17(0, 0x20+board) + mcp.writeRegister(mcp.getAddress(mcp.IODIR, 0), 0x00) # Port A as output + mcp.writeRegister(mcp.getAddress(mcp.IODIR, 8), 0xFF) # Port B as input + mcp.writeRegister(mcp.getAddress(mcp.GPPU, 0), 0x00) # Port A PU OFF + mcp.writeRegister(mcp.getAddress(mcp.GPPU, 8), 0xFF) # Port B PU ON + self.mcp = mcp + self.board = board + + def __str__(self): + return "PiFaceDigital(%d)" % self.board + + def __family__(self): + return "PiFaceDigital" + + def checkChannel(self, channel): + if not channel in range(8): + raise ValueError("Channel %d invalid" % channel) + + @request("GET", "digital/input/%(channel)d") + @response("%d") + def digitalRead(self, channel): + self.checkChannel(channel) + return not self.mcp.digitalRead(channel+8) + + @request("POST", "digital/output/%(channel)d/%(value)d") + @response("%d") + def digitalWrite(self, channel, value): + self.checkChannel(channel) + return self.mcp.digitalWrite(channel, value) + + @request("GET", "digital/output/%(channel)d") + @response("%d") + def digitalReadOutput(self, channel): + self.checkChannel(channel) + return self.mcp.digitalRead(channel) + + @request("GET", "digital/*") + @response(contentType=M_JSON) + def readAll(self): + inputs = {} + outputs = {} + for i in range(8): + inputs[i] = self.digitalRead(i) + outputs[i] = self.digitalReadOutput(i) + return {"input": inputs, "output": outputs} diff --git a/python/webiopi/devices/spi.py b/python/webiopi/devices/spi.py new file mode 100644 index 0000000..d19de79 --- /dev/null +++ b/python/webiopi/devices/spi.py @@ -0,0 +1,145 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import fcntl +import array +import ctypes +import struct + +from webiopi.utils.version import PYTHON_MAJOR +from webiopi.devices.bus import Bus + +# from spi/spidev.h +_IOC_NRBITS = 8 +_IOC_TYPEBITS = 8 +_IOC_SIZEBITS = 14 +_IOC_DIRBITS = 2 + +_IOC_NRSHIFT = 0 +_IOC_TYPESHIFT = (_IOC_NRSHIFT+_IOC_NRBITS) +_IOC_SIZESHIFT = (_IOC_TYPESHIFT+_IOC_TYPEBITS) +_IOC_DIRSHIFT = (_IOC_SIZESHIFT+_IOC_SIZEBITS) + +_IOC_NONE = 0 +_IOC_WRITE = 1 +_IOC_READ = 2 + +def _IOC(direction,t,nr,size): + return (((direction) << _IOC_DIRSHIFT) | + ((size) << _IOC_SIZESHIFT) | + ((t) << _IOC_TYPESHIFT) | + ((nr) << _IOC_NRSHIFT)) +def _IOR(t, number, size): + return _IOC(_IOC_READ, t, number, size) +def _IOW(t, number, size): + return _IOC(_IOC_WRITE, t, number, size) + +SPI_CPHA = 0x01 +SPI_CPOL = 0x02 + +SPI_MODE_0 = (0|0) +SPI_MODE_1 = (0|SPI_CPHA) +SPI_MODE_2 = (SPI_CPOL|0) +SPI_MODE_3 = (SPI_CPOL|SPI_CPHA) + +# does not work +# SPI_CS_HIGH = 0x04 +# SPI_LSB_FIRST = 0x08 +# SPI_3WIRE = 0x10 +# SPI_LOOP = 0x20 +# SPI_NO_CS = 0x40 +# SPI_READY = 0x80 + +SPI_IOC_MAGIC = ord('k') + +def SPI_IOC_MESSAGE(count): + return _IOW(SPI_IOC_MAGIC, 0, count) + +# Read / Write of SPI mode (SPI_MODE_0..SPI_MODE_3) +SPI_IOC_RD_MODE = _IOR(SPI_IOC_MAGIC, 1, 1) +SPI_IOC_WR_MODE = _IOW(SPI_IOC_MAGIC, 1, 1) + +# Read / Write SPI bit justification +# does not work +# SPI_IOC_RD_LSB_FIRST = _IOR(SPI_IOC_MAGIC, 2, 1) +# SPI_IOC_WR_LSB_FIRST = _IOW(SPI_IOC_MAGIC, 2, 1) + +# Read / Write SPI device word length (1..N) +SPI_IOC_RD_BITS_PER_WORD = _IOR(SPI_IOC_MAGIC, 3, 1) +SPI_IOC_WR_BITS_PER_WORD = _IOW(SPI_IOC_MAGIC, 3, 1) + +# Read / Write SPI device default max speed hz +SPI_IOC_RD_MAX_SPEED_HZ = _IOR(SPI_IOC_MAGIC, 4, 4) +SPI_IOC_WR_MAX_SPEED_HZ = _IOW(SPI_IOC_MAGIC, 4, 4) + +class SPI(Bus): + def __init__(self, chip=0, mode=0, bits=8, speed=0): + Bus.__init__(self, "SPI", "/dev/spidev0.%d" % chip) + self.chip = chip + + val8 = array.array('B', [0]) + val8[0] = mode + if fcntl.ioctl(self.fd, SPI_IOC_WR_MODE, val8): + raise Exception("Cannot write SPI Mode") + if fcntl.ioctl(self.fd, SPI_IOC_RD_MODE, val8): + raise Exception("Cannot read SPI Mode") + self.mode = struct.unpack('B', val8)[0] + assert(self.mode == mode) + + val8[0] = bits + if fcntl.ioctl(self.fd, SPI_IOC_WR_BITS_PER_WORD, val8): + raise Exception("Cannot write SPI Bits per word") + if fcntl.ioctl(self.fd, SPI_IOC_RD_BITS_PER_WORD, val8): + raise Exception("Cannot read SPI Bits per word") + self.bits = struct.unpack('B', val8)[0] + assert(self.bits == bits) + + val32 = array.array('I', [0]) + if speed > 0: + val32[0] = speed + if fcntl.ioctl(self.fd, SPI_IOC_WR_MAX_SPEED_HZ, val32): + raise Exception("Cannot write SPI Max speed") + if fcntl.ioctl(self.fd, SPI_IOC_RD_MAX_SPEED_HZ, val32): + raise Exception("Cannot read SPI Max speed") + self.speed = struct.unpack('I', val32)[0] + assert((self.speed == speed) or (speed == 0)) + + def __str__(self): + return "SPI(chip=%d, mode=%d, speed=%dHz)" % (self.chip, self.mode, self.speed) + + def xfer(self, txbuff=None): + length = len(txbuff) + if PYTHON_MAJOR >= 3: + _txbuff = bytes(txbuff) + _txptr = ctypes.create_string_buffer(_txbuff) + else: + _txbuff = str(bytearray(txbuff)) + _txptr = ctypes.create_string_buffer(_txbuff) + _rxptr = ctypes.create_string_buffer(length) + + data = struct.pack("QQLLHBBL", #64 64 32 32 16 8 8 32 b = 32B + ctypes.addressof(_txptr), + ctypes.addressof(_rxptr), + length, + self.speed, + 0, #delay + self.bits, + 0, # cs_change, + 0 # pad + ) + + fcntl.ioctl(self.fd, SPI_IOC_MESSAGE(len(data)), data) + _rxbuff = ctypes.string_at(_rxptr, length) + return bytearray(_rxbuff) + \ No newline at end of file diff --git a/python/webiopi/protocols/__init__.py b/python/webiopi/protocols/__init__.py new file mode 100644 index 0000000..fc4ac9a --- /dev/null +++ b/python/webiopi/protocols/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + diff --git a/python/webiopi/protocols/coap.py b/python/webiopi/protocols/coap.py new file mode 100644 index 0000000..3ed6b3e --- /dev/null +++ b/python/webiopi/protocols/coap.py @@ -0,0 +1,537 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils.version import PYTHON_MAJOR +from webiopi.utils.logger import info, exception + +import socket +import struct +import logging +import threading + +M_PLAIN = "text/plain" +M_JSON = "application/json" + +if PYTHON_MAJOR >= 3: + from urllib.parse import urlparse +else: + from urlparse import urlparse + +try : + import _webiopi.GPIO as GPIO +except: + pass + +def HTTPCode2CoAPCode(code): + return int(code/100) * 32 + (code%100) + + +class COAPContentFormat(): + FORMATS = {0: "text/plain", + 40: "application/link-format", + 41: "application/xml", + 42: "application/octet-stream", + 47: "application/exi", + 50: "application/json" + } + + @staticmethod + def getCode(fmt): + if fmt == None: + return None + for code in COAPContentFormat.FORMATS: + if COAPContentFormat.FORMATS[code] == fmt: + return code + return None + + @staticmethod + def toString(code): + if code == None: + return None + + if code in COAPContentFormat.FORMATS: + return COAPContentFormat.FORMATS[code] + + raise Exception("Unknown content format %d" % code) + + +class COAPOption(): + OPTIONS = {1: "If-Match", + 3: "Uri-Host", + 4: "ETag", + 5: "If-None-Match", + 7: "Uri-Port", + 8: "Location-Path", + 11: "Uri-Path", + 12: "Content-Format", + 14: "Max-Age", + 15: "Uri-Query", + 16: "Accept", + 20: "Location-Query", + 35: "Proxy-Uri", + 39: "Proxy-Scheme" + } + + IF_MATCH = 1 + URI_HOST = 3 + ETAG = 4 + IF_NONE_MATCH = 5 + URI_PORT = 7 + LOCATION_PATH = 8 + URI_PATH = 11 + CONTENT_FORMAT = 12 + MAX_AGE = 14 + URI_QUERY = 15 + ACCEPT = 16 + LOCATION_QUERY = 20 + PROXY_URI = 35 + PROXY_SCHEME = 39 + + +class COAPMessage(): + TYPES = ["CON", "NON", "ACK", "RST"] + CON = 0 + NON = 1 + ACK = 2 + RST = 3 + + def __init__(self, msg_type=0, code=0, uri=None): + self.version = 1 + self.type = msg_type + self.code = code + self.id = 0 + self.token = None + self.options = [] + self.host = "" + self.port = 5683 + self.uri_path = "" + self.content_format = None + self.payload = None + + if uri != None: + p = urlparse(uri) + self.host = p.hostname + if p.port: + self.port = int(p.port) + self.uri_path = p.path + + def __getOptionHeader__(self, byte): + delta = (byte & 0xF0) >> 4 + length = byte & 0x0F + return (delta, length) + + def __str__(self): + result = [] + result.append("Version: %s" % self.version) + result.append("Type: %s" % self.TYPES[self.type]) + result.append("Code: %s" % self.CODES[self.code]) + result.append("Id: %s" % self.id) + result.append("Token: %s" % self.token) + #result.append("Options: %s" % len(self.options)) + #for option in self.options: + # result.append("+ %d: %s" % (option["number"], option["value"])) + result.append("Uri-Path: %s" % self.uri_path) + result.append("Content-Format: %s" % (COAPContentFormat.toString(self.content_format) if self.content_format else M_PLAIN)) + result.append("Payload: %s" % self.payload) + result.append("") + return '\n'.join(result) + + def getOptionHeaderValue(self, value): + if value > 268: + return 14 + if value > 12: + return 13 + return value + + def getOptionHeaderExtension(self, value): + buff = bytearray() + v = self.getOptionHeaderValue(value) + + if v == 14: + value -= 269 + buff.append((value & 0xFF00) >> 8) + buff.append(value & 0x00FF) + + elif v == 13: + value -= 13 + buff.append(value) + + return buff + + def appendOption(self, buff, lastnumber, option, data): + delta = option - lastnumber + length = len(data) + + d = self.getOptionHeaderValue(delta) + l = self.getOptionHeaderValue(length) + + b = 0 + b |= (d << 4) & 0xF0 + b |= l & 0x0F + buff.append(b) + + ext = self.getOptionHeaderExtension(delta); + for b in ext: + buff.append(b) + + ext = self.getOptionHeaderExtension(length); + for b in ext: + buff.append(b) + + for b in data: + buff.append(b) + + return option + + def getBytes(self): + buff = bytearray() + byte = (self.version & 0x03) << 6 + byte |= (self.type & 0x03) << 4 + if self.token: + token_len = min(len(self.token), 8); + else: + token_len = 0 + byte |= token_len + buff.append(byte) + buff.append(self.code) + buff.append((self.id & 0xFF00) >> 8) + buff.append(self.id & 0x00FF) + + if self.token: + for c in self.token: + buff.append(c) + + lastnumber = 0 + + if len(self.uri_path) > 0: + paths = self.uri_path.split("/") + for p in paths: + if len(p) > 0: + if PYTHON_MAJOR >= 3: + data = p.encode() + else: + data = bytearray(p) + lastnumber = self.appendOption(buff, lastnumber, COAPOption.URI_PATH, data) + + if self.content_format != None: + data = bytearray() + fmt_code = self.content_format + if fmt_code > 0xFF: + data.append((fmt_code & 0xFF00) >> 8) + data.append(fmt_code & 0x00FF) + lastnumber = self.appendOption(buff, lastnumber, COAPOption.CONTENT_FORMAT, data) + + buff.append(0xFF) + + if self.payload: + if PYTHON_MAJOR >= 3: + data = self.payload.encode() + else: + data = bytearray(self.payload) + for c in data: + buff.append(c) + + return buff + + def parseByteArray(self, buff): + self.version = (buff[0] & 0xC0) >> 6 + self.type = (buff[0] & 0x30) >> 4 + token_length = buff[0] & 0x0F + index = 4 + if token_length > 0: + self.token = buff[index:index+token_length] + + index += token_length + self.code = buff[1] + self.id = (buff[2] << 8) | buff[3] + + number = 0 + + # process options + while index < len(buff) and buff[index] != 0xFF: + (delta, length) = self.__getOptionHeader__(buff[index]) + offset = 1 + + # delta extended with 1 byte + if delta == 13: + delta += buff[index+offset] + offset += 1 + # delta extended with 2 buff + elif delta == 14: + delta += 255 + ((buff[index+offset] << 8) | buff[index+offset+1]) + offset += 2 + + # length extended with 1 byte + if length == 13: + length += buff[index+offset] + offset += 1 + + # length extended with 2 buff + elif length == 14: + length += 255 + ((buff[index+offset] << 8) | buff[index+offset+1]) + offset += 2 + + number += delta + valueBytes = buff[index+offset:index+offset+length] + # opaque option value + if number in [COAPOption.IF_MATCH, COAPOption.ETAG]: + value = valueBytes + # integer option value + elif number in [COAPOption.URI_PORT, COAPOption.CONTENT_FORMAT, COAPOption.MAX_AGE, COAPOption.ACCEPT]: + value = 0 + for b in valueBytes: + value <<= 8 + value |= b + # string option value + else: + if PYTHON_MAJOR >= 3: + value = valueBytes.decode() + else: + value = str(valueBytes) + self.options.append({'number': number, 'value': value}) + index += offset + length + + index += 1 # skip 0xFF / end-of-options + + if len(buff) > index: + self.payload = buff[index:] + else: + self.payload = "" + + for option in self.options: + (number, value) = option.values() + if number == COAPOption.URI_PATH: + self.uri_path += "/%s" % value + + +class COAPRequest(COAPMessage): + CODES = {0: None, + 1: "GET", + 2: "POST", + 3: "PUT", + 4: "DELETE" + } + + GET = 1 + POST = 2 + PUT = 3 + DELETE = 4 + + def __init__(self, msg_type=0, code=0, uri=None): + COAPMessage.__init__(self, msg_type, code, uri) + +class COAPGet(COAPRequest): + def __init__(self, uri): + COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.GET, uri) + +class COAPPost(COAPRequest): + def __init__(self, uri): + COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.POST, uri) + +class COAPPut(COAPRequest): + def __init__(self, uri): + COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.PUT, uri) + +class COAPDelete(COAPRequest): + def __init__(self, uri): + COAPRequest.__init__(self, COAPMessage.CON, COAPRequest.DELETE, uri) + +class COAPResponse(COAPMessage): + CODES = {0: None, + 64: "2.00 OK", + 65: "2.01 Created", + 66: "2.02 Deleted", + 67: "2.03 Valid", + 68: "2.04 Changed", + 69: "2.05 Content", + 128: "4.00 Bad Request", + 129: "4.01 Unauthorized", + 130: "4.02 Bad Option", + 131: "4.03 Forbidden", + 132: "4.04 Not Found", + 133: "4.05 Method Not Allowed", + 134: "4.06 Not Acceptable", + 140: "4.12 Precondition Failed", + 141: "4.13 Request Entity Too Large", + 143: "4.15 Unsupported Content-Format", + 160: "5.00 Internal Server Error", + 161: "5.01 Not Implemented", + 162: "5.02 Bad Gateway", + 163: "5.03 Service Unavailable", + 164: "5.04 Gateway Timeout", + 165: "5.05 Proxying Not Supported" + } + + # 2.XX + OK = 64 + CREATED = 65 + DELETED = 66 + VALID = 67 + CHANGED = 68 + CONTENT = 69 + + # 4.XX + BAD_REQUEST = 128 + UNAUTHORIZED = 129 + BAD_OPTION = 130 + FORBIDDEN = 131 + NOT_FOUND = 132 + NOT_ALLOWED = 133 + NOT_ACCEPTABLE = 134 + PRECONDITION_FAILED = 140 + ENTITY_TOO_LARGE = 141 + UNSUPPORTED_CONTENT = 143 + + # 5.XX + INTERNAL_ERROR = 160 + NOT_IMPLEMENTED = 161 + BAD_GATEWAY = 162 + SERVICE_UNAVAILABLE = 163 + GATEWAY_TIMEOUT = 164 + PROXYING_NOT_SUPPORTED = 165 + + def __init__(self): + COAPMessage.__init__(self) + +class COAPClient(): + def __init__(self): + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.settimeout(1.0) + self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) + + def sendRequest(self, message): + data = message.getBytes(); + sent = 0 + while sent<4: + try: + self.socket.sendto(data, (message.host, message.port)) + data = self.socket.recv(1500) + response = COAPResponse() + response.parseByteArray(bytearray(data)) + return response + except socket.timeout: + sent+=1 + return None + +class COAPServer(threading.Thread): + logger = logging.getLogger("CoAP") + + def __init__(self, host, port, handler): + threading.Thread.__init__(self, name="COAPThread") + self.handler = COAPHandler(handler) + self.host = host + self.port = port + self.multicast_ip = '224.0.1.123' + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.socket.bind(('', port)) + self.socket.settimeout(1) + self.running = True + self.start() + + def run(self): + info("CoAP Server binded on coap://%s:%s/" % (self.host, self.port)) + while self.running == True: + try: + (request, client) = self.socket.recvfrom(1500) + requestBytes = bytearray(request) + coapRequest = COAPRequest() + coapRequest.parseByteArray(requestBytes) + coapResponse = COAPResponse() + #self.logger.debug("Received Request:\n%s" % coapRequest) + self.processMessage(coapRequest, coapResponse) + #self.logger.debug("Sending Response:\n%s" % coapResponse) + responseBytes = coapResponse.getBytes() + self.socket.sendto(responseBytes, client) + self.logger.debug('"%s %s CoAP/%.1f" %s -' % (coapRequest.CODES[coapRequest.code], coapRequest.uri_path, coapRequest.version, coapResponse.CODES[coapResponse.code])) + + except socket.timeout as e: + continue + except Exception as e: + if self.running == True: + exception(e) + + info("CoAP Server stopped") + + def enableMulticast(self): + while not self.running: + pass + mreq = struct.pack("4sl", socket.inet_aton(self.multicast_ip), socket.INADDR_ANY) + self.socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + info("CoAP Server binded on coap://%s:%s/ (MULTICAST)" % (self.multicast_ip, self.port)) + + def stop(self): + self.running = False + self.socket.close() + + def processMessage(self, request, response): + if request.type == COAPMessage.CON: + response.type = COAPMessage.ACK + else: + response.type = COAPMessage.NON + + if request.token: + response.token = request.token + + response.id = request.id + response.uri_path = request.uri_path + + if request.code == COAPRequest.GET: + self.handler.do_GET(request, response) + elif request.code == COAPRequest.POST: + self.handler.do_POST(request, response) + elif request.code / 32 == 0: + response.code = COAPResponse.NOT_IMPLEMENTED + else: + exception(Exception("Received CoAP Response : %s" % response)) + +class COAPHandler(): + def __init__(self, handler): + self.handler = handler + + def do_GET(self, request, response): + try: + (code, body, contentType) = self.handler.do_GET(request.uri_path[1:], True) + if code == 0: + response.code = COAPResponse.NOT_FOUND + elif code == 200: + response.code = COAPResponse.CONTENT + else: + response.code = HTTPCode2CoAPCode(code) + response.payload = body + response.content_format = COAPContentFormat.getCode(contentType) + except (GPIO.InvalidDirectionException, GPIO.InvalidChannelException, GPIO.SetupException) as e: + response.code = COAPResponse.FORBIDDEN + response.payload = "%s" % e + except Exception as e: + response.code = COAPResponse.INTERNAL_ERROR + raise e + + def do_POST(self, request, response): + try: + (code, body, contentType) = self.handler.do_POST(request.uri_path[1:], request.payload, True) + if code == 0: + response.code = COAPResponse.NOT_FOUND + elif code == 200: + response.code = COAPResponse.CHANGED + else: + response.code = HTTPCode2CoAPCode(code) + response.payload = body + response.content_format = COAPContentFormat.getCode(contentType) + except (GPIO.InvalidDirectionException, GPIO.InvalidChannelException, GPIO.SetupException) as e: + response.code = COAPResponse.FORBIDDEN + response.payload = "%s" % e + except Exception as e: + response.code = COAPResponse.INTERNAL_ERROR + raise e + diff --git a/python/webiopi/protocols/http.py b/python/webiopi/protocols/http.py new file mode 100644 index 0000000..aea6d82 --- /dev/null +++ b/python/webiopi/protocols/http.py @@ -0,0 +1,249 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import threading +import codecs +import mimetypes as mime +import logging + +from webiopi.utils.version import VERSION_STRING, PYTHON_MAJOR +from webiopi.utils.logger import info, exception +from webiopi.utils.crypto import encrypt +from webiopi.utils.types import str2bool + +if PYTHON_MAJOR >= 3: + import http.server as BaseHTTPServer +else: + import BaseHTTPServer + +try : + import _webiopi.GPIO as GPIO +except: + pass + +WEBIOPI_DOCROOT = "/usr/share/webiopi/htdocs" + +class HTTPServer(BaseHTTPServer.HTTPServer, threading.Thread): + def __init__(self, host, port, handler, context, docroot, index, auth=None): + BaseHTTPServer.HTTPServer.__init__(self, ("", port), HTTPHandler) + threading.Thread.__init__(self, name="HTTPThread") + self.host = host + self.port = port + + if context: + self.context = context + if not self.context.startswith("/"): + self.context = "/" + self.context + if not self.context.endswith("/"): + self.context += "/" + else: + self.context = "/" + + self.docroot = docroot + + if index: + self.index = index + else: + self.index = "index.html" + + self.handler = handler + self.auth = auth + + self.running = True + self.start() + + def get_request(self): + sock, addr = self.socket.accept() + sock.settimeout(10.0) + return (sock, addr) + + def run(self): + info("HTTP Server binded on http://%s:%s%s" % (self.host, self.port, self.context)) + try: + self.serve_forever() + except Exception as e: + if self.running == True: + exception(e) + info("HTTP Server stopped") + + def stop(self): + self.running = False + self.server_close() + +class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler): + logger = logging.getLogger("HTTP") + + def log_message(self, fmt, *args): + self.logger.debug(fmt % args) + + def log_error(self, fmt, *args): + pass + + def version_string(self): + return VERSION_STRING + + def checkAuthentication(self): + if self.server.auth == None or len(self.server.auth) == 0: + return True + + authHeader = self.headers.get('Authorization') + if authHeader == None: + return False + + if not authHeader.startswith("Basic "): + return False + + auth = authHeader.replace("Basic ", "") + if PYTHON_MAJOR >= 3: + auth_hash = encrypt(auth.encode()) + else: + auth_hash = encrypt(auth) + + if auth_hash == self.server.auth: + return True + return False + + def requestAuthentication(self): + self.send_response(401) + self.send_header("WWW-Authenticate", 'Basic realm="webiopi"') + self.end_headers(); + + def sendResponse(self, code, body=None, contentType="text/plain"): + if code >= 400: + if body != None: + self.send_error(code, body) + else: + self.send_error(code) + else: + self.send_response(code) + self.send_header("Cache-Control", "no-cache") + if body != None: + self.send_header("Content-Type", contentType); + self.end_headers(); + self.wfile.write(body.encode()) + + def findFile(self, filepath): + if os.path.exists(filepath): + if os.path.isdir(filepath): + filepath += "/" + self.server.index + if os.path.exists(filepath): + return filepath + else: + return filepath + return None + + + def serveFile(self, relativePath): + if self.server.docroot != None: + path = self.findFile(self.server.docroot + "/" + relativePath) + if path == None: + path = self.findFile("./" + relativePath) + + else: + path = self.findFile("./" + relativePath) + if path == None: + path = self.findFile(WEBIOPI_DOCROOT + "/" + relativePath) + + if path == None and (relativePath.startswith("webiopi.") or relativePath.startswith("jquery")): + path = self.findFile(WEBIOPI_DOCROOT + "/" + relativePath) + + if path == None: + return self.sendResponse(404, "Not Found") + + realPath = os.path.realpath(path) + + if realPath.endswith(".py"): + return self.sendResponse(403, "Not Authorized") + + if not (realPath.startswith(os.getcwd()) + or (self.server.docroot and realPath.startswith(self.server.docroot)) + or realPath.startswith(WEBIOPI_DOCROOT)): + return self.sendResponse(403, "Not Authorized") + + (contentType, encoding) = mime.guess_type(path) + f = codecs.open(path, encoding=encoding) + data = f.read() + f.close() + self.send_response(200) + self.send_header("Content-Type", contentType); + self.send_header("Content-Length", os.path.getsize(realPath)) + self.end_headers() + self.wfile.write(data) + + def processRequest(self): + self.request.settimeout(None) + if not self.checkAuthentication(): + return self.requestAuthentication() + + request = self.path.replace(self.server.context, "/").split('?') + relativePath = request[0] + if relativePath[0] == "/": + relativePath = relativePath[1:] + + if relativePath == "webiopi" or relativePath == "webiopi/": + self.send_response(301) + self.send_header("Location", "/") + self.end_headers() + return + + params = {} + if len(request) > 1: + for s in request[1].split('&'): + if s.find('=') > 0: + (name, value) = s.split('=') + params[name] = value + else: + params[s] = None + + compact = False + if 'compact' in params: + compact = str2bool(params['compact']) + + try: + result = (None, None, None) + if self.command == "GET": + result = self.server.handler.do_GET(relativePath, compact) + elif self.command == "POST": + length = 0 + length_header = 'content-length' + if length_header in self.headers: + length = int(self.headers[length_header]) + result = self.server.handler.do_POST(relativePath, self.rfile.read(length), compact) + else: + result = (405, None, None) + + (code, body, contentType) = result + + if code > 0: + self.sendResponse(code, body, contentType) + else: + if self.command == "GET": + self.serveFile(relativePath) + else: + self.sendResponse(404) + + except (GPIO.InvalidDirectionException, GPIO.InvalidChannelException, GPIO.SetupException) as e: + self.sendResponse(403, "%s" % e) + except ValueError as e: + self.sendResponse(403, "%s" % e) + except Exception as e: + self.sendResponse(500) + raise e + + def do_GET(self): + self.processRequest() + + def do_POST(self): + self.processRequest() diff --git a/python/webiopi/protocols/rest.py b/python/webiopi/protocols/rest.py new file mode 100644 index 0000000..6010604 --- /dev/null +++ b/python/webiopi/protocols/rest.py @@ -0,0 +1,254 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from webiopi.utils import types +from webiopi.utils import logger +from webiopi.utils.types import M_JSON, M_PLAIN +from webiopi.utils.version import BOARD_REVISION, VERSION_STRING, MAPPING +from webiopi.devices import manager +from webiopi.devices import instance +from webiopi.devices.bus import BUSLIST + +try: + import _webiopi.GPIO as GPIO +except: + pass + +MACROS = {} + +class RESTHandler(): + def __init__(self): + self.device_mapping = True + self.export = [] + self.routes = {} + self.macros = {} + + + def addMacro(self, macro): + self.macros[macro.__name__] = macro + + def addRoute(self, source, destination): + if source[0] == "/": + source = source[1:] + if destination[0] == "/": + destination = destination[1:] + self.routes[source] = destination + logger.info("Added Route /%s => /%s" % (source, destination)) + + def findRoute(self, path): + for source in self.routes: + if path.startswith(source): + route = path.replace(source, self.routes[source]) + logger.info("Routing /%s => /%s" % (path, route)) + return route + return path + + def extract(self, fmtArray, pathArray, args): + if len(fmtArray) != len(pathArray): + return False + if len(fmtArray) == 0: + return True + fmt = fmtArray[0] + path = pathArray[0] + if fmt == path: + return self.extract(fmtArray[1:], pathArray[1:], args) + if fmt.startswith("%"): + + fmt = fmt[1:] + t = 's' + if fmt[0] == '(': + if fmt[-1] == ')': + name = fmt[1:-1] + elif fmt[-2] == ')': + name = fmt[1:-2] + t = fmt[-1] + else: + raise Exception("Missing closing brace") + else: + name = fmt + + if t == 's': + args[name] = path + elif t == 'b': + args[name] = types.str2bool(path) + elif t == 'd': + args[name] = types.toint(path) + elif t == 'x': + args[name] = int(path, 16) + elif t == 'f': + args[name] = float(path) + else: + raise Exception("Unknown format type : %s" % t) + + return self.extract(fmtArray[1:], pathArray[1:], args) + + return False + + def getDeviceRoute(self, method, path): + pathArray = path.split("/") + deviceName = pathArray[0] + device = instance.DEVICES[deviceName] + if device == None: + return (None, deviceName + " Not Found") + pathArray = pathArray[1:] + funcs = device["functions"][method] + functionName = "/".join(pathArray) + if functionName in funcs: + return (funcs[functionName], {}) + + for fname in funcs: + func = funcs[fname] + funcPathArray = func.path.split("/") + args = {} + if self.extract(funcPathArray, pathArray, args): + return (func, args) + + return (None, functionName + " Not Found") + + def callDeviceFunction(self, method, path, data=None): + (func, args) = self.getDeviceRoute(method, path) + if func == None: + return (404, args, M_PLAIN) + + if func.data != None: + args[func.data] = data + + result = func(**args) + response = None + contentType = None + if result != None: + if hasattr(func, "contentType"): + contentType = func.contentType + if contentType == M_JSON: + response = types.jsonDumps(result) + else: + response = func.format % result + else: + response = result + + return (200, response, contentType) + + def do_GET(self, relativePath, compact=False): + relativePath = self.findRoute(relativePath) + + # JSON full state + if relativePath == "*": + return (200, self.getJSON(compact), M_JSON) + + # RPi header map + elif relativePath == "map": + json = "%s" % MAPPING + json = json.replace("'", '"') + return (200, json, M_JSON) + + # server version + elif relativePath == "version": + return (200, VERSION_STRING, M_PLAIN) + + # board revision + elif relativePath == "revision": + revision = "%s" % BOARD_REVISION + return (200, revision, M_PLAIN) + + # Single GPIO getter + elif relativePath.startswith("GPIO/"): + return self.callDeviceFunction("GET", relativePath) + + elif relativePath == "devices/*": + return (200, manager.getDevicesJSON(compact), M_JSON) + + elif relativePath.startswith("devices/"): + if not self.device_mapping: + return (404, None, None) + path = relativePath.replace("devices/", "") + return self.callDeviceFunction("GET", path) + + else: + return (0, None, None) + + def do_POST(self, relativePath, data, compact=False): + relativePath = self.findRoute(relativePath) + + if relativePath.startswith("GPIO/"): + return self.callDeviceFunction("POST", relativePath) + + elif relativePath.startswith("macros/"): + paths = relativePath.split("/") + mname = paths[1] + if len(paths) > 2: + value = paths[2] + else: + value = "" + + if mname in self.macros: + macro = self.macros[mname] + + if ',' in value: + args = value.split(',') + result = macro(*args) + elif len(value) > 0: + result = macro(value) + else: + result = macro() + + response = "" + if result: + response = "%s" % result + return (200, response, M_PLAIN) + + else: + return (404, mname + " Not Found", M_PLAIN) + + elif relativePath.startswith("devices/"): + if not self.device_mapping: + return (404, None, None) + path = relativePath.replace("devices/", "") + return self.callDeviceFunction("POST", path, data) + + else: # path unknowns + return (0, None, None) + + def getJSON(self, compact=False): + if compact: + f = 'f' + v = 'v' + else: + f = 'function' + v = 'value' + + json = {} + for (bus, value) in BUSLIST.items(): + json[bus] = int(value["enabled"]) + + gpios = {} + if len(self.export) > 0: + export = self.export + else: + export = range(GPIO.GPIO_COUNT) + + for gpio in export: + gpios[gpio] = {} + if compact: + gpios[gpio][f] = GPIO.getFunction(gpio) + else: + gpios[gpio][f] = GPIO.getFunctionString(gpio) + gpios[gpio][v] = int(GPIO.input(gpio)) + + if GPIO.getFunction(gpio) == GPIO.PWM: + (pwmType, value) = GPIO.getPulse(gpio).split(':') + gpios[gpio][pwmType] = value + + json['GPIO'] = gpios + return types.jsonDumps(json) + diff --git a/python/webiopi/server/__init__.py b/python/webiopi/server/__init__.py new file mode 100644 index 0000000..68fdbe6 --- /dev/null +++ b/python/webiopi/server/__init__.py @@ -0,0 +1,139 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import time +import socket + +from webiopi.utils.config import Config +from webiopi.utils import loader +from webiopi.utils import logger +from webiopi.utils import crypto +from webiopi.devices import manager +from webiopi.protocols import rest +from webiopi.protocols import http +from webiopi.protocols import coap +from webiopi.devices.digital.gpio import NativeGPIO + +def getLocalIP(): + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(('8.8.8.8', 53)) + host = s.getsockname()[0] + s.close() + return host + except: + return "localhost" + +class Server(): + def __init__(self, port=8000, coap_port=5683, login=None, password=None, passwdfile=None, configfile=None): + self.host = getLocalIP() + self.gpio = NativeGPIO() + self.restHandler = rest.RESTHandler() + manager.addDeviceInstance("GPIO", self.gpio, []) + + if configfile != None: + logger.info("Loading configuration from %s" % configfile) + config = Config(configfile) + else: + config = Config() + + self.gpio.addSetups(config.items("GPIO")) + self.gpio.addResets(config.items("~GPIO")) + self.gpio.setup() + + devices = config.items("DEVICES") + for (name, params) in devices: + values = params.split(" ") + driver = values[0]; + args = {} + i = 1 + while i < len(values): + (arg, val) = values[i].split(":") + args[arg] = val + i+=1 + manager.addDevice(name, driver, args) + + scripts = config.items("SCRIPTS") + for (name, source) in scripts: + loader.loadScript(name, source, self.restHandler) + + self.restHandler.device_mapping = config.getboolean("REST", "device-mapping", True) + self.gpio.post_value = config.getboolean("REST", "gpio-post-value", True) + self.gpio.post_function = config.getboolean("REST", "gpio-post-function", True) + exports = config.get("REST", "gpio-export", None) + if exports != None: + self.gpio.export = [int(s) for s in exports.split(",")] + self.restHandler.export = self.gpio.export + + http_port = config.getint("HTTP", "port", port) + http_enabled = config.getboolean("HTTP", "enabled", http_port > 0) + http_passwdfile = config.get("HTTP", "passwd-file", passwdfile) + context = config.get("HTTP", "context", None) + docroot = config.get("HTTP", "doc-root", None) + index = config.get("HTTP", "welcome-file", None) + + coap_port = config.getint("COAP", "port", coap_port) + coap_enabled = config.getboolean("COAP", "enabled", coap_port > 0) + coap_multicast = config.getboolean("COAP", "multicast", coap_enabled) + + routes = config.items("ROUTES") + for (source, destination) in routes: + self.restHandler.addRoute(source, destination) + + auth = None + if http_passwdfile != None: + if os.path.exists(http_passwdfile): + f = open(http_passwdfile) + auth = f.read().strip(" \r\n") + f.close() + if len(auth) > 0: + logger.info("Access protected using %s" % http_passwdfile) + else: + logger.info("Passwd file %s is empty" % http_passwdfile) + else: + logger.error("Passwd file %s not found" % http_passwdfile) + + elif login != None or password != None: + auth = crypto.encryptCredentials(login, password) + logger.info("Access protected using login/password") + + if auth == None or len(auth) == 0: + logger.warn("Access unprotected") + + if http_enabled: + self.http_server = http.HTTPServer(self.host, http_port, self.restHandler, context, docroot, index, auth) + else: + self.http_server = None + + if coap_enabled: + self.coap_server = coap.COAPServer(self.host, coap_port, self.restHandler) + if coap_multicast: + self.coap_server.enableMulticast() + else: + self.coap_server = None + + def addMacro(self, macro): + self.restHandler.addMacro(macro) + + def stop(self): + if self.http_server: + self.http_server.stop() + if self.coap_server: + self.coap_server.stop() + loader.unloadScripts() + manager.closeDevices() + + + diff --git a/python/webiopi/utils/__init__.py b/python/webiopi/utils/__init__.py new file mode 100644 index 0000000..ad86c93 --- /dev/null +++ b/python/webiopi/utils/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2012-2013 Eric Ptak - trouch.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + + diff --git a/python/webiopi/utils/config.py b/python/webiopi/utils/config.py new file mode 100644 index 0000000..72302e5 --- /dev/null +++ b/python/webiopi/utils/config.py @@ -0,0 +1,35 @@ +from webiopi.utils.version import PYTHON_MAJOR + +if PYTHON_MAJOR >= 3: + import configparser as parser +else: + import ConfigParser as parser + +class Config(): + + def __init__(self, configfile=None): + config = parser.ConfigParser() + config.optionxform = str + if configfile != None: + config.read(configfile) + self.config = config + + def get(self, section, key, default): + if self.config.has_option(section, key): + return self.config.get(section, key) + return default + + def getboolean(self, section, key, default): + if self.config.has_option(section, key): + return self.config.getboolean(section, key) + return default + + def getint(self, section, key, default): + if self.config.has_option(section, key): + return self.config.getint(section, key) + return default + + def items(self, section): + if self.config.has_section(section): + return self.config.items(section) + return {} diff --git a/python/webiopi/utils/crypto.py b/python/webiopi/utils/crypto.py new file mode 100644 index 0000000..aa48a82 --- /dev/null +++ b/python/webiopi/utils/crypto.py @@ -0,0 +1,17 @@ +import base64 +import hashlib +from webiopi.utils.version import PYTHON_MAJOR + +def encodeCredentials(login, password): + abcd = "%s:%s" % (login, password) + if PYTHON_MAJOR >= 3: + b = base64.b64encode(abcd.encode()) + else: + b = base64.b64encode(abcd) + return b + +def encrypt(value): + return hashlib.sha256(value).hexdigest() + +def encryptCredentials(login, password): + return encrypt(encodeCredentials(login, password)) diff --git a/python/webiopi/utils/loader.py b/python/webiopi/utils/loader.py new file mode 100644 index 0000000..bf7c693 --- /dev/null +++ b/python/webiopi/utils/loader.py @@ -0,0 +1,26 @@ +import imp +import webiopi.utils.logger as logger +import webiopi.utils.thread as thread +SCRIPTS = {} + +def loadScript(name, source, handler = None): + logger.info("Loading %s from %s" % (name, source)) + script = imp.load_source(name, source) + SCRIPTS[name] = script + + if hasattr(script, "setup"): + script.setup() + if handler: + for aname in dir(script): + attr = getattr(script, aname) + if callable(attr) and hasattr(attr, "macro"): + handler.addMacro(attr) + if hasattr(script, "loop"): + thread.runLoop(script.loop, True) + +def unloadScripts(): + for name in SCRIPTS: + script = SCRIPTS[name] + if hasattr(script, "destroy"): + script.destroy() + \ No newline at end of file diff --git a/python/webiopi/utils/logger.py b/python/webiopi/utils/logger.py new file mode 100644 index 0000000..fb92ec5 --- /dev/null +++ b/python/webiopi/utils/logger.py @@ -0,0 +1,45 @@ +import logging + +LOG_FORMATTER = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt="%Y-%m-%d %H:%M:%S") +ROOT_LOGGER = logging.getLogger() +ROOT_LOGGER.setLevel(logging.WARN) + +CONSOLE_HANDLER = logging.StreamHandler() +CONSOLE_HANDLER.setFormatter(LOG_FORMATTER) +ROOT_LOGGER.addHandler(CONSOLE_HANDLER) + +LOGGER = logging.getLogger("WebIOPi") + +def setInfo(): + ROOT_LOGGER.setLevel(logging.INFO) + +def setDebug(): + ROOT_LOGGER.setLevel(logging.DEBUG) + +def debugEnabled(): + return ROOT_LOGGER.level == logging.DEBUG + +def logToFile(filename): + FILE_HANDLER = logging.FileHandler(filename) + FILE_HANDLER.setFormatter(LOG_FORMATTER) + ROOT_LOGGER.addHandler(FILE_HANDLER) + +def debug(message): + LOGGER.debug(message) + +def info(message): + LOGGER.info(message) + +def warn(message): + LOGGER.warn(message) + +def error(message): + LOGGER.error(message) + +def exception(message): + LOGGER.exception(message) + +def printBytes(buff): + for i in range(0, len(buff)): + print("%03d: 0x%02X %03d %c" % (i, buff[i], buff[i], buff[i])) + diff --git a/python/webiopi/utils/thread.py b/python/webiopi/utils/thread.py new file mode 100644 index 0000000..98ed752 --- /dev/null +++ b/python/webiopi/utils/thread.py @@ -0,0 +1,50 @@ +import time +import signal +import threading +from webiopi.utils import logger + +RUNNING = False +TASKS = [] + +class Task(threading.Thread): + def __init__(self, func, loop=False): + threading.Thread.__init__(self) + self.func = func + self.loop = loop + self.running = True + self.start() + + def stop(self): + self.running = False + + def run(self): + if self.loop: + while self.running == True: + self.func() + else: + self.func() + +def stop(signum=0, frame=None): + global RUNNING + if RUNNING: + logger.info("Stopping...") + RUNNING = False + for task in TASKS: + task.stop() + + +def runLoop(func=None, async=False): + global RUNNING + RUNNING = True + signal.signal(signal.SIGINT, stop) + signal.signal(signal.SIGTERM, stop) + + if func != None: + if async: + TASKS.append(Task(func, True)) + else: + while RUNNING: + func() + else: + while RUNNING: + time.sleep(1) diff --git a/python/webiopi/utils/types.py b/python/webiopi/utils/types.py new file mode 100644 index 0000000..0afaa20 --- /dev/null +++ b/python/webiopi/utils/types.py @@ -0,0 +1,30 @@ +import json +from webiopi.utils import logger + +M_PLAIN = "text/plain" +M_JSON = "application/json" + +def jsonDumps(obj): + if logger.debugEnabled(): + return json.dumps(obj, sort_keys=True, indent=4, separators=(',', ': ')) + else: + return json.dumps(obj) + +def str2bool(value): + return (value == "1") or (value == "true") or (value == "True") or (value == "yes") or (value == "Yes") + +def toint(value): + if isinstance(value, str): + if value.startswith("0b"): + return int(value, 2) + elif value.startswith("0x"): + return int(value, 16) + else: + return int(value) + return value + + +def signInteger(value, bitcount): + if value & (1<<(bitcount-1)): + return value - (1<