diff --git a/README.md b/README.md index f4214ab..c7f1af0 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,117 @@ # pyenergenie A python interface to the Energenie line of products -This is a placeholder repo, for the development of an open source set of Python Libraries -to access the Energenie range of power control and monitoring products. -The Energenie product line uses the HopeRF radio transciever, and the OpenHEMS protocol -from Sentec. Energenie have built a RaspberryPi add-on board that interfaces to the HopeRF -RFM69, and allows both control and monitoring of their products from a Raspberry Pi. +This is the beginnings of an open source library to access the Energine range of +power control and monitoring products from within Python. -This code project aims to develop an open source python module, providing access to many or all -of the features of the OpenHEMS, HopeRF and Energenie product line. +The Energenie product line uses the HopeRF radio transciever, and the OpenHEMS +protocol from Sentec. Energenie have built a RaspberryPi add-on board that +interfaces to the HopeRF RFM69, and allows both control and monitoring of their +products from a Raspberry Pi. -Plans -==== +There are some existing Python libraries for some Energenie products, but they +do not support the full radio interface and full product range. -1. Write a minimal ctypes wrapper and build process for the existing bcm2835/HopeRF software distribution -2. Create a proof of concept demo application using the Iotic-Labs IoT infrastructure -3. Enhance the library design to support other features such as new message types, new product types -4. Hopefully provide a platform that could be further innovated on using ScratchGPIO +Energenie also have a modified set of C test code based on the HopeRF test harness, +but this does not support all products, all variants of Raspberry Pi hardware, +or all versions of the Raspbian OS. + +This project aims to develop an open source Python module, providing +access to many or all of the features of the OpenHEMS, HopeRF and Energenie +product line. + Purpose ==== -I have a whole range of plans for building demo applications using the Energenie product line in the future, -and want to help seed further innovations using this technology in schools and in industry. To do this, I believe -that the code needs to be more flexible, accessible to all, and better structured and documented, -so I plan to develop via a series of progressive releases an open source python wrapper that others can fork and further innovate with. +This release, as of 27/09/2015, is the beginnings of this work. +It is not representative of the final API, but it is a starting point for me to +start experimenting with ideas and testing out reliability, with a view to using +these products to integrate into an Internet of Things solution provided by +Iotic-Labs Ltd. -Notes +With it, you can receive monitor payloads from an Energenie MiHome Adaptor Plus, +directly within Python programs. This type of plug can be used for energy monitoring +and also for relay control of the socket. + +I've tried to make this a 'zero install' and 'zero configuration' experience. +In theory (at least) you should be able to download the zip or git-clone, +plug in your Energenie radio, plug in your MiHome Adapter Plus, and run the code +to see data coming back. + + +Getting Going ==== -Sharing of SPI/GPIO with this library will be an interesting challenge, -often it is useful to also use other GPIO's and to use other SPI devices -with an alternative chip select. The bcm2835 library doesn't seem to be -provided or wrapped in a way conjucive to sharing. This might be addressed -in the second pass of coding, it's too much to chew on in the first pass. +1. Plug in your ENER314-RT-VER01 board from energine onto the 26 pin connector of +your Raspberry Pi. At the moment I have only tested this with a Raspberry Pi B, +although there is no reason why it should not work with any of the models currently +available on the market. The underlying GPIO and SPI has been tested in other +projects on a Pi2 for example. -I'm hoping to publish a low level C API to the devices that others can code -to via foreign function interfaces, and possibly a high level API that -implements basic device classes, with higher level abstractions written -in the python. +2. Use the Download As Zip link to the right, and unzip the files onto your +Raspberry Pi. -Getting perfect reuse for all on the first pass is going to be impossible. +3. unzip the software + + unzip pyenergenie-master.zip + cd pyenergenie-master + cd assets/src + +4. run the monitor test program + + sudo python monitor.py + +After a few seconds, you should see some packet dumps appearing on the screen. +The last few bytes will be 0x73 0x01 0x01 or 0x73 0x01 0x00 and these indicate +the switch state of the plug. Press the button on the front of the plug to +turn the switch on and off, and you should see the 0x01 change to 0x00 and +back again. + +If it crashes, it sometimes leaves the radio in an indeterminite state, remove +and replace the radio board and it should reset it (but see notes below about this). + + +Plans +==== + +1. Add RESET support - the radio sometimes gets into an unknown and unrecoverable +state and I have to remove the board to reset the radio. There might be a RESET +line or a RESET command that can be sent at startup to solve this. + +2. Write an OpenHEMS decoder to decode the messages for friendly display. I will +probably decode the hex buffer into a pydict, and then write a pydict to text +formatter. This will expose the whole of OpenHEMS in a really nice Python structure +to improve further innovation within Python. + +3. Write an OpenHEMS encoder to encode friendly messages. I will probably +take a pydict with header and records in it and encode into a buffer that is +then transmitted via the radio interface. As above, this will expose message +creation in a really nice Python structure to improve further innovation within +Python. + +4. Construct commands for switch-on and switch-off, and test sending these to a +specific sensorid. + +5. Write a discovery service that sends a monitor command, then collects all +the receive messages and builds an internal dictionary of devices that respond. +I will probably at this point build a Python object for each device that responds, +and this object will be a proxy that can be used to monitor and control that device, +thus allowing any number of devices to be monitored and controlled in a 'Pythonic' +way. + +6. Push a fair amount of the radio interface and some of OpenHEMS back down into +a C library that implements the same interface as what we have at this point in the +Python. Write a ctypes wrapper around this, so that the identical Python internal +API is presented. The idea being that the first pass of Python coding defines the +API we want to use, and the second pass turns this into a single library that +does everything, exposed to Python via ctypes, but linkable to other applications +and languages too. + David Whale @whaleygeek -July 2015 +September 2015 diff --git a/src/energenie/OpenHEMS.py b/src/energenie/OpenHEMS.py new file mode 100644 index 0000000..6143d0d --- /dev/null +++ b/src/energenie/OpenHEMS.py @@ -0,0 +1,229 @@ +# OpenHEMS.py 27/09/2015 D.J.Whale +# +# Implement OpenHEMS message encoding and decoding + +import crypto + +# report has bit 7 clear +# command has bit 7 set + +PARAM_ALARM = 0x21 +PARAM_DEBUG_OUTPUT = 0x2D +PARAM_IDENTIFY = 0x3F +PARAM_SOURCE_SELECTOR = 0x40 # command only +PARAM_WATER_DETECTOR = 0x41 +PARAM_GLASS_BREAKAGE = 0x42 +PARAM_CLOSURES = 0x43 +PARAM_DOOR_BELL = 0x44 +PARAM_ENERGY = 0x45 +PARAM_FALL_SENSOR = 0x46 +PARAM_GAS_VOLUME = 0x47 +PARAM_AIR_PRESSURE = 0x48 +PARAM_ILLUMINANCE = 0x49 +PARAM_LEVEL = 0x4C +PARAM_RAINFALL = 0x4D +PARAM_APPARENT_POWER = 0x50 +PARAM_POWER_FACTOR = 0x51 +PARAM_REPORT_PERIOD = 0x52 +PARAM_SMOKE_DETECTOR = 0x53 +PARAM_TIME_AND_DATE = 0x54 +PARAM_VIBRATION = 0x56 +PARAM_WATER_VOLUME = 0x57 +PARAM_WIND_SPEED = 0x58 +PARAM_GAS_PRESSURE = 0x61 +PARAM_BATTERY_LEVEL = 0x62 +PARAM_CO_DETECTOR = 0x63 +PARAM_DOOR_SENSOR = 0x64 +PARAM_EMERGENCY = 0x65 +PARAM_FREQUENCY = 0x66 +PARAM_GAS_FLOW_RATE = 0x67 +PARAM_CURRENT = 0x69 +PARAM_JOIN = 0x6A +PARAM_LIGHT_LEVEL = 0x6C +PARAM_MOTION_DETECTOR = 0x6D +PARAM_OCCUPANCY = 0x6F +PARAM_REAL_POWER = 0x70 +PARAM_REACTIVE_POWER = 0x71 +PARAM_ROTATION_SPEED = 0x72 +PARAM_SWITCH_STATE = 0x73 +PARAM_TEMPERATURE = 0x74 +PARAM_VOLTAGE = 0x76 +PARAM_WATER_FLOW_RATE = 0x77 +PARAM_WATER_PRESSURE = 0x78 + +PARAM_TEST = 0xAA + + + +crypt_pid = None +crypt_pip = None + +def init(pid, pip): + global crypt_pid, crypt_pip + crypt_pid = pid + crypt_pip = pip + + +def warning(msg): + print("warning:" + str(msg)) + + +#TODO decode OpenHEMS message payload structure +#TODO decrypt OpenHEMS message payload +#TODO check the CRC is correct + +""" + case S_MSGLEN: // Read message length + case S_MANUFACT_ID: // Read manufacturer identifier + case S_PRODUCT_ID: // Read product identifier + case S_ENCRYPTPIP: // Read encryption pip + case S_SENSORID: // Read sensor ID + /******************* start reading RECORDS ********************/ + case S_DATA_PARAMID: // Read record parameter identifier + msgPtr->paramId = msgPtr->value & 0x7F; + temp = getIdName(msgPtr->paramId); + printf(" %s=", temp); + if (msgPtr->paramId == 0) // Parameter identifier CRC. Go to CRC + { + msgPtr->state = S_CRC; + msgPtr->recordBytesToRead = SIZE_CRC; + } + else + { + msgPtr->state = S_DATA_TYPEDESC; + msgPtr->recordBytesToRead = SIZE_DATA_TYPEDESC; + } + if (strcmp(temp, "Unknown") == 0) // Unknown parameter, finish fetching message + msgPtr->state = S_FINISH; + break; + case S_DATA_TYPEDESC: // Read record type description + if ((msgPtr->value & 0x0F) == 0) // No more data to read in that record + { + msgPtr->state = S_DATA_PARAMID; + msgPtr->recordBytesToRead = SIZE_DATA_PARAMID; + } + else + { + msgPtr->state = S_DATA_VAL; + msgPtr->recordBytesToRead = msgPtr->value & 0x0F; + } + msgPtr->type = msgPtr->value; + break; + case S_DATA_VAL: // Read record data + temp = getValString(msgPtr->value, msgPtr->type >> 4, msgPtr->recordBytesToRead); + printf("%s", temp); + msgPtr->state = S_DATA_PARAMID; + msgPtr->recordBytesToRead = SIZE_DATA_PARAMID; + if (strcmp(temp, "Reserved") == 0) + msgPtr->state = S_FINISH; + break; + /******************* finish reading RECORDS ********************/ + case S_CRC: // Check CRC + msgPtr->state = S_FINISH; + if ((int16_t)msgPtr->value == crc(msgPtr->buf + NON_CRC, msgPtr->bufCnt - NON_CRC - SIZE_CRC)) + { + printf("OK\n"); + } + else + { + printf("FAIL expVal=%04x, pip=%04x, val=%04x\n", (int16_t)msgPtr->value, msgPtr->pip, crc(msgPtr->buf + NON_CRC, msgPtr->bufCnt - NON_CRC - SIZE_CRC)); + } + break; +""" + +#TODO if can't decode message throw an exception +def decode(payload): + buffer = "" + length = payload[0] + if length+1 != len(payload): + warning("rx payload length mismatch") + + mfrId = payload[1] + productId = payload[2] + + buffer += "len:" + str(length) + " " + buffer += "mfr:" + hex(mfrId) + " " + buffer += "prod:" + hex(productId) + " " + + pip = (payload[3]<<8) + payload[4] + crypto.init(crypt_pid, pip) + crypto.cryptPayload(payload, 5, len(payload)-5) + + for n in payload[5:]: + buffer += hex(n) + " " + + #TODO check CRC matches + return buffer + + +#----- MESSAGE ENCODER -------------------------------------------------------- +# +# Encodes a message using the OpenHEMS message payload structure + +# R1 message product id 0x02 monitor and control (in switching program?) +# C1 message product id 0x01 monitor only (in listening program) + + +#TODO change this so it returns a pydict +#write an encoder that turns the pydict into a buffer for the radio + +def make_monitor(): + payload = [ + 7 + 3 + 3, # payload remaining length (header+records+footer) + 0x04, # manufacturer id = Energenie + 0x01, # product id = 0x01=C1(monitor) 0x02=R1(monitor+control) + 0x01, # reserved1 (cryptSeedMSB) + 0x00, # reserved2 (cryptSeedLSB) + # from here up until the NUL is crc'd + # from here up to and including the CRC is crypted + 0xFF, # sensorIdHigh broadcast + 0xFF, # sensorIdMid broadcast + 0xFF, # sensorIdLow broadcast + # RECORDS + PARAM_SWITCH_STATE | 0x80, # set switch state + 0x01, # type/length + 0x00, # value off + 0x00 # NUL + ] + # Calculate and append the CRC bytes + crc = calcCRC(payload, 5, len(payload)-5) + payload.append((crc >> 8) & 0xFF) # MSB, big-endian + payload.append(crc & 0xFF) # LSB + + crypto.init(crypt_pid, crypt_pip) + crypto.cryptPayload(payload, 5, len(payload)-5) # including CRC + return payload + + +#----- CRC CALCULATION -------------------------------------------------------- + +#int16_t crc(uint8_t const mes[], unsigned char siz) +#{ +# uint16_t rem = 0; +# unsigned char byte, bit; +# +# for (byte = 0; byte < siz; ++byte) +# { +# rem ^= (mes[byte] << 8); +# for (bit = 8; bit > 0; --bit) +# { +# rem = ((rem & (1 << 15)) ? ((rem << 1) ^ 0x1021) : (rem << 1)); +# } +# } +# return rem; +#} + +def calcCRC(payload, start, length): + rem = 0 + for b in payload[start:start+length]: + rem ^= (b<<8) + for bit in range(8): + if rem & (1<<15) != 0: + # bit is set + rem = ((rem<<1) ^ 0x1021) & 0xFFFF # always maintain U16 + else: + # bit is clear + rem = (rem<<1) & 0xFFFF # always maintain U16 + return rem + +# END diff --git a/src/energenie/__init__.py b/src/energenie/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/energenie/__init__.py diff --git a/src/energenie/build b/src/energenie/build new file mode 100755 index 0000000..a476cf0 --- /dev/null +++ b/src/energenie/build @@ -0,0 +1,21 @@ +#! /bin/bash + + +# build gpio_test +gcc gpio_test.c gpio.c +mv a.out gpio_test +chmod u+x gpio_test + + +# build spi_test +gcc spi_test.c spi.c gpio.c +mv a.out spi_test +chmod u+x spi_test + + +# build spi .so library on Raspberry Pi +gcc -Wall -shared -o spi_rpi.so -fPIC spi.c gpio.c +#nm -D spi.so + + + diff --git a/src/energenie/crypto.py b/src/energenie/crypto.py new file mode 100644 index 0000000..0c1fa04 --- /dev/null +++ b/src/energenie/crypto.py @@ -0,0 +1,53 @@ +# crypto.py 27/09/2015 D.J.Whale +# +# Crypto engine for OpenHEMS, including crc calculation + + + +ran = None + +#static uint16_t ran; + +#void seed(uint8_t pid, uint16_t pip) +#{ +# ran = ((((uint16_t) pid) << 8) ^ pip); +#} + + +def init(pid, pip): + """Initialise the crypto engine state variables""" + global ran + ran = (((pid&0xFF)<<8) ^ pip) & 0xFFFF # maintain U16 + + +#uint8_t crypt(uint8_t dat) +#{ +# unsigned char i; //(u8) + +# for (i = 0; i < 5; ++i) +# { +# ran = (ran & 1) ? ((ran >> 1) ^ 62965U) : (ran >> 1); +# } +# return (uint8_t)(ran ^ dat ^ 90U); +#} + +def cryptByte(data): + """crypt a byte of data and update the crypto engine state variable""" + global ran + for i in range(5): + if (ran&0x01) != 0: # bit0 + # bit0 set + ran = ((ran>>1) ^ 62965) & 0xFFFF # maintain U16 + else: + # bit0 clear + ran = ran >> 1 + + return (ran ^ data ^ 90) & 0xFF + + +def cryptPayload(payload, start, length): + """Encrypt a range of bytes in place by modifying those payload bytes""" + for i in range(start, start+length): + payload[i] = cryptByte(payload[i]) + +# END diff --git a/src/energenie/gpio.c b/src/energenie/gpio.c new file mode 100644 index 0000000..7d9a144 --- /dev/null +++ b/src/energenie/gpio.c @@ -0,0 +1,182 @@ +/* gpio.c D.J.Whale 8/07/2014 + * + * A very simple interface to the GPIO port on the Raspberry Pi. + */ + +/***** INCLUDES *****/ + +#include +#include +#include +#include +#include +#include +#include + +#include "gpio.h" + + +/***** CONFIGURATION *****/ + +/* uncomment to make this a simulated driver */ +//#define GPIO_SIMULATED + + +/***** CONSTANTS *****/ + +#define BCM2708_PERI_BASE 0x20000000 +//#define GPIO_BASE (BCM2708_PERI_BASE + 0x200000) /* GPIO controller */ +#define GPIO_BASE_OFFSET 0x200000 + +#define PAGE_SIZE (4*1024) +#define BLOCK_SIZE (4*1024) + + +/***** VARIABLES *****/ + +static int mem_fd; +static void *gpio_map; + +static volatile unsigned *gpio; + + +/****** MACROS *****/ + +#define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3)) +#define OUT_GPIO(g) *(gpio+((g)/10)) |= (1<<(((g)%10)*3)) +#define SET_GPIO_ALT(g,a) *(gpio+(((g)/10))) |= (((a)<=3?(a)+4:(a)==4?3:2)<<(((g)%10)*3)) + +#define GPIO_SET *(gpio+7) // sets bits which are 1 ignores bits which are 0 +#define GPIO_CLR *(gpio+10) // clears bits which are 1 ignores bits which are 0 + +#define GPIO_READ(g) ((*(gpio+13)&(1< +#include + +#include "gpio.h" + +static void delay(struct timespec time) +{ + nanosleep(&time, NULL); +} + +static struct timespec delay_1sec = {1, 0}; + + +void main(void) +{ + int i; + + gpio_init(); + gpio_setout(2); + gpio_setout(3); + + for (i=0; i<10; i++) + { + puts("GPIO 2"); + gpio_write(2, 1); + delay(delay_1sec); + gpio_write(2, 0); + delay(delay_1sec); + + puts("GPIO 3"); + gpio_write(3, 1); + delay(delay_1sec); + gpio_write(3, 0); + delay(delay_1sec); + } +} diff --git a/src/energenie/radio.py b/src/energenie/radio.py new file mode 100644 index 0000000..9cfa544 --- /dev/null +++ b/src/energenie/radio.py @@ -0,0 +1,317 @@ +# test1.py 26/09/2015 D.J.Whale +# +# Simple low level test of the HopeRF interface +# Uses direct SPI commands to exercise the interface. +# +# Receives and dumps payload buffers. +# +# Eventually a lot of this will be pushed into a separate module, +# and then pushed back into C once it is proved working. + +import spi + +def warning(msg): + print("warning:" + str(msg)) + +def trace(msg): + print(str(msg)) + + +#----- REGISTER ACCESS -------------------------------------------------------- + +def HRF_writereg(addr, data): + """Write an 8 bit value to a register""" + buf = [addr | MASK_WRITE_DATA, data] + spi.select() + spi.frame(buf) + spi.deselect() + + +def HRF_readreg(addr): + """Read an 8 bit value from a register""" + buf = [addr, 0x00] + spi.select() + res = spi.frame(buf) + spi.deselect() + #print(hex(res[1])) + return res[1] # all registers are 8 bit + + +def HRF_writefifo_burst(buf): + """Write all bytes in buf to the payload FIFO, in a single burst""" + spi.select() + buf.insert(0, ADDR_FIFO | MASK_WRITE_DATA) + spi.frame(buf) + spi.deselect() + + +def HRF_readfifo_burst(): + """Read bytes from the payload FIFO using burst read""" + #first byte read is the length in remaining bytes + buf = [] + spi.select() + spi.frame([ADDR_FIFO]) + count = 1 # read at least the length byte + while count > 0: + rx = spi.frame([ADDR_FIFO]) + data = rx[0] + if len(buf) == 0: + count = data + else: + count -= 1 + buf.append(data) + spi.deselect() + trace("readfifo:" + str(buf)) + return buf + + +def HRF_checkreg(addr, mask, value): + """Check to see if a register matches a specific value or not""" + regval = HRF_readreg(addr) + #print("addr %d mask %d wanted %d actual %d" % (addr,mask,value,regval)) + return (regval & mask) == value + + +def HRF_pollreg(addr, mask, value): + """Poll a register until it meet some criteria""" + while not HRF_checkreg(addr, mask, value): + pass + + +#----- HRF REGISTER PROTOCOL -------------------------------------------------- + +# HopeRF register addresses +# Precise register description can be found on: +# www.hoperf.com/upload/rf/RFM69W-V1.3.pdf +# on page 63 - 74 + +ADDR_FIFO = 0x00 +ADDR_OPMODE = 0x01 +ADDR_REGDATAMODUL = 0x02 +ADDR_BITRATEMSB = 0x03 +ADDR_BITRATELSB = 0x04 +ADDR_FDEVMSB = 0x05 +ADDR_FDEVLSB = 0x06 +ADDR_FRMSB = 0x07 +ADDR_FRMID = 0x08 +ADDR_FRLSB = 0x09 +ADDR_AFCCTRL = 0x0B +ADDR_LNA = 0x18 +ADDR_RXBW = 0x19 +ADDR_AFCFEI = 0x1E +ADDR_IRQFLAGS1 = 0x27 +ADDR_IRQFLAGS2 = 0x28 +ADDR_RSSITHRESH = 0x29 +ADDR_PREAMBLELSB = 0x2D +ADDR_SYNCCONFIG = 0x2E +ADDR_SYNCVALUE1 = 0x2F +ADDR_SYNCVALUE2 = 0x30 +ADDR_SYNCVALUE3 = 0x31 +ADDR_SYNCVALUE4 = 0x32 +ADDR_PACKETCONFIG1 = 0x37 +ADDR_PAYLOADLEN = 0x38 +ADDR_NODEADDRESS = 0x39 +ADDR_FIFOTHRESH = 0x3C + +# HopeRF masks to set and clear bits +MASK_REGDATAMODUL_OOK = 0x08 +MASK_REGDATAMODUL_FSK = 0x00 +MASK_WRITE_DATA = 0x80 +MASK_MODEREADY = 0x80 +MASK_FIFONOTEMPTY = 0x40 +MASK_FIFOLEVEL = 0x20 +MASK_FIFOOVERRUN = 0x10 +MASK_PACKETSENT = 0x08 +MASK_TXREADY = 0x20 +MASK_PACKETMODE = 0x60 +MASK_MODULATION = 0x18 +MASK_PAYLOADRDY = 0x04 + +MODE_STANDBY = 0x04 # Standby +MODE_TRANSMITER = 0x0C # Transmiter +MODE_RECEIVER = 0x10 # Receiver +VAL_REGDATAMODUL_FSK = 0x00 # Modulation scheme FSK +VAL_REGDATAMODUL_OOK = 0x08 # Modulation scheme OOK +VAL_FDEVMSB30 = 0x01 # frequency deviation 5kHz 0x0052 -> 30kHz 0x01EC +VAL_FDEVLSB30 = 0xEC # frequency deviation 5kHz 0x0052 -> 30kHz 0x01EC +VAL_FRMSB434 = 0x6C # carrier freq -> 434.3MHz 0x6C9333 +VAL_FRMID434 = 0x93 # carrier freq -> 434.3MHz 0x6C9333 +VAL_FRLSB434 = 0x33 # carrier freq -> 434.3MHz 0x6C9333 +VAL_FRMSB433 = 0x6C # carrier freq -> 433.92MHz 0x6C7AE1 +VAL_FRMID433 = 0x7A # carrier freq -> 433.92MHz 0x6C7AE1 +VAL_FRLSB433 = 0xE1 # carrier freq -> 433.92MHz 0x6C7AE1 +VAL_AFCCTRLS = 0x00 # standard AFC routine +VAL_AFCCTRLI = 0x20 # improved AFC routine +VAL_LNA50 = 0x08 # LNA input impedance 50 ohms +VAL_LNA50G = 0x0E # LNA input impedance 50 ohms, LNA gain -> 48db +VAL_LNA200 = 0x88 # LNA input impedance 200 ohms +VAL_RXBW60 = 0x43 # channel filter bandwidth 10kHz -> 60kHz page:26 +VAL_RXBW120 = 0x41 # channel filter bandwidth 120kHz +VAL_AFCFEIRX = 0x04 # AFC is performed each time RX mode is entered +VAL_RSSITHRESH220 = 0xDC # RSSI threshold 0xE4 -> 0xDC (220) +VAL_PREAMBLELSB3 = 0x03 # preamble size LSB 3 +VAL_PREAMBLELSB5 = 0x05 # preamble size LSB 5 +VAL_SYNCCONFIG2 = 0x88 # Size of the Synch word = 2 (SyncSize + 1) +VAL_SYNCCONFIG4 = 0x98 # Size of the Synch word = 4 (SyncSize + 1) +VAL_SYNCVALUE1FSK = 0x2D # 1st byte of Sync word +VAL_SYNCVALUE2FSK = 0xD4 # 2nd byte of Sync word +VAL_SYNCVALUE1OOK = 0x80 # 1nd byte of Sync word +VAL_PACKETCONFIG1FSK = 0xA2 # Variable length, Manchester coding, Addr must match NodeAddress +VAL_PACKETCONFIG1FSKNO = 0xA0 # Variable length, Manchester coding +VAL_PACKETCONFIG1OOK = 0 # Fixed length, no Manchester coding +VAL_PAYLOADLEN255 = 0xFF # max Length in RX, not used in Tx +VAL_PAYLOADLEN66 = 66 # max Length in RX, not used in Tx +VAL_PAYLOADLEN_OOK = (13 + 8 * 17) # Payload Length +VAL_NODEADDRESS01 = 0x01 # Node address used in address filtering +VAL_NODEADDRESS04 = 0x04 # Node address used in address filtering +VAL_FIFOTHRESH1 = 0x81 # Condition to start packet transmission: at least one byte in FIFO +VAL_FIFOTHRESH30 = 0x1E # Condition to start packet transmission: wait for 30 bytes in FIFO + +config_FSK = [ + [ADDR_REGDATAMODUL, VAL_REGDATAMODUL_FSK], # modulation scheme FSK + [ADDR_FDEVMSB, VAL_FDEVMSB30], # frequency deviation 5kHz 0x0052 -> 30kHz 0x01EC + [ADDR_FDEVLSB, VAL_FDEVLSB30], # frequency deviation 5kHz 0x0052 -> 30kHz 0x01EC + [ADDR_FRMSB, VAL_FRMSB434], # carrier freq -> 434.3MHz 0x6C9333 + [ADDR_FRMID, VAL_FRMID434], # carrier freq -> 434.3MHz 0x6C9333 + [ADDR_FRLSB, VAL_FRLSB434], # carrier freq -> 434.3MHz 0x6C9333 + [ADDR_AFCCTRL, VAL_AFCCTRLS], # standard AFC routine + [ADDR_LNA, VAL_LNA50], # 200ohms, gain by AGC loop -> 50ohms + [ADDR_RXBW, VAL_RXBW60], # channel filter bandwidth 10kHz -> 60kHz page:26 + [ADDR_BITRATEMSB, 0x1A], # 4800b/s + [ADDR_BITRATELSB, 0x0B], # 4800b/s + #[ADDR_AFCFEI, VAL_AFCFEIRX], # AFC is performed each time rx mode is entered + #[ADDR_RSSITHRESH, VAL_RSSITHRESH220], # RSSI threshold 0xE4 -> 0xDC (220) + #[ADDR_PREAMBLELSB, VAL_PREAMBLELSB5], # preamble size LSB set to 5 + [ADDR_SYNCCONFIG, VAL_SYNCCONFIG2], # Size of the Synch word = 2 (SyncSize + 1) + [ADDR_SYNCVALUE1, VAL_SYNCVALUE1FSK], # 1st byte of Sync word + [ADDR_SYNCVALUE2, VAL_SYNCVALUE2FSK], # 2nd byte of Sync word + #[ADDR_PACKETCONFIG1, VAL_PACKETCONFIG1FSK], # Variable length, Manchester coding, Addr must match NodeAddress + [ADDR_PACKETCONFIG1, VAL_PACKETCONFIG1FSKNO], # Variable length, Manchester coding + [ADDR_PAYLOADLEN, VAL_PAYLOADLEN66], # max Length in RX, not used in Tx + #[ADDR_NODEADDRESS, VAL_NODEADDRESS01], # Node address used in address filtering + [ADDR_NODEADDRESS, 0x06], # Node address used in address filtering + [ADDR_FIFOTHRESH, VAL_FIFOTHRESH1], # Condition to start packet transmission: at least one byte in FIFO + [ADDR_OPMODE, MODE_RECEIVER] # Operating mode to Receiver +] + + +def HRF_wait_ready(): + """Wait for HRF to be ready after last command""" + HRF_pollreg(ADDR_IRQFLAGS1, MASK_MODEREADY, MASK_MODEREADY) + + +def HRF_wait_txready(): + """Wait for HRF to be ready and ready for tx, after last command""" + trace("waiting for transmit ready...") + HRF_pollreg(ADDR_IRQFLAGS1, MASK_MODEREADY|MASK_TXREADY, MASK_MODEREADY|MASK_TXREADY) + trace("transmit ready") + + +def HRF_config_FSK(): + """Configure HRF for FSK modulation""" + for cmd in config_FSK: + HRF_writereg(cmd[0], cmd[1]) + HRF_wait_ready() + + +def HRF_change_mode(mode): + HRF_writereg(ADDR_OPMODE, mode) + + +def HRF_clear_fifo(): + """Clear any data in the HRF payload FIFO by reading until empty""" + while (HRF_readreg(ADDR_IRQFLAGS2) & MASK_FIFONOTEMPTY) == MASK_FIFONOTEMPTY: + HRF_readreg(ADDR_FIFO) + + +def HRF_check_payload(): + """Check if there is a payload in the FIFO waiting to be processed""" + irqflags1 = HRF_readreg(ADDR_IRQFLAGS1) + irqflags2 = HRF_readreg(ADDR_IRQFLAGS2) + print("irq1 %s irq2 %s" % (hex(irqflags1), hex(irqflags2))) + + return (irqflags2 & MASK_PAYLOADRDY) == MASK_PAYLOADRDY + + +def HRF_receive_payload(): + """Receive the whole payload""" + return HRF_readfifo_burst() + + +def HRF_send_payload(payload): + trace("send_payload") + dumpPayloadAsHex(payload) + HRF_writefifo_burst(payload) + trace(" waiting for sent...") + HRF_pollreg(ADDR_IRQFLAGS2, MASK_PACKETSENT, MASK_PACKETSENT) + trace(" sent") + reg = HRF_readreg(ADDR_IRQFLAGS2) + trace(" irqflags2=%s" % hex(reg)) + if ((reg & MASK_FIFONOTEMPTY) != 0) or ((reg & MASK_FIFOOVERRUN) != 0): + warning("Failed to send payload to HRF") + +def dumpPayloadAsHex(payload): + length = payload[0] + print(hex(length)) + if length+1 != len(payload): + print("warning length byte mismatch actual:%d inbuf:%d" % (len(payload), length)) + + for i in range(1,length+1): + print("[%d] = %s" % (i, hex(payload[i]))) + + + + + +#----- USER API --------------------------------------------------------------- +# +# This is only a first-pass at a user API. +# it might change quite a bit in the second pass. + +mode = None + +def init(): + spi.init_defaults() + trace("config FSK") + HRF_config_FSK() + HRF_clear_fifo() + receiver() + + +def transmitter(): + """Change into transmitter mode""" + global mode + trace("transmitter mode") + HRF_change_mode(MODE_TRANSMITER) + mode = "TRANSMITTER" + HRF_wait_txready() + + +def transmit(payload): + HRF_send_payload(payload) + + +def receiver(): + """Change into receiver mode""" + global mode + trace("receiver mode") + HRF_change_mode(MODE_RECEIVER) + HRF_wait_ready() + mode = "RECEIVER" + + +def isReceiveWaiting(): + return HRF_check_payload() + + +def receive(): + return HRF_receive_payload() + + +def finished(): + """Close the library down cleanly when finished""" + spi.finished() + + + +# END diff --git a/src/energenie/spi.c b/src/energenie/spi.c new file mode 100644 index 0000000..0f60a10 --- /dev/null +++ b/src/energenie/spi.c @@ -0,0 +1,172 @@ +/* spi.c D.J.Whale 19/07/2014 + */ + + +/***** INCLUDES *****/ + +#include +#include +//#include +#include +#include + +#include "spi.h" +#include "gpio.h" + + +/***** MACROS *****/ + +#define CLOCK_ACTIVE() gpio_write(config.sclk, config.cpol?0:1) +#define CLOCK_IDLE() gpio_write(config.sclk, config.cpol?1:0) + +#define SELECTED() gpio_write(config.cs, config.spol?1:0) +#define NOT_SELECTED() gpio_write(config.cs, config.spol?0:1) + + +/***** VARIABLES *****/ + +static SPI_CONFIG config; + + +/* Based on code suggested by Gordon Henderson: + * https://github.com/WiringPi/WiringPi/blob/master/wiringPi/wiringPi.c + * + * Note that his trick of using the hardware timer just didn't work, + * and this is the best of a bad bunch. nanosleep() delays at least + * 100uS in some cases. + */ + +static void delayus(unsigned int us) +{ + struct timeval tNow, tLong, tEnd; + + gettimeofday(&tNow, NULL); + tLong.tv_sec = us / 1000000; + tLong.tv_usec = us % 1000000; + timeradd(&tNow, &tLong, &tEnd); + + while (timercmp(&tNow, &tEnd, <)) + { + gettimeofday(&tNow, NULL); + } +} + + +void spi_init_defaults(void) +{ +#define CS 7 //CE1 +#define SCLK 11 +#define MOSI 10 +#define MISO 9 + +/* ms */ +#define TSETTLE (1) /* us settle */ +#define THOLD (1) /* us hold */ +#define TFREQ (1) /* us half clock */ + + SPI_CONFIG defaultConfig = {CS, SCLK, MOSI, MISO, SPI_SPOL0, SPI_CPOL0, SPI_CPHA0, + TSETTLE, THOLD, TFREQ}; + + spi_init(&defaultConfig); +} + + +void spi_init(SPI_CONFIG* pConfig) +{ + /* It's a standalone library, so init GPIO also */ + gpio_init(); + memcpy(&config, pConfig, sizeof(SPI_CONFIG)); + + //TODO: Implement CPHA1 + if (config.cpha != 0) + { + fprintf(stderr, "error: CPHA 1 not yet supported"); + exit(-1); + } + + gpio_setout(config.sclk); + CLOCK_IDLE(); + + gpio_setout(config.mosi); + gpio_low(config.mosi); + gpio_setin(config.miso); + + gpio_setout(config.cs); + NOT_SELECTED(); +} + + +void spi_finished(void) +{ + gpio_setin(config.mosi); + gpio_setin(config.sclk); + gpio_setin(config.cs); +} + + +void spi_select(void) +{ + SELECTED(); + delayus(config.tSettle); +} + + +void spi_deselect(void) +{ + NOT_SELECTED(); + delayus(config.tSettle); +} + + +int spi_byte(int txbyte) +{ + int rxbyte = 0; + int bitno; + int bit ; + + //TODO: Implement CPHA1 + + for (bitno=0; bitno<8; bitno++) + { + /* Transmit MSB first */ + bit = ((txbyte & 0x80) != 0x00); + txbyte <<= 1; + gpio_write(config.mosi, bit); + delayus(config.tSettle); + CLOCK_ACTIVE(); + delayus(config.tHold); + delayus(config.tFreq); + + /* Read MSB first */ + bit = gpio_read(config.miso); + rxbyte = (rxbyte<<1) | bit; + + CLOCK_IDLE(); + delayus(config.tFreq); + } + return rxbyte; +} + + +void spi_frame(unsigned char* pTx, unsigned char* pRx, unsigned char count) +{ + unsigned char tx = 0; + unsigned char rx; + + while (count > 0) + { + if (pTx != NULL) + { + tx = *(pTx++); + } + rx = spi_byte(tx); + if (pRx != NULL) + { + *(pRx++) = rx; + } + count--; + } +} + + +/***** END OF FILE *****/ diff --git a/src/energenie/spi.h b/src/energenie/spi.h new file mode 100644 index 0000000..7534264 --- /dev/null +++ b/src/energenie/spi.h @@ -0,0 +1,64 @@ +/* spi.h D.J.Whale 19/07/2014 */ + + +#ifndef SPI_H +#define SPI_H + + +/***** INCLUDES *****/ + +//#include + + +/***** CONSTANTS *****/ + +#define SPI_CPOL0 0 +#define SPI_CPOL1 1 +#define SPI_SPOL0 0 +#define SPI_SPOL1 1 +#define SPI_CPHA0 0 +#define SPI_CPHA1 1 + + +/***** STRUCTURES *****/ + +typedef struct +{ + unsigned char cs; + unsigned char sclk; + unsigned char mosi; + unsigned char miso; + + unsigned char spol; + unsigned char cpol; + unsigned char cpha; + + //struct timespec tSettle; + //struct timespec tHold; + //struct timespec tFreq; + unsigned int tSettle; + unsigned int tHold; + unsigned int tFreq; +} SPI_CONFIG; + + + +/***** FUNCTION PROTOTYPES *****/ + +void spi_init_defaults(void); + +void spi_init(SPI_CONFIG* pConfig); + +void spi_select(void); + +void spi_deselect(void); + +int spi_byte(int txbyte); + +void spi_frame(unsigned char* pTx, unsigned char* pRx, unsigned char count); + +void spi_finished(void); + +#endif + +/***** END OF FILE *****/ diff --git a/src/energenie/spi.py b/src/energenie/spi.py new file mode 100644 index 0000000..1ce3f42 --- /dev/null +++ b/src/energenie/spi.py @@ -0,0 +1,66 @@ +# spi.py 19/07/2014 D.J.Whale +# +# a C based SPI driver, with a python wrapper + +LIBNAME = "spi_rpi.so" + +import ctypes + +from os import path +mydir = path.dirname(path.abspath(__file__)) + +libspi = ctypes.cdll.LoadLibrary(mydir + "/" + LIBNAME) +spi_init_defaults_fn = libspi["spi_init_defaults"] +spi_init_fn = libspi["spi_init"] +spi_select_fn = libspi["spi_select"] +spi_deselect_fn = libspi["spi_deselect"] +spi_byte_fn = libspi["spi_byte"] +spi_frame_fn = libspi["spi_frame"] +spi_finished_fn = libspi["spi_finished"] + +def trace(msg): + pass #print("spi:" + msg) + +def init_defaults(): + trace("calling init_defaults") + spi_init_defaults_fn() + +def init(): + trace("calling init") + #TODO build a config structure + #TODO pass in pointer to config structure + #spi_init_fn() + +def select(): + trace("calling select") + spi_select_fn() + +def deselect(): + trace("calling deselect") + spi_deselect_fn() + +def byte(tx): + txbyte = ctypes.c_ubyte(tx) + #trace("calling byte") + rxbyte = spi_byte_fn(txbyte) + return rxbyte + + +def frame(txlist): + trace("calling frame ") + framelen = len(txlist) + #print("len:" + str(framelen)) + Frame = ctypes.c_ubyte * framelen + txframe = Frame(*txlist) + rxframe = Frame() + + spi_frame_fn(ctypes.byref(txframe), ctypes.byref(rxframe), framelen) + rxlist = [] + for i in range(framelen): + rxlist.append(rxframe[i]) + return rxlist + +def finished(): + trace("calling finished") + spi_finished_fn() + diff --git a/src/energenie/spi_rpi.so b/src/energenie/spi_rpi.so new file mode 100755 index 0000000..6f81455 --- /dev/null +++ b/src/energenie/spi_rpi.so Binary files differ diff --git a/src/energenie/spi_test.c b/src/energenie/spi_test.c new file mode 100644 index 0000000..a7ca0f7 --- /dev/null +++ b/src/energenie/spi_test.c @@ -0,0 +1,79 @@ +/* spi_test.c D.J.Whale 18/07/2014 + * + * A simple SPI port exerciser. + */ + + +/***** INCLUDES *****/ +#include +#include +#include "gpio.h" +#include "spi.h" + + +/***** CONSTANTS *****/ + +/* GPIO numbers on Raspberry Pi */ +#define CS 8 +#define SCLK 11 +#define MOSI 10 +#define MISO 9 + +/* ms */ +#define TSETTLE (1UL * 1000UL) /* us */ +#define THOLD (1UL * 1000UL) /* us */ +#define TFREQ (1UL * 1000UL) /* 1us = 1MHz */ + +int main(int argc, char **argv) +{ + unsigned char cmd_prog[4] = {0xAC, 0x53, 0x00, 0x00}; + unsigned char cmd_id0[4] = {0x30, 0x00, 0x00, 0x00}; + unsigned char cmd_id1[4] = {0x30, 0x00, 0x01, 0x00}; + unsigned char cmd_id2[4] = {0x30, 0x00, 0x02, 0x00}; + + unsigned char rx[4]; + SPI_CONFIG spiConfig = {CS, SCLK, MOSI, MISO, SPI_SPOL0, SPI_CPOL0, SPI_CPHA0, + {0,TSETTLE},{0,THOLD},{0,TFREQ}}; + int i; + unsigned char id[3]; + + + /* Init */ + + printf("init\n"); + //gpio_init(); + spi_init(&spiConfig); + + + /* Enter programming mode */ + + printf("select\n"); + spi_select(); + spi_frame(cmd_prog, NULL, 4); + + + /* Get ID bytes */ + + printf("read ID bytes\n"); + spi_frame(cmd_id0, rx, 4); + id[0] = rx[3]; + + spi_frame(cmd_id1, rx, 4); + id[1] = rx[3]; + + spi_frame(cmd_id2, rx, 4); + id[2] = rx[3]; + + spi_deselect(); + + + /* Show ID bytes */ + + printf("ID: %02X %02X %02X\n", id[0], id[1], id[2]); + + spi_finished(); + return 0; +} + + +/***** END OF FILE *****/ diff --git a/src/monitor.py b/src/monitor.py new file mode 100644 index 0000000..72dbec4 --- /dev/null +++ b/src/monitor.py @@ -0,0 +1,77 @@ +# monitor.py 27/09/2015 D.J.Whale +# +# Monitor settings of Energine MiHome plugs + +import time +from energenie import radio, OpenHEMS + +CRYPT_PID = 242 +CRYPT_PIP = 0x0100 + +def trace(msg): + print(str(msg)) + + +#----- TIMER ------------------------------------------------------------------ + +class Timer(): + def __init__(self, ratesec=1): + self.rate = ratesec + self.nexttick = time.time() + + + def check(self): + """Maintain the timer and see if it is time for the next tick""" + now = time.time() + + if now >= self.nexttick: + # asynchronous tick, might drift, but won't stack up if late + self.nexttick = now + self.rate + return True + + return False + + +#----- TEST APPLICATION ------------------------------------------------------- + +def monitor(): + """Send monitor poke messages and capture any responses""" + + sendMonitorTimer = Timer(3) + pollReceiveTimer = Timer(1) + + while True: + # Keep in receiver mode as much as possible + # but don't keep trying to switch into receiver if already there + if radio.mode != "RECEIVER": + radio.receiver() + + # See if there is a payload, and if there is, process it + if pollReceiveTimer.check(): + if radio.isReceiveWaiting(): + trace("receiving payload") + payload = radio.receive() + trace("decoding payload") + print(OpenHEMS.decode(payload)) # TODO decode from buffer to pydict + + # If it is time to send a monitor message, send it + if sendMonitorTimer.check(): + trace("time for monitor") + payload = OpenHEMS.make_monitor() # TODO encode from pydict to buffer + radio.transmitter() + trace("sending monitor message") + radio.transmit(payload) + + +if __name__ == "__main__": + + radio.init() + OpenHEMS.init(CRYPT_PID, CRYPT_PIP) + + try: + monitor() + + finally: + radio.finished() + +# END